0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(core): support google one tap (#6395)

* feat(core): support google one tap

support google one tap verification

* fix(core): fix google one tap verification error

fix google one tap verification error

* fix(test): optimize social verification test

optimize social verificaiton tests

* fix(test): update social verification ut

update social verification util unit test
This commit is contained in:
simeng-li 2024-08-07 18:36:52 +08:00 committed by GitHub
parent 6a71448090
commit d927f90a07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 63 additions and 19 deletions

View file

@ -139,8 +139,6 @@ export class SocialVerification implements IdentifierVerificationRecord<Verifica
new RequestError({ code: 'session.verification_failed', status: 400 })
);
// TODO: sync userInfo and link social identity
const user = await this.findUserBySocialIdentity();
if (!user) {

View file

@ -80,7 +80,11 @@ export default function enterpriseSsoVerificationRoutes<
params: z.object({
connectorId: z.string(),
}),
body: socialVerificationCallbackPayloadGuard,
body: socialVerificationCallbackPayloadGuard.merge(
z.object({
verificationId: z.string(),
})
),
response: z.object({
verificationId: z.string(),
}),

View file

@ -1,3 +1,4 @@
import { GoogleConnector } from '@logto/connector-kit';
import {
VerificationType,
socialAuthorizationUrlPayloadGuard,
@ -89,9 +90,12 @@ export default function socialVerificationRoutes<T extends ExperienceInteraction
action: Action.Submit,
}),
async (ctx, next) => {
const { connectorId } = ctx.params;
const { connectorId } = ctx.guard.params;
const { connectorData, verificationId } = ctx.guard.body;
const { verificationAuditLog } = ctx;
const {
socials: { getConnector },
} = libraries;
verificationAuditLog.append({
payload: {
@ -101,10 +105,36 @@ export default function socialVerificationRoutes<T extends ExperienceInteraction
},
});
const socialVerificationRecord = ctx.experienceInteraction.getVerificationRecordByTypeAndId(
VerificationType.Social,
verificationId
);
const connector = await getConnector(connectorId);
const socialVerificationRecord = (() => {
// Check if is Google one tap verification
if (
connector.metadata.id === GoogleConnector.factoryId &&
connectorData[GoogleConnector.oneTapParams.credential]
) {
const socialVerificationRecord = SocialVerification.create(
libraries,
queries,
connectorId
);
ctx.experienceInteraction.setVerificationRecord(socialVerificationRecord);
return socialVerificationRecord;
}
if (verificationId) {
return ctx.experienceInteraction.getVerificationRecordByTypeAndId(
VerificationType.Social,
verificationId
);
}
// No verificationId provided and not Google one tap callback
throw new RequestError({
code: 'session.verification_session_not_found',
status: 404,
});
})();
assertThat(
socialVerificationRecord.connectorId === connectorId,

View file

@ -1,6 +1,7 @@
import { ConnectorType, GoogleConnector } from '@logto/connector-kit';
import { createMockUtils } from '@logto/shared/esm';
import { mockConnector } from '#src/__mocks__/connector.js';
import type { WithLogContext } from '#src/middleware/koa-audit-log.js';
import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js';
import { createMockLogContext } from '#src/test-utils/koa-audit-log.js';
@ -10,9 +11,10 @@ const { jest } = import.meta;
const { mockEsm } = createMockUtils(jest);
const getUserInfo = jest.fn().mockResolvedValue({ id: 'foo' });
const getConnector = jest.fn().mockResolvedValue(mockConnector);
const tenant = new MockTenant(undefined, undefined, undefined, {
socials: { getUserInfo },
socials: { getUserInfo, getConnector },
});
mockEsm('#src/libraries/connector.js', () => ({
@ -49,12 +51,19 @@ describe('verifySocialIdentity', () => {
// @ts-expect-error test mock context
cookies: { get: jest.fn().mockReturnValue('token') },
};
const connectorId = GoogleConnector.factoryId;
getConnector.mockResolvedValueOnce({
...mockConnector,
metadata: {
...mockConnector.metadata,
id: GoogleConnector.factoryId,
},
});
const connectorData = { credential: 'credential' };
await expect(verifySocialIdentity({ connectorId, connectorData }, ctx, tenant)).rejects.toThrow(
'CSRF token mismatch.'
);
await expect(
verifySocialIdentity({ connectorId: 'google', connectorData }, ctx, tenant)
).rejects.toThrow('CSRF token mismatch.');
});
it('should verify Google One Tap verification', async () => {

View file

@ -57,16 +57,18 @@ export const verifySocialIdentity = async (
{ provider, libraries }: TenantContext
): Promise<SocialUserInfo> => {
const {
socials: { getUserInfo },
socials: { getUserInfo, getConnector },
} = libraries;
const log = ctx.createLog('Interaction.SignIn.Identifier.Social.Submit');
log.append({ connectorId, connectorData });
const connector = await getConnector(connectorId);
// Verify the CSRF token if it's a Google connector and has credential (a Google One Tap
// verification)
if (
connectorId === GoogleConnector.factoryId &&
connector.metadata.id === GoogleConnector.factoryId &&
connectorData[GoogleConnector.oneTapParams.credential]
) {
const csrfToken = connectorData[GoogleConnector.oneTapParams.csrfToken];

View file

@ -157,6 +157,7 @@ devFeatureTest.describe('social verification', () => {
it('should throw if the connectorId is different', async () => {
const client = await initExperienceClient();
const connectorId = connectorIdMap.get(mockSocialConnectorId)!;
const emailConnectorId = connectorIdMap.get(mockEmailConnectorId)!;
const { verificationId } = await client.getSocialAuthorizationUri(connectorId, {
redirectUri,
@ -164,7 +165,7 @@ devFeatureTest.describe('social verification', () => {
});
await expectRejects(
client.verifySocialAuthorization('invalid_connector_id', {
client.verifySocialAuthorization(emailConnectorId, {
verificationId,
connectorData: {
authorizationCode,

View file

@ -84,12 +84,12 @@ export const socialAuthorizationUrlPayloadGuard = z.object({
export type SocialVerificationCallbackPayload = {
/** The callback data from the social connector. */
connectorData: Record<string, unknown>;
/** The verification ID returned from the authorization URI. */
verificationId: string;
/** The verification ID returned from the authorization URI. Optional for Google one tap callback */
verificationId?: string;
};
export const socialVerificationCallbackPayloadGuard = z.object({
connectorData: jsonObjectGuard,
verificationId: z.string(),
verificationId: z.string().optional(),
}) satisfies ToZodObject<SocialVerificationCallbackPayload>;
/** Payload type for `POST /api/experience/verification/password`. */