Skip to content

Commit

Permalink
feat(core,schemas): implement social verification
Browse files Browse the repository at this point in the history
implement social verification
  • Loading branch information
simeng-li committed Jul 1, 2024
1 parent 0de8cd5 commit f1c9cee
Show file tree
Hide file tree
Showing 13 changed files with 627 additions and 7 deletions.
18 changes: 15 additions & 3 deletions packages/connectors/connector-mock-social/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { randomUUID } from 'node:crypto';
import { z } from 'zod';

import type {
CreateConnector,
GetAuthorizationUri,
GetUserInfo,
CreateConnector,
SocialConnector,
} from '@logto/connector-kit';
import {
Expand All @@ -17,11 +17,16 @@ import {
import { defaultMetadata } from './constant.js';
import { mockSocialConfigGuard } from './types.js';

const getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
const getAuthorizationUri: GetAuthorizationUri = async (
{ state, redirectUri, connectorId },
setSession
) => {
await setSession({ state, redirectUri, connectorId });

return `http://mock.social.com/?state=${state}&redirect_uri=${redirectUri}`;
};

const getUserInfo: GetUserInfo = async (data) => {
const getUserInfo: GetUserInfo = async (data, getSession) => {
const dataGuard = z.object({
code: z.string(),
userId: z.optional(z.string()),
Expand All @@ -34,6 +39,13 @@ const getUserInfo: GetUserInfo = async (data) => {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(data));
}

const connectorSession = await getSession();

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!connectorSession) {
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed);
}

const { code, userId, ...rest } = result.data;

// For mock use only. Use to track the created user entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default class InteractionSession {
/** Set the verified accountId of the current interaction session from the verification record */
public identifyUser(verificationRecord: VerificationRecord) {
// Throws an 404 error if the user is not found by the given verification record
// TODO: refactor using real-time user verification. Static verifiedUserId will be removed.

Check warning on line 86 in packages/core/src/routes/experience/classes/interaction-session.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/interaction-session.ts#L86

Added line #L86 was not covered by tests
assertThat(
verificationRecord.verifiedUserId,
new RequestError(
Expand All @@ -91,7 +92,8 @@ export default class InteractionSession {
status: 404,
},
{
identifier: verificationRecord.identifier.value,
identifier:
'identifier' in verificationRecord ? verificationRecord.identifier : undefined,

Check warning on line 96 in packages/core/src/routes/experience/classes/interaction-session.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/interaction-session.ts#L95-L96

Added lines #L95 - L96 were not covered by tests
}
)
);
Expand Down
16 changes: 14 additions & 2 deletions packages/core/src/routes/experience/classes/verifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@ import {
passwordVerificationRecordDataGuard,
type PasswordVerificationRecordData,
} from './password-verification.js';
import {
SocialVerification,
socialVerificationRecordDataGuard,
type SocialVerificationRecordData,
} from './social-verification.js';

type VerificationRecordData = PasswordVerificationRecordData | CodeVerificationRecordData;
type VerificationRecordData =
| PasswordVerificationRecordData
| CodeVerificationRecordData
| SocialVerificationRecordData;

export const verificationRecordDataGuard = z.discriminatedUnion('type', [
passwordVerificationRecordDataGuard,
codeVerificationRecordDataGuard,
socialVerificationRecordDataGuard,
]);

export type VerificationRecord = PasswordVerification | CodeVerification;
export type VerificationRecord = PasswordVerification | CodeVerification | SocialVerification;

export const buildVerificationRecord = (
libraries: Libraries,
Expand All @@ -36,5 +45,8 @@ export const buildVerificationRecord = (
case VerificationType.VerificationCode: {
return new CodeVerification(libraries, queries, data);
}
case VerificationType.Social: {
return new SocialVerification(libraries, queries, data);
}

Check warning on line 50 in packages/core/src/routes/experience/classes/verifications/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/index.ts#L48-L50

Added lines #L48 - L50 were not covered by tests
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { socialUserInfoGuard, type SocialUserInfo, type ToZodObject } from '@logto/connector-kit';
import {
VerificationType,
type JsonObject,
type SocialAuthorizationUrlPayload,
type User,
} from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { z } from 'zod';

import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
import {
createSocialAuthorizationUrl,
verifySocialIdentity,
} from '#src/routes/interaction/utils/social-verification.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
import type TenantContext from '#src/tenants/TenantContext.js';

import { type Verification } from './verification.js';

/** The JSON data type for the SocialVerification record stored in the interaction storage */
export type SocialVerificationRecordData = {
id: string;
connectorId: string;
type: VerificationType.Social;
/**
* The social identity returned by the connector.
*/
socialUserInfo?: SocialUserInfo;
/**
* The userId of the user that has been verified by the social identity.
*/
userId?: string;
};

export const socialVerificationRecordDataGuard = z.object({
id: z.string(),
connectorId: z.string(),
type: z.literal(VerificationType.Social),
socialUserInfo: socialUserInfoGuard.optional(),
userId: z.string().optional(),
}) satisfies ToZodObject<SocialVerificationRecordData>;

export class SocialVerification implements Verification {
/**
* Factory method to create a new SocialVerification instance
*/
static create(libraries: Libraries, queries: Queries, connectorId: string) {
return new SocialVerification(libraries, queries, {
id: generateStandardId(),
connectorId,
type: VerificationType.Social,
});
}

Check warning on line 55 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L50-L55

Added lines #L50 - L55 were not covered by tests

public readonly id: string;
public readonly type = VerificationType.Social;
public readonly connectorId: string;
public socialUserInfo?: SocialUserInfo;
public userId?: string;

constructor(
private readonly libraries: Libraries,
private readonly queries: Queries,
data: SocialVerificationRecordData
) {
const { id, connectorId, socialUserInfo, userId } =
socialVerificationRecordDataGuard.parse(data);

this.id = id;
this.connectorId = connectorId;
this.socialUserInfo = socialUserInfo;
this.userId = userId;
}

Check warning on line 75 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L64-L75

Added lines #L64 - L75 were not covered by tests

/**
* Returns true if the social identity has been verified
*/
get isVerified() {
return Boolean(this.socialUserInfo);
}

Check warning on line 82 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L81-L82

Added lines #L81 - L82 were not covered by tests

get verifiedUserId() {
return this.userId;
}

Check warning on line 86 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L85-L86

Added lines #L85 - L86 were not covered by tests

/**
* Create the authorization URL for the social connector.
* Store the connector session result in the provider's interaction storage.
*
* @remarks
* Refers to the {@link createSocialAuthorizationUrl} method in the interaction/utils/social-verification.ts file.
* Currently, all the intermediate connector session results are stored in the provider's interactionDetails separately,
* apart from the new verification record.
* For compatibility reasons, we keep using the old {@link createSocialAuthorizationUrl} method here as a single source of truth.
* Especially for the SAML connectors,
* SAML ACS endpoint will find the connector session result by the jti and assign it to the interaction storage.
* We will need to update the SAML ACS endpoint before move the logic to this new SocialVerification class.
*
* TODO: Consider store the connector session result in the verification record directly.
* SAML ACS endpoint will find the verification record by the jti and assign the connector session result to the verification record.
*/
async createAuthorizationUrl(
ctx: WithLogContext,
tenantContext: TenantContext,
{ state, redirectUri }: SocialAuthorizationUrlPayload
) {
return createSocialAuthorizationUrl(ctx, tenantContext, {
connectorId: this.connectorId,
state,
redirectUri,
});
}

Check warning on line 114 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L105-L114

Added lines #L105 - L114 were not covered by tests

/**
* Verify the social identity and store the social identity in the verification record.
*
* - Store the social identity in the verification record.
* - Find the user by the social identity and store the userId in the verification record if the user exists.
*
* @remarks
* Refer to the {@link verifySocialIdentity} method in the interaction/utils/social-verification.ts file.
* For compatibility reasons, we keep using the old {@link verifySocialIdentity} method here as a single source of truth.
* See the above {@link createAuthorizationUrl} method for more details.
*
* TODO: check the log event
*/
async verify(ctx: WithLogContext, tenantContext: TenantContext, connectorData: JsonObject) {
const socialUserInfo = await verifySocialIdentity(
{ connectorId: this.connectorId, connectorData },
ctx,
tenantContext
);

this.socialUserInfo = socialUserInfo;

const user = await this.findUserBySocialIdentity();
this.userId = user?.id;
}

Check warning on line 140 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L130-L140

Added lines #L130 - L140 were not covered by tests

async findUserBySocialIdentity(): Promise<User | undefined> {
const { socials } = this.libraries;
const {
users: { findUserByIdentity },
} = this.queries;

if (!this.socialUserInfo) {
return;
}

const {
metadata: { target },
} = await socials.getConnector(this.connectorId);

const user = await findUserByIdentity(target, this.socialUserInfo.id);

return user ?? undefined;
}

Check warning on line 159 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L143-L159

Added lines #L143 - L159 were not covered by tests

async findRelatedUserBySocialIdentity(): ReturnType<typeof socials.findSocialRelatedUser> {
const { socials } = this.libraries;

if (!this.socialUserInfo) {
return null;
}

return socials.findSocialRelatedUser(this.socialUserInfo);
}

Check warning on line 169 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L162-L169

Added lines #L162 - L169 were not covered by tests

toJson(): SocialVerificationRecordData {
return {
id: this.id,
connectorId: this.connectorId,
type: this.type,
socialUserInfo: this.socialUserInfo,
};
}

Check warning on line 178 in packages/core/src/routes/experience/classes/verifications/social-verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/social-verification.ts#L172-L178

Added lines #L172 - L178 were not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export abstract class Verification {
abstract readonly type: VerificationType;

abstract get isVerified(): boolean;
/**
* @deprecated
* TODO: Remove this @simeng-li, should get the userId asynchronously in real-time
*/

Check warning on line 14 in packages/core/src/routes/experience/classes/verifications/verification.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/verification.ts#L11-L14

Added lines #L11 - L14 were not covered by tests
abstract get verifiedUserId(): string | undefined;

abstract toJson(): {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/routes/experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import koaInteractionSession, {
type WithInteractionSessionContext,
} from './middleware/koa-interaction-session.js';
import passwordVerificationRoutes from './verification-routes/password-verification.js';
import socialVerificationRoutes from './verification-routes/social-verification.js';
import verificationCodeRoutes from './verification-routes/verification-code.js';

type RouterContext<T> = T extends Router<unknown, infer Context> ? Context : never;
Expand Down Expand Up @@ -60,6 +61,10 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
new RequestError({ code: 'session.verification_session_not_found', status: 404 })
);

// TODO: SIE verification method check
// TODO: forgot password verification method check, only allow email and phone verification code
// TODO: user suspension check

Check warning on line 67 in packages/core/src/routes/experience/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/index.ts#L64-L67

Added lines #L64 - L67 were not covered by tests
ctx.interactionSession.identifyUser(verificationRecord);

await ctx.interactionSession.save();
Expand All @@ -84,4 +89,5 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(

passwordVerificationRoutes(router, tenant);
verificationCodeRoutes(router, tenant);
socialVerificationRoutes(router, tenant);
}
Loading

0 comments on commit f1c9cee

Please sign in to comment.