Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): refactor identifyUser method #6154

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InteractionEvent, type VerificationType } from '@logto/schemas';
import { type ToZodObject } from '@logto/connector-kit';
import { InteractionEvent, VerificationType } from '@logto/schemas';
import { z } from 'zod';

import RequestError from '#src/errors/RequestError/index.js';
Expand All @@ -8,18 +9,27 @@

import type { Interaction } from '../types.js';

import { validateSieVerificationMethod } from './utils.js';
import {
buildVerificationRecord,
verificationRecordDataGuard,
type VerificationRecord,
type VerificationRecordData,
} from './verifications/index.js';

type InteractionStorage = {
interactionEvent?: InteractionEvent;
userId?: string;
profile?: Record<string, unknown>;
verificationRecords?: VerificationRecordData[];
};

const interactionStorageGuard = z.object({
event: z.nativeEnum(InteractionEvent).optional(),
accountId: z.string().optional(),
interactionEvent: z.nativeEnum(InteractionEvent).optional(),
userId: z.string().optional(),
profile: z.object({}).optional(),
verificationRecords: verificationRecordDataGuard.array().optional(),
});
}) satisfies ToZodObject<InteractionStorage>;

/**
* Interaction is a short-lived session session that is initiated when a user starts an interaction flow with the Logto platform.
Expand All @@ -41,10 +51,10 @@
private interactionEvent?: InteractionEvent;
/** The user verification record list for the current interaction. */
private readonly verificationRecords: Map<VerificationType, VerificationRecord>;
/** The accountId of the user for the current interaction. Only available once the user is identified. */
private accountId?: string;
/** The userId of the user for the current interaction. Only available once the user is identified. */
private userId?: string;
/** The user provided profile data in the current interaction that needs to be stored to database. */
private readonly profile?: Record<string, unknown>; // TODO: Fix the type

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

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

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

[no-warning-comments] Unexpected 'todo' comment: 'TODO: Fix the type'.

constructor(
private readonly ctx: WithLogContext,
Expand All @@ -60,10 +70,10 @@
new RequestError({ code: 'session.interaction_not_found', status: 404 })
);

const { verificationRecords = [], profile, accountId, event } = result.data;
const { verificationRecords = [], profile, userId, interactionEvent } = result.data;

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L73 was not covered by tests

this.interactionEvent = event;
this.accountId = accountId; // TODO: @simeng-li replace with userId
this.interactionEvent = interactionEvent;
this.userId = userId; // TODO: @simeng-li replace with userId

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L75-L76

Added lines #L75 - L76 were not covered by tests

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

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

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

[no-warning-comments] Unexpected 'todo' comment: 'TODO: @simeng-li replace with userId'.
this.profile = profile;

this.verificationRecords = new Map();
Expand All @@ -75,40 +85,63 @@
}

/** Set the interaction event for the current interaction */
public setInteractionEvent(event: InteractionEvent) {
public setInteractionEvent(interactionEvent: InteractionEvent) {
// TODO: conflict event check (e.g. reset password session can't be used for sign in)

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

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

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

[no-warning-comments] Unexpected 'todo' comment: 'TODO: conflict event check (e.g. reset...'.
this.interactionEvent = event;
this.interactionEvent = interactionEvent;

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L90 was not covered by tests
}

/** Set the verified `accountId` of the current interaction from the verification record */
public identifyUser(verificationId: string) {
/**
* Identify the user using the verification record.
*
* - Check if the verification record exists.
* - Check if the verification record is valid for the current interaction event.
* - Create a new user using the verification record if the current interaction event is `Register`.
* - Identify the user using the verification record if the current interaction event is `SignIn` or `ForgotPassword`.
* - Set the user id to the current interaction.
*
* @throws RequestError with 404 if the verification record is not found
* @throws RequestError with 404 if the interaction event is not set
* @throws RequestError with 400 if the verification record is not valid for the current interaction event
* @throws RequestError with 401 if the user is suspended
* @throws RequestError with 409 if the current session has already identified a different user
**/
public async identifyUser(verificationId: string) {
const verificationRecord = this.getVerificationRecordById(verificationId);

assertThat(
verificationRecord,
verificationRecord && this.interactionEvent,

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L112 was not covered by tests
new RequestError({ code: 'session.verification_session_not_found', status: 404 })
);

// 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.
assertThat(
verificationRecord.verifiedUserId,
new RequestError({
code: 'user.user_not_exist',
status: 404,
})
);
// Existing user identification flow
validateSieVerificationMethod(this.interactionEvent, verificationRecord);

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L116-L117

Added lines #L116 - L117 were not covered by tests

// Throws an 409 error if the current session has already identified a different user
if (this.accountId) {
assertThat(
this.accountId === verificationRecord.verifiedUserId,
new RequestError({ code: 'session.identity_conflict', status: 409 })
);
// User creation flow
if (this.interactionEvent === InteractionEvent.Register) {
this.createNewUser(verificationRecord);

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L119-L121

Added lines #L119 - L121 were not covered by tests
return;
}

this.accountId = verificationRecord.verifiedUserId;
switch (verificationRecord.type) {
case VerificationType.Password:
case VerificationType.VerificationCode:
case VerificationType.Social: {
const { id, isSuspended } = await verificationRecord.identifyUser();
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));

// Throws an 409 error if the current session has already identified a different user
if (this.userId) {
assertThat(
this.userId === id,
new RequestError({ code: 'session.identity_conflict', status: 409 })
);
return;
}

this.userId = id;
break;
}
}

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L125-L144

Added lines #L125 - L144 were not covered by tests
}

/**
Expand Down Expand Up @@ -144,29 +177,35 @@

/** Submit the current interaction result to the OIDC provider and clear the interaction data */
public async submit() {
// TODO: refine the error code

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

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

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

[no-warning-comments] Unexpected 'todo' comment: 'TODO: refine the error code'.
assertThat(this.accountId, 'session.verification_session_not_found');
assertThat(this.userId, 'session.verification_session_not_found');

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L181 was not covered by tests

const { provider } = this.tenant;

const redirectTo = await provider.interactionResult(this.ctx.req, this.ctx.res, {
login: { accountId: this.accountId },
login: { accountId: this.userId },

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L186 was not covered by tests
});

this.ctx.body = { redirectTo };
}

private get verificationRecordsArray() {
return [...this.verificationRecords.values()];
}

/** Convert the current interaction to JSON, so that it can be stored as the OIDC provider interaction result */
public toJson() {
public toJson(): InteractionStorage {
const { interactionEvent, userId, profile } = this;

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L194-L195

Added lines #L194 - L195 were not covered by tests
return {
event: this.interactionEvent,
accountId: this.accountId,
profile: this.profile,
interactionEvent,
userId,
profile,

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L197-L199

Added lines #L197 - L199 were not covered by tests
verificationRecords: this.verificationRecordsArray.map((record) => record.toJson()),
};
}

private get verificationRecordsArray() {
return [...this.verificationRecords.values()];
}

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L205-L206

Added lines #L205 - L206 were not covered by tests

private createNewUser(verificationRecord: VerificationRecord) {
// TODO: create new user for the Register event

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

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

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

[no-warning-comments] Unexpected 'todo' comment: 'TODO: create new user for the Register...'.
}

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/experience-interaction.ts#L209-L210

Added lines #L209 - L210 were not covered by tests
}
62 changes: 62 additions & 0 deletions packages/core/src/routes/experience/classes/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
InteractionEvent,
InteractionIdentifierType,
VerificationType,
type InteractionIdentifier,
} from '@logto/schemas';

import RequestError from '#src/errors/RequestError/index.js';
import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';

import { type VerificationRecord } from './verifications/index.js';

export const findUserByIdentifier = async (
userQuery: Queries['users'],
{ type, value }: InteractionIdentifier
) => {
switch (type) {
case InteractionIdentifierType.Username: {
return userQuery.findUserByUsername(value);
}
case InteractionIdentifierType.Email: {
return userQuery.findUserByEmail(value);
}
case InteractionIdentifierType.Phone: {
return userQuery.findUserByPhone(value);
}
}
};

Check warning on line 29 in packages/core/src/routes/experience/classes/utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/utils.ts#L15-L29

Added lines #L15 - L29 were not covered by tests

/**
* Check if the verification record is valid for the current interaction event.
*
* This function will compare the verification record for the current interaction event with Logto's SIE settings
*
* @throws RequestError with 400 if the verification record is not valid for the current interaction event
*/
export const validateSieVerificationMethod = (
interactionEvent: InteractionEvent,
verificationRecord: VerificationRecord
) => {
switch (interactionEvent) {
case InteractionEvent.SignIn: {
// TODO: sign-in methods validation

Check warning on line 44 in packages/core/src/routes/experience/classes/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/core/src/routes/experience/classes/utils.ts#L44

[no-warning-comments] Unexpected 'todo' comment: 'TODO: sign-in methods validation'.
break;
}
case InteractionEvent.Register: {
// TODO: sign-up methods validation

Check warning on line 48 in packages/core/src/routes/experience/classes/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/core/src/routes/experience/classes/utils.ts#L48

[no-warning-comments] Unexpected 'todo' comment: 'TODO: sign-up methods validation'.
break;
}
case InteractionEvent.ForgotPassword: {
// Forgot password only supports verification code type verification record
// The verification record's interaction event must be ForgotPassword
assertThat(
verificationRecord.type === VerificationType.VerificationCode &&
verificationRecord.interactionEvent === InteractionEvent.ForgotPassword,
new RequestError({ code: 'session.verification_session_not_found', status: 400 })
);
break;
}
}
};

Check warning on line 62 in packages/core/src/routes/experience/classes/utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/utils.ts#L39-L62

Added lines #L39 - L62 were not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
InteractionEvent,
VerificationType,
verificationCodeIdentifierGuard,
type User,
type VerificationCodeIdentifier,
} from '@logto/schemas';
import { type ToZodObject } from '@logto/schemas/lib/utils/zod.js';
import { generateStandardId } from '@logto/shared';
import { z } from 'zod';

import RequestError from '#src/errors/RequestError/index.js';
import { type createPasscodeLibrary } from '#src/libraries/passcode.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';

import { findUserByIdentifier } from '../../utils.js';
import { findUserByIdentifier } from '../utils.js';

import { type VerificationRecord } from './verification-record.js';

Expand All @@ -39,7 +41,6 @@
type: VerificationType.VerificationCode;
identifier: VerificationCodeIdentifier;
interactionEvent: InteractionEvent;
userId?: string;
verified: boolean;
};

Expand All @@ -48,7 +49,6 @@
type: z.literal(VerificationType.VerificationCode),
identifier: verificationCodeIdentifierGuard,
interactionEvent: z.nativeEnum(InteractionEvent),
userId: z.string().optional(),
verified: z.boolean(),
}) satisfies ToZodObject<CodeVerificationRecordData>;

Expand Down Expand Up @@ -102,22 +102,19 @@
* @remark
* `InteractionEvent.ForgotPassword` triggered verification results can not used as a verification record for other events.
*/
private readonly interactionEvent: InteractionEvent;
/** The userId will be set after the verification if the identifier matches any existing user's record */
private userId?: string;
public readonly interactionEvent: InteractionEvent;
private verified: boolean;

constructor(
private readonly libraries: Libraries,
private readonly queries: Queries,
data: CodeVerificationRecordData
) {
const { id, identifier, userId, verified, interactionEvent } = data;
const { id, identifier, verified, interactionEvent } = data;

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/code-verification.ts#L113

Added line #L113 was not covered by tests

this.id = id;
this.identifier = identifier;
this.interactionEvent = interactionEvent;
this.userId = userId;
this.verified = verified;
}

Expand All @@ -126,14 +123,6 @@
return this.verified;
}

/**
* Returns the userId if it is set
* @deprecated this will be removed in the upcoming PR
*/
get verifiedUserId() {
return this.userId;
}

/**
* Verify the `identifier` with the given code
*
Expand All @@ -157,20 +146,42 @@
);

this.verified = true;
}

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/code-verification.ts#L149

Added line #L149 was not covered by tests

/**
* Identify the user by the current `identifier`.
* Return undefined if the verification record is not verified or no user is found by the identifier.
*/
async identifyUser(): Promise<User> {
assertThat(
this.verified,
new RequestError({ code: 'session.verification_failed', status: 400 })
);

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/code-verification.ts#L156-L159

Added lines #L156 - L159 were not covered by tests

// Try to lookup the user by the identifier
const user = await findUserByIdentifier(this.queries.users, this.identifier);
this.userId = user?.id;

assertThat(
user,
new RequestError(
{ code: 'user.user_not_exist', status: 404 },
{
identifier: this.identifier.value,
}
)
);

return user;

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/code-verification.ts#L162-L173

Added lines #L162 - L173 were not covered by tests
}

toJson(): CodeVerificationRecordData {
const { id, type, identifier, interactionEvent, verified } = this;

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/code-verification.ts#L177-L178

Added lines #L177 - L178 were not covered by tests
return {
id: this.id,
type: this.type,
identifier: this.identifier,
interactionEvent: this.interactionEvent,
userId: this.userId,
verified: this.verified,
id,
type,
identifier,
interactionEvent,
verified,

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

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/experience/classes/verifications/code-verification.ts#L180-L184

Added lines #L180 - L184 were not covered by tests
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
type SocialVerificationRecordData,
} from './social-verification.js';

type VerificationRecordData =
export type VerificationRecordData =
| PasswordVerificationRecordData
| CodeVerificationRecordData
| SocialVerificationRecordData;
Expand Down
Loading
Loading