0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

chore: add social sign-in validation reference to interaction (#2658)

This commit is contained in:
Darcy Ye 2022-12-20 15:05:54 +08:00 committed by GitHub
parent 5dd2eef34c
commit ecb7f80826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 121 additions and 21 deletions

View file

@ -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<SocialUserInfo> => {
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 (

View file

@ -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<T extends AnonymousRouter>(
// 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 };

View file

@ -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' });
});
});

View file

@ -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<SocialUserInfo> => {
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);

View file

@ -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({

View file

@ -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<SocialIdentifier> => {
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

View file

@ -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<T extends AnonymousRouterLegacy>(
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<T extends AnonymousRouterLegacy>(
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);

View file

@ -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<ConnectorSession> => {
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,

View file

@ -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

View file

@ -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}}',

View file

@ -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}}",

View file

@ -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}}',

View file

@ -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}}',

View file

@ -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}}',

View file

@ -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}}',

View file

@ -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}}',

View file

@ -104,6 +104,17 @@ export type ConnectorMetadata = z.infer<typeof connectorMetadataGuard>;
export type ConfigurableConnectorMetadata = z.infer<typeof configurableConnectorMetadataGuard>;
export const connectorSessionGuard = z.object({
nonce: z.string().optional(),
redirectUri: z.string().optional(),
});
export type ConnectorSession = z.infer<typeof connectorSessionGuard>;
export type GetSession = () => Promise<ConnectorSession>;
export type SetSession = (storage: ConnectorSession) => Promise<void>;
export type BaseConnector<Type extends ConnectorType> = {
type: Type;
metadata: ConnectorMetadata;
@ -138,11 +149,15 @@ export type SocialConnector = BaseConnector<ConnectorType.Social> & {
getUserInfo: GetUserInfo;
};
export type GetAuthorizationUri = (payload: {
state: string;
redirectUri: string;
}) => Promise<string>;
export type GetAuthorizationUri = (
payload: {
state: string;
redirectUri: string;
},
setSession?: SetSession
) => Promise<string>;
export type GetUserInfo = (
data: unknown
data: unknown,
getSession?: GetSession
) => Promise<{ id: string } & Record<string, string | boolean | number | undefined>>;