mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Improve OpenID login flow security
This commit is contained in:
parent
d47a11cfc2
commit
22ebf72b6b
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `issuerId` to the `OpenIdLoginFlow` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "OpenIdLoginFlow" ADD COLUMN "applicationId" UUID,
|
||||||
|
ADD COLUMN "issuerId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OpenIdLoginFlow" ADD CONSTRAINT "OpenIdLoginFlow_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[client_id]` on the table `Application` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Application_client_id_key" ON "Application"("client_id");
|
||||||
|
|
@ -10,16 +10,17 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Application {
|
model Application {
|
||||||
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
|
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
|
||||||
name String
|
name String
|
||||||
website String?
|
website String?
|
||||||
vapid_key String?
|
vapid_key String?
|
||||||
client_id String
|
client_id String @unique
|
||||||
secret String
|
secret String
|
||||||
scopes String
|
scopes String
|
||||||
redirect_uris String
|
redirect_uris String
|
||||||
statuses Status[] // One to many relation with Status
|
statuses Status[] // One to many relation with Status
|
||||||
tokens Token[] // One to many relation with Token
|
tokens Token[] // One to many relation with Token
|
||||||
|
openIdLoginFlows OpenIdLoginFlow[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Emoji {
|
model Emoji {
|
||||||
|
|
@ -140,8 +141,11 @@ model Token {
|
||||||
}
|
}
|
||||||
|
|
||||||
model OpenIdLoginFlow {
|
model OpenIdLoginFlow {
|
||||||
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
|
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
|
||||||
codeVerifier String
|
codeVerifier String
|
||||||
|
issuerId String
|
||||||
|
application Application? @relation(fields: [applicationId], references: [id], onDelete: Cascade)
|
||||||
|
applicationId String? @db.Uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
model Attachment {
|
model Attachment {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ import type { MatchedRoute } from "bun";
|
||||||
import {
|
import {
|
||||||
calculatePKCECodeChallenge,
|
calculatePKCECodeChallenge,
|
||||||
discoveryRequest,
|
discoveryRequest,
|
||||||
|
generateRandomCodeVerifier,
|
||||||
processDiscoveryResponse,
|
processDiscoveryResponse,
|
||||||
} from "oauth4webapi";
|
} from "oauth4webapi";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -63,7 +65,22 @@ export default async (
|
||||||
algorithm: "oidc",
|
algorithm: "oidc",
|
||||||
}).then(res => processDiscoveryResponse(issuerUrl, res));
|
}).then(res => processDiscoveryResponse(issuerUrl, res));
|
||||||
|
|
||||||
const codeVerifier = "tempString";
|
const codeVerifier = generateRandomCodeVerifier();
|
||||||
|
|
||||||
|
// Store into database
|
||||||
|
|
||||||
|
const newFlow = await client.openIdLoginFlow.create({
|
||||||
|
data: {
|
||||||
|
codeVerifier,
|
||||||
|
application: {
|
||||||
|
connect: {
|
||||||
|
client_id: clientId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
issuerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
||||||
|
|
||||||
return Response.redirect(
|
return Response.redirect(
|
||||||
|
|
@ -72,7 +89,7 @@ export default async (
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
client_id: issuer.client_id,
|
client_id: issuer.client_id,
|
||||||
redirect_uri:
|
redirect_uri:
|
||||||
oauthRedirectUri(issuerId) + `?clientId=${clientId}`,
|
oauthRedirectUri(issuerId) + `?flow=${newFlow.id}`,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
scope: "openid profile email",
|
scope: "openid profile email",
|
||||||
// PKCE
|
// PKCE
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,18 @@ export default async (
|
||||||
// Remove state query parameter from URL
|
// Remove state query parameter from URL
|
||||||
currentUrl.searchParams.delete("state");
|
currentUrl.searchParams.delete("state");
|
||||||
const issuerParam = matchedRoute.params.issuer;
|
const issuerParam = matchedRoute.params.issuer;
|
||||||
// This is the Lysand client's client_id, not the external OAuth provider's client_id
|
const flow = await client.openIdLoginFlow.findFirst({
|
||||||
const clientId = matchedRoute.query.clientId;
|
where: {
|
||||||
|
id: matchedRoute.query.flow,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
application: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!flow) {
|
||||||
|
return redirectToLogin("Invalid flow");
|
||||||
|
}
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -95,8 +105,8 @@ export default async (
|
||||||
client_secret: issuer.client_secret,
|
client_secret: issuer.client_secret,
|
||||||
},
|
},
|
||||||
parameters,
|
parameters,
|
||||||
oauthRedirectUri(issuerParam) + `?clientId=${clientId}`,
|
oauthRedirectUri(issuerParam) + `?flow=${flow.id}`,
|
||||||
"tempString"
|
flow.codeVerifier
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await processAuthorizationCodeOpenIDResponse(
|
const result = await processAuthorizationCodeOpenIDResponse(
|
||||||
|
|
@ -153,24 +163,19 @@ export default async (
|
||||||
return redirectToLogin("No user found with that account");
|
return redirectToLogin("No user found with that account");
|
||||||
}
|
}
|
||||||
|
|
||||||
const application = await client.application.findFirst({
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
where: {
|
if (!flow.application) return redirectToLogin("Invalid client_id");
|
||||||
client_id: clientId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!application) return redirectToLogin("Invalid client_id");
|
|
||||||
|
|
||||||
const code = randomBytes(32).toString("hex");
|
const code = randomBytes(32).toString("hex");
|
||||||
|
|
||||||
await client.application.update({
|
await client.application.update({
|
||||||
where: { id: application.id },
|
where: { id: flow.application.id },
|
||||||
data: {
|
data: {
|
||||||
tokens: {
|
tokens: {
|
||||||
create: {
|
create: {
|
||||||
access_token: randomBytes(64).toString("base64url"),
|
access_token: randomBytes(64).toString("base64url"),
|
||||||
code: code,
|
code: code,
|
||||||
scope: application.scopes,
|
scope: flow.application.scopes,
|
||||||
token_type: TokenType.BEARER,
|
token_type: TokenType.BEARER,
|
||||||
user: {
|
user: {
|
||||||
connect: {
|
connect: {
|
||||||
|
|
@ -183,5 +188,8 @@ export default async (
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect back to application
|
// Redirect back to application
|
||||||
return Response.redirect(`${application.redirect_uris}?code=${code}`, 302);
|
return Response.redirect(
|
||||||
|
`${flow.application.redirect_uris}?code=${code}`,
|
||||||
|
302
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue