mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor(core): refactor the sso interaction handlers (#6186)
refactor(core): revert the sso utils input refactor revert the sso utils input refactor
This commit is contained in:
parent
0ca307cb5f
commit
d7fa9f5900
5 changed files with 109 additions and 55 deletions
|
@ -31,7 +31,6 @@ export type PasswordIdentifierPayload =
|
|||
export type SocialVerifiedIdentifierPayload = SocialEmailPayload | SocialPhonePayload;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Legacy type for the interaction API.
|
||||
* Use the latest experience API instead.
|
||||
* Moved to `@logto/schemas`
|
||||
|
|
|
@ -3,9 +3,9 @@ import type Provider from 'oidc-provider';
|
|||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type SingleSignOnConnectorSession,
|
||||
singleSignOnConnectorSessionGuard,
|
||||
singleSignOnInteractionIdentifierResultGuard,
|
||||
type SingleSignOnConnectorSession,
|
||||
type SingleSignOnInteractionIdentifierResult,
|
||||
} from '#src/sso/index.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
@ -42,10 +42,33 @@ export const getSingleSignOnSessionResult = async (
|
|||
return singleSignOnSessionResult.data.connectorSession;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign the single sign on session data to the oidc provider session storage.
|
||||
*
|
||||
* @remark Forked from {@link ./social-verification.ts.}
|
||||
* Use the SingleSignOnConnectorSession type instead of ConnectorSession.
|
||||
* Remove the dependency from social-verification utils.
|
||||
*/
|
||||
export const assignSingleSignOnSessionResult = async (
|
||||
ctx: Context,
|
||||
provider: Provider,
|
||||
connectorSession: SingleSignOnConnectorSession
|
||||
) => {
|
||||
const details = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
await provider.interactionResult(ctx.req, ctx.res, {
|
||||
...details.result,
|
||||
connectorSession,
|
||||
});
|
||||
};
|
||||
|
||||
// Remark:
|
||||
// The following functions are used in the legacy interaction single sign on routes only.
|
||||
// Deprecated in the experience APIs. `SingleSignOnAuthenticationResult` will be stored as a verification record in the latest experience API implementation.
|
||||
|
||||
export const assignSingleSignOnAuthenticationResult = async (
|
||||
ctx: Context,
|
||||
provider: Provider,
|
||||
singleSignOnIdentifier: SingleSignOnInteractionIdentifierResult['singleSignOnIdentifier']
|
||||
singleSignOnIdentifier: SingleSignOnInteractionIdentifierResult
|
||||
) => {
|
||||
const details = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
|
||||
|
@ -61,7 +84,7 @@ export const getSingleSignOnAuthenticationResult = async (
|
|||
ctx: Context,
|
||||
provider: Provider,
|
||||
connectorId: string
|
||||
): Promise<SingleSignOnInteractionIdentifierResult['singleSignOnIdentifier']> => {
|
||||
): Promise<SingleSignOnInteractionIdentifierResult> => {
|
||||
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
|
||||
const singleSignOnInteractionIdentifierResult =
|
||||
|
|
|
@ -16,7 +16,7 @@ import { type WithInteractionDetailsContext } from '../middleware/koa-interactio
|
|||
import { type WithInteractionHooksContext } from '../middleware/koa-interaction-hooks.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const { mockEsm } = createMockUtils(jest);
|
||||
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
|
||||
|
||||
const getAuthorizationUrlMock = jest.fn();
|
||||
const getIssuerMock = jest.fn();
|
||||
|
@ -43,7 +43,7 @@ mockEsm('./interaction.js', () => ({
|
|||
const {
|
||||
getSingleSignOnSessionResult: getSingleSignOnSessionResultMock,
|
||||
assignSingleSignOnAuthenticationResult: assignSingleSignOnAuthenticationResultMock,
|
||||
} = mockEsm('./single-sign-on-session.js', () => ({
|
||||
} = await mockEsmWithActual('./single-sign-on-session.js', () => ({
|
||||
getSingleSignOnSessionResult: jest.fn(),
|
||||
assignSingleSignOnAuthenticationResult: jest.fn(),
|
||||
}));
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable max-lines -- will migrate this file to the latest experience APIs */
|
||||
import { ConnectorError, type SocialUserInfo } from '@logto/connector-kit';
|
||||
import { validateRedirectUrl } from '@logto/core-kit';
|
||||
import {
|
||||
|
@ -12,7 +13,6 @@ import { z } from 'zod';
|
|||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import { type WithInteractionDetailsContext } from '#src/routes/interaction/middleware/koa-interaction-details.js';
|
||||
import { ssoConnectorFactories, type SingleSignOnConnectorSession } from '#src/sso/index.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
@ -22,30 +22,25 @@ import { type WithInteractionHooksContext } from '../middleware/koa-interaction-
|
|||
|
||||
import {
|
||||
assignSingleSignOnAuthenticationResult,
|
||||
assignSingleSignOnSessionResult,
|
||||
getSingleSignOnSessionResult,
|
||||
} from './single-sign-on-session.js';
|
||||
import { assignConnectorSessionResult } from './social-verification.js';
|
||||
|
||||
export const authorizationUrlPayloadGuard = z.object({
|
||||
state: z.string().min(1),
|
||||
redirectUri: z.string().refine((url) => validateRedirectUrl(url, 'web')),
|
||||
});
|
||||
|
||||
type AuthorizationUrlPayload = z.infer<typeof authorizationUrlPayloadGuard>;
|
||||
|
||||
// Get the authorization url for the SSO provider
|
||||
export const getSsoAuthorizationUrl = async (
|
||||
ctx: WithInteractionDetailsContext<WithLogContext>,
|
||||
ctx: WithLogContext,
|
||||
{ provider, id: tenantId }: TenantContext,
|
||||
connectorData: SupportedSsoConnector,
|
||||
payload: AuthorizationUrlPayload
|
||||
): Promise<string> => {
|
||||
const { id: connectorId, providerName } = connectorData;
|
||||
|
||||
const {
|
||||
createLog,
|
||||
interactionDetails: { jti },
|
||||
} = ctx;
|
||||
const { createLog } = ctx;
|
||||
|
||||
const log = createLog(`Interaction.SignIn.Identifier.SingleSignOn.Create`);
|
||||
log.append({
|
||||
|
@ -62,10 +57,12 @@ export const getSsoAuthorizationUrl = async (
|
|||
|
||||
assertThat(payload, 'session.insufficient_info');
|
||||
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
|
||||
return await connectorInstance.getAuthorizationUrl(
|
||||
{ jti, ...payload, connectorId },
|
||||
async (connectorSession: SingleSignOnConnectorSession) =>
|
||||
assignConnectorSessionResult(ctx, provider, connectorSession)
|
||||
assignSingleSignOnSessionResult(ctx, provider, connectorSession)
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
// Catch ConnectorError and re-throw as 500 RequestError
|
||||
|
@ -78,23 +75,31 @@ export const getSsoAuthorizationUrl = async (
|
|||
};
|
||||
|
||||
type SsoAuthenticationResult = {
|
||||
/** The issuer of the SSO provider, we need to store this in the user SSO identity to identify the provider. */
|
||||
issuer: string;
|
||||
userInfo: SocialUserInfo;
|
||||
};
|
||||
|
||||
// Get the user authentication result from the SSO provider
|
||||
export const getSsoAuthentication = async (
|
||||
ctx: WithInteractionHooksContext<WithLogContext>,
|
||||
/**
|
||||
* Verify the SSO identity from the SSO provider
|
||||
*
|
||||
* - Load the SSO session from the OIDC provider interaction details
|
||||
* - Verify the SSO identity from the SSO provider
|
||||
*
|
||||
* @returns The SSO authentication result
|
||||
*/
|
||||
const verifySsoIdentity = async (
|
||||
ctx: WithLogContext,
|
||||
{ provider, id: tenantId }: TenantContext,
|
||||
connectorData: SupportedSsoConnector,
|
||||
data: Record<string, unknown>
|
||||
): Promise<SsoAuthenticationResult> => {
|
||||
const { createLog } = ctx;
|
||||
const { id: connectorId, providerName } = connectorData;
|
||||
|
||||
const { createLog } = ctx;
|
||||
|
||||
const log = createLog(`Interaction.SignIn.Identifier.SingleSignOn.Submit`);
|
||||
log.append({ connectorId, data });
|
||||
|
||||
const singleSignOnSession = await getSingleSignOnSessionResult(ctx, provider);
|
||||
|
||||
try {
|
||||
|
@ -103,34 +108,49 @@ export const getSsoAuthentication = async (
|
|||
connectorData,
|
||||
tenantId
|
||||
);
|
||||
|
||||
const issuer = await connectorInstance.getIssuer();
|
||||
const userInfo = await connectorInstance.getUserInfo(singleSignOnSession, data);
|
||||
|
||||
const result = {
|
||||
log.append({ issuer, userInfo });
|
||||
|
||||
return {
|
||||
issuer,
|
||||
userInfo,
|
||||
};
|
||||
|
||||
// Assign the single sign on authentication to the interaction result
|
||||
await assignSingleSignOnAuthenticationResult(ctx, provider, {
|
||||
connectorId,
|
||||
...result,
|
||||
});
|
||||
|
||||
log.append({ issuer, userInfo });
|
||||
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
// Catch ConnectorError and re-throw as 500 RequestError
|
||||
if (error instanceof ConnectorError) {
|
||||
throw new RequestError({ code: `connector.${error.code}`, status: 500 }, error.data);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Remark:
|
||||
// The following functions are used in the legacy interaction single sign on routes only.
|
||||
// The SSO interaction flow will be handled in the latest experience APIs using the SSO verification record.
|
||||
|
||||
/** Verify the SSO identity and assign the authentication result to the interaction result */
|
||||
export const getSsoAuthentication = async (
|
||||
ctx: WithInteractionHooksContext<WithLogContext>,
|
||||
tenantContext: TenantContext,
|
||||
connectorData: SupportedSsoConnector,
|
||||
data: Record<string, unknown>
|
||||
): Promise<SsoAuthenticationResult> => {
|
||||
const { provider } = tenantContext;
|
||||
const { id: connectorId } = connectorData;
|
||||
|
||||
const ssoAuthentication = await verifySsoIdentity(ctx, tenantContext, connectorData, data);
|
||||
|
||||
// Assign the single sign on authentication to the interaction result
|
||||
await assignSingleSignOnAuthenticationResult(ctx, provider, {
|
||||
connectorId,
|
||||
...ssoAuthentication,
|
||||
});
|
||||
|
||||
return ssoAuthentication;
|
||||
};
|
||||
|
||||
// Handle the SSO authentication result and return the user id
|
||||
export const handleSsoAuthentication = async (
|
||||
ctx: WithInteractionHooksContext<WithLogContext>,
|
||||
|
@ -372,3 +392,4 @@ export const registerWithSsoAuthentication = async (
|
|||
|
||||
return user;
|
||||
};
|
||||
/* eslint-enable max-lines */
|
||||
|
|
|
@ -1,53 +1,64 @@
|
|||
import { type ToZodObject } from '@logto/schemas/lib/utils/zod.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { extendedSocialUserInfoGuard } from './saml.js';
|
||||
import { extendedSocialUserInfoGuard, type ExtendedSocialUserInfo } from './saml.js';
|
||||
|
||||
/**
|
||||
* Single sign on connector session
|
||||
*
|
||||
* @property state The state generated by Logto experience client.
|
||||
* @property redirectUri The redirect uri for the identity provider.
|
||||
* @property nonce OIDC only properties, generated by OIDC connector factory, used to verify the identity provider response.
|
||||
* @property userInfo The user info returned by the identity provider.
|
||||
* SAML only properties, parsed from the SAML assertion.
|
||||
* We store the assertion in the session storage after receiving it from the identity provider.
|
||||
* So the client authentication handler can get it later.
|
||||
* @property connectorId The connector id.
|
||||
*
|
||||
* @remark this is a forked version of @logto/connector-kit
|
||||
* Simplified the type definition to only include the properties we need.
|
||||
* Create additional type guard to validate the session data.
|
||||
* @see @logto/connector-kit/types/social.ts
|
||||
*/
|
||||
export type SingleSignOnConnectorSession = {
|
||||
/** The state generated by Logto experience client. */
|
||||
state: string;
|
||||
/** The redirect uri for the identity provider. */
|
||||
redirectUri: string;
|
||||
connectorId: string;
|
||||
/** OIDC only properties, generated by OIDC connector factory, used to verify the identity provider response. */
|
||||
nonce?: string;
|
||||
/** User info returned by the identity provider.
|
||||
* SAML only properties, parsed from the SAML assertion.
|
||||
* We store the assertion in the session storage after receiving it from the identity provider.
|
||||
* So the client authentication handler can get it later.
|
||||
*/
|
||||
userInfo?: ExtendedSocialUserInfo;
|
||||
};
|
||||
|
||||
export const singleSignOnConnectorSessionGuard = z.object({
|
||||
state: z.string(),
|
||||
redirectUri: z.string(),
|
||||
connectorId: z.string(),
|
||||
nonce: z.string().optional(),
|
||||
userInfo: extendedSocialUserInfoGuard.optional(),
|
||||
});
|
||||
|
||||
export type SingleSignOnConnectorSession = z.infer<typeof singleSignOnConnectorSessionGuard>;
|
||||
}) satisfies ToZodObject<SingleSignOnConnectorSession>;
|
||||
|
||||
export type CreateSingleSignOnSession = (storage: SingleSignOnConnectorSession) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Single sign on interaction identifier session
|
||||
* Single sign on interaction identifier result
|
||||
*
|
||||
* @remark this session is used to store the authentication result from the identity provider. {@link /packages/core/src/routes/interaction/utils/single-sign-on.ts}
|
||||
* This session is needed because we need to split the authentication process into sign in and sign up two parts.
|
||||
* @remark this session data is used to store the authentication result from the identity provider. {@link /packages/core/src/routes/interaction/utils/single-sign-on.ts}
|
||||
* This is needed because we need to split the authentication process into sign in and sign up two parts.
|
||||
* If the SSO identity is found in DB we will directly sign in the user.
|
||||
* If the SSO identity is not found in DB we will throw an error and let the client to create a new user.
|
||||
* In the SSO registration endpoint, we will validate this session data and create a new user accordingly.
|
||||
*
|
||||
* Deprecated in the experience APIs. We will reply on the SSO verification record to store the authentication result.
|
||||
*/
|
||||
export type SingleSignOnInteractionIdentifierResult = {
|
||||
connectorId: string;
|
||||
issuer: string;
|
||||
userInfo: ExtendedSocialUserInfo;
|
||||
};
|
||||
export const singleSignOnInteractionIdentifierResultGuard = z.object({
|
||||
singleSignOnIdentifier: z.object({
|
||||
connectorId: z.string(),
|
||||
issuer: z.string(),
|
||||
userInfo: extendedSocialUserInfoGuard,
|
||||
}),
|
||||
});
|
||||
|
||||
export type SingleSignOnInteractionIdentifierResult = z.infer<
|
||||
typeof singleSignOnInteractionIdentifierResultGuard
|
||||
>;
|
||||
}) satisfies ToZodObject<{
|
||||
singleSignOnIdentifier: SingleSignOnInteractionIdentifierResult;
|
||||
}>;
|
||||
|
|
Loading…
Reference in a new issue