/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { ICrypto, IPerformanceClient, Logger, } from "@azure/msal-common/browser"; import { InteractionClientBaseV2 } from "./InteractionClientBaseV2.js"; import { FlowStartParamsV2, FlowSignInStartParamsV2, FlowSignUpStartParamsV2, FlowChallengeParamsV2, FlowSubmitCodeParamsV2, FlowSubmitNewPasswordParamsV2, FlowSubmitSignInPasswordParamsV2, FlowSubmitSignUpAttributesParamsV2, FlowSignInWithContinuationParamsV2, } from "./parameter/FlowParamsV2.js"; import { createFlowMethodSelectionRequiredResultV2, createFlowCodeRequiredResultV2, createFlowPasswordRequiredResultV2, createFlowSignUpPasswordRequiredResultV2, createFlowMFARequiredResultV2, createFlowNewPasswordRequiredResultV2, createFlowAttributesRequiredResultV2, createFlowSignInContinuationRequiredResultV2, createFlowCompletedResultV2, FLOW_CODE_REQUIRED_V2, FLOW_SIGN_IN_CONTINUATION_REQUIRED_V2, } from "./result/FlowActionResultV2.js"; import type { FlowMethodSelectionRequiredResultV2, FlowCodeRequiredResultV2, FlowSignInCodeRequiredResultV2, FlowResetPasswordCodeRequiredResultV2, FlowPasswordRequiredResultV2, FlowSignUpPasswordRequiredResultV2, FlowMFARequiredResultV2, FlowAttributesRequiredResultV2, FlowSignInContinuationRequiredResultV2, FlowCompletedResultV2, FlowSignUpActionResultV2, FlowSubmitCodeResultV2, } from "./result/FlowActionResultV2.js"; import { BrowserConfiguration } from "../../../../config/Configuration.js"; import { BrowserCacheManager } from "../../../../cache/BrowserCacheManager.js"; import { EventHandler } from "../../../../event/EventHandler.js"; import { INavigationClient } from "../../../../navigation/INavigationClient.js"; import { CustomAuthAuthority } from "../../CustomAuthAuthority.js"; import { CustomAuthApiClientV2 } from "../../network_client/custom_auth_api/v2/CustomAuthApiClientV2.js"; import { CustomAuthError } from "../../error/CustomAuthError.js"; import { CONTINUATION_LINK_MISSING, RESET_PASSWORD_UNSUPPORTED, RESET_PASSWORD_TIMEOUT, SIGN_IN_UNSUPPORTED, SIGN_UP_UNSUPPORTED, UNSUPPORTED_FLOW_STEP, UNSUPPORTED_FLOW_TRANSITION, } from "../../network_client/custom_auth_api/v2/ErrorCodesV2.js"; import { CustomAuthFlowScenarioV2 } from "../../auth_flow/v2/CustomAuthFlowScenarioV2.js"; import * as PublicApiId from "../../telemetry/PublicApiId.js"; import { getPublicApiIdV2, FlowStepV2, } from "../../telemetry/FlowApiIdHelperV2.js"; import { AuthenticationFactorV2, ChallengeNextActionV2, type ChallengeVerificationResultV2, VerifyNextActionV2, type VerifyResultV2, } from "../../network_client/custom_auth_api/v2/result/BaseResultsV2.js"; import { AuthenticationMethodTypeV2 } from "../../network_client/custom_auth_api/v2/ApiClientConstantsV2.js"; import type { FlowContinuationStateV2 } from "./FlowContinuationStateV2.js"; import type { AuthenticationMethodV2 } from "../../auth_flow/v2/AuthenticationMethodV2.js"; import { SignUpSubmitAttributesNextActionV2, type SignUpSubmitAttributesApiResultV2, } from "../../network_client/custom_auth_api/v2/result/SignUpResultsV2.js"; /* * Polls up to five times for password-update completion, waiting 1.5 seconds * between attempts. The first request runs immediately. */ const POLL_MAX_ATTEMPTS = 5; // TODO: Replace this fallback with the server-provided polling interval once available. const POLL_INTERVAL_MS = 1500; const delay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); /* * Drives server-directed Native Auth V2 flows using opaque continuation state * and HAL links. Each operation returns only its valid discriminated results. */ export class FlowInteractionClientV2 extends InteractionClientBaseV2 { constructor( config: BrowserConfiguration, storageImpl: BrowserCacheManager, browserCrypto: ICrypto, logger: Logger, eventHandler: EventHandler, navigationClient: INavigationClient, performanceClient: IPerformanceClient, customAuthAuthority: CustomAuthAuthority, protected apiClient: CustomAuthApiClientV2 ) { super( config, storageImpl, browserCrypto, logger, eventHandler, navigationClient, performanceClient, customAuthAuthority ); } /* * Starts sign-in using the preferred supported method offered by the server. */ async signIn( parameters: FlowSignInStartParamsV2 ): Promise< | FlowPasswordRequiredResultV2 | FlowSignInCodeRequiredResultV2 | FlowMFARequiredResultV2 | FlowCompletedResultV2 > { const correlationId = parameters.correlationId; const context = this.createRequestContext( PublicApiId.SIGN_IN_V2_START, correlationId ); this.logger.verbose("Starting V2 sign-in.", correlationId); const entryResult = await this.apiClient.authorizeChallengeStart( context ); const startResult = await this.apiClient.signInStart( this.requireLink(correlationId, entryResult.signInHref, { code: SIGN_IN_UNSUPPORTED, message: "The authorize-challenge entry response did not include a sign-in link.", }), { continuationToken: entryResult.continuationToken, username: parameters.username, }, context ); this.ensureAuthenticationFactor( startResult.authenticationFactor, AuthenticationFactorV2.SINGLE_FACTOR, "sign-in start", correlationId ); const continuationState: FlowContinuationStateV2 = { continuationToken: startResult.continuationToken, scenario: CustomAuthFlowScenarioV2.SignIn, links: {}, tokenRequest: { scopes: parameters.scopes, }, }; const methods = startResult.methods.map((method) => ({ id: method.id, type: method.type ?? "", hint: method.hint, challengeHref: method.challengeHref, })); const selectedMethod = this.selectSignInMethod( methods, parameters.password !== undefined, correlationId ); const challengeResult = await this.requestChallenge({ correlationId, continuationState: { ...continuationState, links: { ...continuationState.links, challenge: selectedMethod.challengeHref, }, }, }); if (challengeResult.type === FLOW_CODE_REQUIRED_V2) { return { ...challengeResult, method: selectedMethod, }; } if (parameters.password) { return this.submitSignInPassword({ correlationId, continuationState: challengeResult.continuationState, password: parameters.password, }); } return challengeResult; } /* * Starts sign-up and resolves the server-provided attribute-submission transition. */ async signUp( parameters: FlowSignUpStartParamsV2 ): Promise< | FlowCodeRequiredResultV2 | FlowSignUpPasswordRequiredResultV2 | FlowAttributesRequiredResultV2 > { const correlationId = parameters.correlationId; const context = this.createRequestContext( PublicApiId.SIGN_UP_V2_START, correlationId ); this.logger.verbose("Starting V2 sign-up.", correlationId); const entryResult = await this.apiClient.authorizeChallengeStart( context, parameters.scopes ); const startResult = await this.apiClient.signUpStart( this.requireLink(correlationId, entryResult.signUpHref, { code: SIGN_UP_UNSUPPORTED, message: "The authorize-challenge entry response did not include a sign-up link.", }), { continuationToken: entryResult.continuationToken, }, context ); const attributes = Object.fromEntries( Object.entries(parameters.attributes ?? {}).filter( ([name]) => name.toLowerCase() !== "email" && name.toLowerCase() !== "password" ) ); attributes.email = parameters.username; if (parameters.password !== undefined) { attributes.password = parameters.password; } const submitResult = await this.apiClient.submitSignUpAttributes( startResult.submitAttributesHref, { continuationToken: startResult.continuationToken, attributes, }, context ); const continuationState: FlowContinuationStateV2 = { continuationToken: startResult.continuationToken, scenario: CustomAuthFlowScenarioV2.SignUp, links: {}, tokenRequest: { scopes: parameters.scopes, }, signUp: { passwordWasSupplied: attributes.password !== undefined, }, }; const result = this.toSignUpActionResult( submitResult, continuationState, correlationId ); if (result.type === FLOW_SIGN_IN_CONTINUATION_REQUIRED_V2) { const message = "Initial sign-up attribute submission cannot complete without code verification or additional attributes."; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } return result; } /* * Starts password reset, automatically challenging a sole method or * returning multiple methods for selection. */ async resetPassword( parameters: FlowStartParamsV2 ): Promise< | FlowMethodSelectionRequiredResultV2 | FlowResetPasswordCodeRequiredResultV2 > { const correlationId = parameters.correlationId; const context = this.createRequestContext( PublicApiId.RESET_PASSWORD_V2_START, correlationId ); this.logger.verbose( "Starting V2 self-service password reset.", correlationId ); const entryResult = await this.apiClient.authorizeChallengeStart( context ); const startResult = await this.apiClient.resetPasswordStart( this.requireLink(correlationId, entryResult.resetPasswordHref, { code: RESET_PASSWORD_UNSUPPORTED, message: "The authorize-challenge entry response did not include a reset-password link.", }), { username: parameters.username, continuationToken: entryResult.continuationToken, }, context ); this.ensureAuthenticationFactor( startResult.authenticationFactor, AuthenticationFactorV2.SINGLE_FACTOR, "password-reset start", correlationId ); const continuationState: FlowContinuationStateV2 = { continuationToken: startResult.continuationToken, scenario: CustomAuthFlowScenarioV2.PasswordReset, links: {}, }; const methods = startResult.methods.map((method) => ({ id: method.id, type: method.type ?? "", hint: method.hint, challengeHref: method.challengeHref, })); if (methods.length === 0) { const message = "The password-reset start response did not include an authentication method."; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } if (methods.length === 1) { const selectedMethod = methods[0]; const challengeResult = await this.requestChallenge({ correlationId, continuationState: { ...continuationState, links: { challenge: selectedMethod.challengeHref, }, }, }); if ( challengeResult.type === FLOW_CODE_REQUIRED_V2 && (challengeResult.channel?.toLowerCase() === AuthenticationMethodTypeV2.EMAIL || challengeResult.channel?.toLowerCase() === AuthenticationMethodTypeV2.SMS) ) { return { ...challengeResult, method: selectedMethod, }; } const channel = challengeResult.type === FLOW_CODE_REQUIRED_V2 ? challengeResult.channel : AuthenticationMethodTypeV2.PASSWORD; const message = `Challenge type '${challengeResult.type}' with channel '${channel}' is not supported for password reset.`; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } this.logger.verbose( "V2 self-service password reset method selection required.", correlationId ); return createFlowMethodSelectionRequiredResultV2({ correlationId, continuationState, methods, }); } /* * Requests a one-time code for the selected authentication method. Returns * the continuation state and code metadata needed for verification. */ async requestChallenge( parameters: FlowChallengeParamsV2 ): Promise { const challengeResult = await this.sendMethodChallenge( parameters, "requestChallenge" ); const challengeType = challengeResult.type ?? AuthenticationMethodTypeV2.EMAIL; const continuationState = this.createChallengeContinuationState( parameters.continuationState, challengeResult ); if ( challengeType.toLowerCase() === AuthenticationMethodTypeV2.PASSWORD ) { return createFlowPasswordRequiredResultV2({ correlationId: parameters.correlationId, continuationState, }); } return createFlowCodeRequiredResultV2({ correlationId: parameters.correlationId, continuationState, channel: challengeType, sentTo: challengeResult.hint, codeLength: challengeResult.codeLength, }); } /* * Verifies the submitted one-time code using the server-provided link. The * response determines the next required action. */ async submitCode( parameters: FlowSubmitCodeParamsV2 ): Promise { const continuationState = parameters.continuationState; const correlationId = parameters.correlationId; const context = this.createRequestContext( this.resolveStepApiId( continuationState.scenario, "submitCode", correlationId ), correlationId ); this.logger.verbose("Submitting V2 one-time code.", correlationId); const verifyHref = this.requireLink( correlationId, continuationState.links.verify ); const verifyRequest = { continuationToken: continuationState.continuationToken, otp: parameters.code, }; const verifyResult = await this.apiClient.verifyChallenge( verifyHref, verifyRequest, context ); if ( continuationState.scenario === CustomAuthFlowScenarioV2.PasswordReset && verifyResult.nextAction === VerifyNextActionV2.UPDATE ) { return createFlowNewPasswordRequiredResultV2({ correlationId, continuationState: { continuationToken: verifyResult.continuationToken, scenario: continuationState.scenario, links: { update: verifyResult.updateHref }, }, }); } if (continuationState.scenario === CustomAuthFlowScenarioV2.SignIn) { return this.handleSignInVerification( continuationState, verifyResult, correlationId ); } if ( continuationState.scenario === CustomAuthFlowScenarioV2.SignUp && verifyResult.nextAction === VerifyNextActionV2.COLLECT_ATTRIBUTES ) { const requiredPasswordAttribute = verifyResult.attributes.find( (attribute) => attribute.attributeId.toLowerCase() === "password" && attribute.required === true ); const nextContinuationState: FlowContinuationStateV2 = { continuationToken: verifyResult.continuationToken, scenario: continuationState.scenario, links: { submitAttributes: verifyResult.submitAttributesHref, }, tokenRequest: continuationState.tokenRequest, signUp: continuationState.signUp, }; const attributes = verifyResult.attributes.filter( (attribute) => attribute.attributeId.toLowerCase() !== "password" ); if ( requiredPasswordAttribute && continuationState.signUp?.passwordWasSupplied !== true ) { return createFlowSignUpPasswordRequiredResultV2({ correlationId, continuationState: nextContinuationState, attributes, requiredPasswordAttribute, }); } return createFlowAttributesRequiredResultV2({ correlationId, continuationState: nextContinuationState, attributes, }); } if ( continuationState.scenario === CustomAuthFlowScenarioV2.SignUp && verifyResult.nextAction === VerifyNextActionV2.CONTINUE ) { return createFlowSignInContinuationRequiredResultV2({ correlationId, continuationState: { continuationToken: verifyResult.continuationToken, scenario: continuationState.scenario, links: {}, tokenRequest: continuationState.tokenRequest, }, }); } const message = `Verification next action '${verifyResult.nextAction}' is not supported for the '${continuationState.scenario}' flow.`; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } /* * Requests a new one-time code using the server-provided resend link. * Returns refreshed continuation state and code metadata. */ async resendCode( parameters: FlowChallengeParamsV2 ): Promise { const challengeResult = await this.sendMethodChallenge( parameters, "resendCode" ); return createFlowCodeRequiredResultV2({ correlationId: parameters.correlationId, continuationState: this.createChallengeContinuationState( parameters.continuationState, challengeResult ), channel: challengeResult.type, sentTo: challengeResult.hint, codeLength: challengeResult.codeLength, }); } async submitSignUpAttributes( parameters: FlowSubmitSignUpAttributesParamsV2 ): Promise { const { continuationState, correlationId, attributes } = parameters; const context = this.createRequestContext( this.resolveStepApiId( continuationState.scenario, "submitAttributes", correlationId ), correlationId ); this.logger.verbose( "Submitting additional V2 sign-up attributes.", correlationId ); const submitResult = await this.apiClient.submitSignUpAttributes( this.requireLink( correlationId, continuationState.links.submitAttributes ), { continuationToken: continuationState.continuationToken, attributes, }, context ); return this.toSignUpActionResult( submitResult, { ...continuationState, signUp: { passwordWasSupplied: continuationState.signUp?.passwordWasSupplied === true || attributes.password !== undefined, }, }, correlationId ); } /* * Submits the new password and polls until the reset completes. The result * contains the continuation state required for explicit sign-in. */ async submitNewPassword( parameters: FlowSubmitNewPasswordParamsV2 ): Promise { const continuationState = parameters.continuationState; const correlationId = parameters.correlationId; const context = this.createRequestContext( this.resolveStepApiId( continuationState.scenario, "submitNewPassword", correlationId ), correlationId ); this.logger.verbose("Submitting V2 new password.", correlationId); const updateResult = await this.apiClient.submitNewPassword( this.requireLink(correlationId, continuationState.links.update), { continuationToken: continuationState.continuationToken, newPassword: parameters.newPassword, }, context ); let pollToken = updateResult.continuationToken; let pollHref = updateResult.pollHref; let completionToken: string | undefined; for (let attempt = 1; attempt <= POLL_MAX_ATTEMPTS; attempt++) { const pollResult = await this.apiClient.poll( pollHref, { continuationToken: pollToken }, context ); if (pollResult.isCompleted) { completionToken = pollResult.continuationToken; break; } pollToken = pollResult.continuationToken; pollHref = pollResult.pollHref ?? pollHref; if (attempt < POLL_MAX_ATTEMPTS) { await delay(POLL_INTERVAL_MS); } } if (!completionToken) { const message = "The password reset did not complete within the allotted number of polling attempts."; this.logger.error(message, correlationId); throw new CustomAuthError( RESET_PASSWORD_TIMEOUT, message, correlationId ); } this.logger.verbose( "V2 password reset applied; sign-in required.", correlationId ); return createFlowSignInContinuationRequiredResultV2({ correlationId, continuationState: { continuationToken: completionToken, scenario: continuationState.scenario, links: {}, }, }); } /* * Verifies a sign-in password and returns the next sign-in action. */ async submitSignInPassword( parameters: FlowSubmitSignInPasswordParamsV2 ): Promise { const verifyResult = await this.verifySignInPassword(parameters); return this.handleSignInVerification( parameters.continuationState, verifyResult, parameters.correlationId ); } /* * Redeems the completed flow continuation for tokens. Returns the * authentication result after validating and caching the response. */ async signInWithContinuation( parameters: FlowSignInWithContinuationParamsV2 ): Promise { const continuationState = parameters.continuationState; const correlationId = parameters.correlationId; const apiId = this.resolveStepApiId( continuationState.scenario, "signInWithContinuation", correlationId ); const context = this.createRequestContext(apiId, correlationId); this.logger.verbose( "Signing in with a V2 continuation.", correlationId ); const scopes = this.getScopes(parameters.scopes); const tokenResponse = await this.apiClient.completeWithTokens( { continuationToken: continuationState.continuationToken, scopes, }, context ); const authenticationResult = await this.handleTokenResponse( tokenResponse, scopes, correlationId, apiId ); this.logger.verbose( "V2 continuation sign-in completed.", correlationId ); return createFlowCompletedResultV2({ correlationId, authenticationResult, }); } private async handleSignInVerification( continuationState: FlowContinuationStateV2, verifyResult: VerifyResultV2, correlationId: string ): Promise { if (verifyResult.nextAction === VerifyNextActionV2.CHALLENGE) { this.ensureAuthenticationFactor( verifyResult.authenticationFactor, AuthenticationFactorV2.MULTI_FACTOR, "after sign-in verification", correlationId ); return createFlowMFARequiredResultV2({ correlationId, continuationState: { continuationToken: verifyResult.continuationToken, scenario: continuationState.scenario, links: {}, tokenRequest: continuationState.tokenRequest, }, methods: verifyResult.methods.map((method) => ({ id: method.id, type: method.type ?? "", hint: method.hint, challengeHref: method.challengeHref, })), }); } if (verifyResult.nextAction === VerifyNextActionV2.CONTINUE) { return this.completeSignInAfterVerification( continuationState, verifyResult.continuationToken, correlationId ); } const message = `Verification next action '${verifyResult.nextAction}' is not supported for sign-in.`; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } private selectSignInMethod( methods: AuthenticationMethodV2[], passwordProvided: boolean, correlationId: string ): AuthenticationMethodV2 { const passwordMethod = methods.find( (method) => method.type.toLowerCase() === AuthenticationMethodTypeV2.PASSWORD ); const emailMethod = methods.find( (method) => method.type.toLowerCase() === AuthenticationMethodTypeV2.EMAIL ); if (passwordProvided) { if (passwordMethod) { return passwordMethod; } const message = "A password was supplied, but the sign-in response did not include a password method."; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } const selectedMethod = emailMethod ?? passwordMethod; if (selectedMethod) { return selectedMethod; } const message = "The sign-in start response did not include a supported email or password method."; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } private toSignUpActionResult( result: SignUpSubmitAttributesApiResultV2, continuationState: FlowContinuationStateV2, correlationId: string ): FlowSignUpActionResultV2 { const nextAction: string = result.nextAction; if (result.nextAction === SignUpSubmitAttributesNextActionV2.VERIFY) { return createFlowCodeRequiredResultV2({ correlationId, continuationState: { continuationToken: result.continuationToken, scenario: continuationState.scenario, links: { verify: result.verifyHref, resend: result.resendHref, }, tokenRequest: continuationState.tokenRequest, signUp: continuationState.signUp, }, channel: result.type, sentTo: result.hint, codeLength: result.codeLength, }); } if ( result.nextAction === SignUpSubmitAttributesNextActionV2.COLLECT_ATTRIBUTES ) { const requiredPasswordAttribute = result.attributes.find( (attribute) => attribute.attributeId.toLowerCase() === "password" && attribute.required === true ); const nextContinuationState: FlowContinuationStateV2 = { continuationToken: result.continuationToken, scenario: continuationState.scenario, links: { submitAttributes: result.submitAttributesHref, }, tokenRequest: continuationState.tokenRequest, signUp: continuationState.signUp, }; const attributes = result.attributes.filter( (attribute) => attribute.attributeId.toLowerCase() !== "password" ); if ( requiredPasswordAttribute && continuationState.signUp?.passwordWasSupplied !== true ) { return createFlowSignUpPasswordRequiredResultV2({ correlationId, continuationState: nextContinuationState, attributes, requiredPasswordAttribute, }); } return createFlowAttributesRequiredResultV2({ correlationId, continuationState: nextContinuationState, attributes, }); } if (result.nextAction === SignUpSubmitAttributesNextActionV2.CONTINUE) { return createFlowSignInContinuationRequiredResultV2({ correlationId, continuationState: { continuationToken: result.continuationToken, scenario: continuationState.scenario, links: {}, tokenRequest: continuationState.tokenRequest, }, }); } const message = `Sign-up attribute submission next action '${nextAction}' is not supported.`; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } private ensureAuthenticationFactor( authenticationFactor: AuthenticationFactorV2, expectedFactor: AuthenticationFactorV2, transition: string, correlationId: string ): void { if (authenticationFactor === expectedFactor) { return; } const message = `Authentication factor '${authenticationFactor}' is not supported for ${transition}.`; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } private async verifySignInPassword( parameters: FlowSubmitSignInPasswordParamsV2 ): Promise { const { continuationState, correlationId } = parameters; const context = this.createRequestContext( this.resolveStepApiId( continuationState.scenario, "submitPassword", correlationId ), correlationId ); this.logger.verbose("Submitting V2 sign-in password.", correlationId); return this.apiClient.verifyChallenge( this.requireLink(correlationId, continuationState.links.verify), { continuationToken: continuationState.continuationToken, password: parameters.password, }, context ); } private completeSignInAfterVerification( continuationState: FlowContinuationStateV2, continuationToken: string, correlationId: string ): Promise { return this.signInWithContinuation({ correlationId, continuationState: { continuationToken, scenario: continuationState.scenario, links: {}, tokenRequest: continuationState.tokenRequest, }, scopes: continuationState.tokenRequest?.scopes, }); } private async sendMethodChallenge( parameters: FlowChallengeParamsV2, step: "requestChallenge" | "resendCode" ): Promise { const continuationState = parameters.continuationState; const correlationId = parameters.correlationId; const challengeHref = step === "resendCode" ? continuationState.links.resend : continuationState.links.challenge; const context = this.createRequestContext( this.resolveStepApiId( continuationState.scenario, step, correlationId ), correlationId ); this.logger.verbose( step === "resendCode" ? "Resending V2 one-time code." : "Requesting V2 challenge.", correlationId ); const challengeResult = await this.apiClient.requestChallenge( this.requireLink(correlationId, challengeHref), { continuationToken: continuationState.continuationToken }, context ); const nextAction: string = challengeResult.nextAction; if (challengeResult.nextAction === ChallengeNextActionV2.VERIFY) { return challengeResult; } if (challengeResult.nextAction === ChallengeNextActionV2.RISK_VERIFY) { return this.apiClient.verifyRisk( challengeResult.riskVerifyHref, { continuationToken: challengeResult.continuationToken }, context ); } const message = `Challenge next action '${nextAction}' is not supported.`; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_TRANSITION, message, correlationId ); } private createChallengeContinuationState( continuationState: FlowContinuationStateV2, challengeResult: ChallengeVerificationResultV2 ): FlowContinuationStateV2 { return { continuationToken: challengeResult.continuationToken, scenario: continuationState.scenario, links: { challenge: continuationState.links.challenge, verify: challengeResult.verifyHref, resend: challengeResult.resendHref ?? continuationState.links.resend, }, ...(continuationState.tokenRequest && { tokenRequest: continuationState.tokenRequest, }), ...(continuationState.signUp && { signUp: continuationState.signUp, }), }; } private resolveStepApiId( scenario: CustomAuthFlowScenarioV2, step: FlowStepV2, correlationId: string ): number { const apiId = getPublicApiIdV2(scenario, step); if (apiId === undefined) { const message = `No telemetry API id is registered for step '${step}' of the '${scenario}' flow.`; this.logger.error(message, correlationId); throw new CustomAuthError( UNSUPPORTED_FLOW_STEP, message, correlationId ); } return apiId; } private requireLink( correlationId: string, href: string | undefined, missingLinkError?: { code: string; message: string } ): string { if (!href) { const errorCode = missingLinkError?.code ?? CONTINUATION_LINK_MISSING; const message = missingLinkError?.message ?? "The continuation state is missing a link required to advance the flow."; this.logger.error(message, correlationId); throw new CustomAuthError(errorCode, message, correlationId); } return href; } }