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:
parent
6a71448090
commit
d927f90a07
7 changed files with 63 additions and 19 deletions
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`. */
|
||||
|
|
Loading…
Reference in a new issue