UNPKG

@azure/msal-browser

Version:
533 lines (478 loc) 18.8 kB
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { ChallengeType, DefaultCustomAuthApiCodeLength, GrantType, } from "../../CustomAuthConstants.js"; import { CustomAuthApiError } from "../../core/error/CustomAuthApiError.js"; import * as CustomAuthApiErrorCode from "../../core/network_client/custom_auth_api/types/ApiErrorCodes.js"; import { MFA_REQUIRED, REGISTRATION_REQUIRED, } from "../../core/network_client/custom_auth_api/types/ApiSuberrors.js"; import { CustomAuthInteractionClientBase } from "../../core/interaction_client/CustomAuthInteractionClientBase.js"; import { SignInStartParams, SignInResendCodeParams, SignInSubmitCodeParams, SignInSubmitPasswordParams, SignInContinuationTokenParams, } from "./parameter/SignInParams.js"; import { createSignInCodeSendResult, createSignInCompleteResult, createSignInPasswordRequiredResult, createSignInJitRequiredResult, SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE, SignInCodeSendResult, SignInCompletedResult, SignInPasswordRequiredResult, SignInJitRequiredResult, SignInMfaRequiredResult, createSignInMfaRequiredResult, } from "./result/SignInActionResult.js"; import * as PublicApiId from "../../core/telemetry/PublicApiId.js"; import { SignInChallengeRequest, SignInContinuationTokenRequest, SignInInitiateRequest, SignInOobTokenRequest, SignInPasswordTokenRequest, RegisterIntrospectRequest, SignInIntrospectRequest, } from "../../core/network_client/custom_auth_api/types/ApiRequestTypes.js"; import { SignInTokenResponse } from "../../core/network_client/custom_auth_api/types/ApiResponseTypes.js"; import { SignInScenario, SignInScenarioType, } from "../auth_flow/SignInScenario.js"; import { UnexpectedError } from "../../core/error/UnexpectedError.js"; import { ensureArgumentIsNotEmptyString } from "../../core/utils/ArgumentValidator.js"; import { ServerTelemetryManager } from "@azure/msal-common/browser"; import { initializeServerTelemetryManager } from "../../../interaction_client/BaseInteractionClient.js"; export class SignInClient extends CustomAuthInteractionClientBase { /** * Starts the signin flow. * @param parameters The parameters required to start the sign-in flow. * @returns The result of the sign-in start operation. */ async start( parameters: SignInStartParams ): Promise<SignInPasswordRequiredResult | SignInCodeSendResult> { const correlationId = parameters.correlationId || this.correlationId; const apiId = !parameters.password ? PublicApiId.SIGN_IN_WITH_CODE_START : PublicApiId.SIGN_IN_WITH_PASSWORD_START; const telemetryManager = initializeServerTelemetryManager( apiId, this.config.auth.clientId, correlationId, this.browserStorage, this.logger ); this.logger.verbose( "Calling initiate endpoint for sign in.", correlationId ); const initReq: SignInInitiateRequest = { challenge_type: this.getChallengeTypes(parameters.challengeType), username: parameters.username, correlationId: correlationId, telemetryManager: telemetryManager, }; const initiateResponse = await this.customAuthApiClient.signInApi.initiate(initReq); this.logger.verbose( "Initiate endpoint called for sign in.", initiateResponse.correlation_id || correlationId ); const challengeReq: SignInChallengeRequest = { challenge_type: this.getChallengeTypes(parameters.challengeType), continuation_token: initiateResponse.continuation_token ?? "", correlationId: initiateResponse.correlation_id || correlationId, telemetryManager: telemetryManager, }; return this.performChallengeRequest(challengeReq); } /** * Resends the code for sign-in flow. * @param parameters The parameters required to resend the code. * @returns The result of the sign-in resend code action. */ async resendCode( parameters: SignInResendCodeParams ): Promise<SignInCodeSendResult> { const correlationId = parameters.correlationId || this.correlationId; const apiId = PublicApiId.SIGN_IN_RESEND_CODE; const telemetryManager = initializeServerTelemetryManager( apiId, this.config.auth.clientId, correlationId, this.browserStorage, this.logger ); const challengeReq: SignInChallengeRequest = { challenge_type: this.getChallengeTypes(parameters.challengeType), continuation_token: parameters.continuationToken ?? "", correlationId: correlationId, telemetryManager: telemetryManager, }; const result = await this.performChallengeRequest(challengeReq); if (result.type === SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE) { this.logger.error( "Resend code operation failed due to the challenge type 'password' is not supported.", result.correlationId || correlationId ); throw new CustomAuthApiError( CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, "Unsupported challenge type 'password'.", result.correlationId || correlationId ); } return result; } /** * Submits the code for sign-in flow. * @param parameters The parameters required to submit the code. * @returns The result of the sign-in submit code action. */ async submitCode( parameters: SignInSubmitCodeParams ): Promise< | SignInCompletedResult | SignInJitRequiredResult | SignInMfaRequiredResult > { const correlationId = parameters.correlationId || this.correlationId; ensureArgumentIsNotEmptyString( "parameters.code", parameters.code, correlationId ); const apiId = PublicApiId.SIGN_IN_SUBMIT_CODE; const telemetryManager = initializeServerTelemetryManager( apiId, this.config.auth.clientId, correlationId, this.browserStorage, this.logger ); const scopes = this.getScopes(parameters.scopes); const request: SignInOobTokenRequest = { continuation_token: parameters.continuationToken, oob: parameters.code, grant_type: GrantType.OOB, scope: scopes.join(" "), correlationId: correlationId, telemetryManager: telemetryManager, ...(parameters.claims && { claims: parameters.claims, }), }; return this.performTokenRequest( () => this.customAuthApiClient.signInApi.requestTokensWithOob( request ), scopes, correlationId, telemetryManager, apiId ); } /** * Submits the password for sign-in flow. * @param parameters The parameters required to submit the password. * @returns The result of the sign-in submit password action. */ async submitPassword( parameters: SignInSubmitPasswordParams ): Promise< | SignInCompletedResult | SignInJitRequiredResult | SignInMfaRequiredResult > { const correlationId = parameters.correlationId || this.correlationId; ensureArgumentIsNotEmptyString( "parameters.password", parameters.password, correlationId ); const apiId = PublicApiId.SIGN_IN_SUBMIT_PASSWORD; const telemetryManager = initializeServerTelemetryManager( apiId, this.config.auth.clientId, correlationId, this.browserStorage, this.logger ); const scopes = this.getScopes(parameters.scopes); const request: SignInPasswordTokenRequest = { continuation_token: parameters.continuationToken, password: parameters.password, scope: scopes.join(" "), correlationId: correlationId, telemetryManager: telemetryManager, ...(parameters.claims && { claims: parameters.claims, }), }; return this.performTokenRequest( () => this.customAuthApiClient.signInApi.requestTokensWithPassword( request ), scopes, correlationId, telemetryManager, apiId ); } /** * Signs in with continuation token. * @param parameters The parameters required to sign in with continuation token. * @returns The result of the sign-in complete action. */ async signInWithContinuationToken( parameters: SignInContinuationTokenParams ): Promise< | SignInCompletedResult | SignInJitRequiredResult | SignInMfaRequiredResult > { const correlationId = parameters.correlationId || this.correlationId; const apiId = this.getPublicApiIdBySignInScenario( parameters.signInScenario, correlationId ); const telemetryManager = initializeServerTelemetryManager( apiId, this.config.auth.clientId, correlationId, this.browserStorage, this.logger ); const scopes = this.getScopes(parameters.scopes); // Create token request. const request: SignInContinuationTokenRequest = { continuation_token: parameters.continuationToken, username: parameters.username, correlationId: correlationId, telemetryManager: telemetryManager, scope: scopes.join(" "), ...(parameters.claims && { claims: parameters.claims, }), }; // Call token endpoint. return this.performTokenRequest( () => this.customAuthApiClient.signInApi.requestTokenWithContinuationToken( request ), scopes, correlationId, telemetryManager, apiId ); } /** * Common method to handle token endpoint calls and create sign-in results. * @param tokenEndpointCaller Function that calls the specific token endpoint * @param scopes Scopes for the token request * @param correlationId Correlation ID for logging and result * @param telemetryManager Telemetry manager for telemetry logging * @returns SignInCompletedResult | SignInJitRequiredResult | SignInMfaRequiredResult with authentication result */ private async performTokenRequest( tokenEndpointCaller: () => Promise<SignInTokenResponse>, scopes: string[], correlationId: string, telemetryManager: ServerTelemetryManager, apiId: number ): Promise< | SignInCompletedResult | SignInJitRequiredResult | SignInMfaRequiredResult > { this.logger.verbose( "Calling token endpoint for sign in.", correlationId ); try { const tokenResponse = await tokenEndpointCaller(); this.logger.verbose( "Token endpoint response received for sign in.", tokenResponse.correlation_id || correlationId ); const authResult = await this.handleTokenResponse( tokenResponse, scopes, tokenResponse.correlation_id || correlationId, apiId ); return createSignInCompleteResult({ correlationId: tokenResponse.correlation_id || correlationId, authenticationResult: authResult, }); } catch (error) { if ( error instanceof CustomAuthApiError && error.subError === REGISTRATION_REQUIRED ) { return this.handleJitRequiredError( error, telemetryManager, error.correlationId || correlationId ); } else if ( error instanceof CustomAuthApiError && error.subError === MFA_REQUIRED ) { return this.handleMfaRequiredError( error, telemetryManager, error.correlationId || correlationId ); } // Re-throw any other errors or JIT errors when handleJit is false throw error; } } private async performChallengeRequest( request: SignInChallengeRequest ): Promise<SignInPasswordRequiredResult | SignInCodeSendResult> { this.logger.verbose( "Calling challenge endpoint for sign in.", request.correlationId ); const challengeResponse = await this.customAuthApiClient.signInApi.requestChallenge(request); this.logger.verbose( "Challenge endpoint called for sign in.", challengeResponse.correlation_id || request.correlationId ); if (challengeResponse.challenge_type === ChallengeType.OOB) { // Code is required this.logger.verbose( "Challenge type is oob for sign in.", challengeResponse.correlation_id || request.correlationId ); return createSignInCodeSendResult({ correlationId: challengeResponse.correlation_id || request.correlationId, continuationToken: challengeResponse.continuation_token ?? "", challengeChannel: challengeResponse.challenge_channel ?? "", challengeTargetLabel: challengeResponse.challenge_target_label ?? "", codeLength: challengeResponse.code_length ?? DefaultCustomAuthApiCodeLength, bindingMethod: challengeResponse.binding_method ?? "", }); } if (challengeResponse.challenge_type === ChallengeType.PASSWORD) { // Password is required this.logger.verbose( "Challenge type is password for sign in.", challengeResponse.correlation_id || request.correlationId ); return createSignInPasswordRequiredResult({ correlationId: challengeResponse.correlation_id || request.correlationId, continuationToken: challengeResponse.continuation_token ?? "", }); } this.logger.error( `Unsupported challenge type '${challengeResponse.challenge_type}' for sign in.`, challengeResponse.correlation_id || request.correlationId ); throw new CustomAuthApiError( CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, `Unsupported challenge type '${challengeResponse.challenge_type}'.`, challengeResponse.correlation_id || request.correlationId ); } private getPublicApiIdBySignInScenario( scenario: SignInScenarioType, correlationId: string ): number { switch (scenario) { case SignInScenario.SignInAfterSignUp: return PublicApiId.SIGN_IN_AFTER_SIGN_UP; case SignInScenario.SignInAfterPasswordReset: return PublicApiId.SIGN_IN_AFTER_PASSWORD_RESET; default: throw new UnexpectedError( `Unsupported sign-in scenario '${scenario}'.`, correlationId ); } } private async handleJitRequiredError( error: CustomAuthApiError, telemetryManager: ServerTelemetryManager, correlationId: string ): Promise<SignInJitRequiredResult> { this.logger.verbose( "Auth method registration required for sign in.", correlationId ); // Call register introspect endpoint to get available authentication methods const introspectRequest: RegisterIntrospectRequest = { continuation_token: error.continuationToken ?? "", correlationId: correlationId, telemetryManager, }; this.logger.verbose( "Calling introspect endpoint for getting auth methods.", correlationId ); const introspectResponse = await this.customAuthApiClient.registerApi.introspect( introspectRequest ); this.logger.verbose( "Introspect endpoint called for getting auth methods.", introspectResponse.correlation_id || correlationId ); return createSignInJitRequiredResult({ correlationId: introspectResponse.correlation_id || correlationId, continuationToken: introspectResponse.continuation_token ?? "", authMethods: introspectResponse.methods, }); } private async handleMfaRequiredError( error: CustomAuthApiError, telemetryManager: ServerTelemetryManager, correlationId: string ): Promise<SignInMfaRequiredResult> { this.logger.verbose("MFA required for sign in.", correlationId); // Call sign-in introspect endpoint to get available MFA methods const introspectRequest: SignInIntrospectRequest = { continuation_token: error.continuationToken ?? "", correlationId: correlationId, telemetryManager, }; this.logger.verbose( "Calling introspect endpoint for MFA auth methods.", correlationId ); const introspectResponse = await this.customAuthApiClient.signInApi.requestAuthMethods( introspectRequest ); this.logger.verbose( "Introspect endpoint called for MFA auth methods.", introspectResponse.correlation_id || correlationId ); return createSignInMfaRequiredResult({ correlationId: introspectResponse.correlation_id || correlationId, continuationToken: introspectResponse.continuation_token ?? "", authMethods: introspectResponse.methods, }); } }