0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

feat(core): add experience APIs openapi docs (#6436)

* feat(core): add experience APIs openapi docs

add experience APIs openapi docs

* fix(core): adjust the format

adjust the format

* chore: update experience API description

update experience API description

* fix(core): fix integration tests

fix integration tests

* chore(core): add devFeature tag in openapi doc

add devFeature tag in openapi doc

* fix(core): fix the integration test

remove the redundent path paramter def
This commit is contained in:
simeng-li 2024-08-15 10:05:52 +08:00 committed by GitHub
parent 976558af9c
commit db42279ed4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1083 additions and 34 deletions

View file

@ -368,7 +368,6 @@ export default class ExperienceInteraction {
/**
* Submit the current interaction result to the OIDC provider and clear the interaction data
*
* @throws {RequestError} with 404 if the interaction event is not set
* @throws {RequestError} with 404 if the user is not identified
* @throws {RequestError} with 403 if the mfa verification is required but not verified
* @throws {RequestError} with 422 if the profile data is conflicting with the current user account
@ -380,12 +379,6 @@ export default class ExperienceInteraction {
queries: { users: userQueries },
} = this.tenant;
// Initiated
assertThat(
this.interactionEvent,
new RequestError({ code: 'session.interaction_not_found', status: 404 })
);
// Identified
const user = await this.getIdentifiedUser();

View file

@ -95,7 +95,6 @@ export class NewPasswordIdentityVerification
* - Validate the password against the password policy
*
* @throws {RequestError} with status 422 if the identifier is in use by another user
* @throws {RequestError} with status 422 if the password is not provided
* @throws {RequestError} with status 422 if the password does not meet the password policy
*/
async verify(password: string) {
@ -103,11 +102,6 @@ export class NewPasswordIdentityVerification
const identifierProfile = interactionIdentifierToUserProfile(identifier);
await this.profileValidator.guardProfileUniquenessAcrossUsers(identifierProfile);
assertThat(
password,
new RequestError({ code: 'user.password_required_in_profile', status: 422 })
);
const passwordPolicy = await this.signInExperienceValidator.getPasswordPolicy();
const passwordValidator = new PasswordValidator(passwordPolicy);
await passwordValidator.validatePassword(password, identifierProfile);

View file

@ -0,0 +1,124 @@
{
"tags": [
{
"name": "Experience",
"description": "The Experience endpoints allow end-users to interact with Logto for identity verification and profile completion."
},
{ "name": "Dev feature" }
],
"paths": {
"/api/experience": {
"put": {
"tags": ["Dev feature"],
"summary": "Init a new experience interaction",
"description": "Init a new experience interaction with the given interaction type. Any existing experience interaction data will be cleared.",
"responses": {
"204": {
"description": "A new experience interaction has been successfully initiated."
}
}
}
},
"/api/experience/interaction-event": {
"put": {
"tags": ["Dev feature"],
"summary": "Update current experience interaction event",
"description": "Update the current experience interaction event to the given event type. This API is used to switch the interaction event between `SignIn` and `Register`, while keeping all the verification records data.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"interactionEvent": {
"description": "The type of the interaction event. Only `SignIn` and `Register` are supported."
}
}
}
}
}
},
"responses": {
"204": {
"description": "The interaction event has been successfully updated."
},
"400": {
"description": "The interaction event is invalid or cannot be updated. Only `SignIn` and `Register` are interchangeable. If the current interaction event is `ForgotPassword`, it cannot be updated."
},
"403": {
"description": "The given interaction event is not enabled in the sign-in experience settings."
}
}
}
},
"/api/experience/identification": {
"post": {
"tags": ["Dev feature"],
"summary": "Identify the user within the current experience interaction using the provided verification data",
"description": "This API identifies the user based on the verificationId within the current experience interaction: <br/>- `SignIn` and `ForgotPassword` interactions: Verifies the user's identity using the provided `verificationId`. <br/>- `Register` interaction: Creates a new user account using the profile data from the current interaction. If a verificationId is provided, the profile data will first be updated with the verification record before creating the account. If not, the account is created directly from the stored profile data.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The ID of the verification record used to identify the user. <br/>- `SignIn` and `ForgotPassword` interactions: Required to verify the user's identity. <br/>- `Register` interaction: Optional. If provided, it updates the profile data with the verification record before account creation. If omitted, the account is created using existing profile data in the current interaction."
},
"linkSocialIdentity": {
"description": "Applies to the SignIn interaction only, and is used when a SocialVerification type verificationId is provided. <br/>- If `true`, the user is identified using the verified email or phone number from the social identity provider, and the social identity is linked to the user's account. <br/>- If `false` or not provided, the API identifies the user solely through the social identity. <br/> This parameters is used for linking a non-existing social identity to a related user account that can be identified through the verified email or phone number."
}
}
}
}
}
},
"responses": {
"201": {
"description": "`Register` interaction: The user account has been successfully created and identified."
},
"204": {
"description": "`SignIn` and `ForgotPassword` interactions: The user has been successfully identified."
},
"400": {
"description": "The provided verificationId is invalid, not verified, or cannot be used to identify the user. <br/>- `session.verification_failed:` The verification is not verified or can not be used to identify the user. <br/>- `guard.invalid_target:` The `verificationId` is missing, but required for the `SignIn` and `ForgotPassword` interactions."
},
"401": {
"description": "The user is suspended or banned from the service. (SignIn and ForgotPassword only)"
},
"403": {
"description": "The `SignIn` or `Register` interaction is disabled in the experience settings."
},
"404": {
"description": "Entity not found. <br/>- `session.verification_session_not_found:` The verification record is not found. <br/>- `user.user_not_exist:` The user account is not found (SignIn and ForgotPassword only). "
},
"409": {
"description": "The interaction has already been identified with a different user account."
},
"422": {
"description": "The user account cannot be created due to validation errors, check error message for more details (Register only). <br/>- `user.<identifier>_already_in_use:` The given identifier is already in use by another user account. <br/>- `user.missing_profile:` Sign-in experience required user identifier or profile data is missing. (Register only)"
}
}
}
},
"/api/experience/submit": {
"post": {
"tags": ["Dev feature"],
"summary": "Submit experience interaction",
"description": "Submit the current interaction. <br/>- Submit the verified user identity to the OIDC provider for further authentication (SignIn and Register). <br/>- Update the user's profile data if any (SignIn and Register). <br/>- Reset the password and clear all the interaction records (ForgotPassword).",
"responses": {
"200": {
"description": "The interaction has been successfully submitted."
},
"403": {
"description": "Multi-Factor Authentication (MFA) is enabled for the user but has not been verified."
},
"404": {
"description": "The user has not been identified. "
},
"422": {
"description": "The user profile can not been processed, check error message for more details. <br/>- The profile data is invalid or conflicts with existing user data. <br/>- Required profile data is missing. <br/>- The profile data is already in use by another user account."
}
}
}
}
}
}

View file

@ -58,7 +58,7 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
body: z.object({
interactionEvent: z.nativeEnum(InteractionEvent),
}),
status: [204, 403],
status: [204],
}),
async (ctx, next) => {
const { interactionEvent } = ctx.guard.body;

View file

@ -0,0 +1,142 @@
{
"paths": {
"/api/experience/profile": {
"post": {
"tags": ["Dev feature"],
"summary": "Fulfill user profile data",
"description": "Adds user profile data to the current experience interaction. <br/>- For `Register`: The profile data provided before the identification request will be used to create a new user account. <br/>- For `SignIn` and `Register`: The profile data provided after the user is identified will be used to update the user's profile when the interaction is submitted. <br/>- `ForgotPassword`: Not supported.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"type": {
"description": "The type of profile data to add. `email`, `phone`, `username`, `password`, etc."
},
"value": {
"description": "The plain text value of the profile data. Only supported for profile data types that does not require verification, such as `username` and `password`."
},
"verificationId": {
"description": "The ID of the verification record used to verify the profile data. Required for profile data types that require verification, such as `email`, `phone` and `social`."
}
}
}
}
}
},
"responses": {
"204": {
"description": "The profile data has been successfully added to the current experience interaction."
},
"400": {
"description": "Invalid request. <br/> - `session.not_supported_for_forgot_password:` This API can not be used in the `ForgotPassword` interaction. <br/>- `session.verification_failed:` The verification record is not verified. "
},
"404": {
"description": "Entity not found. <br/> - `session.identifier_not_found:` (`SignIn` interaction only) The current interaction is not identified yet. All profile data must be associated with a identified user. <br/>- `session.verification_session_not_found:` The verification record is not found."
},
"403": {
"description": "`SignIn` interaction only: Multi-Factor Authentication (MFA) is enabled for the user but has not been verified. The user must verify the MFA before updating the profile data."
},
"422": {
"description": "The user profile can not been processed, check error message for more details. <br/>- The profile data is invalid or conflicts with existing user data. <br/>- The profile data is already in use by another user account."
}
}
}
},
"/api/experience/profile/password": {
"put": {
"tags": ["Dev feature"],
"summary": "Reset user password",
"description": "Update the user's password. (`ForgotPassword` interaction only)",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"password": {
"description": "The new password to update. The password must meet the password policy requirements and can not be the same as the current password."
}
}
}
}
}
},
"responses": {
"204": {
"description": "The password has been successfully updated."
},
"400": {
"description": "The current interaction event is not `ForgotPassword`. The password can only be updated through the `ForgotPassword` interaction."
},
"404": {
"description": "The user has not been identified yet. The user must be identified before updating the password."
},
"422": {
"description": "The password can not be updated due to validation errors, check error message for more details. <br/>- `user.password_policy_violation:` The password does not meet the password policy requirements. <br/>- `user.same_password:` The new password is the same as the current password."
}
}
}
},
"/api/experience/profile/mfa/mfa-skipped": {
"post": {
"tags": ["Dev feature"],
"summary": "Skip Multi-Factor Authentication (MFA) binding flow",
"description": "Skip MFA verification binding flow. If the MFA is enabled in the sign-in experience settings and marked as `UserControlled`, the user can skip the MFA verification binding flow by calling this API.",
"responses": {
"204": {
"description": "The MFA verification has been successfully skipped."
},
"400": {
"description": "Not supported for the current interaction event. The MFA profile API can only be used in the `SignIn` or `Register` interaction."
},
"403": {
"description": "Some MFA factors has already been enabled for the user. The user must verify the MFA before updating the MFA settings."
},
"404": {
"description": "The user has not been identified yet. The `mfa-skipped` configuration must be associated with a identified user."
},
"422": {
"description": "The MFA verification binding is `Mandatory`, user can not skip the MFA verification binding flow."
}
}
}
},
"/api/experience/profile/mfa": {
"post": {
"tags": ["Dev feature"],
"summary": "Bind Multi-Factor Authentication (MFA) verification by verificationId",
"description": "Bind new MFA verification to the user profile using the verificationId.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"type": {
"description": "The type of MFA."
},
"verificationId": {
"description": "The ID of the MFA verification record."
}
}
}
}
}
},
"responses": {
"204": {
"description": "The MFA verification has been successfully added to the user profile."
},
"400": {
"description": "Invalid request. <br/>- `session.verification_failed:` The MFA verification record is invalid or not verified. <br/>- `session.mfa.mfa_factor_not_enabled:` The MFA factor is not enabled in the sign-in experience settings. <br/>- `session.mfa.pending_info_not_found:` The MFA verification record does not have the required information to bind the MFA verification."
},
"404": {
"description": "Entity not found. <br/> - `session.identifier_not_found:` The user has not been identified yet. The MFA verification can only be added to a identified user. <br/>- `session.verification_session_not_found:` The MFA verification record is not found."
},
"422": {
"description": "The MFA verification can not been processed, check error message for more details. <br/>- `user.totp_already_in_use`: A TOTP MFA secret is already in use in the current user profile. <br/>- `session.mfa.backup_code_can_not_be_alone`: The backup code can not be the only MFA factor in the user profile."
}
}
}
}
}
}

View file

@ -0,0 +1,75 @@
{
"paths": {
"/api/experience/verification/backup-code/generate": {
"post": {
"tags": ["Dev feature"],
"summary": "Generate backup codes",
"description": "Generate backup codes for the current user. A new BackupCode verification record will be created in the current interaction.",
"responses": {
"200": {
"description": "Backup codes have been successfully generated.",
"content": {
"application/json": {
"schema": {
"properties": {
"codes": {
"description": "The generated backup codes."
},
"verificationId": {
"description": "The unique verification ID of the newly created BackupCode verification record. This ID is required when adding the backup codes to the user profile via the Profile API."
}
}
}
}
}
},
"404": {
"description": "The current interaction is not identified yet. All MFA verification records must be associated with a identified user."
}
}
}
},
"/api/experience/verification/backup-code/verify": {
"post": {
"tags": ["Dev feature"],
"summary": "Verify backup code",
"description": "Verify the provided backup code against the user's backup codes. A new BackupCode verification record will be created and marked as verified if the code is correct.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"code": {
"description": "The backup code to verify."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The backup code has been successfully verified.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The unique verification ID of the BackupCode verification record."
}
}
}
}
}
},
"404": {
"description": "Entity not found. <br/> - `session.identifier_not_found:` The current interaction is not identified yet. All MFA verification records must be associated with a identified user."
},
"400": {
"description": "The provided backup code is invalid."
}
}
}
}
}
}

View file

@ -3,6 +3,7 @@ import { Action } from '@logto/schemas/lib/types/log/interaction.js';
import type Router from 'koa-router';
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import assertThat from '#src/utils/assert-that.js';
@ -34,7 +35,10 @@ export default function backupCodeVerificationRoutes<T extends ExperienceInterac
async (ctx, next) => {
const { experienceInteraction } = ctx;
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
assertThat(
experienceInteraction.identifiedUserId,
new RequestError({ code: 'session.identifier_not_found', status: 404 })
);
const backupCodeVerificationRecord = BackupCodeVerification.create(
libraries,
@ -80,7 +84,13 @@ export default function backupCodeVerificationRoutes<T extends ExperienceInterac
},
});
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
assertThat(
experienceInteraction.identifiedUserId,
new RequestError({
code: 'session.identifier_not_found',
status: 404,
})
);
const backupCodeVerificationRecord = BackupCodeVerification.create(
libraries,

View file

@ -0,0 +1,133 @@
{
"paths": {
"/api/experience/verification/sso/{connectorId}/authorization-uri": {
"post": {
"tags": ["Dev feature"],
"summary": "Get SSO authorization URI",
"description": "Create a new EnterpriseSSO verification record and return the authorization URI for the given connector.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"redirectUri": {
"description": "The URI to redirect the user after the SSO authorization is completed."
},
"state": {
"description": "The state parameter to pass to the SSO connector."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The SSO authorization URI has been successfully generated.",
"content": {
"application/json": {
"schema": {
"properties": {
"authorizationUri": {
"description": "The SSO authorization URI."
},
"verificationId": {
"description": "The unique verification ID of the newly created EnterpriseSSO verification record. The `verificationId` is required when verifying the SSO authorization response."
}
}
}
}
}
},
"404": {
"description": "The SSO connector is not found."
},
"500": {
"description": "Connector error. Failed to generate the SSO authorization URI."
}
}
}
},
"/api/experience/verification/sso/{connectorId}/verify": {
"post": {
"tags": ["Dev feature"],
"summary": "Verify SSO authorization response",
"description": "Verify the SSO authorization response data and get the user's profile data from the SSO connector.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The ID of the EnterpriseSSO verification record."
},
"connectorData": {
"description": "Arbitrary data returned by the SSO provider to complete the verification process."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The SSO authorization response has been successfully verified.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The current verified EnterpriseSSO verification record ID. This ID is required when identifying the user in the current interaction."
}
}
}
}
}
},
"400": {
"description": "The SSO authorization response is invalid or cannot be verified."
},
"404": {
"description": "The verification record or the SSO connector is not found."
},
"500": {
"description": "Connector error. Failed to verify the SSO authorization response or fetch the user info from the SSO provider."
}
}
}
},
"/api/experience/verification/sso/connectors": {
"get": {
"tags": ["Dev feature"],
"summary": "Get all the enabled SSO connectors by the given email's domain",
"description": "Extract the email domain from the provided email address. Returns all the enabled SSO connectors that match the email domain.",
"parameters": [
{
"name": "email",
"in": "query",
"description": "The email address to find the enabled SSO connectors."
}
],
"responses": {
"200": {
"description": "The enabled SSO connectors have been successfully retrieved.",
"content": {
"application/json": {
"schema": {
"properties": {
"connectorIds": {
"description": "The list of enabled SSO connectorIds. Returns an empty array if no enabled SSO connectors are found."
}
}
}
}
}
},
"400": {
"description": "The email address is invalid, can not extract a valid domain from it."
}
}
}
}
}
}

View file

@ -0,0 +1,46 @@
{
"paths": {
"/api/experience/verification/new-password-identity": {
"post": {
"tags": ["Dev feature"],
"summary": "Create a new password identity for new user registration use",
"description": "Create a NewPasswordIdentity verification record for the new user registration use. The verification record includes a unique user identifier and a password that can be used to verify the user's identity.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"password": {
"description": "The new user password. (A password digest will be created and stored securely in the verification record.)"
},
"identifier": {
"description": "The unique user identifier. <br/> Currently, only `username` is accepted. For `email` or `phone` registration, a `CodeVerification` record must be created and used to verify the user's email or phone number identifier."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The NewPasswordIdentity verification record has been successfully created.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The unique verification ID of the newly created NewPasswordIdentity verification record. The `verificationId` is required when creating a new user account via the `Identification` API."
}
}
}
}
}
},
"422": {
"description": "Unable to process the request. <br/>- `user.username_already_in_use:` The provided username is already in use. <br/>- `password.rejected:` The provided password is rejected by the password policy. Detailed password violation information is included in the response."
}
}
}
}
}
}

View file

@ -0,0 +1,52 @@
{
"paths": {
"/api/experience/verification/password": {
"post": {
"tags": ["Dev feature"],
"summary": "Create and verify a new password verification record",
"description": "Generate a new Password verification record, which can be used to identify the user through the `Identification` API.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"password": {
"description": "The user password."
},
"identifier": {
"description": "The unique identifier of the user that will be used to identify the user along with the provided password."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The Password verification record has been successfully created and verified.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The unique verification ID of the newly created Password verification record. The `verificationId` is required when verifying the user's identity via the `Identification` API."
}
}
}
}
}
},
"400": {
"description": "The verification attempts have exceeded the maximum limit."
},
"401": {
"description": "The user is suspended or banned from the service."
},
"422": {
"description": "`session.invalid_credentials:` Either the user is not found or the provided password is incorrect."
}
}
}
}
}
}

View file

@ -24,7 +24,7 @@ export default function passwordVerificationRoutes<T extends ExperienceInteracti
`${experienceRoutes.verification}/password`,
koaGuard({
body: passwordVerificationPayloadGuard,
status: [200, 400, 422],
status: [200, 400, 401, 422],
response: z.object({
verificationId: z.string(),
}),

View file

@ -0,0 +1,100 @@
{
"paths": {
"/api/experience/verification/social/{connectorId}/authorization-uri": {
"post": {
"tags": ["Dev feature"],
"summary": "Get social authorization URI",
"description": "Create a new SocialVerification record and return the authorization URI for the given connector.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"redirectUri": {
"description": "The URI to redirect the user after the social authorization is completed."
},
"state": {
"description": "The state parameter to pass to the social connector."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The social authorization URI has been successfully generated.",
"content": {
"application/json": {
"schema": {
"properties": {
"authorizationUri": {
"description": "The social authorization URI."
},
"verificationId": {
"description": "The unique verification ID of the newly created SocialVerification record. The `verificationId` is required when verifying the social authorization response."
}
}
}
}
}
},
"404": {
"description": "The social connector is not found."
},
"500": {
"description": "Connector error. Failed to generate the social authorization URI."
}
}
}
},
"/api/experience/verification/social/{connectorId}/verify": {
"post": {
"tags": ["Dev feature"],
"summary": "Verify social authorization response",
"description": "Verify the social authorization response data and get the user's profile data from the social connector.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The ID of the Social verification record."
},
"connectorData": {
"description": "Arbitrary data returned by the social provider to complete the verification process."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The social authorization response has been successfully verified.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The unique verification ID of the SocialVerification record. This ID is required when identifying the user in the current interaction."
}
}
}
}
}
},
"400": {
"description": "The social authorization response is invalid or cannot be verified."
},
"404": {
"description": "The social connector is not found."
},
"500": {
"description": "Connector error. Failed to verify the social authorization response or fetch the user info from the social provider."
}
}
}
}
}
}

View file

@ -0,0 +1,81 @@
{
"paths": {
"/api/experience/verification/totp/secret": {
"post": {
"tags": ["Dev feature"],
"summary": "Generate TOTP secret for new TOTP binding",
"description": "Creates a new TOTP secret for the user to bind a new TOTP verification to their account.",
"responses": {
"200": {
"description": "TOTP secret successfully generated.",
"content": {
"application/json": {
"schema": {
"properties": {
"secret": {
"description": "The newly generated TOTP secret."
},
"verificationId": {
"description": "The unique verification ID for the TOTP record. This ID is required to verify the TOTP code."
},
"secretQrCode": {
"description": "A QR code image data URL for the TOTP secret. The user can scan this QR code with their TOTP authenticator app."
}
}
}
}
}
},
"404": {
"description": "Entity not found. <br/> - `session.identifier_not_found:` The current interaction is not identified yet. All MFA verification records must be associated with a identified user."
}
}
}
},
"/api/experience/verification/totp/verify": {
"post": {
"tags": ["Dev feature"],
"summary": "Verify TOTP code",
"description": "Verifies the provided TOTP code against the user's TOTP secret. If the code is correct, a new TOTP verification record will be created and marked as verified..",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"code": {
"description": "The TOTP code to be verified."
},
"verificationId": {
"description": "The verification ID of the newly created TOTP secret. This ID is required to verify a newly created TOTP secret that needs to be bound to the user account. If not provided, the API will create a new TOTP verification record and verify the code against the user's existing TOTP secret."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The TOTP code has been successfully verified.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The unique verification ID of the TOTP verification record. For newly created TOTP secret verification record, this ID is required to bind the TOTP secret to the user account through `Profile` API."
}
}
}
}
}
},
"400": {
"description": "Invalid TOTP code."
},
"404": {
"description": "Verification record not found."
}
}
}
}
}
}

View file

@ -36,7 +36,13 @@ export default function totpVerificationRoutes<T extends ExperienceInteractionRo
async (ctx, next) => {
const { experienceInteraction } = ctx;
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
assertThat(
experienceInteraction.identifiedUserId,
new RequestError({
code: 'session.identifier_not_found',
status: 404,
})
);
const totpVerification = TotpVerification.create(
libraries,
@ -84,7 +90,13 @@ export default function totpVerificationRoutes<T extends ExperienceInteractionRo
},
});
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
assertThat(
experienceInteraction.identifiedUserId,
new RequestError({
code: 'session.identifier_not_found',
status: 404,
})
);
// Verify new generated secret
if (verificationId) {

View file

@ -0,0 +1,100 @@
{
"paths": {
"/api/experience/verification/verification-code": {
"post": {
"tags": ["Dev feature"],
"summary": "Create and send verification code",
"description": "Creates a new `CodeVerification` record and sends the code to the specified identifier. The code verification can be used to verify the user's identity or bind a new identifier to the user's account.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"identifier": {
"description": "The identifier (email address or phone number) to send the verification code to."
},
"interactionEvent": {
"description": "The interaction event for which the verification code will be used. Supported values are `SignIn`, `Register`, and `ForgotPassword`. This determines the template for the verification code."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The verification code has been successfully sent.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The unique ID of the verification record. Required to verify the code."
}
}
}
}
}
},
"400": {
"description": "An invalid identifier was provided."
},
"501": {
"description": "The connector for sending the verification code is not configured."
}
}
}
},
"/api/experience/verification/verification-code/verify": {
"post": {
"tags": ["Dev feature"],
"summary": "Verify verification code",
"description": "Verifies the provided verification code against the user's identifier. If successful, the verification record is marked as verified.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"code": {
"description": "The verification code to be verified."
},
"identifier": {
"description": "The identifier (email address or phone number) to verify the code against. Must match the identifier used to send the verification code."
},
"verificationId": {
"description": "The verification ID of the CodeVerification record."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The verification code was successfully verified.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "he unique ID of the verification record. Required for user identification via the `Identification` API or to bind the identifier to the user's account via the `Profile` API."
}
}
}
}
}
},
"400": {
"description": "The verification code is invalid or the maximum number of attempts has been exceeded. Check the error message for details."
},
"404": {
"description": "Verification record not found."
},
"501": {
"description": "The connector for sending the verification code is not configured."
}
}
}
}
}
}

View file

@ -0,0 +1,155 @@
{
"paths": {
"/api/experience/verification/web-authn/registration": {
"post": {
"tags": ["Dev feature"],
"summary": "Create WebAuthn registration",
"description": "Creates a new WebAuthn registration verification record to allow the user to bind a new WebAuthn credential to their account.",
"responses": {
"200": {
"description": "WebAuthn registration successfully created.",
"content": {
"application/json": {
"schema": {
"properties": {
"registrationOptions": {
"description": "The WebAuthn registration options that the user needs to create a new WebAuthn credential."
},
"verificationId": {
"description": "The unique verification ID for the WebAuthn registration record. This ID is required to verify the WebAuthn registration challenge."
}
}
}
}
}
},
"404": {
"description": "Entity not found. <br/> - `session.identifier_not_found:` The current interaction is not identified yet. All MFA verification records must be associated with a identified user."
}
}
}
},
"/api/experience/verification/web-authn/registration/verify": {
"post": {
"tags": ["Dev feature"],
"summary": "Verify WebAuthn registration",
"description": "Verifies the WebAuthn registration response against the user's WebAuthn registration challenge. If the response is valid, the WebAuthn registration record will be marked as verified.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"payload": {
"description": "The WebAuthn attestation response from the user's WebAuthn credential."
},
"verificationId": {
"description": "The verification ID of the WebAuthn registration record."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The WebAuthn registration has been successfully verified.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The unique verification ID of the WebAuthn registration record. This `verificationId` is required to bind the WebAuthn credential to the user account via the `Profile` API."
}
}
}
}
}
},
"400": {
"description": "Invalid request. <br/> - `session.mfa.pending_info_not_found:` The WebAuthn registration challenge is missing from the current verification record. <br/>- `session.mfa.webauthn_verification_failed:` The WebAuthn attestation response is invalid or cannot be verified."
},
"404": {
"description": "Verification record not found."
}
}
}
},
"/api/experience/verification/web-authn/authentication": {
"post": {
"tags": ["Dev feature"],
"summary": "Initiate WebAuthn authentication",
"description": "Creates a new WebAuthn authentication verification record, allowing the user to authenticate with their WebAuthn credential.",
"responses": {
"200": {
"description": "WebAuthn authentication successfully initiated.",
"content": {
"application/json": {
"schema": {
"properties": {
"authenticationOptions": {
"description": "Options for the user to authenticate with their WebAuthn credential."
},
"verificationId": {
"description": "The unique ID for the WebAuthn authentication record, required to verify the WebAuthn authentication challenge."
}
}
}
}
}
},
"400": {
"description": "The user does not have a verified WebAuthn credential."
},
"404": {
"description": "The current interaction is not yet identified. All MFA verification records must be associated with an identified user."
}
}
}
},
"/api/experience/verification/web-authn/authentication/verify": {
"post": {
"tags": ["Dev feature"],
"summary": "Verify WebAuthn authentication",
"description": "Verifies the WebAuthn authentication response against the user's authentication challenge. Upon successful verification, the WebAuthn authentication verification record will be marked as verified.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"payload": {
"description": "The WebAuthn assertion response from the user's WebAuthn credential."
},
"verificationId": {
"description": "The verification ID of the WebAuthn authentication verification record."
}
}
}
}
}
},
"responses": {
"200": {
"description": "The WebAuthn authentication has been successfully verified.",
"content": {
"application/json": {
"schema": {
"properties": {
"verificationId": {
"description": "The unique verification ID of the WebAuthn authentication verification record."
}
}
}
}
}
},
"400": {
"description": "Invalid request. <br/> - `session.mfa.pending_info_not_found:` The WebAuthn authentication challenge is missing in the current verification record. <br/>- `session.mfa.webauthn_verification_failed:` The WebAuthn assertion response is invalid or cannot be verified."
},
"404": {
"description": "Verification record not found."
}
}
}
}
}
}

View file

@ -41,7 +41,13 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
async (ctx, next) => {
const { experienceInteraction } = ctx;
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
assertThat(
experienceInteraction.identifiedUserId,
new RequestError({
code: 'session.identifier_not_found',
status: 404,
})
);
const webAuthnVerification = WebAuthnVerification.create(
libraries,
@ -95,7 +101,13 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
},
});
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
assertThat(
experienceInteraction.identifiedUserId,
new RequestError({
code: 'session.identifier_not_found',
status: 404,
})
);
const webAuthnVerification = experienceInteraction.getVerificationRecordByTypeAndId(
VerificationType.WebAuthn,
@ -140,7 +152,13 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
async (ctx, next) => {
const { experienceInteraction } = ctx;
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
assertThat(
experienceInteraction.identifiedUserId,
new RequestError({
code: 'session.identifier_not_found',
status: 404,
})
);
const webAuthnVerification = WebAuthnVerification.create(
libraries,
@ -193,7 +211,13 @@ export default function webAuthnVerificationRoute<T extends ExperienceInteractio
},
});
assertThat(experienceInteraction.identifiedUserId, 'session.identifier_not_found');
assertThat(
experienceInteraction.identifiedUserId,
new RequestError({
code: 'session.identifier_not_found',
status: 404,
})
);
const webAuthnVerification = experienceInteraction.getVerificationRecordByTypeAndId(
VerificationType.WebAuthn,

View file

@ -100,7 +100,12 @@ const createRouters = (tenant: TenantContext) => {
statusRoutes(anonymousRouter, tenant);
authnRoutes(anonymousRouter, tenant);
// The swagger.json should contain all API routers.
swaggerRoutes(anonymousRouter, [interactionRouter, managementRouter, anonymousRouter]);
swaggerRoutes(anonymousRouter, [
interactionRouter,
managementRouter,
anonymousRouter,
experienceRouter,
]);
return [experienceRouter, interactionRouter, managementRouter, anonymousRouter];
};

View file

@ -49,6 +49,7 @@ const anonymousPaths = new Set<string>([
'authn',
'swagger.json',
'status',
'experience',
]);
const advancedSearchPaths = new Set<string>([

View file

@ -122,7 +122,7 @@ export const throwByDifference = (builtCustomRoutes: Set<string>) => {
};
/** Path segments that are treated as namespace prefixes. */
const namespacePrefixes = Object.freeze(['jit', '.well-known']);
const namespacePrefixes = Object.freeze(['jit', '.well-known', 'experience']);
const isPathParameter = (segment?: string) =>
Boolean(segment && (segment.startsWith(':') || segment.startsWith('{')));
@ -154,6 +154,7 @@ const throwIfNeeded = (method: OpenAPIV3.HttpMethods, path: string) => {
* @see {@link methodToVerb} for the mapping of HTTP methods to verbs.
* @see {@link namespacePrefixes} for the list of namespace prefixes.
*/
// eslint-disable-next-line complexity
export const buildOperationId = (method: OpenAPIV3.HttpMethods, path: string) => {
const customOperationId = customRoutes[`${method} ${path}`];
@ -162,7 +163,8 @@ export const buildOperationId = (method: OpenAPIV3.HttpMethods, path: string) =>
}
// Skip interactions APIs as they are going to replaced by the new APIs soon.
if (path.startsWith('/interaction')) {
// Skip experience APIs as they are not strictly RESTful.
if (path.startsWith('/interaction') || path.startsWith('/experience')) {
return;
}

View file

@ -21,12 +21,12 @@ devFeatureTest.describe('backup code verification APIs', () => {
await userApi.cleanUp();
});
it('should throw 400 if the user is not identified', async () => {
it('should throw 404 if the user is not identified', async () => {
const client = await initExperienceClient();
await expectRejects(client.verifyBackupCode({ code: '1234' }), {
code: 'session.identifier_not_found',
status: 400,
status: 404,
});
});

View file

@ -27,12 +27,12 @@ devFeatureTest.describe('TOTP verification APIs', () => {
});
describe('Create new TOTP secret', () => {
it('should throw 400 if the user is not identified', async () => {
it('should throw 404 if the user is not identified', async () => {
const client = await initExperienceClient();
await expectRejects(client.createTotpSecret(), {
code: 'session.identifier_not_found',
status: 400,
status: 404,
});
});
@ -46,12 +46,12 @@ devFeatureTest.describe('TOTP verification APIs', () => {
});
describe('Verify new TOTP secret', () => {
it('should throw 400 if the user is not identified', async () => {
it('should throw 404 if the user is not identified', async () => {
const client = await initExperienceClient();
await expectRejects(client.verifyTotp({ code: '1234' }), {
code: 'session.identifier_not_found',
status: 400,
status: 404,
});
});
@ -103,12 +103,12 @@ devFeatureTest.describe('TOTP verification APIs', () => {
});
describe('Verify existing TOTP secret', () => {
it('should throw 400 if the user is not identified', async () => {
it('should throw 404 if the user is not identified', async () => {
const client = await initExperienceClient();
await expectRejects(client.verifyTotp({ code: '1234' }), {
code: 'session.identifier_not_found',
status: 400,
status: 404,
});
});