/*
|
* 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,
|
});
|
}
|
}
|