2024-11-04 14:58:17 +01:00
|
|
|
import { type Application, db } from "@versia/kit/db";
|
2024-11-02 00:43:33 +01:00
|
|
|
import type { InferSelectModel, SQL } from "@versia/kit/drizzle";
|
2024-11-04 14:58:17 +01:00
|
|
|
import type { OpenIdLoginFlows } from "@versia/kit/tables";
|
2024-09-24 14:42:39 +02:00
|
|
|
import {
|
2024-10-11 15:40:54 +02:00
|
|
|
type AuthorizationResponseError,
|
2024-09-24 14:42:39 +02:00
|
|
|
type AuthorizationServer,
|
2024-10-11 15:40:54 +02:00
|
|
|
ClientSecretPost,
|
|
|
|
|
type ResponseBodyError,
|
|
|
|
|
type TokenEndpointResponse,
|
2024-11-02 00:43:33 +01:00
|
|
|
type UserInfoResponse,
|
2024-10-11 15:15:06 +02:00
|
|
|
authorizationCodeGrantRequest,
|
2024-09-24 14:42:39 +02:00
|
|
|
discoveryRequest,
|
2024-10-11 15:15:06 +02:00
|
|
|
expectNoState,
|
|
|
|
|
getValidatedIdTokenClaims,
|
2024-10-11 15:40:54 +02:00
|
|
|
processAuthorizationCodeResponse,
|
2024-09-24 14:42:39 +02:00
|
|
|
processDiscoveryResponse,
|
2024-10-11 15:15:06 +02:00
|
|
|
processUserInfoResponse,
|
|
|
|
|
userInfoRequest,
|
|
|
|
|
validateAuthResponse,
|
2024-09-24 14:42:39 +02:00
|
|
|
} 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));
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
export const oauthRedirectUri = (baseUrl: string, issuer: string): string =>
|
2024-09-24 14:42:39 +02:00
|
|
|
new URL(`/oauth/sso/${issuer}/callback`, baseUrl).toString();
|
2024-10-11 15:15:06 +02:00
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
const getFlow = (
|
|
|
|
|
flowId: string,
|
|
|
|
|
): Promise<
|
|
|
|
|
| (InferSelectModel<typeof OpenIdLoginFlows> & {
|
2024-11-04 14:58:17 +01:00
|
|
|
application?: typeof Application.$type | null;
|
2024-11-02 00:43:33 +01:00
|
|
|
})
|
|
|
|
|
| undefined
|
|
|
|
|
> => {
|
2024-10-11 15:15:06 +02:00
|
|
|
return db.query.OpenIdLoginFlows.findFirst({
|
2024-11-02 00:43:33 +01:00
|
|
|
where: (flow, { eq }): SQL | undefined => eq(flow.id, flowId),
|
2024-10-11 15:15:06 +02:00
|
|
|
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 => {
|
2024-10-11 15:15:06 +02:00
|
|
|
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),
|
2024-10-11 15:15:06 +02:00
|
|
|
parameters,
|
|
|
|
|
redirectUri,
|
|
|
|
|
codeVerifier,
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const processOIDCResponse = (
|
|
|
|
|
authServer: AuthorizationServer,
|
|
|
|
|
clientId: string,
|
|
|
|
|
oidcResponse: Response,
|
2024-10-11 15:40:54 +02:00
|
|
|
): Promise<TokenEndpointResponse> => {
|
|
|
|
|
return processAuthorizationCodeResponse(
|
2024-10-11 15:15:06 +02:00
|
|
|
authServer,
|
|
|
|
|
{
|
|
|
|
|
client_id: clientId,
|
|
|
|
|
},
|
|
|
|
|
oidcResponse,
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getUserInfo = (
|
|
|
|
|
authServer: AuthorizationServer,
|
|
|
|
|
clientId: string,
|
|
|
|
|
accessToken: string,
|
|
|
|
|
sub: string,
|
2024-11-02 00:43:33 +01:00
|
|
|
): Promise<UserInfoResponse> => {
|
2024-10-11 15:15:06 +02:00
|
|
|
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,
|
2024-10-11 17:23:51 +02:00
|
|
|
flow:
|
|
|
|
|
| (InferSelectModel<typeof OpenIdLoginFlows> & {
|
2024-11-04 14:58:17 +01:00
|
|
|
application?: typeof Application.$type | null;
|
2024-10-11 17:23:51 +02:00
|
|
|
})
|
|
|
|
|
| null,
|
2024-10-11 15:15:06 +02:00
|
|
|
) => Response,
|
2024-11-02 00:43:33 +01:00
|
|
|
): Promise<
|
|
|
|
|
| Response
|
|
|
|
|
| {
|
|
|
|
|
userInfo: UserInfoResponse;
|
|
|
|
|
flow: InferSelectModel<typeof OpenIdLoginFlows> & {
|
2024-11-04 14:58:17 +01:00
|
|
|
application?: typeof Application.$type | null;
|
2024-11-02 00:43:33 +01:00
|
|
|
};
|
|
|
|
|
claims: Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
> => {
|
2024-10-11 15:15:06 +02:00
|
|
|
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:15:06 +02:00
|
|
|
|
2024-10-11 15:40:54 +02:00
|
|
|
const authServer = await getAuthServer(issuerUrl);
|
2024-10-11 15:15:06 +02:00
|
|
|
|
2024-10-11 15:40:54 +02:00
|
|
|
const parameters = await getParameters(
|
|
|
|
|
authServer,
|
|
|
|
|
issuer.client_id,
|
|
|
|
|
currentUrl,
|
|
|
|
|
);
|
2024-10-11 15:15:06 +02:00
|
|
|
|
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:15:06 +02:00
|
|
|
);
|
|
|
|
|
|
2024-10-11 15:40:54 +02:00
|
|
|
const result = await processOIDCResponse(
|
|
|
|
|
authServer,
|
|
|
|
|
issuer.client_id,
|
|
|
|
|
oidcResponse,
|
|
|
|
|
);
|
2024-10-11 15:15:06 +02:00
|
|
|
|
2024-10-11 15:40:54 +02:00
|
|
|
const { access_token } = result;
|
2024-10-11 15:15:06 +02:00
|
|
|
|
2024-10-11 15:40:54 +02:00
|
|
|
const claims = getValidatedIdTokenClaims(result);
|
2024-10-11 15:15:06 +02:00
|
|
|
|
2024-10-11 15:40:54 +02:00
|
|
|
if (!claims) {
|
2024-10-11 17:23:51 +02:00
|
|
|
return errorFn("invalid_request", "Invalid claims", flow);
|
2024-10-11 15:40:54 +02:00
|
|
|
}
|
2024-10-11 15:15:06 +02:00
|
|
|
|
2024-10-11 15:40:54 +02:00
|
|
|
const { sub } = claims;
|
2024-10-11 15:15:06 +02:00
|
|
|
|
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:15:06 +02:00
|
|
|
|
2024-10-11 15:40:54 +02:00
|
|
|
return {
|
|
|
|
|
userInfo,
|
|
|
|
|
flow,
|
|
|
|
|
claims,
|
|
|
|
|
};
|
|
|
|
|
} catch (e) {
|
|
|
|
|
const error = e as ResponseBodyError | AuthorizationResponseError;
|
2024-10-11 17:23:51 +02:00
|
|
|
return errorFn(error.error, error.error_description || "", flow);
|
2024-10-11 15:40:54 +02:00
|
|
|
}
|
2024-10-11 15:15:06 +02:00
|
|
|
};
|