Skip to content

Commit

Permalink
feat(core): support google one tap (#6395)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
simeng-li authored Aug 7, 2024
1 parent 6a71448 commit d927f90
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GoogleConnector } from '@logto/connector-kit';
import {
VerificationType,
socialAuthorizationUrlPayloadGuard,
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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', () => ({
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,15 @@ 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,
state,
});

await expectRejects(
client.verifySocialAuthorization('invalid_connector_id', {
client.verifySocialAuthorization(emailConnectorId, {
verificationId,
connectorData: {
authorizationCode,
Expand Down
6 changes: 3 additions & 3 deletions packages/schemas/src/types/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`. */
Expand Down

0 comments on commit d927f90

Please sign in to comment.