server/plugins/openid/utils.ts

220 lines
5.4 KiB
TypeScript
Raw Normal View History

import { type Application, db } from "@versia/kit/db";
import type { InferSelectModel, SQL } from "@versia/kit/drizzle";
import type { OpenIdLoginFlows } from "@versia/kit/tables";
import {
2024-10-11 15:40:54 +02:00
type AuthorizationResponseError,
type AuthorizationServer,
2024-10-11 15:40:54 +02:00
ClientSecretPost,
type ResponseBodyError,
type TokenEndpointResponse,
type UserInfoResponse,
authorizationCodeGrantRequest,
discoveryRequest,
expectNoState,
getValidatedIdTokenClaims,
2024-10-11 15:40:54 +02:00
processAuthorizationCodeResponse,
processDiscoveryResponse,
processUserInfoResponse,
userInfoRequest,
validateAuthResponse,
} from "oauth4webapi";
export const oauthDiscoveryRequest = (
issuerUrl: string | URL,
): Promise<AuthorizationServer> => {
const issuerUrlurl = new URL(issuerUrl);
return discoveryRequest(issuerUrlurl, {
algorithm: "oidc",
}).then((res) => processDiscoveryResponse(issuerUrlurl, res));
};
export const oauthRedirectUri = (baseUrl: string, issuer: string): string =>
new URL(`/oauth/sso/${issuer}/callback`, baseUrl).toString();
const getFlow = (
flowId: string,
): Promise<
| (InferSelectModel<typeof OpenIdLoginFlows> & {
application?: typeof Application.$type | null;
})
| undefined
> => {
return db.query.OpenIdLoginFlows.findFirst({
where: (flow, { eq }): SQL | undefined => eq(flow.id, flowId),
with: {
application: true,
},
});
};
const getAuthServer = (issuerUrl: URL): Promise<AuthorizationServer> => {
return discoveryRequest(issuerUrl, {
algorithm: "oidc",
}).then((res) => processDiscoveryResponse(issuerUrl, res));
};
const getParameters = (
authServer: AuthorizationServer,
clientId: string,
currentUrl: URL,
2024-10-11 15:40:54 +02:00
): URLSearchParams => {
return validateAuthResponse(
authServer,
{
client_id: clientId,
},
currentUrl,
expectNoState,
);
};
const getOIDCResponse = (
authServer: AuthorizationServer,
clientId: string,
clientSecret: string,
redirectUri: string,
codeVerifier: string,
parameters: URLSearchParams,
): Promise<Response> => {
return authorizationCodeGrantRequest(
authServer,
{
client_id: clientId,
},
2024-10-11 15:40:54 +02:00
ClientSecretPost(clientSecret),
parameters,
redirectUri,
codeVerifier,
);
};
const processOIDCResponse = (
authServer: AuthorizationServer,
clientId: string,
oidcResponse: Response,
2024-10-11 15:40:54 +02:00
): Promise<TokenEndpointResponse> => {
return processAuthorizationCodeResponse(
authServer,
{
client_id: clientId,
},
oidcResponse,
);
};
const getUserInfo = (
authServer: AuthorizationServer,
clientId: string,
accessToken: string,
sub: string,
): Promise<UserInfoResponse> => {
return userInfoRequest(
authServer,
{
client_id: clientId,
},
accessToken,
).then(
async (res) =>
await processUserInfoResponse(
authServer,
{
client_id: clientId,
},
sub,
res,
),
);
};
export const automaticOidcFlow = async (
issuer: {
url: string;
client_id: string;
client_secret: string;
},
flowId: string,
currentUrl: URL,
redirectUrl: URL,
errorFn: (
error: string,
message: string,
flow:
| (InferSelectModel<typeof OpenIdLoginFlows> & {
application?: typeof Application.$type | null;
})
| null,
) => Response,
): Promise<
| Response
| {
userInfo: UserInfoResponse;
flow: InferSelectModel<typeof OpenIdLoginFlows> & {
application?: typeof Application.$type | null;
};
claims: Record<string, unknown>;
}
> => {
const flow = await getFlow(flowId);
if (!flow) {
return errorFn("invalid_request", "Invalid flow", null);
}
2024-10-11 15:40:54 +02:00
try {
const issuerUrl = new URL(issuer.url);
2024-10-11 15:40:54 +02:00
const authServer = await getAuthServer(issuerUrl);
2024-10-11 15:40:54 +02:00
const parameters = await getParameters(
authServer,
issuer.client_id,
currentUrl,
);
2024-10-11 15:40:54 +02:00
const oidcResponse = await getOIDCResponse(
authServer,
issuer.client_id,
issuer.client_secret,
redirectUrl.toString(),
flow.codeVerifier,
parameters,
);
2024-10-11 15:40:54 +02:00
const result = await processOIDCResponse(
authServer,
issuer.client_id,
oidcResponse,
);
2024-10-11 15:40:54 +02:00
const { access_token } = result;
2024-10-11 15:40:54 +02:00
const claims = getValidatedIdTokenClaims(result);
2024-10-11 15:40:54 +02:00
if (!claims) {
return errorFn("invalid_request", "Invalid claims", flow);
2024-10-11 15:40:54 +02:00
}
2024-10-11 15:40:54 +02:00
const { sub } = claims;
2024-10-11 15:40:54 +02:00
// Validate `sub`
// Later, we'll use this to automatically set the user's data
const userInfo = await getUserInfo(
authServer,
issuer.client_id,
access_token,
sub,
);
2024-10-11 15:40:54 +02:00
return {
userInfo,
flow,
claims,
};
} catch (e) {
const error = e as ResponseBodyError | AuthorizationResponseError;
return errorFn(error.error, error.error_description || "", flow);
2024-10-11 15:40:54 +02:00
}
};