From ecb7f80826e35da4b1734597f5e954fbfe17e303 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 20 Dec 2022 15:05:54 +0800 Subject: [PATCH] chore: add social sign-in validation reference to interaction (#2658) --- packages/core/src/libraries/social.ts | 6 ++-- packages/core/src/routes/interaction/index.ts | 8 ++++- .../utils/social-verification.test.ts | 9 ++++-- .../interaction/utils/social-verification.ts | 13 +++++--- .../identifier-payload-verification.test.ts | 6 +++- .../identifier-payload-verification.ts | 11 +++++-- packages/core/src/routes/session/social.ts | 18 +++++++++-- packages/core/src/routes/session/utils.ts | 31 +++++++++++++++++++ packages/phrases/src/locales/de/errors.ts | 2 ++ packages/phrases/src/locales/en/errors.ts | 2 ++ packages/phrases/src/locales/fr/errors.ts | 2 ++ packages/phrases/src/locales/ko/errors.ts | 2 ++ packages/phrases/src/locales/pt-br/errors.ts | 2 ++ packages/phrases/src/locales/pt-pt/errors.ts | 2 ++ packages/phrases/src/locales/tr-tr/errors.ts | 2 ++ packages/phrases/src/locales/zh-cn/errors.ts | 1 + packages/toolkit/connector-kit/src/types.ts | 25 ++++++++++++--- 17 files changed, 121 insertions(+), 21 deletions(-) diff --git a/packages/core/src/libraries/social.ts b/packages/core/src/libraries/social.ts index 6662186af..05eee7b5a 100644 --- a/packages/core/src/libraries/social.ts +++ b/packages/core/src/libraries/social.ts @@ -1,3 +1,4 @@ +import type { GetSession } from '@logto/connector-kit'; import type { User } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; import type { Nullable } from '@silverhand/essentials'; @@ -34,7 +35,8 @@ const getConnector = async (connectorId: string) => { export const getUserInfoByAuthCode = async ( connectorId: string, - data: unknown + data: unknown, + getConnectorSession?: GetSession ): Promise => { const connector = await getConnector(connectorId); @@ -47,7 +49,7 @@ export const getUserInfoByAuthCode = async ( }) ); - return connector.getUserInfo(data); + return connector.getUserInfo(data, getConnectorSession); }; export const getUserInfoFromInteractionResult = async ( diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index 57c357704..e7791561b 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -1,3 +1,4 @@ +import type { ConnectorSession } from '@logto/connector-kit'; import type { LogtoErrorCode } from '@logto/phrases'; import { Event } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; @@ -7,6 +8,7 @@ import RequestError from '#src/errors/RequestError/index.js'; import { assignInteractionResults } from '#src/libraries/session.js'; import koaAuditLog from '#src/middleware/koa-audit-log.js'; import koaGuard from '#src/middleware/koa-guard.js'; +import { assignConnectorSessionResult } from '#src/routes/session/utils.js'; import assertThat from '#src/utils/assert-that.js'; import type { AnonymousRouter } from '../types.js'; @@ -121,7 +123,11 @@ export default function interactionRoutes( // Check interaction session await provider.interactionDetails(ctx.req, ctx.res); - const redirectTo = await createSocialAuthorizationUrl(ctx.guard.body); + const redirectTo = await createSocialAuthorizationUrl( + ctx.guard.body, + async (connectorStorage: ConnectorSession) => + assignConnectorSessionResult(ctx, provider, connectorStorage) + ); ctx.body = { redirectTo }; diff --git a/packages/core/src/routes/interaction/utils/social-verification.test.ts b/packages/core/src/routes/interaction/utils/social-verification.test.ts index 910f17849..ad4b56011 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.test.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.test.ts @@ -24,11 +24,16 @@ const log = createMockLogContext(); describe('social-verification', () => { it('verifySocialIdentity', async () => { + const getSession = jest.fn(); const connectorId = 'connector'; const connectorData = { authCode: 'code' }; - const userInfo = await verifySocialIdentity({ connectorId, connectorData }, log.createLog); + const userInfo = await verifySocialIdentity( + { connectorId, connectorData }, + log.createLog, + getSession + ); - expect(getUserInfoByAuthCode).toBeCalledWith(connectorId, connectorData); + expect(getUserInfoByAuthCode).toBeCalledWith(connectorId, connectorData, getSession); expect(userInfo).toEqual({ id: 'foo' }); }); }); diff --git a/packages/core/src/routes/interaction/utils/social-verification.ts b/packages/core/src/routes/interaction/utils/social-verification.ts index a6cda4038..f4c2c40fb 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.ts @@ -1,3 +1,4 @@ +import type { GetSession, SetSession } from '@logto/connector-kit'; import type { SocialConnectorPayload } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; @@ -9,7 +10,10 @@ import assertThat from '#src/utils/assert-that.js'; import type { SocialAuthorizationUrlPayload } from '../types/index.js'; -export const createSocialAuthorizationUrl = async (payload: SocialAuthorizationUrlPayload) => { +export const createSocialAuthorizationUrl = async ( + payload: SocialAuthorizationUrlPayload, + setConnectorSession?: SetSession +) => { const { connectorId, state, redirectUri } = payload; assertThat(state && redirectUri, 'session.insufficient_info'); @@ -17,17 +21,18 @@ export const createSocialAuthorizationUrl = async (payload: SocialAuthorizationU assertThat(connector.type === ConnectorType.Social, 'connector.unexpected_type'); - return connector.getAuthorizationUri({ state, redirectUri }); + return connector.getAuthorizationUri({ state, redirectUri }, setConnectorSession); }; export const verifySocialIdentity = async ( { connectorId, connectorData }: SocialConnectorPayload, - createLog: LogContext['createLog'] + createLog: LogContext['createLog'], + getSession?: GetSession ): Promise => { const log = createLog('Interaction.SignIn.Identifier.Social.Submit'); log.append({ connectorId, connectorData }); - const userInfo = await getUserInfoByAuthCode(connectorId, connectorData); + const userInfo = await getUserInfoByAuthCode(connectorId, connectorData, getSession); log.append(userInfo); diff --git a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.test.ts b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.test.ts index 8c373077f..1e001a83a 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.test.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.test.ts @@ -199,7 +199,11 @@ describe('identifier verification', () => { const result = await identifierPayloadVerification(ctx, createMockProvider()); - expect(verifySocialIdentity).toBeCalledWith(identifier, logContext.createLog); + expect(verifySocialIdentity).toBeCalledWith( + identifier, + logContext.createLog, + expect.anything() + ); expect(findUserByIdentifier).not.toBeCalled(); expect(result).toEqual({ diff --git a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts index 1c8224778..556b8dd7d 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts @@ -1,8 +1,10 @@ +import type { GetSession } from '@logto/connector-kit'; import type { Event, SocialConnectorPayload, SocialIdentityPayload } from '@logto/schemas'; import type { Provider } from 'oidc-provider'; import RequestError from '#src/errors/RequestError/index.js'; import { verifyUserPassword } from '#src/libraries/user.js'; +import { getConnectorSessionResult } from '#src/routes/session/utils.js'; import assertThat from '#src/utils/assert-that.js'; import type { @@ -55,9 +57,10 @@ const verifyPasscodeIdentifier = async ( const verifySocialIdentifier = async ( identifier: SocialConnectorPayload, - ctx: InteractionContext + ctx: InteractionContext, + getSession?: GetSession ): Promise => { - const userInfo = await verifySocialIdentity(identifier, ctx.createLog); + const userInfo = await verifySocialIdentity(identifier, ctx.createLog, getSession); return { key: 'social', connectorId: identifier.connectorId, userInfo }; }; @@ -103,7 +106,9 @@ const verifyIdentifierPayload = async ( } if (isSocialIdentifier(identifier)) { - return verifySocialIdentifier(identifier, ctx); + return verifySocialIdentifier(identifier, ctx, async () => + getConnectorSessionResult(ctx, provider) + ); } // Sign-In with social verified email or phone diff --git a/packages/core/src/routes/session/social.ts b/packages/core/src/routes/session/social.ts index d23efb01a..e13249585 100644 --- a/packages/core/src/routes/session/social.ts +++ b/packages/core/src/routes/session/social.ts @@ -1,3 +1,4 @@ +import type { ConnectorSession } from '@logto/connector-kit'; import { validateRedirectUrl } from '@logto/core-kit'; import { ConnectorType, userInfoSelectFields } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; @@ -29,7 +30,12 @@ import assertThat from '#src/utils/assert-that.js'; import { maskUserInfo } from '#src/utils/format.js'; import type { AnonymousRouterLegacy } from '../types.js'; -import { checkRequiredProfile, getRoutePrefix } from './utils.js'; +import { + checkRequiredProfile, + getRoutePrefix, + assignConnectorSessionResult, + getConnectorSessionResult, +} from './utils.js'; export const registerRoute = getRoutePrefix('register', 'social'); export const signInRoute = getRoutePrefix('sign-in', 'social'); @@ -53,7 +59,11 @@ export default function socialRoutes( assertThat(state && redirectUri, 'session.insufficient_info'); const connector = await getLogtoConnectorById(connectorId); assertThat(connector.type === ConnectorType.Social, 'connector.unexpected_type'); - const redirectTo = await connector.getAuthorizationUri({ state, redirectUri }); + const redirectTo = await connector.getAuthorizationUri( + { state, redirectUri }, + async (connectorStorage: ConnectorSession) => + assignConnectorSessionResult(ctx, provider, connectorStorage) + ); ctx.body = { redirectTo }; return next(); @@ -79,7 +89,9 @@ export default function socialRoutes( dbEntry: { syncProfile }, } = await getLogtoConnectorById(connectorId); - const userInfo = await getUserInfoByAuthCode(connectorId, data); + const userInfo = await getUserInfoByAuthCode(connectorId, data, async () => + getConnectorSessionResult(ctx, provider) + ); ctx.log(type, { userInfo }); const user = await findUserByIdentity(target, userInfo.id); diff --git a/packages/core/src/routes/session/utils.ts b/packages/core/src/routes/session/utils.ts index 1d70e61fa..766fb3378 100644 --- a/packages/core/src/routes/session/utils.ts +++ b/packages/core/src/routes/session/utils.ts @@ -1,3 +1,5 @@ +import type { ConnectorSession } from '@logto/connector-kit'; +import { connectorSessionGuard } from '@logto/connector-kit'; import type { PasscodeType, SignInExperience, User } from '@logto/schemas'; import { SignInIdentifier } from '@logto/schemas'; import type { LogPayload, LogType } from '@logto/schemas/lib/types/log-legacy.js'; @@ -155,6 +157,35 @@ export const getContinueSignInResult = async ( return rest; }; +export const assignConnectorSessionResult = async ( + ctx: Context, + provider: Provider, + connectorSession: ConnectorSession +) => { + const details = await provider.interactionDetails(ctx.req, ctx.res); + await provider.interactionResult(ctx.req, ctx.res, { + ...details.result, + connectorSession, + }); +}; + +export const getConnectorSessionResult = async ( + ctx: Context, + provider: Provider +): Promise => { + const { result } = await provider.interactionDetails(ctx.req, ctx.res); + + const signInResult = z + .object({ + connectorSession: connectorSessionGuard, + }) + .safeParse(result); + + assertThat(signInResult.success, 'session.connector_validation_session_not_found'); + + return signInResult.data.connectorSession; +}; + export const isUserPasswordSet = ({ passwordEncrypted, identities, diff --git a/packages/phrases/src/locales/de/errors.ts b/packages/phrases/src/locales/de/errors.ts index 58a86a380..aec0d67c6 100644 --- a/packages/phrases/src/locales/de/errors.ts +++ b/packages/phrases/src/locales/de/errors.ts @@ -85,6 +85,8 @@ const errors = { forgot_password_not_enabled: 'Forgot password is not enabled.', verification_failed: 'Die Verifizierung war nicht erfolgreich. Starte die Verifizierung neu und versuche es erneut.', + connector_validation_session_not_found: + 'The connector session for token validation is not found.', // UNTRANSLATED }, connector: { // UNTRANSLATED diff --git a/packages/phrases/src/locales/en/errors.ts b/packages/phrases/src/locales/en/errors.ts index 2d43ed811..dd3646655 100644 --- a/packages/phrases/src/locales/en/errors.ts +++ b/packages/phrases/src/locales/en/errors.ts @@ -85,6 +85,8 @@ const errors = { forgot_password_not_enabled: 'Forgot password is not enabled.', verification_failed: 'The verification was not successful. Restart the verification flow and try again.', + connector_validation_session_not_found: + 'The connector session for token validation is not found.', }, connector: { general: 'An unexpected error occurred in connector.{{errorDescription}}', diff --git a/packages/phrases/src/locales/fr/errors.ts b/packages/phrases/src/locales/fr/errors.ts index 5ea209162..5b8300922 100644 --- a/packages/phrases/src/locales/fr/errors.ts +++ b/packages/phrases/src/locales/fr/errors.ts @@ -90,6 +90,8 @@ const errors = { forgot_password_not_enabled: 'Forgot password is not enabled.', // UNTRANSLATED verification_failed: 'The verification was not successful. Restart the verification flow and try again.', // UNTRANSLATED + connector_validation_session_not_found: + 'The connector session for token validation is not found.', // UNTRANSLATED }, connector: { general: "Une erreur inattendue s'est produite dans le connecteur. {{errorDescription}}", diff --git a/packages/phrases/src/locales/ko/errors.ts b/packages/phrases/src/locales/ko/errors.ts index b4cb1c7e9..8208fc2ff 100644 --- a/packages/phrases/src/locales/ko/errors.ts +++ b/packages/phrases/src/locales/ko/errors.ts @@ -84,6 +84,8 @@ const errors = { forgot_password_not_enabled: 'Forgot password is not enabled.', // UNTRANSLATED verification_failed: 'The verification was not successful. Restart the verification flow and try again.', // UNTRANSLATED + connector_validation_session_not_found: + 'The connector session for token validation is not found.', // UNTRANSLATED }, connector: { general: '연동 중에 알 수 없는 오류가 발생했어요. {{errorDescription}}', diff --git a/packages/phrases/src/locales/pt-br/errors.ts b/packages/phrases/src/locales/pt-br/errors.ts index 49c7ed3c8..d34e41190 100644 --- a/packages/phrases/src/locales/pt-br/errors.ts +++ b/packages/phrases/src/locales/pt-br/errors.ts @@ -86,6 +86,8 @@ const errors = { forgot_password_not_enabled: 'Esqueceu a senha não está ativado.', verification_failed: 'A verificação não foi bem-sucedida. Reinicie o fluxo de verificação e tente novamente.', + connector_validation_session_not_found: + 'The connector session for token validation is not found.', // UNTRANSLATED }, connector: { general: 'Ocorreu um erro inesperado no conector.{{errorDescription}}', diff --git a/packages/phrases/src/locales/pt-pt/errors.ts b/packages/phrases/src/locales/pt-pt/errors.ts index 0217c40bd..f353ddac7 100644 --- a/packages/phrases/src/locales/pt-pt/errors.ts +++ b/packages/phrases/src/locales/pt-pt/errors.ts @@ -86,6 +86,8 @@ const errors = { forgot_password_not_enabled: 'Forgot password is not enabled.', // UNTRANSLATED verification_failed: 'The verification was not successful. Restart the verification flow and try again.', // UNTRANSLATED + connector_validation_session_not_found: + 'The connector session for token validation is not found.', // UNTRANSLATED }, connector: { general: 'Ocorreu um erro inesperado no conector.{{errorDescription}}', diff --git a/packages/phrases/src/locales/tr-tr/errors.ts b/packages/phrases/src/locales/tr-tr/errors.ts index 68e535920..b34755db8 100644 --- a/packages/phrases/src/locales/tr-tr/errors.ts +++ b/packages/phrases/src/locales/tr-tr/errors.ts @@ -86,6 +86,8 @@ const errors = { forgot_password_not_enabled: 'Forgot password is not enabled.', // UNTRANSLATED verification_failed: 'The verification was not successful. Restart the verification flow and try again.', // UNTRANSLATED + connector_validation_session_not_found: + 'The connector session for token validation is not found.', // UNTRANSLATED }, connector: { general: 'Bağlayıcıda beklenmeyen bir hata oldu.{{errorDescription}}', diff --git a/packages/phrases/src/locales/zh-cn/errors.ts b/packages/phrases/src/locales/zh-cn/errors.ts index c443693e7..169145ad2 100644 --- a/packages/phrases/src/locales/zh-cn/errors.ts +++ b/packages/phrases/src/locales/zh-cn/errors.ts @@ -79,6 +79,7 @@ const errors = { unsupported_prompt_name: '不支持的 prompt name', forgot_password_not_enabled: '忘记密码功能没有开启。', verification_failed: '验证失败,请重新验证。', + connector_validation_session_not_found: '找不到连接器用于验证 token 的信息。', }, connector: { general: '连接器发生未知错误{{errorDescription}}', diff --git a/packages/toolkit/connector-kit/src/types.ts b/packages/toolkit/connector-kit/src/types.ts index 96cf0a49e..3eea349fa 100644 --- a/packages/toolkit/connector-kit/src/types.ts +++ b/packages/toolkit/connector-kit/src/types.ts @@ -104,6 +104,17 @@ export type ConnectorMetadata = z.infer; export type ConfigurableConnectorMetadata = z.infer; +export const connectorSessionGuard = z.object({ + nonce: z.string().optional(), + redirectUri: z.string().optional(), +}); + +export type ConnectorSession = z.infer; + +export type GetSession = () => Promise; + +export type SetSession = (storage: ConnectorSession) => Promise; + export type BaseConnector = { type: Type; metadata: ConnectorMetadata; @@ -138,11 +149,15 @@ export type SocialConnector = BaseConnector & { getUserInfo: GetUserInfo; }; -export type GetAuthorizationUri = (payload: { - state: string; - redirectUri: string; -}) => Promise; +export type GetAuthorizationUri = ( + payload: { + state: string; + redirectUri: string; + }, + setSession?: SetSession +) => Promise; export type GetUserInfo = ( - data: unknown + data: unknown, + getSession?: GetSession ) => Promise<{ id: string } & Record>;