diff --git a/packages/core/src/saml-applications/libraries/consts.ts b/packages/core/src/saml-applications/libraries/consts.ts new file mode 100644 index 000000000..cf8abae46 --- /dev/null +++ b/packages/core/src/saml-applications/libraries/consts.ts @@ -0,0 +1,22 @@ +export const samlLogInResponseTemplate = ` + + {Issuer} + + + + + {Issuer} + + {NameID} + + + + + + + {Audience} + + + {AttributeStatement} + +`; diff --git a/packages/core/src/saml-applications/routes/anonymous.ts b/packages/core/src/saml-applications/routes/anonymous.ts index b3b7ab646..98e3d99a9 100644 --- a/packages/core/src/saml-applications/routes/anonymous.ts +++ b/packages/core/src/saml-applications/routes/anonymous.ts @@ -1,8 +1,5 @@ -import { parseJson } from '@logto/connector-kit'; import { tryThat } from '@silverhand/essentials'; -import camelcaseKeys from 'camelcase-keys'; -import { got } from 'got'; -import saml from 'samlify'; +import * as saml from 'samlify'; import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; @@ -10,10 +7,11 @@ import koaGuard from '#src/middleware/koa-guard.js'; import type { AnonymousRouter, RouterInitArgs } from '#src/routes/types.js'; import { fetchOidcConfig, getUserInfo } from '#src/sso/OidcConnector/utils.js'; import { SsoConnectorError } from '#src/sso/types/error.js'; -import { oidcTokenResponseGuard } from '#src/sso/types/oidc.js'; import assertThat from '#src/utils/assert-that.js'; -import { createSamlTemplateCallback, samlLogInResponseTemplate } from './utils.js'; +import { samlLogInResponseTemplate } from '../libraries/consts.js'; + +import { exchangeAuthorizationCode, generateAutoSubmitForm, createSamlResponse } from './utils.js'; const samlApplicationSignInCallbackQueryParametersGuard = z.union([ z.object({ @@ -97,41 +95,20 @@ export default function samlApplicationAnonymousRoutes - -
- - -
- - - - `; + // Return auto-submit form + ctx.body = generateAutoSubmitForm(entityEndpoint, context); return next(); } ); diff --git a/packages/core/src/saml-applications/routes/utils.ts b/packages/core/src/saml-applications/routes/utils.ts index 3d0e5f342..e6dc68c0b 100644 --- a/packages/core/src/saml-applications/routes/utils.ts +++ b/packages/core/src/saml-applications/routes/utils.ts @@ -1,32 +1,13 @@ +import { parseJson } from '@logto/connector-kit'; import { generateStandardId } from '@logto/shared'; -import saml from 'samlify'; +import camelcaseKeys from 'camelcase-keys'; +import { got } from 'got'; +import * as saml from 'samlify'; -import { type idTokenProfileStandardClaims } from '#src/sso/types/oidc.js'; +import RequestError from '#src/errors/RequestError/index.js'; +import { oidcTokenResponseGuard, type idTokenProfileStandardClaims } from '#src/sso/types/oidc.js'; -export const samlLogInResponseTemplate = ` - - {Issuer} - - - - - {Issuer} - - {NameID} - - - - - - - {Audience} - - - {AttributeStatement} - -`; - -export const createSamlTemplateCallback = +const createSamlTemplateCallback = ( idp: saml.IdentityProviderInstance, sp: saml.ServiceProviderInstance, @@ -72,3 +53,82 @@ export const createSamlTemplateCallback = context, }; }; + +export const exchangeAuthorizationCode = async ( + tokenEndpoint: string, + { + code, + clientId, + clientSecret, + redirectUri, + }: { + code: string; + clientId: string; + clientSecret: string; + redirectUri?: string; + } +) => { + const headers = { + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`, 'utf8').toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + const tokenRequestParameters = new URLSearchParams({ + grant_type: 'authorization_code', + code, + client_id: clientId, + ...(redirectUri ? { redirect_uri: redirectUri } : {}), + }); + + const httpResponse = await got.post(tokenEndpoint, { + body: tokenRequestParameters.toString(), + headers, + }); + + const result = oidcTokenResponseGuard.safeParse(parseJson(httpResponse.body)); + + if (!result.success) { + throw new RequestError({ + code: 'oidc.invalid_token', + message: 'Invalid token response', + }); + } + + return camelcaseKeys(result.data); +}; + +export const createSamlResponse = async ( + idp: saml.IdentityProviderInstance, + sp: saml.ServiceProviderInstance, + userInfo: idTokenProfileStandardClaims +): Promise<{ context: string; entityEndpoint: string }> => { + // TODO: fix binding method + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { context, entityEndpoint } = await idp.createLoginResponse( + sp, + // @ts-expect-error --fix request object later + null, + 'post', + userInfo, + createSamlTemplateCallback(idp, sp, userInfo) + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + return { context, entityEndpoint }; +}; + +export const generateAutoSubmitForm = (actionUrl: string, samlResponse: string) => { + return ` + + +
+ + +
+ + + + `; +};