Estructura inicial del proyecto

This commit is contained in:
2026-06-02 16:57:08 +00:00
commit 8b306b9afc
9864 changed files with 1435687 additions and 0 deletions

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { UserAccountAttributes } from "./UserAccountAttributes.js";
export type CustomAuthActionInputs = {
correlationId?: string;
};
export type AccountRetrievalInputs = CustomAuthActionInputs;
export type SignInInputs = CustomAuthActionInputs & {
username: string;
password?: string;
scopes?: Array<string>;
claims?: string;
};
export type SignUpInputs = CustomAuthActionInputs & {
username: string;
password?: string;
attributes?: UserAccountAttributes;
};
export type ResetPasswordInputs = CustomAuthActionInputs & {
username: string;
};
export type AccessTokenRetrievalInputs = {
forceRefresh: boolean;
scopes?: Array<string>;
claims?: string;
};
export type SignInWithContinuationTokenInputs = {
scopes?: Array<string>;
claims?: string;
};

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Constants } from "@azure/msal-common/browser";
import { version } from "../packageMetadata.js";
export const GrantType = {
PASSWORD: "password",
OOB: "oob",
CONTINUATION_TOKEN: "continuation_token",
REDIRECT: "redirect",
ATTRIBUTES: "attributes",
MFA_OOB: "mfa_oob",
} as const;
export const ChallengeType = {
PASSWORD: "password",
OOB: "oob",
REDIRECT: "redirect",
PREVERIFIED: "preverified",
} as const;
export const DefaultScopes = [
Constants.OPENID_SCOPE,
Constants.PROFILE_SCOPE,
Constants.OFFLINE_ACCESS_SCOPE,
] as const;
export const HttpHeaderKeys = {
CONTENT_TYPE: "Content-Type",
X_MS_REQUEST_ID: "x-ms-request-id",
} as const;
export const CustomHeaderConstants = {
REQUIRED_PREFIX: "x-",
RESERVED_PREFIXES: [
"x-client-",
"x-ms-",
"x-broker-",
"x-app-",
] as ReadonlyArray<string>,
} as const;
export const DefaultPackageInfo = {
SKU: "msal.browser",
VERSION: version,
OS: "",
CPU: "",
} as const;
export const ResetPasswordPollStatus = {
IN_PROGRESS: "in_progress",
SUCCEEDED: "succeeded",
FAILED: "failed",
NOT_STARTED: "not_started",
} as const;
export const DefaultCustomAuthApiCodeLength = -1; // Default value indicating that the code length is not specified
export const DefaultCustomAuthApiCodeResendIntervalInSec = 300; // seconds
export const PasswordResetPollingTimeoutInMs = 300000; // milliseconds

View File

@ -0,0 +1,158 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { GetAccountResult } from "./get_account/auth_flow/result/GetAccountResult.js";
import { SignInResult } from "./sign_in/auth_flow/result/SignInResult.js";
import { SignUpResult } from "./sign_up/auth_flow/result/SignUpResult.js";
import { ICustomAuthStandardController } from "./controller/ICustomAuthStandardController.js";
import { CustomAuthStandardController } from "./controller/CustomAuthStandardController.js";
import { ICustomAuthPublicClientApplication } from "./ICustomAuthPublicClientApplication.js";
import {
AccountRetrievalInputs,
SignInInputs,
SignUpInputs,
ResetPasswordInputs,
} from "./CustomAuthActionInputs.js";
import { CustomAuthConfiguration } from "./configuration/CustomAuthConfiguration.js";
import { CustomAuthOperatingContext } from "./operating_context/CustomAuthOperatingContext.js";
import { ResetPasswordStartResult } from "./reset_password/auth_flow/result/ResetPasswordStartResult.js";
import { InvalidConfigurationError } from "./core/error/InvalidConfigurationError.js";
import { ChallengeType } from "./CustomAuthConstants.js";
import { PublicClientApplication } from "../app/PublicClientApplication.js";
import {
InvalidAuthority,
InvalidChallengeType,
MissingConfiguration,
} from "./core/error/InvalidConfigurationErrorCodes.js";
export class CustomAuthPublicClientApplication
extends PublicClientApplication
implements ICustomAuthPublicClientApplication
{
private readonly customAuthController: ICustomAuthStandardController;
/**
* Creates a new instance of a PublicClientApplication with the given configuration and controller to start Native authentication flows
* @param {CustomAuthConfiguration} config - A configuration object for the PublicClientApplication instance
* @returns {Promise<ICustomAuthPublicClientApplication>} - A promise that resolves to a CustomAuthPublicClientApplication instance
*/
static async create(
config: CustomAuthConfiguration
): Promise<ICustomAuthPublicClientApplication> {
CustomAuthPublicClientApplication.validateConfig(config);
const customAuthController = new CustomAuthStandardController(
new CustomAuthOperatingContext(config)
);
await customAuthController.initialize();
const app = new CustomAuthPublicClientApplication(
config,
customAuthController
);
return app;
}
private constructor(
config: CustomAuthConfiguration,
controller: ICustomAuthStandardController
) {
super(config, controller);
this.customAuthController = controller;
}
/**
* Gets the current account from the browser cache.
* @param {AccountRetrievalInputs} accountRetrievalInputs?:AccountRetrievalInputs
* @returns {GetAccountResult} - The result of the get account operation
*/
getCurrentAccount(
accountRetrievalInputs?: AccountRetrievalInputs
): GetAccountResult {
return this.customAuthController.getCurrentAccount(
accountRetrievalInputs
);
}
/**
* Initiates the sign-in flow.
* This method results in sign-in completion, or extra actions (password, code, etc.) required to complete the sign-in.
* Create result with error details if any exception thrown.
* @param {SignInInputs} signInInputs - Inputs for the sign-in flow
* @returns {Promise<SignInResult>} - A promise that resolves to SignInResult
*/
signIn(signInInputs: SignInInputs): Promise<SignInResult> {
return this.customAuthController.signIn(signInInputs);
}
/**
* Initiates the sign-up flow.
* This method results in sign-up completion, or extra actions (password, code, etc.) required to complete the sign-up.
* Create result with error details if any exception thrown.
* @param {SignUpInputs} signUpInputs
* @returns {Promise<SignUpResult>} - A promise that resolves to SignUpResult
*/
signUp(signUpInputs: SignUpInputs): Promise<SignUpResult> {
return this.customAuthController.signUp(signUpInputs);
}
/**
* Initiates the reset password flow.
* This method results in triggering extra action (submit code) to complete the reset password.
* Create result with error details if any exception thrown.
* @param {ResetPasswordInputs} resetPasswordInputs - Inputs for the reset password flow
* @returns {Promise<ResetPasswordStartResult>} - A promise that resolves to ResetPasswordStartResult
*/
resetPassword(
resetPasswordInputs: ResetPasswordInputs
): Promise<ResetPasswordStartResult> {
return this.customAuthController.resetPassword(resetPasswordInputs);
}
/**
* Validates the configuration to ensure it is a valid CustomAuthConfiguration object.
* @param {CustomAuthConfiguration} config - The configuration object for the PublicClientApplication.
* @returns {void}
*/
private static validateConfig(config: CustomAuthConfiguration): void {
// Ensure the configuration object has a valid CIAM authority URL.
if (!config) {
throw new InvalidConfigurationError(
MissingConfiguration,
"The configuration is missing."
);
}
if (!config.auth?.authority) {
throw new InvalidConfigurationError(
InvalidAuthority,
`The authority URL '${config.auth?.authority}' is not set.`
);
}
const challengeTypes = config.customAuth.challengeTypes;
if (!!challengeTypes && challengeTypes.length > 0) {
challengeTypes.forEach((challengeType) => {
const lowerCaseChallengeType = challengeType.toLowerCase();
if (
lowerCaseChallengeType !== ChallengeType.PASSWORD &&
lowerCaseChallengeType !== ChallengeType.OOB &&
lowerCaseChallengeType !== ChallengeType.REDIRECT
) {
throw new InvalidConfigurationError(
InvalidChallengeType,
`Challenge type ${challengeType} in the configuration are not valid. Supported challenge types are ${Object.values(
ChallengeType
)}`
);
}
});
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { GetAccountResult } from "./get_account/auth_flow/result/GetAccountResult.js";
import { SignInResult } from "./sign_in/auth_flow/result/SignInResult.js";
import { SignUpResult } from "./sign_up/auth_flow/result/SignUpResult.js";
import {
AccountRetrievalInputs,
ResetPasswordInputs,
SignInInputs,
SignUpInputs,
} from "./CustomAuthActionInputs.js";
import { ResetPasswordStartResult } from "./reset_password/auth_flow/result/ResetPasswordStartResult.js";
import { IPublicClientApplication } from "../app/IPublicClientApplication.js";
export interface ICustomAuthPublicClientApplication
extends IPublicClientApplication {
/**
* Gets the current account from the cache.
* @param {AccountRetrievalInputs} accountRetrievalInputs - Inputs for getting the current cached account
* @returns {GetAccountResult} The result of the operation
*/
getCurrentAccount(
accountRetrievalInputs?: AccountRetrievalInputs
): GetAccountResult;
/**
* Initiates the sign-in flow.
* @param {SignInInputs} signInInputs - Inputs for the sign-in flow
* @returns {Promise<SignInResult>} A promise that resolves to SignInResult
*/
signIn(signInInputs: SignInInputs): Promise<SignInResult>;
/**
* Initiates the sign-up flow.
* @param {SignUpInputs} signUpInputs - Inputs for the sign-up flow
* @returns {Promise<SignUpResult>} A promise that resolves to SignUpResult
*/
signUp(signUpInputs: SignUpInputs): Promise<SignUpResult>;
/**
* Initiates the reset password flow.
* @param {ResetPasswordInputs} resetPasswordInputs - Inputs for the reset password flow
* @returns {Promise<ResetPasswordStartResult>} A promise that resolves to ResetPasswordStartResult
*/
resetPassword(
resetPasswordInputs: ResetPasswordInputs
): Promise<ResetPasswordStartResult>;
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type UserAccountAttributes = Record<string, string> & {
city?: string;
country?: string;
displayName?: string;
flatusername?: string;
givenName?: string;
jobTitle?: string;
postalCode?: string;
state?: string;
streetAddress?: string;
surname?: string;
};

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
BrowserConfiguration,
Configuration,
} from "../../config/Configuration.js";
import { CustomAuthRequestInterceptor } from "./CustomAuthRequestInterceptor.js";
export type CustomAuthOptions = {
challengeTypes?: Array<string>;
authApiProxyUrl: string;
customAuthApiQueryParams?: Record<string, string>;
capabilities?: Array<string>;
requestInterceptor?: CustomAuthRequestInterceptor;
};
export type CustomAuthConfiguration = Configuration & {
customAuth: CustomAuthOptions;
};
export type CustomAuthBrowserConfiguration = BrowserConfiguration & {
customAuth: CustomAuthOptions;
};

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Result type returned by {@link CustomAuthRequestInterceptor.addAdditionalHeaderFields}.
*
* Implementations may return either a synchronous value or a `Promise` resolving to one of
* the following:
* - A `Record<string, string>` of additional headers to add to the outgoing request.
* - `null` (or `undefined`) when no additional headers should be added for the request.
*/
export type CustomAuthAdditionalHeaderFieldsResult =
| Record<string, string>
| null
| undefined;
/**
* Interface for intercepting custom auth network requests in order to attach additional
* headers to outgoing requests.
*
* Implementations are invoked by MSAL before each backend request used by custom auth
* (sign-in, sign-up, reset-password, and register endpoints). Use this hook to integrate
* with third-party fraud and bot-detection SDKs that require custom `x-*` headers.
*
* MSAL applies the following rules when evaluating the headers you provide:
* - Headers must start with `x-` (case-insensitive). Headers that don't start with `x-`
* are ignored.
* - Headers that start with any of the following reserved prefixes are ignored:
* `x-client-`, `x-ms-`, `x-broker-`, `x-app-`.
* - Headers that pass both rules are added to the network request. If a header you
* provide has the same name as one of MSAL's own internal headers, your value takes
* precedence.
*/
export interface CustomAuthRequestInterceptor {
/**
* Returns additional headers to add to a custom-auth network request.
*
* Scope your headers to specific endpoints by inspecting `requestUrl`. Sending
* headers to unrelated endpoints can degrade signal quality and increase false
* positives for fraud/bot-detection vendors.
*
* @param requestUrl - The full URL of the outgoing custom-auth request.
* @returns A record of headers to add (or `null`/`undefined` if no extra headers
* are needed for the request). May be returned synchronously or as a
* `Promise`.
*/
addAdditionalHeaderFields(
requestUrl: URL
):
| CustomAuthAdditionalHeaderFieldsResult
| Promise<CustomAuthAdditionalHeaderFieldsResult>;
}

View File

@ -0,0 +1,604 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { GetAccountResult } from "../get_account/auth_flow/result/GetAccountResult.js";
import { SignInResult } from "../sign_in/auth_flow/result/SignInResult.js";
import { SignUpResult } from "../sign_up/auth_flow/result/SignUpResult.js";
import {
SignInStartParams,
SignInSubmitPasswordParams,
} from "../sign_in/interaction_client/parameter/SignInParams.js";
import { SignInClient } from "../sign_in/interaction_client/SignInClient.js";
import {
AccountRetrievalInputs,
SignInInputs,
SignUpInputs,
ResetPasswordInputs,
CustomAuthActionInputs,
} from "../CustomAuthActionInputs.js";
import { CustomAuthBrowserConfiguration } from "../configuration/CustomAuthConfiguration.js";
import { CustomAuthOperatingContext } from "../operating_context/CustomAuthOperatingContext.js";
import { ICustomAuthStandardController } from "./ICustomAuthStandardController.js";
import { CustomAuthAccountData } from "../get_account/auth_flow/CustomAuthAccountData.js";
import { UnexpectedError } from "../core/error/UnexpectedError.js";
import { ResetPasswordStartResult } from "../reset_password/auth_flow/result/ResetPasswordStartResult.js";
import { CustomAuthAuthority } from "../core/CustomAuthAuthority.js";
import { DefaultPackageInfo } from "../CustomAuthConstants.js";
import {
SIGN_IN_CODE_SEND_RESULT_TYPE,
SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE,
SIGN_IN_COMPLETED_RESULT_TYPE,
SIGN_IN_JIT_REQUIRED_RESULT_TYPE,
SIGN_IN_MFA_REQUIRED_RESULT_TYPE,
} from "../sign_in/interaction_client/result/SignInActionResult.js";
import { SignUpClient } from "../sign_up/interaction_client/SignUpClient.js";
import { CustomAuthInterationClientFactory } from "../core/interaction_client/CustomAuthInterationClientFactory.js";
import {
SIGN_UP_CODE_REQUIRED_RESULT_TYPE,
SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE,
} from "../sign_up/interaction_client/result/SignUpActionResult.js";
import { ICustomAuthApiClient } from "../core/network_client/custom_auth_api/ICustomAuthApiClient.js";
import { CustomAuthApiClient } from "../core/network_client/custom_auth_api/CustomAuthApiClient.js";
import { FetchHttpClient } from "../core/network_client/http_client/FetchHttpClient.js";
import { ResetPasswordClient } from "../reset_password/interaction_client/ResetPasswordClient.js";
import { JitClient } from "../core/interaction_client/jit/JitClient.js";
import { MfaClient } from "../core/interaction_client/mfa/MfaClient.js";
import { NoCachedAccountFoundError } from "../core/error/NoCachedAccountFoundError.js";
import * as ArgumentValidator from "../core/utils/ArgumentValidator.js";
import { UserAlreadySignedInError } from "../core/error/UserAlreadySignedInError.js";
import { CustomAuthSilentCacheClient } from "../get_account/interaction_client/CustomAuthSilentCacheClient.js";
import { UnsupportedEnvironmentError } from "../core/error/UnsupportedEnvironmentError.js";
import { SignInCodeRequiredState } from "../sign_in/auth_flow/state/SignInCodeRequiredState.js";
import { SignInPasswordRequiredState } from "../sign_in/auth_flow/state/SignInPasswordRequiredState.js";
import { SignInCompletedState } from "../sign_in/auth_flow/state/SignInCompletedState.js";
import { AuthMethodRegistrationRequiredState } from "../core/auth_flow/jit/state/AuthMethodRegistrationState.js";
import { MfaAwaitingState } from "../core/auth_flow/mfa/state/MfaState.js";
import { SignUpCodeRequiredState } from "../sign_up/auth_flow/state/SignUpCodeRequiredState.js";
import { SignUpPasswordRequiredState } from "../sign_up/auth_flow/state/SignUpPasswordRequiredState.js";
import { ResetPasswordCodeRequiredState } from "../reset_password/auth_flow/state/ResetPasswordCodeRequiredState.js";
import { StandardController } from "../../controllers/StandardController.js";
/*
* Controller for standard native auth operations.
*/
export class CustomAuthStandardController
extends StandardController
implements ICustomAuthStandardController
{
private readonly signInClient: SignInClient;
private readonly signUpClient: SignUpClient;
private readonly resetPasswordClient: ResetPasswordClient;
private readonly jitClient: JitClient;
private readonly mfaClient: MfaClient;
private readonly cacheClient: CustomAuthSilentCacheClient;
private readonly customAuthConfig: CustomAuthBrowserConfiguration;
private readonly authority: CustomAuthAuthority;
/*
* Constructor for CustomAuthStandardController.
* @param operatingContext - The operating context for the controller.
* @param customAuthApiClient - The client to use for custom auth API operations.
*/
constructor(
operatingContext: CustomAuthOperatingContext,
customAuthApiClient?: ICustomAuthApiClient
) {
super(operatingContext);
if (!this.isBrowserEnvironment) {
this.logger.verbose(
"The SDK can only be used in a browser environment.",
""
);
throw new UnsupportedEnvironmentError();
}
this.logger = this.logger.clone(
DefaultPackageInfo.SKU,
DefaultPackageInfo.VERSION
);
this.customAuthConfig = operatingContext.getCustomAuthConfig();
this.authority = new CustomAuthAuthority(
this.customAuthConfig.auth.authority,
this.customAuthConfig,
this.networkClient,
this.browserStorage,
this.logger,
this.performanceClient,
this.customAuthConfig.customAuth?.authApiProxyUrl
);
const interactionClientFactory = new CustomAuthInterationClientFactory(
this.customAuthConfig,
this.browserStorage,
this.browserCrypto,
this.logger,
this.eventHandler,
this.navigationClient,
this.performanceClient,
customAuthApiClient ??
new CustomAuthApiClient(
this.authority.getCustomAuthApiDomain(),
this.customAuthConfig.auth.clientId,
new FetchHttpClient(this.logger),
this.customAuthConfig.customAuth?.capabilities?.join(" "),
this.customAuthConfig.customAuth?.customAuthApiQueryParams,
this.customAuthConfig.customAuth?.requestInterceptor,
this.logger
),
this.authority
);
this.signInClient = interactionClientFactory.create(SignInClient);
this.signUpClient = interactionClientFactory.create(SignUpClient);
this.resetPasswordClient =
interactionClientFactory.create(ResetPasswordClient);
this.jitClient = interactionClientFactory.create(JitClient);
this.mfaClient = interactionClientFactory.create(MfaClient);
this.cacheClient = interactionClientFactory.create(
CustomAuthSilentCacheClient
);
}
/*
* Gets the current account from the cache.
* @param accountRetrievalInputs - Inputs for getting the current cached account
* @returns {GetAccountResult} The account result
*/
getCurrentAccount(
accountRetrievalInputs?: AccountRetrievalInputs
): GetAccountResult {
const correlationId = this.getCorrelationId(accountRetrievalInputs);
try {
this.logger.verbose("Getting current account data.", correlationId);
const account = this.cacheClient.getCurrentAccount(correlationId);
if (account) {
this.logger.verbose("Account data found.", correlationId);
return new GetAccountResult(
new CustomAuthAccountData(
account,
this.customAuthConfig,
this.cacheClient,
this.logger,
correlationId
)
);
}
throw new NoCachedAccountFoundError(correlationId);
} catch (error) {
this.logger.errorPii(
`An error occurred during getting current account: '${error}'`,
correlationId
);
return GetAccountResult.createWithError(error);
}
}
/*
* Signs the user in.
* @param signInInputs - Inputs for signing in the user.
* @returns {Promise<SignInResult>} The result of the operation.
*/
async signIn(signInInputs: SignInInputs): Promise<SignInResult> {
const correlationId = this.getCorrelationId(signInInputs);
try {
ArgumentValidator.ensureArgumentIsNotNullOrUndefined(
"signInInputs",
signInInputs,
correlationId
);
ArgumentValidator.ensureArgumentIsNotEmptyString(
"signInInputs.username",
signInInputs.username,
correlationId
);
this.ensureUserNotSignedIn(correlationId);
if (signInInputs.claims) {
ArgumentValidator.ensureArgumentIsJSONString(
"signInInputs.claims",
signInInputs.claims,
correlationId
);
}
// start the signin flow
const signInStartParams: SignInStartParams = {
clientId: this.customAuthConfig.auth.clientId,
correlationId: correlationId,
challengeType:
this.customAuthConfig.customAuth.challengeTypes ?? [],
username: signInInputs.username,
password: signInInputs.password,
};
this.logger.verbose(
`Starting sign-in flow '${
!!signInInputs.password ? "with" : "without"
}' password.`,
correlationId
);
const startResult = await this.signInClient.start(
signInStartParams
);
this.logger.verbose("Sign-in flow started.", correlationId);
if (startResult.type === SIGN_IN_CODE_SEND_RESULT_TYPE) {
// require code
this.logger.verbose(
"Code required for sign-in.",
correlationId
);
return new SignInResult(
new SignInCodeRequiredState({
correlationId: startResult.correlationId,
continuationToken: startResult.continuationToken,
logger: this.logger,
config: this.customAuthConfig,
signInClient: this.signInClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: signInInputs.username,
codeLength: startResult.codeLength,
scopes: signInInputs.scopes ?? [],
claims: signInInputs.claims,
})
);
} else if (
startResult.type === SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE
) {
// require password
this.logger.verbose(
"Password required for sign-in.",
correlationId
);
if (!signInInputs.password) {
this.logger.verbose(
"Password required but not provided. Returning password required state.",
correlationId
);
return new SignInResult(
new SignInPasswordRequiredState({
correlationId: startResult.correlationId,
continuationToken: startResult.continuationToken,
logger: this.logger,
config: this.customAuthConfig,
signInClient: this.signInClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: signInInputs.username,
scopes: signInInputs.scopes ?? [],
claims: signInInputs.claims,
})
);
}
this.logger.verbose(
"Submitting password for sign-in.",
correlationId
);
// if the password is provided, then try to get token silently.
const submitPasswordParams: SignInSubmitPasswordParams = {
clientId: this.customAuthConfig.auth.clientId,
correlationId: correlationId,
challengeType:
this.customAuthConfig.customAuth.challengeTypes ?? [],
scopes: signInInputs.scopes ?? [],
continuationToken: startResult.continuationToken,
password: signInInputs.password,
username: signInInputs.username,
claims: signInInputs.claims,
};
const submitPasswordResult =
await this.signInClient.submitPassword(
submitPasswordParams
);
this.logger.verbose("Sign-in flow completed.", correlationId);
if (
submitPasswordResult.type === SIGN_IN_COMPLETED_RESULT_TYPE
) {
const accountInfo = new CustomAuthAccountData(
submitPasswordResult.authenticationResult.account,
this.customAuthConfig,
this.cacheClient,
this.logger,
correlationId
);
return new SignInResult(
new SignInCompletedState(),
accountInfo
);
} else if (
submitPasswordResult.type ===
SIGN_IN_JIT_REQUIRED_RESULT_TYPE
) {
// Authentication method registration is required - create AuthMethodRegistrationRequiredState
this.logger.verbose(
"Authentication method registration required for sign-in.",
correlationId
);
return new SignInResult(
new AuthMethodRegistrationRequiredState({
correlationId: correlationId,
continuationToken:
submitPasswordResult.continuationToken,
logger: this.logger,
config: this.customAuthConfig,
jitClient: this.jitClient,
cacheClient: this.cacheClient,
authMethods: submitPasswordResult.authMethods,
username: signInInputs.username,
scopes: signInInputs.scopes ?? [],
claims: signInInputs.claims,
})
);
} else if (
submitPasswordResult.type ===
SIGN_IN_MFA_REQUIRED_RESULT_TYPE
) {
// MFA is required - create MfaAwaitingState
this.logger.verbose(
"MFA required for sign-in.",
correlationId
);
return new SignInResult(
new MfaAwaitingState({
correlationId: correlationId,
continuationToken:
submitPasswordResult.continuationToken,
logger: this.logger,
config: this.customAuthConfig,
mfaClient: this.mfaClient,
cacheClient: this.cacheClient,
scopes: signInInputs.scopes ?? [],
authMethods: submitPasswordResult.authMethods ?? [],
})
);
} else {
// Unexpected result type
const result = submitPasswordResult as { type: string };
const error = new Error(
`Unexpected result type: ${result.type}`
);
return SignInResult.createWithError(error);
}
}
this.logger.error(
"Unexpected sign-in result type. Returning error.",
correlationId
);
throw new UnexpectedError(
"Unknow sign-in result type",
correlationId
);
} catch (error) {
this.logger.errorPii(
`An error occurred during starting sign-in: '${error}'`,
correlationId
);
return SignInResult.createWithError(error);
}
}
/*
* Signs the user up.
* @param signUpInputs - Inputs for signing up the user.
* @returns {Promise<SignUpResult>} The result of the operation
*/
async signUp(signUpInputs: SignUpInputs): Promise<SignUpResult> {
const correlationId = this.getCorrelationId(signUpInputs);
try {
ArgumentValidator.ensureArgumentIsNotNullOrUndefined(
"signUpInputs",
signUpInputs,
correlationId
);
ArgumentValidator.ensureArgumentIsNotEmptyString(
"signUpInputs.username",
signUpInputs.username,
correlationId
);
this.ensureUserNotSignedIn(correlationId);
this.logger.verbose(
`Starting sign-up flow'${
!!signUpInputs.password
? ` with ${
!!signUpInputs.attributes
? "password and attributes"
: "password"
}`
: ""
}'.`,
correlationId
);
const startResult = await this.signUpClient.start({
clientId: this.customAuthConfig.auth.clientId,
correlationId: correlationId,
challengeType:
this.customAuthConfig.customAuth.challengeTypes ?? [],
username: signUpInputs.username,
password: signUpInputs.password,
attributes: signUpInputs.attributes,
});
this.logger.verbose("Sign-up flow started.", correlationId);
if (startResult.type === SIGN_UP_CODE_REQUIRED_RESULT_TYPE) {
// Code required
this.logger.verbose(
"Code required for sign-up.",
correlationId
);
return new SignUpResult(
new SignUpCodeRequiredState({
correlationId: startResult.correlationId,
continuationToken: startResult.continuationToken,
logger: this.logger,
config: this.customAuthConfig,
signInClient: this.signInClient,
signUpClient: this.signUpClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: signUpInputs.username,
codeLength: startResult.codeLength,
codeResendInterval: startResult.interval,
})
);
} else if (
startResult.type === SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE
) {
// Password required
this.logger.verbose(
"Password required for sign-up.",
correlationId
);
return new SignUpResult(
new SignUpPasswordRequiredState({
correlationId: startResult.correlationId,
continuationToken: startResult.continuationToken,
logger: this.logger,
config: this.customAuthConfig,
signInClient: this.signInClient,
signUpClient: this.signUpClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: signUpInputs.username,
})
);
}
this.logger.error(
"Unexpected sign-up result type. Returning error.",
correlationId
);
throw new UnexpectedError(
"Unknown sign-up result type",
correlationId
);
} catch (error) {
this.logger.errorPii(
`An error occurred during starting sign-up: '${error}'`,
correlationId
);
return SignUpResult.createWithError(error);
}
}
/*
* Resets the user's password.
* @param resetPasswordInputs - Inputs for resetting the user's password.
* @returns {Promise<ResetPasswordStartResult>} The result of the operation.
*/
async resetPassword(
resetPasswordInputs: ResetPasswordInputs
): Promise<ResetPasswordStartResult> {
const correlationId = this.getCorrelationId(resetPasswordInputs);
try {
ArgumentValidator.ensureArgumentIsNotNullOrUndefined(
"resetPasswordInputs",
resetPasswordInputs,
correlationId
);
ArgumentValidator.ensureArgumentIsNotEmptyString(
"resetPasswordInputs.username",
resetPasswordInputs.username,
correlationId
);
this.ensureUserNotSignedIn(correlationId);
this.logger.verbose("Starting password-reset flow.", correlationId);
const startResult = await this.resetPasswordClient.start({
clientId: this.customAuthConfig.auth.clientId,
correlationId: correlationId,
challengeType:
this.customAuthConfig.customAuth.challengeTypes ?? [],
username: resetPasswordInputs.username,
});
this.logger.verbose("Password-reset flow started.", correlationId);
return new ResetPasswordStartResult(
new ResetPasswordCodeRequiredState({
correlationId: startResult.correlationId,
continuationToken: startResult.continuationToken,
logger: this.logger,
config: this.customAuthConfig,
signInClient: this.signInClient,
resetPasswordClient: this.resetPasswordClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: resetPasswordInputs.username,
codeLength: startResult.codeLength,
})
);
} catch (error) {
this.logger.errorPii(
`An error occurred during starting reset-password: '${error}'`,
correlationId
);
return ResetPasswordStartResult.createWithError(error);
}
}
private getCorrelationId(
actionInputs: CustomAuthActionInputs | undefined
): string {
return (
actionInputs?.correlationId || this.browserCrypto.createNewGuid()
);
}
private ensureUserNotSignedIn(correlationId: string): void {
const account = this.getCurrentAccount({
correlationId: correlationId,
});
if (account && !!account.data) {
this.logger.error("User has already signed in.", correlationId);
throw new UserAlreadySignedInError(correlationId);
}
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { GetAccountResult } from "../get_account/auth_flow/result/GetAccountResult.js";
import { SignInResult } from "../sign_in/auth_flow/result/SignInResult.js";
import { SignUpResult } from "../sign_up/auth_flow/result/SignUpResult.js";
import {
AccountRetrievalInputs,
ResetPasswordInputs,
SignInInputs,
SignUpInputs,
} from "../CustomAuthActionInputs.js";
import { ResetPasswordStartResult } from "../reset_password/auth_flow/result/ResetPasswordStartResult.js";
import { IController } from "../../controllers/IController.js";
/*
* Controller interface for standard authentication operations.
*/
export interface ICustomAuthStandardController extends IController {
/*
* Gets the current account from the cache.
* @param accountRetrievalInputs - Inputs for getting the current cached account
* @returns - The result of the operation
*/
getCurrentAccount(
accountRetrievalInputs?: AccountRetrievalInputs
): GetAccountResult;
/*
* Signs the current user out.
* @param signInInputs - Inputs for signing in.
* @returns The result of the operation.
*/
signIn(signInInputs: SignInInputs): Promise<SignInResult>;
/*
* Signs the current user up.
* @param signUpInputs - Inputs for signing up.
* @returns The result of the operation.
*/
signUp(signUpInputs: SignUpInputs): Promise<SignUpResult>;
/*
* Resets the password for the current user.
* @param resetPasswordInputs - Inputs for resetting the password.
* @returns The result of the operation.
*/
resetPassword(
resetPasswordInputs: ResetPasswordInputs
): Promise<ResetPasswordStartResult>;
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
Authority,
AuthorityOptions,
INetworkModule,
IPerformanceClient,
Logger,
} from "@azure/msal-common/browser";
import * as CustomAuthApiEndpoint from "./network_client/custom_auth_api/CustomAuthApiEndpoint.js";
import { buildUrl } from "./utils/UrlUtils.js";
import { BrowserConfiguration } from "../../config/Configuration.js";
import { BrowserCacheManager } from "../../cache/BrowserCacheManager.js";
/**
* Authority class which can be used to create an authority object for Custom Auth features.
*/
export class CustomAuthAuthority extends Authority {
/**
* Constructor for the Custom Auth Authority.
* @param authority - The authority URL for the authority.
* @param networkInterface - The network interface implementation to make requests.
* @param cacheManager - The cache manager.
* @param authorityOptions - The options for the authority.
* @param logger - The logger for the authority.
* @param customAuthProxyDomain - The custom auth proxy domain.
*/
constructor(
authority: string,
config: BrowserConfiguration,
networkInterface: INetworkModule,
cacheManager: BrowserCacheManager,
logger: Logger,
performanceClient: IPerformanceClient,
private customAuthProxyDomain?: string
) {
const ciamAuthorityUrl =
CustomAuthAuthority.transformCIAMAuthority(authority);
const authorityOptions: AuthorityOptions = {
protocolMode: config.system.protocolMode,
OIDCOptions: config.auth.OIDCOptions,
knownAuthorities: config.auth.knownAuthorities,
cloudDiscoveryMetadata: config.auth.cloudDiscoveryMetadata,
authorityMetadata: config.auth.authorityMetadata,
};
super(
ciamAuthorityUrl,
networkInterface,
cacheManager,
authorityOptions,
logger,
"",
performanceClient
);
// Set the metadata for the authority
const metadataEntity = {
aliases: [this.hostnameAndPort],
preferred_cache: this.getPreferredCache(),
preferred_network: this.hostnameAndPort,
canonical_authority: this.canonicalAuthority,
authorization_endpoint: "",
token_endpoint: this.tokenEndpoint,
end_session_endpoint: "",
issuer: "",
aliasesFromNetwork: false,
endpointsFromNetwork: false,
/*
* give max value to make sure it doesn't expire,
* as we only initiate the authority metadata entity once and it doesn't change
*/
expiresAt: Number.MAX_SAFE_INTEGER,
jwks_uri: "",
};
const cacheKey = this.cacheManager.generateAuthorityMetadataCacheKey(
metadataEntity.preferred_cache,
this.correlationId
);
cacheManager.setAuthorityMetadata(
cacheKey,
metadataEntity,
this.correlationId
);
}
/**
* Gets the custom auth endpoint.
* The open id configuration doesn't have the correct endpoint for the auth APIs.
* We need to generate the endpoint manually based on the authority URL.
* @returns The custom auth endpoint
*/
getCustomAuthApiDomain(): string {
/*
* The customAuthProxyDomain is used to resolve the CORS issue when calling the auth APIs.
* If the customAuthProxyDomain is not provided, we will generate the auth API domain based on the authority URL.
*/
return !this.customAuthProxyDomain
? this.canonicalAuthority
: this.customAuthProxyDomain;
}
override getPreferredCache(): string {
return this.canonicalAuthorityUrlComponents.HostNameAndPort;
}
override get tokenEndpoint(): string {
const endpointUrl = buildUrl(
this.getCustomAuthApiDomain(),
CustomAuthApiEndpoint.SIGNIN_TOKEN
);
return endpointUrl.href;
}
}

View File

@ -0,0 +1,179 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
CustomAuthApiError,
RedirectError,
} from "../error/CustomAuthApiError.js";
import { CustomAuthError } from "../error/CustomAuthError.js";
import { NoCachedAccountFoundError } from "../error/NoCachedAccountFoundError.js";
import { InvalidArgumentError } from "../error/InvalidArgumentError.js";
import * as CustomAuthApiErrorCode from "../network_client/custom_auth_api/types/ApiErrorCodes.js";
import * as CustomAuthApiSuberror from "../network_client/custom_auth_api/types/ApiSuberrors.js";
/**
* Base class for all auth flow errors.
*/
export abstract class AuthFlowErrorBase {
constructor(public errorData: CustomAuthError) {}
protected isUserNotFoundError(): boolean {
return this.errorData.error === CustomAuthApiErrorCode.USER_NOT_FOUND;
}
protected isUserInvalidError(): boolean {
return (
(this.errorData instanceof InvalidArgumentError &&
this.errorData.errorDescription?.includes("username")) ||
(this.errorData instanceof CustomAuthApiError &&
!!this.errorData.errorDescription?.includes(
"username parameter is empty or not valid"
) &&
!!this.errorData.errorCodes?.includes(90100))
);
}
protected isUnsupportedChallengeTypeError(): boolean {
return (
(this.errorData.error === CustomAuthApiErrorCode.INVALID_REQUEST &&
(this.errorData.errorDescription?.includes(
"The challenge_type list parameter contains an unsupported challenge type"
) ??
false)) ||
this.errorData.error ===
CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE
);
}
protected isPasswordIncorrectError(): boolean {
const isIncorrectPassword =
this.errorData.error === CustomAuthApiErrorCode.INVALID_GRANT &&
this.errorData instanceof CustomAuthApiError &&
(this.errorData.errorCodes ?? []).includes(50126);
const isPasswordEmpty =
this.errorData instanceof InvalidArgumentError &&
this.errorData.errorDescription?.includes("password") === true;
return isIncorrectPassword || isPasswordEmpty;
}
protected isInvalidCodeError(): boolean {
return (
(this.errorData.error === CustomAuthApiErrorCode.INVALID_GRANT &&
this.errorData instanceof CustomAuthApiError &&
this.errorData.subError ===
CustomAuthApiSuberror.INVALID_OOB_VALUE) ||
(this.errorData instanceof InvalidArgumentError &&
(this.errorData.errorDescription?.includes("code") ||
this.errorData.errorDescription?.includes("challenge")) ===
true)
);
}
protected isRedirectError(): boolean {
return this.errorData instanceof RedirectError;
}
protected isInvalidNewPasswordError(): boolean {
const invalidPasswordSubErrors = new Set<string>([
CustomAuthApiSuberror.PASSWORD_BANNED,
CustomAuthApiSuberror.PASSWORD_IS_INVALID,
CustomAuthApiSuberror.PASSWORD_RECENTLY_USED,
CustomAuthApiSuberror.PASSWORD_TOO_LONG,
CustomAuthApiSuberror.PASSWORD_TOO_SHORT,
CustomAuthApiSuberror.PASSWORD_TOO_WEAK,
]);
return (
this.errorData instanceof CustomAuthApiError &&
this.errorData.error === CustomAuthApiErrorCode.INVALID_GRANT &&
invalidPasswordSubErrors.has(this.errorData.subError ?? "")
);
}
protected isUserAlreadyExistsError(): boolean {
return (
this.errorData instanceof CustomAuthApiError &&
this.errorData.error === CustomAuthApiErrorCode.USER_ALREADY_EXISTS
);
}
protected isAttributeRequiredError(): boolean {
return (
this.errorData instanceof CustomAuthApiError &&
this.errorData.error === CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED
);
}
protected isAttributeValidationFailedError(): boolean {
return (
(this.errorData instanceof CustomAuthApiError &&
this.errorData.error === CustomAuthApiErrorCode.INVALID_GRANT &&
this.errorData.subError ===
CustomAuthApiSuberror.ATTRIBUTE_VALIATION_FAILED) ||
(this.errorData instanceof InvalidArgumentError &&
this.errorData.errorDescription?.includes("attributes") ===
true)
);
}
protected isNoCachedAccountFoundError(): boolean {
return this.errorData instanceof NoCachedAccountFoundError;
}
protected isTokenExpiredError(): boolean {
return (
this.errorData instanceof CustomAuthApiError &&
this.errorData.error === CustomAuthApiErrorCode.EXPIRED_TOKEN
);
}
/**
* @todo verify the password change required error can be detected once the MFA is in place.
* This error will be raised during signin and refresh tokens when calling /token endpoint.
*/
protected isPasswordResetRequiredError(): boolean {
return (
this.errorData instanceof CustomAuthApiError &&
this.errorData.error === CustomAuthApiErrorCode.INVALID_REQUEST &&
this.errorData.errorCodes?.includes(50142) === true
);
}
protected isInvalidInputError(): boolean {
return (
this.errorData instanceof CustomAuthApiError &&
this.errorData.error === CustomAuthApiErrorCode.INVALID_REQUEST &&
this.errorData.errorCodes?.includes(901001) === true
);
}
protected isVerificationContactBlockedError(): boolean {
return (
this.errorData instanceof CustomAuthApiError &&
this.errorData.error === CustomAuthApiErrorCode.ACCESS_DENIED &&
this.errorData.subError ===
CustomAuthApiSuberror.PROVIDER_BLOCKED_BY_REPUTATION
);
}
}
export abstract class AuthActionErrorBase extends AuthFlowErrorBase {
/**
* Checks if the error is due to the expired continuation token.
* @returns {boolean} True if the error is due to the expired continuation token, false otherwise.
*/
isTokenExpired(): boolean {
return this.isTokenExpiredError();
}
/**
* Check if client app supports the challenge type configured in Entra.
* @returns {boolean} True if client app doesn't support the challenge type configured in Entra, "loginPopup" function is required to continue the operation.
*/
isRedirectRequired(): boolean {
return this.isRedirectError();
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "@azure/msal-common/browser";
import { CustomAuthError } from "../error/CustomAuthError.js";
import { MsalCustomAuthError } from "../error/MsalCustomAuthError.js";
import { UnexpectedError } from "../error/UnexpectedError.js";
import { AuthFlowErrorBase } from "./AuthFlowErrorBase.js";
import { AuthFlowStateBase } from "./AuthFlowState.js";
/*
* Base class for a result of an authentication operation.
* @typeParam TState - The type of the auth flow state.
* @typeParam TError - The type of error.
* @typeParam TData - The type of the result data.
*/
export abstract class AuthFlowResultBase<
TState extends AuthFlowStateBase,
TError extends AuthFlowErrorBase,
TData = void
> {
/*
*constructor for ResultBase
* @param state - The state.
* @param data - The result data.
*/
constructor(public state: TState, public data?: TData) {}
/*
* The error that occurred during the authentication operation.
*/
error?: TError;
/*
* Creates a CustomAuthError with an error.
* @param error - The error that occurred.
* @returns The auth error.
*/
protected static createErrorData(error: unknown): CustomAuthError {
if (error instanceof CustomAuthError) {
return error;
} else if (error instanceof AuthError) {
const errorCodes: Array<number> = [];
if ("errorNo" in error) {
if (typeof error.errorNo === "string") {
const code = Number(error.errorNo);
if (!isNaN(code)) {
errorCodes.push(code);
}
} else if (typeof error.errorNo === "number") {
errorCodes.push(error.errorNo);
}
}
return new MsalCustomAuthError(
error.errorCode,
error.errorMessage,
error.subError,
errorCodes,
error.correlationId
);
} else {
return new UnexpectedError(error);
}
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { InvalidArgumentError } from "../error/InvalidArgumentError.js";
import { CustomAuthBrowserConfiguration } from "../../configuration/CustomAuthConfiguration.js";
import { Logger } from "@azure/msal-common/browser";
import { ensureArgumentIsNotEmptyString } from "../utils/ArgumentValidator.js";
import { DefaultCustomAuthApiCodeLength } from "../../CustomAuthConstants.js";
export interface AuthFlowActionRequiredStateParameters {
correlationId: string;
logger: Logger;
config: CustomAuthBrowserConfiguration;
continuationToken?: string;
}
/**
* Base class for the state of an authentication flow.
*/
export abstract class AuthFlowStateBase {
/**
* The type of the state.
*/
abstract stateType: string;
}
/**
* Base class for the action requried state in an authentication flow.
*/
export abstract class AuthFlowActionRequiredStateBase<
TParameter extends AuthFlowActionRequiredStateParameters
> extends AuthFlowStateBase {
/**
* Creates a new instance of AuthFlowActionRequiredStateBase.
* @param stateParameters The parameters for the auth state.
*/
constructor(protected readonly stateParameters: TParameter) {
ensureArgumentIsNotEmptyString(
"correlationId",
stateParameters.correlationId
);
super();
}
protected ensureCodeIsValid(code: string, codeLength: number): void {
if (
codeLength !== DefaultCustomAuthApiCodeLength &&
(!code || code.length !== codeLength)
) {
this.stateParameters.logger.error(
"Code parameter is not provided or invalid for authentication flow.",
this.stateParameters.correlationId
);
throw new InvalidArgumentError(
"code",
this.stateParameters.correlationId
);
}
}
protected ensurePasswordIsNotEmpty(password: string): void {
if (!password) {
this.stateParameters.logger.error(
"Password parameter is not provided for authentication flow.",
this.stateParameters.correlationId
);
throw new InvalidArgumentError(
"password",
this.stateParameters.correlationId
);
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
// Sign in state types
export const SIGN_IN_CODE_REQUIRED_STATE_TYPE = "SignInCodeRequiredState";
export const SIGN_IN_PASSWORD_REQUIRED_STATE_TYPE =
"SignInPasswordRequiredState";
export const SIGN_IN_CONTINUATION_STATE_TYPE = "SignInContinuationState";
export const SIGN_IN_COMPLETED_STATE_TYPE = "SignInCompletedState";
export const SIGN_IN_FAILED_STATE_TYPE = "SignInFailedState";
// Sign up state types
export const SIGN_UP_CODE_REQUIRED_STATE_TYPE = "SignUpCodeRequiredState";
export const SIGN_UP_PASSWORD_REQUIRED_STATE_TYPE =
"SignUpPasswordRequiredState";
export const SIGN_UP_ATTRIBUTES_REQUIRED_STATE_TYPE =
"SignUpAttributesRequiredState";
export const SIGN_UP_COMPLETED_STATE_TYPE = "SignUpCompletedState";
export const SIGN_UP_FAILED_STATE_TYPE = "SignUpFailedState";
// Reset password state types
export const RESET_PASSWORD_CODE_REQUIRED_STATE_TYPE =
"ResetPasswordCodeRequiredState";
export const RESET_PASSWORD_PASSWORD_REQUIRED_STATE_TYPE =
"ResetPasswordPasswordRequiredState";
export const RESET_PASSWORD_COMPLETED_STATE_TYPE =
"ResetPasswordCompletedState";
export const RESET_PASSWORD_FAILED_STATE_TYPE = "ResetPasswordFailedState";
// Get account state types
export const GET_ACCOUNT_COMPLETED_STATE_TYPE = "GetAccountCompletedState";
export const GET_ACCOUNT_FAILED_STATE_TYPE = "GetAccountFailedState";
// Get access token state types
export const GET_ACCESS_TOKEN_COMPLETED_STATE_TYPE =
"GetAccessTokenCompletedState";
export const GET_ACCESS_TOKEN_FAILED_STATE_TYPE = "GetAccessTokenFailedState";
// Sign out state types
export const SIGN_OUT_COMPLETED_STATE_TYPE = "SignOutCompletedState";
export const SIGN_OUT_FAILED_STATE_TYPE = "SignOutFailedState";
// MFA state types
export const MFA_AWAITING_STATE_TYPE = "MfaAwaitingState";
export const MFA_VERIFICATION_REQUIRED_STATE_TYPE =
"MfaVerificationRequiredState";
export const MFA_COMPLETED_STATE_TYPE = "MfaCompletedState";
export const MFA_FAILED_STATE_TYPE = "MfaFailedState";
// Auth method registration (JIT) state types
export const AUTH_METHOD_REGISTRATION_REQUIRED_STATE_TYPE =
"AuthMethodRegistrationRequiredState";
export const AUTH_METHOD_VERIFICATION_REQUIRED_STATE_TYPE =
"AuthMethodVerificationRequiredState";
export const AUTH_METHOD_REGISTRATION_COMPLETED_STATE_TYPE =
"AuthMethodRegistrationCompletedState";
export const AUTH_METHOD_REGISTRATION_FAILED_STATE_TYPE =
"AuthMethodRegistrationFailedState";

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthenticationMethod } from "../../network_client/custom_auth_api/types/ApiResponseTypes.js";
/**
* Details for an authentication method to be registered.
*/
export interface AuthMethodDetails {
/**
* The authentication method type to register.
*/
authMethodType: AuthenticationMethod;
/**
* The verification contact (email, phone number) for the authentication method.
*/
verificationContact: string;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthActionErrorBase } from "../../AuthFlowErrorBase.js";
/**
* Error that occurred during authentication method challenge request.
*/
export class AuthMethodRegistrationChallengeMethodError extends AuthActionErrorBase {
/**
* Checks if the input for auth method registration is incorrect.
* @returns true if the input is incorrect, false otherwise.
*/
isInvalidInput(): boolean {
return this.isInvalidInputError();
}
/**
* Checks if the error is due to the verification contact (e.g., phone number or email) being blocked. Consider using a different email/phone number or a different authentication method.
* @returns true if the error is due to the verification contact being blocked, false otherwise.
*/
isVerificationContactBlocked(): boolean {
return this.isVerificationContactBlockedError();
}
}
/**
* Error that occurred during authentication method challenge submission.
*/
export class AuthMethodRegistrationSubmitChallengeError extends AuthActionErrorBase {
/**
* Checks if the submitted challenge code is incorrect.
* @returns true if the challenge code is incorrect, false otherwise.
*/
isIncorrectChallenge(): boolean {
return this.isInvalidCodeError();
}
}

View File

@ -0,0 +1,92 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../AuthFlowResultBase.js";
import { AuthMethodRegistrationChallengeMethodError } from "../error_type/AuthMethodRegistrationError.js";
import type { AuthMethodVerificationRequiredState } from "../state/AuthMethodRegistrationState.js";
import { CustomAuthAccountData } from "../../../../get_account/auth_flow/CustomAuthAccountData.js";
import { AuthMethodRegistrationCompletedState } from "../state/AuthMethodRegistrationCompletedState.js";
import { AuthMethodRegistrationFailedState } from "../state/AuthMethodRegistrationFailedState.js";
import {
AUTH_METHOD_VERIFICATION_REQUIRED_STATE_TYPE,
AUTH_METHOD_REGISTRATION_COMPLETED_STATE_TYPE,
AUTH_METHOD_REGISTRATION_FAILED_STATE_TYPE,
} from "../../AuthFlowStateTypes.js";
/**
* Result of challenging an authentication method for registration.
* Uses base state type to avoid circular dependencies.
*/
export class AuthMethodRegistrationChallengeMethodResult extends AuthFlowResultBase<
AuthMethodRegistrationChallengeMethodResultState,
AuthMethodRegistrationChallengeMethodError,
CustomAuthAccountData
> {
/**
* Creates an AuthMethodRegistrationChallengeMethodResult with an error.
* @param error The error that occurred.
* @returns The AuthMethodRegistrationChallengeMethodResult with error.
*/
static createWithError(
error: unknown
): AuthMethodRegistrationChallengeMethodResult {
const result = new AuthMethodRegistrationChallengeMethodResult(
new AuthMethodRegistrationFailedState()
);
result.error = new AuthMethodRegistrationChallengeMethodError(
AuthMethodRegistrationChallengeMethodResult.createErrorData(error)
);
return result;
}
/**
* Checks if the result indicates that verification is required.
* @returns true if verification is required, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isVerificationRequired(): this is AuthMethodRegistrationChallengeMethodResult & {
state: AuthMethodVerificationRequiredState;
} {
return (
this.state.stateType ===
AUTH_METHOD_VERIFICATION_REQUIRED_STATE_TYPE
);
}
/**
* Checks if the result indicates that registration is completed (fast-pass scenario).
* @returns true if registration is completed, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isCompleted(): this is AuthMethodRegistrationChallengeMethodResult & {
state: AuthMethodRegistrationCompletedState;
} {
return (
this.state.stateType ===
AUTH_METHOD_REGISTRATION_COMPLETED_STATE_TYPE
);
}
/**
* Checks if the result is in a failed state.
* @returns true if the result is failed, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isFailed(): this is AuthMethodRegistrationChallengeMethodResult & {
state: AuthMethodRegistrationFailedState;
} {
return (
this.state.stateType === AUTH_METHOD_REGISTRATION_FAILED_STATE_TYPE
);
}
}
/**
* Type definition for possible states in AuthMethodRegistrationChallengeMethodResult.
*/
export type AuthMethodRegistrationChallengeMethodResultState =
| AuthMethodVerificationRequiredState
| AuthMethodRegistrationCompletedState
| AuthMethodRegistrationFailedState;

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../AuthFlowResultBase.js";
import { AuthMethodRegistrationSubmitChallengeError } from "../error_type/AuthMethodRegistrationError.js";
import { CustomAuthAccountData } from "../../../../get_account/auth_flow/CustomAuthAccountData.js";
import { AuthMethodRegistrationFailedState } from "../state/AuthMethodRegistrationFailedState.js";
import { AuthMethodRegistrationCompletedState } from "../state/AuthMethodRegistrationCompletedState.js";
import {
AUTH_METHOD_REGISTRATION_COMPLETED_STATE_TYPE,
AUTH_METHOD_REGISTRATION_FAILED_STATE_TYPE,
} from "../../AuthFlowStateTypes.js";
/**
* Result of submitting a challenge for authentication method registration.
*/
export class AuthMethodRegistrationSubmitChallengeResult extends AuthFlowResultBase<
AuthMethodRegistrationSubmitChallengeResultState,
AuthMethodRegistrationSubmitChallengeError,
CustomAuthAccountData
> {
/**
* Creates an AuthMethodRegistrationSubmitChallengeResult with an error.
* @param error The error that occurred.
* @returns The AuthMethodRegistrationSubmitChallengeResult with error.
*/
static createWithError(
error: unknown
): AuthMethodRegistrationSubmitChallengeResult {
const result = new AuthMethodRegistrationSubmitChallengeResult(
new AuthMethodRegistrationFailedState()
);
result.error = new AuthMethodRegistrationSubmitChallengeError(
AuthMethodRegistrationSubmitChallengeResult.createErrorData(error)
);
return result;
}
/**
* Checks if the result indicates that registration is completed.
* @returns true if registration is completed, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isCompleted(): this is AuthMethodRegistrationSubmitChallengeResult & {
state: AuthMethodRegistrationCompletedState;
} {
return (
this.state.stateType ===
AUTH_METHOD_REGISTRATION_COMPLETED_STATE_TYPE
);
}
/**
* Checks if the result is in a failed state.
* @returns true if the result is failed, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isFailed(): this is AuthMethodRegistrationSubmitChallengeResult & {
state: AuthMethodRegistrationFailedState;
} {
return (
this.state.stateType === AUTH_METHOD_REGISTRATION_FAILED_STATE_TYPE
);
}
}
/**
* Type definition for possible states in AuthMethodRegistrationSubmitChallengeResult.
*/
export type AuthMethodRegistrationSubmitChallengeResultState =
| AuthMethodRegistrationCompletedState
| AuthMethodRegistrationFailedState;

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowStateBase } from "../../AuthFlowState.js";
import { AUTH_METHOD_REGISTRATION_COMPLETED_STATE_TYPE } from "../../AuthFlowStateTypes.js";
/**
* State indicating that the auth method registration flow has completed successfully.
*/
export class AuthMethodRegistrationCompletedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = AUTH_METHOD_REGISTRATION_COMPLETED_STATE_TYPE;
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowStateBase } from "../../AuthFlowState.js";
import { AUTH_METHOD_REGISTRATION_FAILED_STATE_TYPE } from "../../AuthFlowStateTypes.js";
/**
* State indicating that the auth method registration flow has failed.
*/
export class AuthMethodRegistrationFailedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = AUTH_METHOD_REGISTRATION_FAILED_STATE_TYPE;
}

View File

@ -0,0 +1,274 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthMethodRegistrationStateParameters,
AuthMethodRegistrationRequiredStateParameters,
AuthMethodVerificationRequiredStateParameters,
} from "./AuthMethodRegistrationStateParameters.js";
import { AuthMethodDetails } from "../AuthMethodDetails.js";
import {
JitChallengeAuthMethodParams,
JitSubmitChallengeParams,
} from "../../../interaction_client/jit/parameter/JitParams.js";
import { CustomAuthAccountData } from "../../../../get_account/auth_flow/CustomAuthAccountData.js";
import {
JIT_VERIFICATION_REQUIRED_RESULT_TYPE,
JIT_COMPLETED_RESULT_TYPE,
} from "../../../interaction_client/jit/result/JitActionResult.js";
import { UnexpectedError } from "../../../error/UnexpectedError.js";
import { AuthenticationMethod } from "../../../network_client/custom_auth_api/types/ApiResponseTypes.js";
import { AuthFlowActionRequiredStateBase } from "../../AuthFlowState.js";
import { GrantType } from "../../../../CustomAuthConstants.js";
import { AuthMethodRegistrationChallengeMethodResult } from "../result/AuthMethodRegistrationChallengeMethodResult.js";
import { AuthMethodRegistrationSubmitChallengeResult } from "../result/AuthMethodRegistrationSubmitChallengeResult.js";
import { AuthMethodRegistrationCompletedState } from "./AuthMethodRegistrationCompletedState.js";
import {
AUTH_METHOD_REGISTRATION_REQUIRED_STATE_TYPE,
AUTH_METHOD_VERIFICATION_REQUIRED_STATE_TYPE,
} from "../../AuthFlowStateTypes.js";
/**
* Abstract base class for authentication method registration states.
*/
abstract class AuthMethodRegistrationState<
TParameters extends AuthMethodRegistrationStateParameters
> extends AuthFlowActionRequiredStateBase<TParameters> {
/**
* Internal method to challenge an authentication method.
* @param authMethodDetails The authentication method details to challenge.
* @returns Promise that resolves to AuthMethodRegistrationChallengeMethodResult.
*/
protected async challengeAuthMethodInternal(
authMethodDetails: AuthMethodDetails
): Promise<AuthMethodRegistrationChallengeMethodResult> {
try {
this.stateParameters.logger.verbose(
`Challenging authentication method - '${authMethodDetails.authMethodType.id}' for auth method registration.`,
this.stateParameters.correlationId
);
const challengeParams: JitChallengeAuthMethodParams = {
correlationId: this.stateParameters.correlationId,
continuationToken: this.stateParameters.continuationToken ?? "",
authMethod: authMethodDetails.authMethodType,
verificationContact: authMethodDetails.verificationContact,
scopes: this.stateParameters.scopes ?? [],
username: this.stateParameters.username,
claims: this.stateParameters.claims,
};
const result =
await this.stateParameters.jitClient.challengeAuthMethod(
challengeParams
);
this.stateParameters.logger.verbose(
"Authentication method challenged successfully for auth method registration.",
this.stateParameters.correlationId
);
if (result.type === JIT_VERIFICATION_REQUIRED_RESULT_TYPE) {
// Verification required
this.stateParameters.logger.verbose(
"Auth method verification required.",
this.stateParameters.correlationId
);
return new AuthMethodRegistrationChallengeMethodResult(
new AuthMethodVerificationRequiredState({
correlationId: result.correlationId,
continuationToken: result.continuationToken,
config: this.stateParameters.config,
logger: this.stateParameters.logger,
jitClient: this.stateParameters.jitClient,
cacheClient: this.stateParameters.cacheClient,
challengeChannel: result.challengeChannel,
challengeTargetLabel: result.challengeTargetLabel,
codeLength: result.codeLength,
scopes: this.stateParameters.scopes ?? [],
username: this.stateParameters.username,
claims: this.stateParameters.claims,
})
);
} else if (result.type === JIT_COMPLETED_RESULT_TYPE) {
// Registration completed (fast-pass scenario)
this.stateParameters.logger.verbose(
"Auth method registration completed via fast-pass.",
this.stateParameters.correlationId
);
const accountInfo = new CustomAuthAccountData(
result.authenticationResult.account,
this.stateParameters.config,
this.stateParameters.cacheClient,
this.stateParameters.logger,
this.stateParameters.correlationId
);
return new AuthMethodRegistrationChallengeMethodResult(
new AuthMethodRegistrationCompletedState(),
accountInfo
);
} else {
// Handle unexpected result type with proper typing
this.stateParameters.logger.error(
"Unexpected result type from auth challenge method",
this.stateParameters.correlationId
);
throw new UnexpectedError(
"Unexpected result type from auth challenge method"
);
}
} catch (error) {
this.stateParameters.logger.errorPii(
`Failed to challenge authentication method for auth method registration. Error: '${error}'.`,
this.stateParameters.correlationId
);
return AuthMethodRegistrationChallengeMethodResult.createWithError(
error
);
}
}
}
/**
* State indicating that authentication method registration is required.
*/
export class AuthMethodRegistrationRequiredState extends AuthMethodRegistrationState<AuthMethodRegistrationRequiredStateParameters> {
/**
* The type of the state.
*/
stateType = AUTH_METHOD_REGISTRATION_REQUIRED_STATE_TYPE;
/**
* Gets the available authentication methods for registration.
* @returns Array of available authentication methods.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
getAuthMethods(): AuthenticationMethod[] {
return this.stateParameters.authMethods;
}
/**
* Challenges an authentication method for registration.
* @param authMethodDetails The authentication method details to challenge.
* @returns Promise that resolves to AuthMethodRegistrationChallengeMethodResult.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
async challengeAuthMethod(
authMethodDetails: AuthMethodDetails
): Promise<AuthMethodRegistrationChallengeMethodResult> {
return this.challengeAuthMethodInternal(authMethodDetails);
}
}
/**
* State indicating that verification is required for the challenged authentication method.
*/
export class AuthMethodVerificationRequiredState extends AuthMethodRegistrationState<AuthMethodVerificationRequiredStateParameters> {
/**
* The type of the state.
*/
stateType = AUTH_METHOD_VERIFICATION_REQUIRED_STATE_TYPE;
/**
* Gets the length of the expected verification code.
* @returns The code length.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
getCodeLength(): number {
return this.stateParameters.codeLength;
}
/**
* Gets the channel through which the challenge was sent.
* @returns The challenge channel (e.g., "email").
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
getChannel(): string {
return this.stateParameters.challengeChannel;
}
/**
* Gets the target label indicating where the challenge was sent.
* @returns The challenge target label (e.g., masked email address).
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
getSentTo(): string {
return this.stateParameters.challengeTargetLabel;
}
/**
* Submits the verification challenge to complete the authentication method registration.
* @param code The verification code entered by the user.
* @returns Promise that resolves to AuthMethodRegistrationSubmitChallengeResult.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
async submitChallenge(
code: string
): Promise<AuthMethodRegistrationSubmitChallengeResult> {
try {
this.ensureCodeIsValid(code, this.getCodeLength());
this.stateParameters.logger.verbose(
"Submitting auth method challenge.",
this.stateParameters.correlationId
);
const submitParams: JitSubmitChallengeParams = {
correlationId: this.stateParameters.correlationId,
continuationToken: this.stateParameters.continuationToken ?? "",
scopes: this.stateParameters.scopes ?? [],
grantType: GrantType.OOB,
challenge: code,
username: this.stateParameters.username,
claims: this.stateParameters.claims,
};
const result = await this.stateParameters.jitClient.submitChallenge(
submitParams
);
this.stateParameters.logger.verbose(
"Auth method challenge submitted successfully.",
this.stateParameters.correlationId
);
const accountInfo = new CustomAuthAccountData(
result.authenticationResult.account,
this.stateParameters.config,
this.stateParameters.cacheClient,
this.stateParameters.logger,
this.stateParameters.correlationId
);
return new AuthMethodRegistrationSubmitChallengeResult(
new AuthMethodRegistrationCompletedState(),
accountInfo
);
} catch (error) {
this.stateParameters.logger.errorPii(
`Failed to submit auth method challenge. Error: '${error}'.`,
this.stateParameters.correlationId
);
return AuthMethodRegistrationSubmitChallengeResult.createWithError(
error
);
}
}
/**
* Challenges a different authentication method for registration.
* @param authMethodDetails The authentication method details to challenge.
* @returns Promise that resolves to AuthMethodRegistrationChallengeMethodResult.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
async challengeAuthMethod(
authMethodDetails: AuthMethodDetails
): Promise<AuthMethodRegistrationChallengeMethodResult> {
return this.challengeAuthMethodInternal(authMethodDetails);
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowActionRequiredStateParameters } from "../../AuthFlowState.js";
import { JitClient } from "../../../interaction_client/jit/JitClient.js";
import { AuthenticationMethod } from "../../../network_client/custom_auth_api/types/ApiResponseTypes.js";
import { CustomAuthSilentCacheClient } from "../../../../get_account/interaction_client/CustomAuthSilentCacheClient.js";
export interface AuthMethodRegistrationStateParameters
extends AuthFlowActionRequiredStateParameters {
jitClient: JitClient;
cacheClient: CustomAuthSilentCacheClient;
scopes?: string[];
username?: string;
claims?: string;
}
export interface AuthMethodRegistrationRequiredStateParameters
extends AuthMethodRegistrationStateParameters {
authMethods: AuthenticationMethod[];
}
export interface AuthMethodVerificationRequiredStateParameters
extends AuthMethodRegistrationStateParameters {
challengeChannel: string;
challengeTargetLabel: string;
codeLength: number;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthActionErrorBase } from "../../AuthFlowErrorBase.js";
/**
* Error that occurred during MFA challenge request.
*/
export class MfaRequestChallengeError extends AuthActionErrorBase {
/**
* Checks if the input for MFA challenge is incorrect.
* @returns true if the input is incorrect, false otherwise.
*/
isInvalidInput(): boolean {
return this.isInvalidInputError();
}
/**
* Checks if the error is due to the verification contact (e.g., phone number or email) being blocked. Consider contacting customer support for assistance.
* @returns true if the error is due to the verification contact being blocked, false otherwise.
*/
isVerificationContactBlocked(): boolean {
return this.isVerificationContactBlockedError();
}
}
/**
* Error that occurred during MFA challenge submission.
*/
export class MfaSubmitChallengeError extends AuthActionErrorBase {
/**
* Checks if the submitted challenge code (e.g., OTP code) is incorrect.
* @returns true if the challenge code is invalid, false otherwise.
*/
isIncorrectChallenge(): boolean {
return this.isInvalidCodeError();
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../AuthFlowResultBase.js";
import { MfaRequestChallengeError } from "../error_type/MfaError.js";
import { MfaFailedState } from "../state/MfaFailedState.js";
import type { MfaVerificationRequiredState } from "../state/MfaState.js";
import {
MFA_VERIFICATION_REQUIRED_STATE_TYPE,
MFA_FAILED_STATE_TYPE,
} from "../../AuthFlowStateTypes.js";
/**
* Result of requesting an MFA challenge.
* Uses base state type to avoid circular dependencies.
*/
export class MfaRequestChallengeResult extends AuthFlowResultBase<
MfaRequestChallengeResultState,
MfaRequestChallengeError
> {
/**
* Creates an MfaRequestChallengeResult with an error.
* @param error The error that occurred.
* @returns The MfaRequestChallengeResult with error.
*/
static createWithError(error: unknown): MfaRequestChallengeResult {
const result = new MfaRequestChallengeResult(new MfaFailedState());
result.error = new MfaRequestChallengeError(
MfaRequestChallengeResult.createErrorData(error)
);
return result;
}
/**
* Checks if the result indicates that verification is required.
* @returns true if verification is required, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isVerificationRequired(): this is MfaRequestChallengeResult & {
state: MfaVerificationRequiredState;
} {
return this.state.stateType === MFA_VERIFICATION_REQUIRED_STATE_TYPE;
}
/**
* Checks if the result is in a failed state.
* @returns true if the result is failed, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isFailed(): this is MfaRequestChallengeResult & {
state: MfaFailedState;
} {
return this.state.stateType === MFA_FAILED_STATE_TYPE;
}
}
/**
* The possible states for the MfaRequestChallengeResult.
* This includes:
* - MfaVerificationRequiredState: The user needs to verify their challenge.
* - MfaFailedState: The MFA request failed.
*/
export type MfaRequestChallengeResultState =
| MfaVerificationRequiredState
| MfaFailedState;

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../AuthFlowResultBase.js";
import { MfaSubmitChallengeError } from "../error_type/MfaError.js";
import { CustomAuthAccountData } from "../../../../get_account/auth_flow/CustomAuthAccountData.js";
import { MfaCompletedState } from "../state/MfaCompletedState.js";
import { MfaFailedState } from "../state/MfaFailedState.js";
import {
MFA_COMPLETED_STATE_TYPE,
MFA_FAILED_STATE_TYPE,
} from "../../AuthFlowStateTypes.js";
/**
* Result of submitting an MFA challenge.
*/
export class MfaSubmitChallengeResult extends AuthFlowResultBase<
MfaSubmitChallengeResultState,
MfaSubmitChallengeError,
CustomAuthAccountData
> {
/**
* Creates an MfaSubmitChallengeResult with an error.
* @param error The error that occurred.
* @returns The MfaSubmitChallengeResult with error.
*/
static createWithError(error: unknown): MfaSubmitChallengeResult {
const result = new MfaSubmitChallengeResult(new MfaFailedState());
result.error = new MfaSubmitChallengeError(
MfaSubmitChallengeResult.createErrorData(error)
);
return result;
}
/**
* Checks if the MFA flow is completed successfully.
* @returns true if completed, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isCompleted(): this is MfaSubmitChallengeResult & {
state: MfaCompletedState;
} {
return this.state.stateType === MFA_COMPLETED_STATE_TYPE;
}
/**
* Checks if the result is in a failed state.
* @returns true if the result is failed, false otherwise.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isFailed(): this is MfaSubmitChallengeResult & {
state: MfaFailedState;
} {
return this.state.stateType === MFA_FAILED_STATE_TYPE;
}
}
export type MfaSubmitChallengeResultState = MfaCompletedState | MfaFailedState;

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowStateBase } from "../../AuthFlowState.js";
import { MFA_COMPLETED_STATE_TYPE } from "../../AuthFlowStateTypes.js";
/**
* State indicating that the MFA flow has completed successfully.
*/
export class MfaCompletedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = MFA_COMPLETED_STATE_TYPE;
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowStateBase } from "../../AuthFlowState.js";
import { MFA_FAILED_STATE_TYPE } from "../../AuthFlowStateTypes.js";
/**
* State indicating that the MFA flow has failed.
*/
export class MfaFailedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = MFA_FAILED_STATE_TYPE;
}

View File

@ -0,0 +1,202 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
MfaAwaitingStateParameters,
MfaStateParameters,
MfaVerificationRequiredStateParameters,
} from "./MfaStateParameters.js";
import { MfaSubmitChallengeResult } from "../result/MfaSubmitChallengeResult.js";
import { MfaRequestChallengeResult } from "../result/MfaRequestChallengeResult.js";
import {
MfaSubmitChallengeParams,
MfaRequestChallengeParams,
} from "../../../interaction_client/mfa/parameter/MfaClientParameters.js";
import { CustomAuthAccountData } from "../../../../get_account/auth_flow/CustomAuthAccountData.js";
import { MfaCompletedState } from "./MfaCompletedState.js";
import { ensureArgumentIsNotEmptyString } from "../../../utils/ArgumentValidator.js";
import { AuthenticationMethod } from "../../../network_client/custom_auth_api/types/ApiResponseTypes.js";
import { AuthFlowActionRequiredStateBase } from "../../AuthFlowState.js";
import {
MFA_AWAITING_STATE_TYPE,
MFA_VERIFICATION_REQUIRED_STATE_TYPE,
} from "../../AuthFlowStateTypes.js";
abstract class MfaState<
TParameters extends MfaStateParameters
> extends AuthFlowActionRequiredStateBase<TParameters> {
/**
* Requests an MFA challenge for a specific authentication method.
* @param authMethodId The authentication method ID to use for the challenge.
* @returns Promise that resolves to MfaRequestChallengeResult.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
async requestChallenge(
authMethodId: string
): Promise<MfaRequestChallengeResult> {
try {
ensureArgumentIsNotEmptyString("authMethodId", authMethodId);
this.stateParameters.logger.verbose(
`Requesting MFA challenge with authentication method - '${authMethodId}'.`,
this.stateParameters.correlationId
);
const requestParams: MfaRequestChallengeParams = {
correlationId: this.stateParameters.correlationId,
continuationToken: this.stateParameters.continuationToken ?? "",
challengeType:
this.stateParameters.config.customAuth.challengeTypes ?? [],
authMethodId: authMethodId,
};
const result =
await this.stateParameters.mfaClient.requestChallenge(
requestParams
);
this.stateParameters.logger.verbose(
"MFA challenge requested successfully.",
this.stateParameters.correlationId
);
return new MfaRequestChallengeResult(
new MfaVerificationRequiredState({
correlationId: result.correlationId,
continuationToken: result.continuationToken,
config: this.stateParameters.config,
logger: this.stateParameters.logger,
mfaClient: this.stateParameters.mfaClient,
cacheClient: this.stateParameters.cacheClient,
challengeChannel: result.challengeChannel,
challengeTargetLabel: result.challengeTargetLabel,
codeLength: result.codeLength,
selectedAuthMethodId: authMethodId,
scopes: this.stateParameters.scopes ?? [],
})
);
} catch (error) {
this.stateParameters.logger.errorPii(
`Failed to request MFA challenge. Error: '${error}'.`,
this.stateParameters.correlationId
);
return MfaRequestChallengeResult.createWithError(error);
}
}
}
/**
* State indicating that MFA is required and awaiting user action.
* This state allows the developer to pause execution before sending the code to the user's email.
*/
export class MfaAwaitingState extends MfaState<MfaAwaitingStateParameters> {
/**
* The type of the state.
*/
stateType = MFA_AWAITING_STATE_TYPE;
/**
* Gets the available authentication methods for MFA.
* @returns Array of available authentication methods.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
getAuthMethods(): AuthenticationMethod[] {
return this.stateParameters.authMethods;
}
}
/**
* State indicating that MFA verification is required.
* The challenge has been sent and the user needs to provide the code.
*/
export class MfaVerificationRequiredState extends MfaState<MfaVerificationRequiredStateParameters> {
/**
* The type of the state.
*/
stateType = MFA_VERIFICATION_REQUIRED_STATE_TYPE;
/**
* Gets the length of the code that the user needs to provide.
* @returns The expected code length.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
getCodeLength(): number {
return this.stateParameters.codeLength;
}
/**
* Gets the channel through which the challenge was sent.
* @returns The challenge channel (e.g., "email").
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
getChannel(): string {
return this.stateParameters.challengeChannel;
}
/**
* Gets the target label indicating where the challenge was sent.
* @returns The challenge target label (e.g., masked email address).
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
getSentTo(): string {
return this.stateParameters.challengeTargetLabel;
}
/**
* Submits the MFA challenge (e.g., OTP code) to complete the authentication.
* @param challenge The challenge code (e.g., OTP code) entered by the user.
* @returns Promise that resolves to MfaSubmitChallengeResult.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
async submitChallenge(
challenge: string
): Promise<MfaSubmitChallengeResult> {
try {
this.ensureCodeIsValid(challenge, this.getCodeLength());
this.stateParameters.logger.verbose(
"Submitting MFA challenge.",
this.stateParameters.correlationId
);
const submitParams: MfaSubmitChallengeParams = {
correlationId: this.stateParameters.correlationId,
continuationToken: this.stateParameters.continuationToken ?? "",
scopes: this.stateParameters.scopes ?? [],
challenge: challenge,
};
const result = await this.stateParameters.mfaClient.submitChallenge(
submitParams
);
this.stateParameters.logger.verbose(
"MFA challenge submitted successfully.",
this.stateParameters.correlationId
);
const accountInfo = new CustomAuthAccountData(
result.authenticationResult.account,
this.stateParameters.config,
this.stateParameters.cacheClient,
this.stateParameters.logger,
this.stateParameters.correlationId
);
return new MfaSubmitChallengeResult(
new MfaCompletedState(),
accountInfo
);
} catch (error) {
this.stateParameters.logger.errorPii(
`Failed to submit MFA challenge. Error: '${error}'.`,
this.stateParameters.correlationId
);
return MfaSubmitChallengeResult.createWithError(error);
}
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowActionRequiredStateParameters } from "../../AuthFlowState.js";
import { MfaClient } from "../../../interaction_client/mfa/MfaClient.js";
import { AuthenticationMethod } from "../../../network_client/custom_auth_api/types/ApiResponseTypes.js";
import { CustomAuthSilentCacheClient } from "../../../../get_account/interaction_client/CustomAuthSilentCacheClient.js";
export interface MfaStateParameters
extends AuthFlowActionRequiredStateParameters {
mfaClient: MfaClient;
cacheClient: CustomAuthSilentCacheClient;
scopes?: string[];
}
export interface MfaVerificationRequiredStateParameters
extends MfaStateParameters {
challengeChannel: string;
challengeTargetLabel: string;
codeLength: number;
selectedAuthMethodId?: string;
}
export interface MfaAwaitingStateParameters extends MfaStateParameters {
authMethods: AuthenticationMethod[];
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { UserAttribute } from "../network_client/custom_auth_api/types/ApiErrorResponseTypes.js";
import { CustomAuthError } from "./CustomAuthError.js";
/**
* Error when no required authentication method by Microsoft Entra is supported
*/
export class RedirectError extends CustomAuthError {
constructor(correlationId?: string, public redirectReason?: string) {
super(
"redirect",
redirectReason ||
"Redirect Error, a fallback to the browser-delegated authentication is needed. Use loginPopup instead.",
correlationId
);
Object.setPrototypeOf(this, RedirectError.prototype);
}
}
/**
* Custom Auth API error.
*/
export class CustomAuthApiError extends CustomAuthError {
constructor(
error: string,
errorDescription: string,
correlationId?: string,
errorCodes?: Array<number>,
subError?: string,
public attributes?: Array<UserAttribute>,
public continuationToken?: string,
public traceId?: string,
public timestamp?: string
) {
super(error, errorDescription, correlationId, errorCodes, subError);
Object.setPrototypeOf(this, CustomAuthApiError.prototype);
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export class CustomAuthError extends Error {
constructor(
public error: string,
public errorDescription?: string,
public correlationId?: string,
public errorCodes?: Array<number>,
public subError?: string
) {
super(`${error}: ${errorDescription ?? ""}`);
Object.setPrototypeOf(this, CustomAuthError.prototype);
this.errorCodes = errorCodes ?? [];
this.subError = subError ?? "";
}
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class HttpError extends CustomAuthError {
constructor(error: string, message: string, correlationId?: string) {
super(error, message, correlationId);
Object.setPrototypeOf(this, HttpError.prototype);
}
}

View File

@ -0,0 +1,7 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const NoNetworkConnectivity = "no_network_connectivity";
export const FailedSendRequest = "failed_send_request";

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class InvalidArgumentError extends CustomAuthError {
constructor(argName: string, correlationId?: string) {
const errorDescription = `The argument '${argName}' is invalid.`;
super("invalid_argument", errorDescription, correlationId);
Object.setPrototypeOf(this, InvalidArgumentError.prototype);
}
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class InvalidConfigurationError extends CustomAuthError {
constructor(error: string, message: string, correlationId?: string) {
super(error, message, correlationId);
Object.setPrototypeOf(this, InvalidConfigurationError.prototype);
}
}

View File

@ -0,0 +1,8 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const MissingConfiguration = "missing_configuration";
export const InvalidAuthority = "invalid_authority";
export const InvalidChallengeType = "invalid_challenge_type";

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class MethodNotImplementedError extends CustomAuthError {
constructor(method: string, correlationId?: string) {
const errorDescription = `The method '${method}' is not implemented, please do not use.`;
super("method_not_implemented", errorDescription, correlationId);
Object.setPrototypeOf(this, MethodNotImplementedError.prototype);
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class MsalCustomAuthError extends CustomAuthError {
constructor(
error: string,
errorDescription?: string,
subError?: string,
errorCodes?: Array<number>,
correlationId?: string
) {
super(error, errorDescription, correlationId, errorCodes, subError);
Object.setPrototypeOf(this, MsalCustomAuthError.prototype);
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class NoCachedAccountFoundError extends CustomAuthError {
constructor(correlationId?: string) {
super(
"no_cached_account_found",
"No account found in the cache",
correlationId
);
Object.setPrototypeOf(this, NoCachedAccountFoundError.prototype);
}
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class ParsedUrlError extends CustomAuthError {
constructor(error: string, message: string, correlationId?: string) {
super(error, message, correlationId);
Object.setPrototypeOf(this, ParsedUrlError.prototype);
}
}

View File

@ -0,0 +1,6 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const InvalidUrl = "invalid_url";

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class UnexpectedError extends CustomAuthError {
constructor(errorData: unknown, correlationId?: string) {
let errorDescription: string;
if (errorData instanceof Error) {
errorDescription = errorData.message;
} else if (typeof errorData === "string") {
errorDescription = errorData;
} else if (typeof errorData === "object" && errorData !== null) {
errorDescription = JSON.stringify(errorData);
} else {
errorDescription = "An unexpected error occurred.";
}
super("unexpected_error", errorDescription, correlationId);
Object.setPrototypeOf(this, UnexpectedError.prototype);
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class UnsupportedEnvironmentError extends CustomAuthError {
constructor(correlationId?: string) {
super(
"unsupported_env",
"The current environment is not browser",
correlationId
);
Object.setPrototypeOf(this, UnsupportedEnvironmentError.prototype);
}
}

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class UserAccountAttributeError extends CustomAuthError {
constructor(error: string, attributeName: string, attributeValue: string) {
const errorDescription = `Failed to set attribute '${attributeName}' with value '${attributeValue}'`;
super(error, errorDescription);
Object.setPrototypeOf(this, UserAccountAttributeError.prototype);
}
}

View File

@ -0,0 +1,6 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const InvalidAttributeErrorCode = "invalid_attribute";

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthError } from "./CustomAuthError.js";
export class UserAlreadySignedInError extends CustomAuthError {
constructor(correlationId?: string) {
super(
"user_already_signed_in",
"The user has already signed in.",
correlationId
);
Object.setPrototypeOf(this, UserAlreadySignedInError.prototype);
}
}

View File

@ -0,0 +1,142 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ICustomAuthApiClient } from "../network_client/custom_auth_api/ICustomAuthApiClient.js";
import { MethodNotImplementedError } from "../error/MethodNotImplementedError.js";
import { CustomAuthAuthority } from "../CustomAuthAuthority.js";
import { ChallengeType } from "../../CustomAuthConstants.js";
import { StandardInteractionClient } from "../../../interaction_client/StandardInteractionClient.js";
import { BrowserConfiguration } from "../../../config/Configuration.js";
import { BrowserCacheManager } from "../../../cache/BrowserCacheManager.js";
import {
Constants,
ICrypto,
IPerformanceClient,
Logger,
ResponseHandler,
} from "@azure/msal-common/browser";
import { EventHandler } from "../../../event/EventHandler.js";
import { INavigationClient } from "../../../navigation/INavigationClient.js";
import { RedirectRequest } from "../../../request/RedirectRequest.js";
import { PopupRequest } from "../../../request/PopupRequest.js";
import { SsoSilentRequest } from "../../../request/SsoSilentRequest.js";
import { EndSessionRequest } from "../../../request/EndSessionRequest.js";
import { ClearCacheRequest } from "../../../request/ClearCacheRequest.js";
import { AuthenticationResult } from "../../../response/AuthenticationResult.js";
import { SignInTokenResponse } from "../network_client/custom_auth_api/types/ApiResponseTypes.js";
export abstract class CustomAuthInteractionClientBase extends StandardInteractionClient {
private readonly tokenResponseHandler: ResponseHandler;
constructor(
config: BrowserConfiguration,
storageImpl: BrowserCacheManager,
browserCrypto: ICrypto,
logger: Logger,
eventHandler: EventHandler,
navigationClient: INavigationClient,
performanceClient: IPerformanceClient,
protected customAuthApiClient: ICustomAuthApiClient,
protected customAuthAuthority: CustomAuthAuthority
) {
super(
config,
storageImpl,
browserCrypto,
logger,
eventHandler,
navigationClient,
performanceClient,
""
);
this.tokenResponseHandler = new ResponseHandler(
this.config.auth.clientId,
this.browserStorage,
this.browserCrypto,
this.logger,
this.performanceClient,
null,
null
);
}
protected getChallengeTypes(
configuredChallengeTypes: string[] | undefined
): string {
const challengeType = configuredChallengeTypes ?? [];
if (
!challengeType.some(
(type) => type.toLowerCase() === ChallengeType.REDIRECT
)
) {
challengeType.push(ChallengeType.REDIRECT);
}
return challengeType.join(" ");
}
protected getScopes(scopes: string[] | undefined): string[] {
if (!!scopes && scopes.length > 0) {
return scopes;
}
return [
Constants.OPENID_SCOPE,
Constants.PROFILE_SCOPE,
Constants.OFFLINE_ACCESS_SCOPE,
];
}
/**
* Common method to handle token response processing.
* @param tokenResponse The token response from the API
* @param requestScopes Scopes for the token request
* @param correlationId Correlation ID for logging
* @returns Authentication result from the token response
*/
protected async handleTokenResponse(
tokenResponse: SignInTokenResponse,
requestScopes: string[],
correlationId: string,
apiId: number
): Promise<AuthenticationResult> {
this.logger.verbose("Processing token response.", correlationId);
const requestTimestamp = Math.round(new Date().getTime() / 1000.0);
// Save tokens and create authentication result
const result =
await this.tokenResponseHandler.handleServerTokenResponse(
tokenResponse,
this.customAuthAuthority,
requestTimestamp,
{
authority: this.customAuthAuthority.canonicalAuthority,
correlationId:
tokenResponse.correlation_id ?? correlationId,
scopes: requestScopes,
},
apiId
);
return result as AuthenticationResult;
}
// It is not necessary to implement this method from base class.
acquireToken(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request: RedirectRequest | PopupRequest | SsoSilentRequest
): Promise<AuthenticationResult | void> {
throw new MethodNotImplementedError("SignInClient.acquireToken");
}
// It is not necessary to implement this method from base class.
logout(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request: EndSessionRequest | ClearCacheRequest | undefined
): Promise<void> {
throw new MethodNotImplementedError("SignInClient.logout");
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ICustomAuthApiClient } from "../network_client/custom_auth_api/ICustomAuthApiClient.js";
import { CustomAuthAuthority } from "../CustomAuthAuthority.js";
import { CustomAuthInteractionClientBase } from "./CustomAuthInteractionClientBase.js";
import { BrowserConfiguration } from "../../../config/Configuration.js";
import { BrowserCacheManager } from "../../../cache/BrowserCacheManager.js";
import {
ICrypto,
IPerformanceClient,
Logger,
} from "@azure/msal-common/browser";
import { EventHandler } from "../../../event/EventHandler.js";
import { INavigationClient } from "../../../navigation/INavigationClient.js";
export class CustomAuthInterationClientFactory {
constructor(
private config: BrowserConfiguration,
private storageImpl: BrowserCacheManager,
private browserCrypto: ICrypto,
private logger: Logger,
private eventHandler: EventHandler,
private navigationClient: INavigationClient,
private performanceClient: IPerformanceClient,
private customAuthApiClient: ICustomAuthApiClient,
private customAuthAuthority: CustomAuthAuthority
) {}
create<TClient extends CustomAuthInteractionClientBase>(
clientConstructor: new (
config: BrowserConfiguration,
storageImpl: BrowserCacheManager,
browserCrypto: ICrypto,
logger: Logger,
eventHandler: EventHandler,
navigationClient: INavigationClient,
performanceClient: IPerformanceClient,
customAuthApiClient: ICustomAuthApiClient,
customAuthAuthority: CustomAuthAuthority
) => TClient
): TClient {
return new clientConstructor(
this.config,
this.storageImpl,
this.browserCrypto,
this.logger,
this.eventHandler,
this.navigationClient,
this.performanceClient,
this.customAuthApiClient,
this.customAuthAuthority
);
}
}

View File

@ -0,0 +1,184 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthInteractionClientBase } from "../CustomAuthInteractionClientBase.js";
import {
JitChallengeAuthMethodParams,
JitSubmitChallengeParams,
} from "./parameter/JitParams.js";
import {
JitVerificationRequiredResult,
JitCompletedResult,
createJitVerificationRequiredResult,
createJitCompletedResult,
} from "./result/JitActionResult.js";
import {
DefaultCustomAuthApiCodeLength,
ChallengeType,
GrantType,
} from "../../../CustomAuthConstants.js";
import * as PublicApiId from "../../telemetry/PublicApiId.js";
import {
RegisterChallengeRequest,
RegisterContinueRequest,
SignInContinuationTokenRequest,
} from "../../network_client/custom_auth_api/types/ApiRequestTypes.js";
import { initializeServerTelemetryManager } from "../../../../interaction_client/BaseInteractionClient.js";
/**
* JIT client for handling just-in-time authentication method registration flows.
*/
export class JitClient extends CustomAuthInteractionClientBase {
/**
* Challenges an authentication method for JIT registration.
* @param parameters The parameters for challenging the auth method.
* @returns Promise that resolves to either JitVerificationRequiredResult or JitCompletedResult.
*/
async challengeAuthMethod(
parameters: JitChallengeAuthMethodParams
): Promise<JitVerificationRequiredResult | JitCompletedResult> {
const correlationId = parameters.correlationId || this.correlationId;
const apiId = PublicApiId.JIT_CHALLENGE_AUTH_METHOD;
const telemetryManager = initializeServerTelemetryManager(
apiId,
this.config.auth.clientId,
correlationId,
this.browserStorage,
this.logger
);
this.logger.verbose(
"Calling challenge endpoint for getting auth method.",
correlationId
);
const challengeReq: RegisterChallengeRequest = {
continuation_token: parameters.continuationToken,
challenge_type: parameters.authMethod.challenge_type,
challenge_target: parameters.verificationContact,
challenge_channel: parameters.authMethod.challenge_channel,
correlationId: correlationId,
telemetryManager: telemetryManager,
};
const challengeResponse =
await this.customAuthApiClient.registerApi.challenge(challengeReq);
this.logger.verbose(
"Challenge endpoint called for auth method registration.",
challengeResponse.correlation_id || correlationId
);
/*
* Handle fast-pass scenario (preverified)
* This occurs when the user selects the same email used during sign-up
* Since the email was already verified during sign-up, no additional verification is needed
*/
if (challengeResponse.challenge_type === ChallengeType.PREVERIFIED) {
this.logger.verbose(
"Fast-pass scenario detected - completing registration without additional verification.",
challengeResponse.correlation_id || correlationId
);
// Use submitChallenge for fast-pass scenario with continuation_token grant type
const fastPassParams: JitSubmitChallengeParams = {
correlationId:
challengeResponse.correlation_id || correlationId,
continuationToken: challengeResponse.continuation_token,
grantType: GrantType.CONTINUATION_TOKEN,
scopes: parameters.scopes,
username: parameters.username,
claims: parameters.claims,
};
const completedResult = await this.submitChallenge(fastPassParams);
return completedResult;
}
// Verification required
return createJitVerificationRequiredResult({
correlationId: challengeResponse.correlation_id || correlationId,
continuationToken: challengeResponse.continuation_token,
challengeChannel: challengeResponse.challenge_channel,
challengeTargetLabel: challengeResponse.challenge_target,
codeLength:
challengeResponse.code_length || DefaultCustomAuthApiCodeLength,
});
}
/**
* Submits challenge response and completes JIT registration.
* @param parameters The parameters for submitting the challenge.
* @returns Promise that resolves to JitCompletedResult.
*/
async submitChallenge(
parameters: JitSubmitChallengeParams
): Promise<JitCompletedResult> {
const correlationId = parameters.correlationId || this.correlationId;
const apiId = PublicApiId.JIT_SUBMIT_CHALLENGE;
const telemetryManager = initializeServerTelemetryManager(
apiId,
this.config.auth.clientId,
correlationId,
this.browserStorage,
this.logger
);
this.logger.verbose(
"Calling continue endpoint for auth method challenge submission.",
correlationId
);
// Submit challenge to complete registration
const continueReq: RegisterContinueRequest = {
continuation_token: parameters.continuationToken,
grant_type: parameters.grantType,
...(parameters.challenge && {
oob: parameters.challenge,
}),
correlationId: correlationId,
telemetryManager: telemetryManager,
};
const continueResponse =
await this.customAuthApiClient.registerApi.continue(continueReq);
this.logger.verbose(
"Continue endpoint called for auth method challenge submission.",
parameters.correlationId
);
// Use continuation token to get authentication tokens
const scopes = this.getScopes(parameters.scopes);
const tokenRequest: SignInContinuationTokenRequest = {
continuation_token: continueResponse.continuation_token,
scope: scopes.join(" "),
correlationId: continueResponse.correlation_id || correlationId,
telemetryManager: telemetryManager,
...(parameters.claims && {
claims: parameters.claims,
}),
};
const tokenResponse =
await this.customAuthApiClient.signInApi.requestTokenWithContinuationToken(
tokenRequest
);
const authResult = await this.handleTokenResponse(
tokenResponse,
scopes,
tokenResponse.correlation_id ||
continueResponse.correlation_id ||
correlationId,
apiId
);
return createJitCompletedResult({
correlationId: continueResponse.correlation_id || correlationId,
authenticationResult: authResult,
});
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthenticationMethod } from "../../../network_client/custom_auth_api/types/ApiResponseTypes.js";
export interface JitClientParametersBase {
correlationId: string;
continuationToken: string;
}
export interface JitChallengeAuthMethodParams extends JitClientParametersBase {
authMethod: AuthenticationMethod;
verificationContact: string;
scopes: string[];
username?: string;
claims?: string;
}
export interface JitSubmitChallengeParams extends JitClientParametersBase {
grantType: string;
challenge?: string;
scopes: string[];
username?: string;
claims?: string;
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthenticationResult } from "../../../../../response/AuthenticationResult.js";
interface JitActionResult {
type: string;
correlationId: string;
}
export interface JitVerificationRequiredResult extends JitActionResult {
type: typeof JIT_VERIFICATION_REQUIRED_RESULT_TYPE;
continuationToken: string;
challengeChannel: string;
challengeTargetLabel: string;
codeLength: number;
}
export interface JitCompletedResult extends JitActionResult {
type: typeof JIT_COMPLETED_RESULT_TYPE;
authenticationResult: AuthenticationResult;
}
// Result type constants
export const JIT_VERIFICATION_REQUIRED_RESULT_TYPE =
"JitVerificationRequiredResult";
export const JIT_COMPLETED_RESULT_TYPE = "JitCompletedResult";
export function createJitVerificationRequiredResult(
input: Omit<JitVerificationRequiredResult, "type">
): JitVerificationRequiredResult {
return {
type: JIT_VERIFICATION_REQUIRED_RESULT_TYPE,
...input,
};
}
export function createJitCompletedResult(
input: Omit<JitCompletedResult, "type">
): JitCompletedResult {
return {
type: JIT_COMPLETED_RESULT_TYPE,
...input,
};
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthInteractionClientBase } from "../CustomAuthInteractionClientBase.js";
import {
MfaRequestChallengeParams,
MfaSubmitChallengeParams,
} from "./parameter/MfaClientParameters.js";
import {
MfaVerificationRequiredResult,
MfaCompletedResult,
createMfaVerificationRequiredResult,
createMfaCompletedResult,
} from "./result/MfaActionResult.js";
import {
DefaultCustomAuthApiCodeLength,
ChallengeType,
GrantType,
} from "../../../CustomAuthConstants.js";
import * as PublicApiId from "../../telemetry/PublicApiId.js";
import {
SignInChallengeRequest,
SignInOobTokenRequest,
} from "../../network_client/custom_auth_api/types/ApiRequestTypes.js";
import { ensureArgumentIsNotEmptyString } from "../../utils/ArgumentValidator.js";
import { CustomAuthApiError } from "../../error/CustomAuthApiError.js";
import * as CustomAuthApiErrorCode from "../../network_client/custom_auth_api/types/ApiErrorCodes.js";
import { initializeServerTelemetryManager } from "../../../../interaction_client/BaseInteractionClient.js";
/**
* MFA client for handling multi-factor authentication flows.
*/
export class MfaClient extends CustomAuthInteractionClientBase {
/**
* Requests an MFA challenge to be sent to the user.
* @param parameters The parameters for requesting the challenge.
* @returns Promise that resolves to either MfaVerificationRequiredResult.
*/
async requestChallenge(
parameters: MfaRequestChallengeParams
): Promise<MfaVerificationRequiredResult> {
const apiId = PublicApiId.MFA_REQUEST_CHALLENGE;
const correlationId = parameters.correlationId || this.correlationId;
const telemetryManager = initializeServerTelemetryManager(
apiId,
this.config.auth.clientId,
correlationId,
this.browserStorage,
this.logger
);
this.logger.verbose(
"Calling challenge endpoint for MFA.",
correlationId
);
const challengeReq: SignInChallengeRequest = {
challenge_type: this.getChallengeTypes(parameters.challengeType),
continuation_token: parameters.continuationToken,
id: parameters.authMethodId,
correlationId: correlationId,
telemetryManager: telemetryManager,
};
const challengeResponse =
await this.customAuthApiClient.signInApi.requestChallenge(
challengeReq
);
this.logger.verbose(
"Challenge endpoint called for MFA.",
correlationId
);
if (challengeResponse.challenge_type === ChallengeType.OOB) {
// Verification required - code will be sent
return createMfaVerificationRequiredResult({
correlationId:
challengeResponse.correlation_id || correlationId,
continuationToken: challengeResponse.continuation_token ?? "",
challengeChannel: challengeResponse.challenge_channel ?? "",
challengeTargetLabel:
challengeResponse.challenge_target_label ?? "",
codeLength:
challengeResponse.code_length ??
DefaultCustomAuthApiCodeLength,
bindingMethod: challengeResponse.binding_method ?? "",
});
}
this.logger.error(
`Unsupported challenge type '${challengeResponse.challenge_type}' for MFA.`,
challengeResponse.correlation_id || correlationId
);
throw new CustomAuthApiError(
CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE,
`Unsupported challenge type '${challengeResponse.challenge_type}'.`,
challengeResponse.correlation_id || correlationId
);
}
/**
* Submits the MFA challenge response (e.g., OTP code).
* @param parameters The parameters for submitting the challenge.
* @returns Promise that resolves to MfaCompletedResult.
*/
async submitChallenge(
parameters: MfaSubmitChallengeParams
): Promise<MfaCompletedResult> {
const correlationId = parameters.correlationId || this.correlationId;
ensureArgumentIsNotEmptyString(
"parameters.challenge",
parameters.challenge,
correlationId
);
const apiId = PublicApiId.MFA_SUBMIT_CHALLENGE;
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.challenge,
grant_type: GrantType.MFA_OOB,
scope: scopes.join(" "),
correlationId: correlationId,
telemetryManager: telemetryManager,
...(parameters.claims && {
claims: parameters.claims,
}),
};
this.logger.verbose(
"Calling token endpoint for MFA challenge submission.",
correlationId
);
const tokenResponse =
await this.customAuthApiClient.signInApi.requestTokensWithOob(
request
);
// Save tokens and create authentication result
const result = await this.handleTokenResponse(
tokenResponse,
scopes,
tokenResponse.correlation_id || correlationId,
apiId
);
return createMfaCompletedResult({
correlationId: tokenResponse.correlation_id || correlationId,
authenticationResult: result,
});
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface MfaClientParametersBase {
correlationId: string;
continuationToken: string;
}
export interface MfaRequestChallengeParams extends MfaClientParametersBase {
challengeType: string[];
authMethodId: string;
}
export interface MfaSubmitChallengeParams extends MfaClientParametersBase {
challenge: string;
scopes: string[];
claims?: string;
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthenticationResult } from "../../../../../response/AuthenticationResult.js";
interface MfaActionResult {
type: string;
correlationId: string;
}
export interface MfaVerificationRequiredResult extends MfaActionResult {
type: typeof MFA_VERIFICATION_REQUIRED_RESULT_TYPE;
continuationToken: string;
challengeChannel: string;
challengeTargetLabel: string;
codeLength: number;
bindingMethod: string;
}
export interface MfaCompletedResult extends MfaActionResult {
type: typeof MFA_COMPLETED_RESULT_TYPE;
authenticationResult: AuthenticationResult;
}
// Result type constants
export const MFA_VERIFICATION_REQUIRED_RESULT_TYPE =
"MfaVerificationRequiredResult";
export const MFA_COMPLETED_RESULT_TYPE = "MfaCompletedResult";
export function createMfaVerificationRequiredResult(
input: Omit<MfaVerificationRequiredResult, "type">
): MfaVerificationRequiredResult {
return {
type: MFA_VERIFICATION_REQUIRED_RESULT_TYPE,
...input,
};
}
export function createMfaCompletedResult(
input: Omit<MfaCompletedResult, "type">
): MfaCompletedResult {
return {
type: MFA_COMPLETED_RESULT_TYPE,
...input,
};
}

View File

@ -0,0 +1,215 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ChallengeType,
DefaultPackageInfo,
HttpHeaderKeys,
} from "../../../CustomAuthConstants.js";
import { IHttpClient } from "../http_client/IHttpClient.js";
import * as CustomAuthApiErrorCode from "./types/ApiErrorCodes.js";
import { buildUrl, parseUrl } from "../../utils/UrlUtils.js";
import {
CustomAuthApiError,
RedirectError,
} from "../../error/CustomAuthApiError.js";
import {
AADServerParamKeys,
Logger,
ServerTelemetryManager,
} from "@azure/msal-common/browser";
import { ApiErrorResponse } from "./types/ApiErrorResponseTypes.js";
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
import { filterCustomHeaders } from "../../utils/CustomHeaderUtils.js";
export abstract class BaseApiClient {
private readonly baseRequestUrl: URL;
constructor(
baseUrl: string,
private readonly clientId: string,
private httpClient: IHttpClient,
private customAuthApiQueryParams?: Record<string, string>,
private requestInterceptor?: CustomAuthRequestInterceptor,
private logger?: Logger
) {
this.baseRequestUrl = parseUrl(
!baseUrl.endsWith("/") ? `${baseUrl}/` : baseUrl
);
}
protected async request<T>(
endpoint: string,
data: Record<string, string | boolean>,
telemetryManager: ServerTelemetryManager,
correlationId: string
): Promise<T> {
const formData = new URLSearchParams({
client_id: this.clientId,
...data,
});
const commonHeaders = this.getCommonHeaders(
correlationId,
telemetryManager
);
const url = buildUrl(
this.baseRequestUrl.href,
endpoint,
this.customAuthApiQueryParams
);
const additionalHeaders = await this.getAdditionalHeaders(
url,
correlationId
);
const headers = { ...commonHeaders, ...additionalHeaders };
let response: Response;
try {
response = await this.httpClient.post(url, formData, headers);
} catch (e) {
throw new CustomAuthApiError(
CustomAuthApiErrorCode.HTTP_REQUEST_FAILED,
`Failed to perform '${endpoint}' request: ${e}`,
correlationId
);
}
return this.handleApiResponse(response, correlationId);
}
protected ensureContinuationTokenIsValid(
continuationToken: string | undefined,
correlationId: string
): void {
if (!continuationToken) {
throw new CustomAuthApiError(
CustomAuthApiErrorCode.CONTINUATION_TOKEN_MISSING,
"Continuation token is missing in the response body",
correlationId
);
}
}
private readResponseCorrelationId(
response: Response,
requestCorrelationId: string
): string {
return (
response.headers.get(HttpHeaderKeys.X_MS_REQUEST_ID) ||
requestCorrelationId
);
}
private getCommonHeaders(
correlationId: string,
telemetryManager: ServerTelemetryManager
): Record<string, string> {
return {
[HttpHeaderKeys.CONTENT_TYPE]: "application/x-www-form-urlencoded",
[AADServerParamKeys.X_CLIENT_SKU]: DefaultPackageInfo.SKU,
[AADServerParamKeys.X_CLIENT_VER]: DefaultPackageInfo.VERSION,
[AADServerParamKeys.X_CLIENT_OS]: DefaultPackageInfo.OS,
[AADServerParamKeys.X_CLIENT_CPU]: DefaultPackageInfo.CPU,
[AADServerParamKeys.X_CLIENT_CURR_TELEM]:
telemetryManager.generateCurrentRequestHeaderValue(),
[AADServerParamKeys.X_CLIENT_LAST_TELEM]:
telemetryManager.generateLastRequestHeaderValue(),
[AADServerParamKeys.CLIENT_REQUEST_ID]: correlationId,
};
}
private async handleApiResponse<T>(
response: Response | undefined,
requestCorrelationId: string
): Promise<T> {
if (!response) {
throw new CustomAuthApiError(
"empty_response",
"Response is empty",
requestCorrelationId
);
}
const correlationId = this.readResponseCorrelationId(
response,
requestCorrelationId
);
const responseData = await response.json();
if (response.ok) {
// Ensure the response doesn't have redirect challenge type
if (
typeof responseData === "object" &&
responseData.challenge_type === ChallengeType.REDIRECT
) {
throw new RedirectError(
correlationId,
responseData.redirect_reason
);
}
return {
...responseData,
correlation_id: correlationId,
};
}
const responseError = responseData as ApiErrorResponse;
if (!responseError) {
throw new CustomAuthApiError(
CustomAuthApiErrorCode.INVALID_RESPONSE_BODY,
"Response error body is empty or invalid",
correlationId
);
}
const attributes =
!!responseError.required_attributes &&
responseError.required_attributes.length > 0
? responseError.required_attributes
: responseError.invalid_attributes ?? [];
throw new CustomAuthApiError(
responseError.error,
responseError.error_description,
responseError.correlation_id,
responseError.error_codes,
responseError.suberror,
attributes,
responseError.continuation_token,
responseError.trace_id,
responseError.timestamp
);
}
private async getAdditionalHeaders(
url: URL,
correlationId: string
): Promise<Record<string, string>> {
if (!this.requestInterceptor) {
return {};
}
try {
const result = await Promise.resolve(
this.requestInterceptor.addAdditionalHeaderFields(url)
);
return filterCustomHeaders(result, this.logger, correlationId);
} catch (e) {
this.logger?.warningPii(
`CustomAuthRequestInterceptor.addAdditionalHeaderFields threw an error; continuing without additional headers: ${e}`,
correlationId
);
return {};
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ResetPasswordApiClient } from "./ResetPasswordApiClient.js";
import { SignupApiClient } from "./SignupApiClient.js";
import { SignInApiClient } from "./SignInApiClient.js";
import { RegisterApiClient } from "./RegisterApiClient.js";
import { ICustomAuthApiClient } from "./ICustomAuthApiClient.js";
import { IHttpClient } from "../http_client/IHttpClient.js";
import { Logger } from "@azure/msal-common/browser";
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
export class CustomAuthApiClient implements ICustomAuthApiClient {
signInApi: SignInApiClient;
signUpApi: SignupApiClient;
resetPasswordApi: ResetPasswordApiClient;
registerApi: RegisterApiClient;
constructor(
customAuthApiBaseUrl: string,
clientId: string,
httpClient: IHttpClient,
capabilities?: string,
customAuthApiQueryParams?: Record<string, string>,
requestInterceptor?: CustomAuthRequestInterceptor,
logger?: Logger
) {
this.signInApi = new SignInApiClient(
customAuthApiBaseUrl,
clientId,
httpClient,
capabilities,
customAuthApiQueryParams,
requestInterceptor,
logger
);
this.signUpApi = new SignupApiClient(
customAuthApiBaseUrl,
clientId,
httpClient,
capabilities,
customAuthApiQueryParams,
requestInterceptor,
logger
);
this.resetPasswordApi = new ResetPasswordApiClient(
customAuthApiBaseUrl,
clientId,
httpClient,
capabilities,
customAuthApiQueryParams,
requestInterceptor,
logger
);
this.registerApi = new RegisterApiClient(
customAuthApiBaseUrl,
clientId,
httpClient,
customAuthApiQueryParams,
requestInterceptor,
logger
);
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const SIGNIN_INITIATE = "/oauth2/v2.0/initiate";
export const SIGNIN_CHALLENGE = "/oauth2/v2.0/challenge";
export const SIGNIN_TOKEN = "/oauth2/v2.0/token";
export const SIGNIN_INTROSPECT = "/oauth2/v2.0/introspect";
export const SIGNUP_START = "/signup/v1.0/start";
export const SIGNUP_CHALLENGE = "/signup/v1.0/challenge";
export const SIGNUP_CONTINUE = "/signup/v1.0/continue";
export const RESET_PWD_START = "/resetpassword/v1.0/start";
export const RESET_PWD_CHALLENGE = "/resetpassword/v1.0/challenge";
export const RESET_PWD_CONTINUE = "/resetpassword/v1.0/continue";
export const RESET_PWD_SUBMIT = "/resetpassword/v1.0/submit";
export const RESET_PWD_POLL = "/resetpassword/v1.0/poll_completion";
export const REGISTER_INTROSPECT = "/register/v1.0/introspect";
export const REGISTER_CHALLENGE = "/register/v1.0/challenge";
export const REGISTER_CONTINUE = "/register/v1.0/continue";

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ResetPasswordApiClient } from "./ResetPasswordApiClient.js";
import { SignupApiClient } from "./SignupApiClient.js";
import { SignInApiClient } from "./SignInApiClient.js";
import { RegisterApiClient } from "./RegisterApiClient.js";
export interface ICustomAuthApiClient {
signInApi: SignInApiClient;
signUpApi: SignupApiClient;
resetPasswordApi: ResetPasswordApiClient;
registerApi: RegisterApiClient;
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseApiClient } from "./BaseApiClient.js";
import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js";
import {
RegisterIntrospectRequest,
RegisterChallengeRequest,
RegisterContinueRequest,
} from "./types/ApiRequestTypes.js";
import {
RegisterIntrospectResponse,
RegisterChallengeResponse,
RegisterContinueResponse,
} from "./types/ApiResponseTypes.js";
export class RegisterApiClient extends BaseApiClient {
/**
* Gets available authentication methods for registration
*/
async introspect(
params: RegisterIntrospectRequest
): Promise<RegisterIntrospectResponse> {
const result = await this.request<RegisterIntrospectResponse>(
CustomAuthApiEndpoint.REGISTER_INTROSPECT,
{
continuation_token: params.continuation_token,
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Sends challenge to specified authentication method
*/
async challenge(
params: RegisterChallengeRequest
): Promise<RegisterChallengeResponse> {
const result = await this.request<RegisterChallengeResponse>(
CustomAuthApiEndpoint.REGISTER_CHALLENGE,
{
continuation_token: params.continuation_token,
challenge_type: params.challenge_type,
challenge_target: params.challenge_target,
...(params.challenge_channel && {
challenge_channel: params.challenge_channel,
}),
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Submits challenge response and continues registration
*/
async continue(
params: RegisterContinueRequest
): Promise<RegisterContinueResponse> {
const result = await this.request<RegisterContinueResponse>(
CustomAuthApiEndpoint.REGISTER_CONTINUE,
{
continuation_token: params.continuation_token,
grant_type: params.grant_type,
...(params.oob && { oob: params.oob }),
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
}

View File

@ -0,0 +1,200 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
GrantType,
ResetPasswordPollStatus,
} from "../../../CustomAuthConstants.js";
import { CustomAuthApiError } from "../../error/CustomAuthApiError.js";
import { BaseApiClient } from "./BaseApiClient.js";
import { IHttpClient } from "../http_client/IHttpClient.js";
import { Logger } from "@azure/msal-common/browser";
import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js";
import * as CustomAuthApiErrorCode from "./types/ApiErrorCodes.js";
import {
ResetPasswordChallengeRequest,
ResetPasswordContinueRequest,
ResetPasswordPollCompletionRequest,
ResetPasswordStartRequest,
ResetPasswordSubmitRequest,
} from "./types/ApiRequestTypes.js";
import {
ResetPasswordChallengeResponse,
ResetPasswordContinueResponse,
ResetPasswordPollCompletionResponse,
ResetPasswordStartResponse,
ResetPasswordSubmitResponse,
} from "./types/ApiResponseTypes.js";
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
export class ResetPasswordApiClient extends BaseApiClient {
private readonly capabilities?: string;
constructor(
customAuthApiBaseUrl: string,
clientId: string,
httpClient: IHttpClient,
capabilities?: string,
customAuthApiQueryParams?: Record<string, string>,
requestInterceptor?: CustomAuthRequestInterceptor,
logger?: Logger
) {
super(
customAuthApiBaseUrl,
clientId,
httpClient,
customAuthApiQueryParams,
requestInterceptor,
logger
);
this.capabilities = capabilities;
}
/**
* Start the password reset flow
*/
async start(
params: ResetPasswordStartRequest
): Promise<ResetPasswordStartResponse> {
const result = await this.request<ResetPasswordStartResponse>(
CustomAuthApiEndpoint.RESET_PWD_START,
{
challenge_type: params.challenge_type,
username: params.username,
...(this.capabilities && {
capabilities: this.capabilities,
}),
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Request a challenge (OTP) to be sent to the user's email
* @param ChallengeResetPasswordRequest Parameters for the challenge request
*/
async requestChallenge(
params: ResetPasswordChallengeRequest
): Promise<ResetPasswordChallengeResponse> {
const result = await this.request<ResetPasswordChallengeResponse>(
CustomAuthApiEndpoint.RESET_PWD_CHALLENGE,
{
challenge_type: params.challenge_type,
continuation_token: params.continuation_token,
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Submit the code for verification
* @param ContinueResetPasswordRequest Token from previous response
*/
async continueWithCode(
params: ResetPasswordContinueRequest
): Promise<ResetPasswordContinueResponse> {
const result = await this.request<ResetPasswordContinueResponse>(
CustomAuthApiEndpoint.RESET_PWD_CONTINUE,
{
continuation_token: params.continuation_token,
grant_type: GrantType.OOB,
oob: params.oob,
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Submit the new password
* @param SubmitResetPasswordResponse Token from previous response
*/
async submitNewPassword(
params: ResetPasswordSubmitRequest
): Promise<ResetPasswordSubmitResponse> {
const result = await this.request<ResetPasswordSubmitResponse>(
CustomAuthApiEndpoint.RESET_PWD_SUBMIT,
{
continuation_token: params.continuation_token,
new_password: params.new_password,
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
if (result.poll_interval === 0) {
result.poll_interval = 2;
}
return result;
}
/**
* Poll for password reset completion status
* @param continuationToken Token from previous response
*/
async pollCompletion(
params: ResetPasswordPollCompletionRequest
): Promise<ResetPasswordPollCompletionResponse> {
const result = await this.request<ResetPasswordPollCompletionResponse>(
CustomAuthApiEndpoint.RESET_PWD_POLL,
{
continuation_token: params.continuation_token,
},
params.telemetryManager,
params.correlationId
);
this.ensurePollStatusIsValid(result.status, params.correlationId);
return result;
}
protected ensurePollStatusIsValid(
status: string,
correlationId: string
): void {
if (
status !== ResetPasswordPollStatus.FAILED &&
status !== ResetPasswordPollStatus.IN_PROGRESS &&
status !== ResetPasswordPollStatus.SUCCEEDED &&
status !== ResetPasswordPollStatus.NOT_STARTED
) {
throw new CustomAuthApiError(
CustomAuthApiErrorCode.INVALID_POLL_STATUS,
`The poll status '${status}' for password reset is invalid`,
correlationId
);
}
}
}

View File

@ -0,0 +1,242 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Logger, ServerTelemetryManager } from "@azure/msal-common/browser";
import { GrantType } from "../../../CustomAuthConstants.js";
import { CustomAuthApiError } from "../../error/CustomAuthApiError.js";
import { BaseApiClient } from "./BaseApiClient.js";
import { IHttpClient } from "../http_client/IHttpClient.js";
import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js";
import * as CustomAuthApiErrorCode from "./types/ApiErrorCodes.js";
import {
SignInChallengeRequest,
SignInContinuationTokenRequest,
SignInInitiateRequest,
SignInIntrospectRequest,
SignInOobTokenRequest,
SignInPasswordTokenRequest,
} from "./types/ApiRequestTypes.js";
import {
SignInChallengeResponse,
SignInInitiateResponse,
SignInIntrospectResponse,
SignInTokenResponse,
} from "./types/ApiResponseTypes.js";
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
export class SignInApiClient extends BaseApiClient {
private readonly capabilities?: string;
constructor(
customAuthApiBaseUrl: string,
clientId: string,
httpClient: IHttpClient,
capabilities?: string,
customAuthApiQueryParams?: Record<string, string>,
requestInterceptor?: CustomAuthRequestInterceptor,
logger?: Logger
) {
super(
customAuthApiBaseUrl,
clientId,
httpClient,
customAuthApiQueryParams,
requestInterceptor,
logger
);
this.capabilities = capabilities;
}
/**
* Initiates the sign-in flow
* @param username User's email
* @param authMethod 'email-otp' | 'email-password'
*/
async initiate(
params: SignInInitiateRequest
): Promise<SignInInitiateResponse> {
const result = await this.request<SignInInitiateResponse>(
CustomAuthApiEndpoint.SIGNIN_INITIATE,
{
username: params.username,
challenge_type: params.challenge_type,
...(this.capabilities && {
capabilities: this.capabilities,
}),
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Requests authentication challenge (OTP or password validation)
* @param continuationToken Token from initiate response
* @param authMethod 'email-otp' | 'email-password'
*/
async requestChallenge(
params: SignInChallengeRequest
): Promise<SignInChallengeResponse> {
const result = await this.request<SignInChallengeResponse>(
CustomAuthApiEndpoint.SIGNIN_CHALLENGE,
{
continuation_token: params.continuation_token,
challenge_type: params.challenge_type,
...(params.id && { id: params.id }),
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Requests security tokens using either password or OTP
* @param continuationToken Token from challenge response
* @param credentials Password or OTP
* @param authMethod 'email-otp' | 'email-password'
*/
async requestTokensWithPassword(
params: SignInPasswordTokenRequest
): Promise<SignInTokenResponse> {
return this.requestTokens(
{
continuation_token: params.continuation_token,
grant_type: GrantType.PASSWORD,
scope: params.scope,
password: params.password,
...(params.claims && { claims: params.claims }),
},
params.telemetryManager,
params.correlationId
);
}
async requestTokensWithOob(
params: SignInOobTokenRequest
): Promise<SignInTokenResponse> {
return this.requestTokens(
{
continuation_token: params.continuation_token,
scope: params.scope,
oob: params.oob,
grant_type: params.grant_type,
...(params.claims && { claims: params.claims }),
},
params.telemetryManager,
params.correlationId
);
}
async requestTokenWithContinuationToken(
params: SignInContinuationTokenRequest
): Promise<SignInTokenResponse> {
return this.requestTokens(
{
continuation_token: params.continuation_token,
scope: params.scope,
grant_type: GrantType.CONTINUATION_TOKEN,
...(params.claims && { claims: params.claims }),
...(params.username && { username: params.username }),
},
params.telemetryManager,
params.correlationId
);
}
/**
* Requests available authentication methods for MFA
* @param continuationToken Token from previous response
*/
async requestAuthMethods(
params: SignInIntrospectRequest
): Promise<SignInIntrospectResponse> {
const result = await this.request<SignInIntrospectResponse>(
CustomAuthApiEndpoint.SIGNIN_INTROSPECT,
{
continuation_token: params.continuation_token,
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
private async requestTokens(
requestData: Record<string, string>,
telemetryManager: ServerTelemetryManager,
correlationId: string
): Promise<SignInTokenResponse> {
// The client_info parameter is required for MSAL to return the uid and utid in the response.
requestData.client_info = "1";
const result = await this.request<SignInTokenResponse>(
CustomAuthApiEndpoint.SIGNIN_TOKEN,
requestData,
telemetryManager,
correlationId
);
SignInApiClient.ensureTokenResponseIsValid(result);
return result;
}
private static ensureTokenResponseIsValid(
tokenResponse: SignInTokenResponse
): void {
let errorCode = "";
let errorDescription = "";
if (!tokenResponse.access_token) {
errorCode = CustomAuthApiErrorCode.ACCESS_TOKEN_MISSING;
errorDescription = "Access token is missing in the response body";
} else if (!tokenResponse.id_token) {
errorCode = CustomAuthApiErrorCode.ID_TOKEN_MISSING;
errorDescription = "Id token is missing in the response body";
} else if (!tokenResponse.refresh_token) {
errorCode = CustomAuthApiErrorCode.REFRESH_TOKEN_MISSING;
errorDescription = "Refresh token is missing in the response body";
} else if (!tokenResponse.expires_in || tokenResponse.expires_in <= 0) {
errorCode = CustomAuthApiErrorCode.INVALID_EXPIRES_IN;
errorDescription = "Expires in is invalid in the response body";
} else if (tokenResponse.token_type !== "Bearer") {
errorCode = CustomAuthApiErrorCode.INVALID_TOKEN_TYPE;
errorDescription = `Token type '${tokenResponse.token_type}' is invalid in the response body`;
} else if (!tokenResponse.client_info) {
errorCode = CustomAuthApiErrorCode.CLIENT_INFO_MISSING;
errorDescription = "Client info is missing in the response body";
}
if (!errorCode && !errorDescription) {
return;
}
throw new CustomAuthApiError(
errorCode,
errorDescription,
tokenResponse.correlation_id
);
}
}

View File

@ -0,0 +1,169 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { GrantType } from "../../../CustomAuthConstants.js";
import { BaseApiClient } from "./BaseApiClient.js";
import { IHttpClient } from "../http_client/IHttpClient.js";
import { Logger } from "@azure/msal-common/browser";
import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js";
import {
SignUpChallengeRequest,
SignUpContinueWithAttributesRequest,
SignUpContinueWithOobRequest,
SignUpContinueWithPasswordRequest,
SignUpStartRequest,
} from "./types/ApiRequestTypes.js";
import {
SignUpChallengeResponse,
SignUpContinueResponse,
SignUpStartResponse,
} from "./types/ApiResponseTypes.js";
import { CustomAuthRequestInterceptor } from "../../../configuration/CustomAuthRequestInterceptor.js";
export class SignupApiClient extends BaseApiClient {
private readonly capabilities?: string;
constructor(
customAuthApiBaseUrl: string,
clientId: string,
httpClient: IHttpClient,
capabilities?: string,
customAuthApiQueryParams?: Record<string, string>,
requestInterceptor?: CustomAuthRequestInterceptor,
logger?: Logger
) {
super(
customAuthApiBaseUrl,
clientId,
httpClient,
customAuthApiQueryParams,
requestInterceptor,
logger
);
this.capabilities = capabilities;
}
/**
* Start the sign-up flow
*/
async start(params: SignUpStartRequest): Promise<SignUpStartResponse> {
const result = await this.request<SignUpStartResponse>(
CustomAuthApiEndpoint.SIGNUP_START,
{
username: params.username,
...(params.password && { password: params.password }),
...(params.attributes && {
attributes: JSON.stringify(params.attributes),
}),
challenge_type: params.challenge_type,
...(this.capabilities && {
capabilities: this.capabilities,
}),
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Request challenge (e.g., OTP)
*/
async requestChallenge(
params: SignUpChallengeRequest
): Promise<SignUpChallengeResponse> {
const result = await this.request<SignUpChallengeResponse>(
CustomAuthApiEndpoint.SIGNUP_CHALLENGE,
{
continuation_token: params.continuation_token,
challenge_type: params.challenge_type,
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
/**
* Continue sign-up flow with code.
*/
async continueWithCode(
params: SignUpContinueWithOobRequest
): Promise<SignUpContinueResponse> {
const result = await this.request<SignUpContinueResponse>(
CustomAuthApiEndpoint.SIGNUP_CONTINUE,
{
continuation_token: params.continuation_token,
grant_type: GrantType.OOB,
oob: params.oob,
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
async continueWithPassword(
params: SignUpContinueWithPasswordRequest
): Promise<SignUpContinueResponse> {
const result = await this.request<SignUpContinueResponse>(
CustomAuthApiEndpoint.SIGNUP_CONTINUE,
{
continuation_token: params.continuation_token,
grant_type: GrantType.PASSWORD,
password: params.password,
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
async continueWithAttributes(
params: SignUpContinueWithAttributesRequest
): Promise<SignUpContinueResponse> {
const result = await this.request<SignUpContinueResponse>(
CustomAuthApiEndpoint.SIGNUP_CONTINUE,
{
continuation_token: params.continuation_token,
grant_type: GrantType.ATTRIBUTES,
attributes: JSON.stringify(params.attributes),
},
params.telemetryManager,
params.correlationId
);
this.ensureContinuationTokenIsValid(
result.continuation_token,
params.correlationId
);
return result;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const CONTINUATION_TOKEN_MISSING = "continuation_token_missing";
export const INVALID_RESPONSE_BODY = "invalid_response_body";
export const EMPTY_RESPONSE = "empty_response";
export const UNSUPPORTED_CHALLENGE_TYPE = "unsupported_challenge_type";
export const ACCESS_TOKEN_MISSING = "access_token_missing";
export const ID_TOKEN_MISSING = "id_token_missing";
export const REFRESH_TOKEN_MISSING = "refresh_token_missing";
export const INVALID_EXPIRES_IN = "invalid_expires_in";
export const INVALID_TOKEN_TYPE = "invalid_token_type";
export const HTTP_REQUEST_FAILED = "http_request_failed";
export const INVALID_REQUEST = "invalid_request";
export const USER_NOT_FOUND = "user_not_found";
export const INVALID_GRANT = "invalid_grant";
export const CREDENTIAL_REQUIRED = "credential_required";
export const ATTRIBUTES_REQUIRED = "attributes_required";
export const USER_ALREADY_EXISTS = "user_already_exists";
export const INVALID_POLL_STATUS = "invalid_poll_status";
export const PASSWORD_CHANGE_FAILED = "password_change_failed";
export const PASSWORD_RESET_TIMEOUT = "password_reset_timeout";
export const CLIENT_INFO_MISSING = "client_info_missing";
export const EXPIRED_TOKEN = "expired_token";
export const ACCESS_DENIED = "access_denied";

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface InvalidAttribute {
name: string;
reason: string;
}
/**
* Detailed error interface for Microsoft Entra signup errors
*/
export interface ApiErrorResponse {
error: string;
error_description: string;
correlation_id: string;
error_codes?: number[];
suberror?: string;
continuation_token?: string;
timestamp?: string;
trace_id?: string;
required_attributes?: Array<UserAttribute>;
invalid_attributes?: Array<UserAttribute>;
}
export interface UserAttribute {
name: string;
type?: string;
required?: boolean;
options?: UserAttributeOption;
}
export interface UserAttributeOption {
regex?: string;
}

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { GrantType } from "../../../../CustomAuthConstants.js";
import { ApiRequestBase } from "./ApiTypesBase.js";
/* Sign-in API request types */
export interface SignInInitiateRequest extends ApiRequestBase {
challenge_type: string;
username: string;
}
export interface SignInChallengeRequest extends ApiRequestBase {
challenge_type: string;
continuation_token: string;
id?: string;
}
interface SignInTokenRequestBase extends ApiRequestBase {
continuation_token: string;
scope: string;
claims?: string;
}
export interface SignInPasswordTokenRequest extends SignInTokenRequestBase {
password: string;
}
export interface SignInOobTokenRequest extends SignInTokenRequestBase {
oob: string;
grant_type: typeof GrantType.OOB | typeof GrantType.MFA_OOB;
}
export interface SignInContinuationTokenRequest extends SignInTokenRequestBase {
username?: string;
}
export interface SignInIntrospectRequest extends ApiRequestBase {
continuation_token: string;
}
/* Sign-up API request types */
export interface SignUpStartRequest extends ApiRequestBase {
username: string;
challenge_type: string;
password?: string;
attributes?: Record<string, string>;
}
export interface SignUpChallengeRequest extends ApiRequestBase {
continuation_token: string;
challenge_type: string;
}
interface SignUpContinueRequestBase extends ApiRequestBase {
continuation_token: string;
}
export interface SignUpContinueWithOobRequest
extends SignUpContinueRequestBase {
oob: string;
}
export interface SignUpContinueWithPasswordRequest
extends SignUpContinueRequestBase {
password: string;
}
export interface SignUpContinueWithAttributesRequest
extends SignUpContinueRequestBase {
attributes: Record<string, string>;
}
/* Reset password API request types */
export interface ResetPasswordStartRequest extends ApiRequestBase {
challenge_type: string;
username: string;
}
export interface ResetPasswordChallengeRequest extends ApiRequestBase {
challenge_type: string;
continuation_token: string;
}
export interface ResetPasswordContinueRequest extends ApiRequestBase {
continuation_token: string;
oob: string;
}
export interface ResetPasswordSubmitRequest extends ApiRequestBase {
continuation_token: string;
new_password: string;
}
export interface ResetPasswordPollCompletionRequest extends ApiRequestBase {
continuation_token: string;
}
/* Register API request types */
export interface RegisterIntrospectRequest extends ApiRequestBase {
continuation_token: string;
}
export interface RegisterChallengeRequest extends ApiRequestBase {
continuation_token: string;
challenge_type: string;
challenge_target: string;
challenge_channel?: string;
}
export interface RegisterContinueRequest extends ApiRequestBase {
continuation_token: string;
grant_type: string;
oob?: string;
}

View File

@ -0,0 +1,98 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ApiResponseBase } from "./ApiTypesBase.js";
interface ContinuousResponse extends ApiResponseBase {
continuation_token?: string;
}
interface InitiateResponse extends ContinuousResponse {
challenge_type?: string;
}
interface ChallengeResponse extends ApiResponseBase {
continuation_token?: string;
challenge_type?: string;
binding_method?: string;
challenge_channel?: string;
challenge_target_label?: string;
code_length?: number;
}
/* Sign-in API response types */
export type SignInInitiateResponse = InitiateResponse;
export type SignInChallengeResponse = ChallengeResponse;
export interface SignInTokenResponse extends ApiResponseBase {
token_type: "Bearer";
scope: string;
expires_in: number;
access_token: string;
refresh_token: string;
id_token: string;
client_info: string;
ext_expires_in?: number;
}
export interface AuthenticationMethod {
id: string;
challenge_type: string;
challenge_channel: string;
login_hint?: string;
}
export interface SignInIntrospectResponse extends ApiResponseBase {
continuation_token: string;
methods: AuthenticationMethod[];
}
/* Sign-up API response types */
export type SignUpStartResponse = InitiateResponse;
export interface SignUpChallengeResponse extends ChallengeResponse {
interval?: number;
}
export type SignUpContinueResponse = InitiateResponse;
/* Reset password API response types */
export type ResetPasswordStartResponse = InitiateResponse;
export type ResetPasswordChallengeResponse = ChallengeResponse;
export interface ResetPasswordContinueResponse extends ContinuousResponse {
expires_in: number;
}
export interface ResetPasswordSubmitResponse extends ContinuousResponse {
poll_interval: number;
}
export interface ResetPasswordPollCompletionResponse
extends ContinuousResponse {
status: string;
}
/* Register API response types */
export interface RegisterIntrospectResponse extends ApiResponseBase {
continuation_token: string;
methods: AuthenticationMethod[];
}
export interface RegisterChallengeResponse extends ApiResponseBase {
continuation_token: string;
challenge_type: string;
binding_method: string;
challenge_target: string;
challenge_channel: string;
code_length?: number;
interval?: number;
}
export interface RegisterContinueResponse extends ApiResponseBase {
continuation_token: string;
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const PASSWORD_TOO_WEAK = "password_too_weak";
export const PASSWORD_TOO_SHORT = "password_too_short";
export const PASSWORD_TOO_LONG = "password_too_long";
export const PASSWORD_RECENTLY_USED = "password_recently_used";
export const PASSWORD_BANNED = "password_banned";
export const PASSWORD_IS_INVALID = "password_is_invalid";
export const INVALID_OOB_VALUE = "invalid_oob_value";
export const ATTRIBUTE_VALIATION_FAILED = "attribute_validation_failed";
export const NATIVEAUTHAPI_DISABLED = "nativeauthapi_disabled";
export const REGISTRATION_REQUIRED = "registration_required";
export const MFA_REQUIRED = "mfa_required";
export const PROVIDER_BLOCKED_BY_REPUTATION = "provider_blocked_by_rep";

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ServerTelemetryManager } from "@azure/msal-common/browser";
export type ApiRequestBase = {
correlationId: string;
telemetryManager: ServerTelemetryManager;
};
export type ApiResponseBase = {
correlation_id: string;
};

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { HttpMethod, IHttpClient, RequestBody } from "./IHttpClient.js";
import { HttpError } from "../../error/HttpError.js";
import { AADServerParamKeys, Logger } from "@azure/msal-common/browser";
import {
FailedSendRequest,
NoNetworkConnectivity,
} from "../../error/HttpErrorCodes.js";
/**
* Implementation of IHttpClient using fetch.
*/
export class FetchHttpClient implements IHttpClient {
constructor(private logger: Logger) {}
async sendAsync(
url: string | URL,
options: RequestInit
): Promise<Response> {
const headers = options.headers as Record<string, string>;
const correlationId =
headers?.[AADServerParamKeys.CLIENT_REQUEST_ID] || "";
try {
this.logger.verbosePii(
`Sending request to '${url}'`,
correlationId
);
const startTime = performance.now();
const response = await fetch(url, options);
const endTime = performance.now();
this.logger.verbosePii(
`Request to '${url}' completed in '${
endTime - startTime
}'ms with status code '${response.status}'`,
correlationId
);
return response;
} catch (e) {
this.logger.errorPii(
`Failed to send request to '${url}': '${e}'`,
correlationId
);
if (!window.navigator.onLine) {
throw new HttpError(
NoNetworkConnectivity,
`No network connectivity: ${e}`,
correlationId
);
}
throw new HttpError(
FailedSendRequest,
`Failed to send request: ${e}`,
correlationId
);
}
}
async post(
url: string | URL,
body: RequestBody,
headers: Record<string, string> = {}
): Promise<Response> {
return this.sendAsync(url, {
method: HttpMethod.POST,
headers,
body,
});
}
async get(
url: string | URL,
headers: Record<string, string> = {}
): Promise<Response> {
return this.sendAsync(url, {
method: HttpMethod.GET,
headers,
});
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type RequestBody =
| string
| ArrayBuffer
| DataView
| Blob
| File
| URLSearchParams
| FormData
| ReadableStream;
/**
* Interface for HTTP client.
*/
export interface IHttpClient {
/**
* Sends a request.
* @param url The URL to send the request to.
* @param options Additional fetch options.
*/
sendAsync(url: string | URL, options: RequestInit): Promise<Response>;
/**
* Sends a POST request.
* @param url The URL to send the request to.
* @param body The body of the request.
* @param headers Optional headers for the request.
*/
post(
url: string | URL,
body: RequestBody,
headers?: Record<string, string>
): Promise<Response>;
/**
* Sends a GET request.
* @param url The URL to send the request to.
* @param headers Optional headers for the request.
*/
get(url: string | URL, headers?: Record<string, string>): Promise<Response>;
}
/**
* Represents an HTTP method type.
*/
export const HttpMethod = {
GET: "GET",
POST: "POST",
PUT: "PUT",
DELETE: "DELETE",
} as const;

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/*
* The public API ids should be claim in the MSAL telemtry tracker.
* All the following ids are hardcoded; so we need to find a way to claim them in the future and update them here.
*/
// Sign in
export const SIGN_IN_WITH_CODE_START = 100001;
export const SIGN_IN_WITH_PASSWORD_START = 100002;
export const SIGN_IN_SUBMIT_CODE = 100003;
export const SIGN_IN_SUBMIT_PASSWORD = 100004;
export const SIGN_IN_RESEND_CODE = 100005;
export const SIGN_IN_AFTER_SIGN_UP = 100006;
export const SIGN_IN_AFTER_PASSWORD_RESET = 100007;
// Sign up
export const SIGN_UP_WITH_PASSWORD_START = 100021;
export const SIGN_UP_START = 100022;
export const SIGN_UP_SUBMIT_CODE = 100023;
export const SIGN_UP_SUBMIT_PASSWORD = 100024;
export const SIGN_UP_SUBMIT_ATTRIBUTES = 100025;
export const SIGN_UP_RESEND_CODE = 100026;
// Password reset
export const PASSWORD_RESET_START = 100041;
export const PASSWORD_RESET_SUBMIT_CODE = 100042;
export const PASSWORD_RESET_SUBMIT_PASSWORD = 100043;
export const PASSWORD_RESET_RESEND_CODE = 100044;
// Get account
export const ACCOUNT_GET_ACCOUNT = 100061;
export const ACCOUNT_SIGN_OUT = 100062;
export const ACCOUNT_GET_ACCESS_TOKEN = 100063;
// JIT (Just-In-Time) Auth Method Registration
export const JIT_CHALLENGE_AUTH_METHOD = 100081;
export const JIT_SUBMIT_CHALLENGE = 100082;
// MFA
export const MFA_REQUEST_CHALLENGE = 100101;
export const MFA_SUBMIT_CHALLENGE = 100102;

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { InvalidArgumentError } from "../error/InvalidArgumentError.js";
export function ensureArgumentIsNotNullOrUndefined<T>(
argName: string,
argValue: T | undefined | null,
correlationId?: string
): asserts argValue is T {
if (argValue === null || argValue === undefined) {
throw new InvalidArgumentError(argName, correlationId);
}
}
export function ensureArgumentIsNotEmptyString(
argName: string,
argValue: string | undefined,
correlationId?: string
): void {
if (!argValue || argValue.trim() === "") {
throw new InvalidArgumentError(argName, correlationId);
}
}
export function ensureArgumentIsJSONString(
argName: string,
argValue: string,
correlationId?: string
): void {
try {
const parsed = JSON.parse(argValue);
if (
typeof parsed !== "object" ||
parsed === null ||
Array.isArray(parsed)
) {
throw new InvalidArgumentError(argName, correlationId);
}
} catch (e) {
if (e instanceof SyntaxError) {
throw new InvalidArgumentError(argName, correlationId);
}
throw e; // Rethrow unexpected errors
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Logger } from "@azure/msal-common/browser";
import { CustomHeaderConstants } from "../../CustomAuthConstants.js";
/**
* Filters the headers returned by a {@link CustomAuthRequestInterceptor},
* keeping only those that conform to the custom-auth header naming rules.
*
* Rules (mirrors the iOS / Android native auth implementations):
* - Header names must start with `x-` (case-insensitive); others are dropped.
* - Header names that start with any reserved prefix (`x-client-`, `x-ms-`,
* `x-broker-`, `x-app-`) are dropped.
* - Headers with empty/whitespace-only names or null/undefined values are dropped.
*
* Dropped headers are logged as warnings (PII-safe) when a logger is provided.
*
* @param headers - Raw headers returned by the interceptor.
* @param logger - Optional logger used to emit warnings for dropped headers.
* @param correlationId - Optional correlation id forwarded to the logger.
* @returns A new record containing only the headers that pass the filter,
* preserving the original casing of header names.
*/
export function filterCustomHeaders(
headers: Record<string, string> | null | undefined,
logger?: Logger,
correlationId?: string
): Record<string, string> {
const filtered: Record<string, string> = {};
if (!headers) {
return filtered;
}
for (const [name, value] of Object.entries(headers)) {
if (!name || value === undefined || value === null) {
continue;
}
const trimmedName = name.trim();
if (!trimmedName) {
continue;
}
const lowerName = trimmedName.toLowerCase();
if (!lowerName.startsWith(CustomHeaderConstants.REQUIRED_PREFIX)) {
logger?.warningPii(
`Additional header field "${trimmedName}" must start with the "${CustomHeaderConstants.REQUIRED_PREFIX}" prefix. Ignoring.`,
correlationId ?? ""
);
continue;
}
const reservedPrefix = CustomHeaderConstants.RESERVED_PREFIXES.find(
(prefix) => lowerName.startsWith(prefix)
);
if (reservedPrefix) {
logger?.warningPii(
`Additional header field "${trimmedName}" uses reserved prefix "${reservedPrefix}". Ignoring.`,
correlationId ?? ""
);
continue;
}
filtered[trimmedName] = value;
}
return filtered;
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ParsedUrlError } from "../error/ParsedUrlError.js";
import { InvalidUrl } from "../error/ParsedUrlErrorCodes.js";
export function parseUrl(url: string): URL {
try {
return new URL(url);
} catch (e) {
throw new ParsedUrlError(
InvalidUrl,
`The URL "${url}" is invalid: ${e}`
);
}
}
export function buildUrl(
baseUrl: string,
path: string,
queryParams?: Record<string, string>
): URL {
const newBaseUrl = !baseUrl.endsWith("/") ? `${baseUrl}/` : baseUrl;
const newPath = path.startsWith("/") ? path.slice(1) : path;
const url = new URL(newPath, newBaseUrl);
// Add query parameters if provided
if (queryParams) {
Object.entries(queryParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
});
}
return url;
}

View File

@ -0,0 +1,200 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthBrowserConfiguration } from "../../configuration/CustomAuthConfiguration.js";
import { SignOutResult } from "./result/SignOutResult.js";
import { GetAccessTokenResult } from "./result/GetAccessTokenResult.js";
import { CustomAuthSilentCacheClient } from "../interaction_client/CustomAuthSilentCacheClient.js";
import { NoCachedAccountFoundError } from "../../core/error/NoCachedAccountFoundError.js";
import { DefaultScopes } from "../../CustomAuthConstants.js";
import { AccessTokenRetrievalInputs } from "../../CustomAuthActionInputs.js";
import {
AccountInfo,
CommonSilentFlowRequest,
Constants,
Logger,
TokenClaims,
} from "@azure/msal-common/browser";
import { SilentRequest } from "../../../request/SilentRequest.js";
import * as ArgumentValidator from "../../core/utils/ArgumentValidator.js";
/*
* Account information.
*/
export class CustomAuthAccountData {
constructor(
private readonly account: AccountInfo,
private readonly config: CustomAuthBrowserConfiguration,
private readonly cacheClient: CustomAuthSilentCacheClient,
private readonly logger: Logger,
private readonly correlationId: string
) {
ArgumentValidator.ensureArgumentIsNotEmptyString(
"correlationId",
correlationId
);
ArgumentValidator.ensureArgumentIsNotNullOrUndefined(
"account",
account,
correlationId
);
}
/**
* This method triggers a sign-out operation,
* which removes the current account info and its tokens from browser cache.
* If sign-out successfully, redirect the page to postLogoutRedirectUri if provided in the configuration.
* @returns {Promise<SignOutResult>} The result of the SignOut operation.
*/
async signOut(): Promise<SignOutResult> {
try {
const currentAccount = this.cacheClient.getCurrentAccount(
this.correlationId
);
if (!currentAccount) {
throw new NoCachedAccountFoundError(this.correlationId);
}
this.logger.verbose("Signing out user", this.correlationId);
await this.cacheClient.logout({
correlationId: this.correlationId,
account: currentAccount,
});
this.logger.verbose("User signed out", this.correlationId);
return new SignOutResult();
} catch (error) {
this.logger.errorPii(
`An error occurred during sign out: '${error}'`,
this.correlationId
);
return SignOutResult.createWithError(error);
}
}
getAccount(): AccountInfo {
return this.account;
}
/**
* Gets the raw id-token of current account.
* Idtoken is only issued if openid scope is present in the scopes parameter when requesting for tokens,
* otherwise will return undefined from the response.
* @returns {string|undefined} The account id-token.
*/
getIdToken(): string | undefined {
return this.account.idToken;
}
/**
* Gets the id token claims extracted from raw IdToken of current account.
* @returns {AuthTokenClaims|undefined} The token claims.
*/
getClaims(): AuthTokenClaims | undefined {
return this.account.idTokenClaims;
}
/**
* Gets the access token of current account from browser cache if it is not expired,
* otherwise renew the token using cached refresh token if valid.
* If no refresh token is found or it is expired, then throws error.
* @param {AccessTokenRetrievalInputs} accessTokenRetrievalInputs - The inputs for retrieving the access token.
* @returns {Promise<GetAccessTokenResult>} The result of the operation.
*/
async getAccessToken(
accessTokenRetrievalInputs: AccessTokenRetrievalInputs
): Promise<GetAccessTokenResult> {
try {
ArgumentValidator.ensureArgumentIsNotNullOrUndefined(
"accessTokenRetrievalInputs",
accessTokenRetrievalInputs,
this.correlationId
);
if (accessTokenRetrievalInputs.claims) {
ArgumentValidator.ensureArgumentIsJSONString(
"accessTokenRetrievalInputs.claims",
accessTokenRetrievalInputs.claims,
this.correlationId
);
}
this.logger.verbose("Getting current account.", this.correlationId);
const currentAccount = this.cacheClient.getCurrentAccount(
this.account.username
);
if (!currentAccount) {
throw new NoCachedAccountFoundError(this.correlationId);
}
this.logger.verbose("Getting access token.", this.correlationId);
const newScopes =
accessTokenRetrievalInputs.scopes &&
accessTokenRetrievalInputs.scopes.length > 0
? accessTokenRetrievalInputs.scopes
: [...DefaultScopes];
const commonSilentFlowRequest = this.createCommonSilentFlowRequest(
currentAccount,
accessTokenRetrievalInputs.forceRefresh,
newScopes,
accessTokenRetrievalInputs.claims
);
const result = await this.cacheClient.acquireToken(
commonSilentFlowRequest
);
this.logger.verbose(
"Successfully got access token from cache.",
this.correlationId
);
return new GetAccessTokenResult(result);
} catch (error) {
this.logger.error(
"Failed to get access token from cache.",
this.correlationId
);
return GetAccessTokenResult.createWithError(error);
}
}
private createCommonSilentFlowRequest(
accountInfo: AccountInfo,
forceRefresh: boolean = false,
requestScopes: Array<string>,
claims?: string
): CommonSilentFlowRequest {
const silentRequest: SilentRequest = {
authority: this.config.auth.authority,
correlationId: this.correlationId,
scopes: requestScopes || [],
account: accountInfo,
forceRefresh: forceRefresh || false,
storeInCache: {
idToken: true,
accessToken: true,
refreshToken: true,
},
...(claims && { claims: claims }),
};
return {
...silentRequest,
authenticationScheme: Constants.AuthenticationScheme.BEARER,
} as CommonSilentFlowRequest;
}
}
export type AuthTokenClaims = TokenClaims & {
[key: string]: string | number | string[] | object | undefined | unknown;
};

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowErrorBase } from "../../../core/auth_flow/AuthFlowErrorBase.js";
/**
* The error class for get account errors.
*/
export class GetAccountError extends AuthFlowErrorBase {
/**
* Checks if the error is due to no cached account found.
* @returns true if the error is due to no cached account found, false otherwise.
*/
isCurrentAccountNotFound(): boolean {
return this.isNoCachedAccountFoundError();
}
}
/**
* The error class for sign-out errors.
*/
export class SignOutError extends AuthFlowErrorBase {
/**
* Checks if the error is due to the user is not signed in.
* @returns true if the error is due to the user is not signed in, false otherwise.
*/
isUserNotSignedIn(): boolean {
return this.isNoCachedAccountFoundError();
}
}
/**
* The error class for getting the current account access token errors.
*/
export class GetCurrentAccountAccessTokenError extends AuthFlowErrorBase {
/**
* Checks if the error is due to no cached account found.
* @returns true if the error is due to no cached account found, false otherwise.
*/
isCurrentAccountNotFound(): boolean {
return this.isNoCachedAccountFoundError();
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthenticationResult } from "../../../../response/AuthenticationResult.js";
import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js";
import { GetCurrentAccountAccessTokenError } from "../error_type/GetAccountError.js";
import {
GetAccessTokenCompletedState,
GetAccessTokenFailedState,
} from "../state/GetAccessTokenState.js";
import {
GET_ACCESS_TOKEN_COMPLETED_STATE_TYPE,
GET_ACCESS_TOKEN_FAILED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Result of getting an access token.
*/
export class GetAccessTokenResult extends AuthFlowResultBase<
GetAccessTokenResultState,
GetCurrentAccountAccessTokenError,
AuthenticationResult
> {
/**
* Creates a new instance of GetAccessTokenResult.
* @param resultData The result data of the access token.
*/
constructor(resultData?: AuthenticationResult) {
super(new GetAccessTokenCompletedState(), resultData);
}
/**
* Creates a new instance of GetAccessTokenResult with an error.
* @param error The error that occurred.
* @return {GetAccessTokenResult} The result with the error.
*/
static createWithError(error: unknown): GetAccessTokenResult {
const result = new GetAccessTokenResult();
result.error = new GetCurrentAccountAccessTokenError(
GetAccessTokenResult.createErrorData(error)
);
result.state = new GetAccessTokenFailedState();
return result;
}
/**
* Checks if the result is completed.
*/
isCompleted(): this is GetAccessTokenResult & {
state: GetAccessTokenCompletedState;
} {
return this.state.stateType === GET_ACCESS_TOKEN_COMPLETED_STATE_TYPE;
}
/**
* Checks if the result is failed.
*/
isFailed(): this is GetAccessTokenResult & {
state: GetAccessTokenFailedState;
} {
return this.state.stateType === GET_ACCESS_TOKEN_FAILED_STATE_TYPE;
}
}
/**
* The possible states for the GetAccessTokenResult.
* This includes:
* - GetAccessTokenCompletedState: The access token was successfully retrieved.
* - GetAccessTokenFailedState: The access token retrieval failed.
*/
export type GetAccessTokenResultState =
| GetAccessTokenCompletedState
| GetAccessTokenFailedState;

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js";
import { CustomAuthAccountData } from "../CustomAuthAccountData.js";
import { GetAccountError } from "../error_type/GetAccountError.js";
import {
GetAccountCompletedState,
GetAccountFailedState,
} from "../state/GetAccountState.js";
import {
GET_ACCOUNT_COMPLETED_STATE_TYPE,
GET_ACCOUNT_FAILED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Result of getting an account.
*/
export class GetAccountResult extends AuthFlowResultBase<
GetAccountResultState,
GetAccountError,
CustomAuthAccountData
> {
/**
* Creates a new instance of GetAccountResult.
* @param resultData The result data.
*/
constructor(resultData?: CustomAuthAccountData) {
super(new GetAccountCompletedState(), resultData);
}
/**
* Creates a new instance of GetAccountResult with an error.
* @param error The error data.
*/
static createWithError(error: unknown): GetAccountResult {
const result = new GetAccountResult();
result.error = new GetAccountError(
GetAccountResult.createErrorData(error)
);
result.state = new GetAccountFailedState();
return result;
}
/**
* Checks if the result is in a completed state.
*/
isCompleted(): this is GetAccountResult & {
state: GetAccountCompletedState;
} {
return this.state.stateType === GET_ACCOUNT_COMPLETED_STATE_TYPE;
}
/**
* Checks if the result is in a failed state.
*/
isFailed(): this is GetAccountResult & { state: GetAccountFailedState } {
return this.state.stateType === GET_ACCOUNT_FAILED_STATE_TYPE;
}
}
/**
* The possible states for the GetAccountResult.
* This includes:
* - GetAccountCompletedState: The account was successfully retrieved.
* - GetAccountFailedState: The account retrieval failed.
*/
export type GetAccountResultState =
| GetAccountCompletedState
| GetAccountFailedState;

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js";
import { SignOutError } from "../error_type/GetAccountError.js";
import {
SignOutCompletedState,
SignOutFailedState,
} from "../state/SignOutState.js";
import {
SIGN_OUT_COMPLETED_STATE_TYPE,
SIGN_OUT_FAILED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Result of a sign-out operation.
*/
export class SignOutResult extends AuthFlowResultBase<
SignOutResultState,
SignOutError,
void
> {
/**
* Creates a new instance of SignOutResult.
* @param state The state of the result.
*/
constructor() {
super(new SignOutCompletedState());
}
/**
* Creates a new instance of SignOutResult with an error.
* @param error The error that occurred during the sign-out operation.
*/
static createWithError(error: unknown): SignOutResult {
const result = new SignOutResult();
result.error = new SignOutError(SignOutResult.createErrorData(error));
result.state = new SignOutFailedState();
return result;
}
/**
* Checks if the sign-out operation is completed.
*/
isCompleted(): this is SignOutResult & { state: SignOutCompletedState } {
return this.state.stateType === SIGN_OUT_COMPLETED_STATE_TYPE;
}
/**
* Checks if the sign-out operation failed.
*/
isFailed(): this is SignOutResult & { state: SignOutFailedState } {
return this.state.stateType === SIGN_OUT_FAILED_STATE_TYPE;
}
}
/**
* The possible states for the SignOutResult.
* This includes:
* - SignOutCompletedState: The sign-out operation was successful.
* - SignOutFailedState: The sign-out operation failed.
*/
export type SignOutResultState = SignOutCompletedState | SignOutFailedState;

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js";
import {
GET_ACCESS_TOKEN_COMPLETED_STATE_TYPE,
GET_ACCESS_TOKEN_FAILED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/**
* The completed state of the get access token flow.
*/
export class GetAccessTokenCompletedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = GET_ACCESS_TOKEN_COMPLETED_STATE_TYPE;
}
/**
* The failed state of the get access token flow.
*/
export class GetAccessTokenFailedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = GET_ACCESS_TOKEN_FAILED_STATE_TYPE;
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js";
import {
GET_ACCOUNT_COMPLETED_STATE_TYPE,
GET_ACCOUNT_FAILED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/**
* The completed state of the get account flow.
*/
export class GetAccountCompletedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = GET_ACCOUNT_COMPLETED_STATE_TYPE;
}
/**
* The failed state of the get account flow.
*/
export class GetAccountFailedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = GET_ACCOUNT_FAILED_STATE_TYPE;
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js";
import {
SIGN_OUT_COMPLETED_STATE_TYPE,
SIGN_OUT_FAILED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/**
* The completed state of the sign-out flow.
*/
export class SignOutCompletedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = SIGN_OUT_COMPLETED_STATE_TYPE;
}
/**
* The failed state of the sign-out flow.
*/
export class SignOutFailedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = SIGN_OUT_FAILED_STATE_TYPE;
}

View File

@ -0,0 +1,226 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomAuthAuthority } from "../../core/CustomAuthAuthority.js";
import { DefaultPackageInfo } from "../../CustomAuthConstants.js";
import * as PublicApiId from "../../core/telemetry/PublicApiId.js";
import { CustomAuthInteractionClientBase } from "../../core/interaction_client/CustomAuthInteractionClientBase.js";
import {
AccountInfo,
ClientAuthError,
ClientAuthErrorCodes,
ClientConfiguration,
CommonSilentFlowRequest,
RefreshTokenClient,
ServerTelemetryManager,
SilentFlowClient,
UrlString,
} from "@azure/msal-common/browser";
import { AuthenticationResult } from "../../../response/AuthenticationResult.js";
import { ClearCacheRequest } from "../../../request/ClearCacheRequest.js";
import { ApiId } from "../../../utils/BrowserConstants.js";
import { getCurrentUri } from "../../../utils/BrowserUtils.js";
import {
clearCacheOnLogout,
initializeServerTelemetryManager,
} from "../../../interaction_client/BaseInteractionClient.js";
export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase {
/**
* Acquires a token from the cache if it is not expired. Otherwise, makes a request to renew the token.
* If forceRresh is set to false, then looks up the access token in cache first.
* If access token is expired or not found, then uses refresh token to get a new access token.
* If no refresh token is found or it is expired, then throws error.
* If forceRefresh is set to true, then skips token cache lookup and fetches a new token using refresh token
* If no refresh token is found or it is expired, then throws error.
* @param silentRequest The silent request object.
* @returns {Promise<AuthenticationResult>} The promise that resolves to an AuthenticationResult.
*/
override async acquireToken(
silentRequest: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
const correlationId = silentRequest.correlationId || this.correlationId;
const telemetryManager = initializeServerTelemetryManager(
PublicApiId.ACCOUNT_GET_ACCESS_TOKEN,
this.config.auth.clientId,
correlationId,
this.browserStorage,
this.logger,
silentRequest.forceRefresh
);
const clientConfig = this.getCustomAuthClientConfiguration(
telemetryManager,
this.customAuthAuthority,
correlationId
);
const silentFlowClient = new SilentFlowClient(
clientConfig,
this.performanceClient
);
try {
this.logger.verbose(
"Starting silent flow to acquire token from cache",
correlationId
);
const result = await silentFlowClient.acquireCachedToken(
silentRequest
);
this.logger.verbose(
"Silent flow to acquire token from cache is completed and token is found",
correlationId
);
return result[0] as AuthenticationResult;
} catch (error) {
if (
error instanceof ClientAuthError &&
error.errorCode === ClientAuthErrorCodes.tokenRefreshRequired
) {
this.logger.verbose(
"Token refresh is required to acquire token silently",
correlationId
);
const refreshTokenClient = new RefreshTokenClient(
clientConfig,
this.performanceClient
);
this.logger.verbose(
"Starting refresh flow to refresh token",
correlationId
);
const refreshTokenResult =
await refreshTokenClient.acquireTokenByRefreshToken(
silentRequest,
PublicApiId.ACCOUNT_GET_ACCESS_TOKEN
);
this.logger.verbose(
"Refresh flow to refresh token is completed",
correlationId
);
return refreshTokenResult as AuthenticationResult;
}
throw error;
}
}
override async logout(logoutRequest?: ClearCacheRequest): Promise<void> {
const validLogoutRequest = this.initializeLogoutRequest(logoutRequest);
const correlationId =
logoutRequest?.correlationId || this.correlationId;
// Clear the cache
this.logger.verbose("Start to clear the cache", correlationId);
await clearCacheOnLogout(
this.browserStorage,
this.browserCrypto,
this.logger,
correlationId,
validLogoutRequest?.account
);
this.logger.verbose("Cache cleared", correlationId);
const postLogoutRedirectUri = this.config.auth.postLogoutRedirectUri;
if (postLogoutRedirectUri) {
const absoluteRedirectUri = UrlString.getAbsoluteUrl(
postLogoutRedirectUri,
getCurrentUri()
);
this.logger.verbose(
"Post logout redirect uri is set, redirecting to uri",
correlationId
);
// Redirect to post logout redirect uri
await this.navigationClient.navigateExternal(absoluteRedirectUri, {
apiId: ApiId.logout,
timeout: this.config.system.redirectNavigationTimeout,
noHistory: false,
});
}
}
getCurrentAccount(correlationId: string): AccountInfo | null {
let account: AccountInfo | null = null;
this.logger.verbose(
"Getting the first account from cache.",
correlationId
);
const allAccounts = this.browserStorage.getAllAccounts(
{},
correlationId
);
if (allAccounts.length > 0) {
if (allAccounts.length !== 1) {
this.logger.warning(
"Multiple accounts found in cache. This is not supported in the Native Auth scenario.",
correlationId
);
}
account = allAccounts[0];
}
if (account) {
this.logger.verbose("Account data found.", correlationId);
} else {
this.logger.verbose("No account data found.", correlationId);
}
return account;
}
private getCustomAuthClientConfiguration(
serverTelemetryManager: ServerTelemetryManager,
customAuthAuthority: CustomAuthAuthority,
correlationId: string
): ClientConfiguration {
const logger = this.config.system.loggerOptions;
return {
authOptions: {
clientId: this.config.auth.clientId,
authority: customAuthAuthority,
clientCapabilities: this.config.auth.clientCapabilities,
redirectUri: this.config.auth.redirectUri,
},
systemOptions: {
tokenRenewalOffsetSeconds:
this.config.system.tokenRenewalOffsetSeconds,
preventCorsPreflight: true,
},
loggerOptions: {
loggerCallback: logger.loggerCallback,
piiLoggingEnabled: logger.piiLoggingEnabled,
logLevel: logger.logLevel,
correlationId: correlationId,
},
cryptoInterface: this.browserCrypto,
networkInterface: this.networkClient,
storageInterface: this.browserStorage,
serverTelemetryManager: serverTelemetryManager,
libraryInfo: {
sku: DefaultPackageInfo.SKU,
version: DefaultPackageInfo.VERSION,
cpu: DefaultPackageInfo.CPU,
os: DefaultPackageInfo.OS,
},
telemetry: this.config.telemetry,
};
}
}

View File

@ -0,0 +1,242 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* @packageDocumentation
* @module @azure/msal-browser/custom-auth
*/
/**
* This file is the entrypoint when importing with the custom-auth subpath e.g. "import { someExport } from @azure/msal-browser/custom-auth"
* Additional exports should be added to the applicable exports-*.ts files
*/
// Application and Controller
export { CustomAuthPublicClientApplication } from "./CustomAuthPublicClientApplication.js";
export { ICustomAuthPublicClientApplication } from "./ICustomAuthPublicClientApplication.js";
// Configuration
export { CustomAuthConfiguration } from "./configuration/CustomAuthConfiguration.js";
export {
CustomAuthRequestInterceptor,
CustomAuthAdditionalHeaderFieldsResult,
} from "./configuration/CustomAuthRequestInterceptor.js";
// Models
export { CustomAuthAccountData } from "./get_account/auth_flow/CustomAuthAccountData.js";
export { AuthenticationMethod } from "./core/network_client/custom_auth_api/types/ApiResponseTypes.js";
// Operation Inputs
export {
SignInInputs,
SignUpInputs,
ResetPasswordInputs,
AccountRetrievalInputs,
AccessTokenRetrievalInputs,
SignInWithContinuationTokenInputs,
} from "./CustomAuthActionInputs.js";
// Operation Base State
export { AuthFlowStateBase } from "./core/auth_flow/AuthFlowState.js";
export { AuthFlowActionRequiredStateBase } from "./core/auth_flow/AuthFlowState.js";
// Sign-in State
export { SignInState } from "./sign_in/auth_flow/state/SignInState.js";
export { SignInCodeRequiredState } from "./sign_in/auth_flow/state/SignInCodeRequiredState.js";
export { SignInContinuationState } from "./sign_in/auth_flow/state/SignInContinuationState.js";
export { SignInPasswordRequiredState } from "./sign_in/auth_flow/state/SignInPasswordRequiredState.js";
export { SignInCompletedState } from "./sign_in/auth_flow/state/SignInCompletedState.js";
export { SignInFailedState } from "./sign_in/auth_flow/state/SignInFailedState.js";
// Sign-in Results
export {
SignInResult,
SignInResultState,
} from "./sign_in/auth_flow/result/SignInResult.js";
export {
SignInSubmitCodeResult,
SignInSubmitCodeResultState,
} from "./sign_in/auth_flow/result/SignInSubmitCodeResult.js";
export {
SignInResendCodeResult,
SignInResendCodeResultState,
} from "./sign_in/auth_flow/result/SignInResendCodeResult.js";
export {
SignInSubmitPasswordResult,
SignInSubmitPasswordResultState,
} from "./sign_in/auth_flow/result/SignInSubmitPasswordResult.js";
// Sign-in Errors
export {
SignInError,
SignInSubmitPasswordError,
SignInSubmitCodeError,
SignInResendCodeError,
} from "./sign_in/auth_flow/error_type/SignInError.js";
// Sign-up User Account Attributes
export { UserAccountAttributes } from "./UserAccountAttributes.js";
// Sign-up State
export { SignUpState } from "./sign_up/auth_flow/state/SignUpState.js";
export { SignUpAttributesRequiredState } from "./sign_up/auth_flow/state/SignUpAttributesRequiredState.js";
export { SignUpCodeRequiredState } from "./sign_up/auth_flow/state/SignUpCodeRequiredState.js";
export { SignUpPasswordRequiredState } from "./sign_up/auth_flow/state/SignUpPasswordRequiredState.js";
export { SignUpCompletedState } from "./sign_up/auth_flow/state/SignUpCompletedState.js";
export { SignUpFailedState } from "./sign_up/auth_flow/state/SignUpFailedState.js";
// Sign-up Results
export {
SignUpResult,
SignUpResultState,
} from "./sign_up/auth_flow/result/SignUpResult.js";
export {
SignUpSubmitAttributesResult,
SignUpSubmitAttributesResultState,
} from "./sign_up/auth_flow/result/SignUpSubmitAttributesResult.js";
export {
SignUpSubmitCodeResult,
SignUpSubmitCodeResultState,
} from "./sign_up/auth_flow/result/SignUpSubmitCodeResult.js";
export {
SignUpResendCodeResult,
SignUpResendCodeResultState,
} from "./sign_up/auth_flow/result/SignUpResendCodeResult.js";
export {
SignUpSubmitPasswordResult,
SignUpSubmitPasswordResultState,
} from "./sign_up/auth_flow/result/SignUpSubmitPasswordResult.js";
// Sign-up Errors
export {
SignUpError,
SignUpSubmitPasswordError,
SignUpSubmitCodeError,
SignUpSubmitAttributesError,
SignUpResendCodeError,
} from "./sign_up/auth_flow/error_type/SignUpError.js";
// Reset-password State
export { ResetPasswordState } from "./reset_password/auth_flow/state/ResetPasswordState.js";
export { ResetPasswordCodeRequiredState } from "./reset_password/auth_flow/state/ResetPasswordCodeRequiredState.js";
export { ResetPasswordPasswordRequiredState } from "./reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.js";
export { ResetPasswordCompletedState } from "./reset_password/auth_flow/state/ResetPasswordCompletedState.js";
export { ResetPasswordFailedState } from "./reset_password/auth_flow/state/ResetPasswordFailedState.js";
// Reset-password Results
export {
ResetPasswordStartResult,
ResetPasswordStartResultState,
} from "./reset_password/auth_flow/result/ResetPasswordStartResult.js";
export {
ResetPasswordSubmitCodeResult,
ResetPasswordSubmitCodeResultState,
} from "./reset_password/auth_flow/result/ResetPasswordSubmitCodeResult.js";
export {
ResetPasswordResendCodeResult,
ResetPasswordResendCodeResultState,
} from "./reset_password/auth_flow/result/ResetPasswordResendCodeResult.js";
export {
ResetPasswordSubmitPasswordResult,
ResetPasswordSubmitPasswordResultState,
} from "./reset_password/auth_flow/result/ResetPasswordSubmitPasswordResult.js";
// Reset-password Errors
export {
ResetPasswordError,
ResetPasswordSubmitPasswordError,
ResetPasswordSubmitCodeError,
ResetPasswordResendCodeError,
} from "./reset_password/auth_flow/error_type/ResetPasswordError.js";
// Get Access Token Results
export {
GetAccessTokenResult,
GetAccessTokenResultState,
} from "./get_account/auth_flow/result/GetAccessTokenResult.js";
// Get Account Results
export {
GetAccountResult,
GetAccountResultState,
} from "./get_account/auth_flow/result/GetAccountResult.js";
// Sign Out Results
export {
SignOutResult,
SignOutResultState,
} from "./get_account/auth_flow/result/SignOutResult.js";
// Token Management Errors
export {
GetAccountError,
SignOutError,
GetCurrentAccountAccessTokenError,
} from "./get_account/auth_flow/error_type/GetAccountError.js";
// Errors
export { CustomAuthApiError } from "./core/error/CustomAuthApiError.js";
export { CustomAuthError } from "./core/error/CustomAuthError.js";
export { HttpError } from "./core/error/HttpError.js";
export { InvalidArgumentError } from "./core/error/InvalidArgumentError.js";
export { InvalidConfigurationError } from "./core/error/InvalidConfigurationError.js";
export { MethodNotImplementedError } from "./core/error/MethodNotImplementedError.js";
export { MsalCustomAuthError } from "./core/error/MsalCustomAuthError.js";
export { NoCachedAccountFoundError } from "./core/error/NoCachedAccountFoundError.js";
export { ParsedUrlError } from "./core/error/ParsedUrlError.js";
export { UnexpectedError } from "./core/error/UnexpectedError.js";
export { UnsupportedEnvironmentError } from "./core/error/UnsupportedEnvironmentError.js";
export { UserAccountAttributeError } from "./core/error/UserAccountAttributeError.js";
export { UserAlreadySignedInError } from "./core/error/UserAlreadySignedInError.js";
// Auth Method Registration State
export { AuthMethodRegistrationRequiredState } from "./core/auth_flow/jit/state/AuthMethodRegistrationState.js";
export { AuthMethodVerificationRequiredState } from "./core/auth_flow/jit/state/AuthMethodRegistrationState.js";
export { AuthMethodRegistrationCompletedState } from "./core/auth_flow/jit/state/AuthMethodRegistrationCompletedState.js";
export { AuthMethodRegistrationFailedState } from "./core/auth_flow/jit/state/AuthMethodRegistrationFailedState.js";
// Auth Method Registration Results
export {
AuthMethodRegistrationChallengeMethodResult,
AuthMethodRegistrationChallengeMethodResultState,
} from "./core/auth_flow/jit/result/AuthMethodRegistrationChallengeMethodResult.js";
export {
AuthMethodRegistrationSubmitChallengeResult,
AuthMethodRegistrationSubmitChallengeResultState,
} from "./core/auth_flow/jit/result/AuthMethodRegistrationSubmitChallengeResult.js";
// Auth Method Registration Errors
export {
AuthMethodRegistrationChallengeMethodError,
AuthMethodRegistrationSubmitChallengeError,
} from "./core/auth_flow/jit/error_type/AuthMethodRegistrationError.js";
// Auth Method Registration Types
export { AuthMethodDetails } from "./core/auth_flow/jit/AuthMethodDetails.js";
// MFA State
export { MfaAwaitingState } from "./core/auth_flow/mfa/state/MfaState.js";
export { MfaVerificationRequiredState } from "./core/auth_flow/mfa/state/MfaState.js";
export { MfaCompletedState } from "./core/auth_flow/mfa/state/MfaCompletedState.js";
export { MfaFailedState } from "./core/auth_flow/mfa/state/MfaFailedState.js";
// MFA Results
export {
MfaRequestChallengeResult,
MfaRequestChallengeResultState,
} from "./core/auth_flow/mfa/result/MfaRequestChallengeResult.js";
export {
MfaSubmitChallengeResult,
MfaSubmitChallengeResultState,
} from "./core/auth_flow/mfa/result/MfaSubmitChallengeResult.js";
// MFA Errors
export {
MfaRequestChallengeError,
MfaSubmitChallengeError,
} from "./core/auth_flow/mfa/error_type/MfaError.js";
// Components from msal_browser
export { LogLevel } from "@azure/msal-common/browser";

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseOperatingContext } from "../../operatingcontext/BaseOperatingContext.js";
import {
CustomAuthBrowserConfiguration,
CustomAuthConfiguration,
CustomAuthOptions,
} from "../configuration/CustomAuthConfiguration.js";
export class CustomAuthOperatingContext extends BaseOperatingContext {
private readonly customAuthOptions: CustomAuthOptions;
private static readonly MODULE_NAME: string = "";
private static readonly ID: string = "CustomAuthOperatingContext";
constructor(configuration: CustomAuthConfiguration) {
super(configuration);
this.customAuthOptions = configuration.customAuth;
}
getModuleName(): string {
return CustomAuthOperatingContext.MODULE_NAME;
}
getId(): string {
return CustomAuthOperatingContext.ID;
}
getCustomAuthConfig(): CustomAuthBrowserConfiguration {
return {
...this.getConfig(),
customAuth: this.customAuthOptions,
};
}
async initialize(): Promise<boolean> {
this.available = typeof window !== "undefined";
return this.available;
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthActionErrorBase } from "../../../core/auth_flow/AuthFlowErrorBase.js";
import { CustomAuthApiError } from "../../../core/error/CustomAuthApiError.js";
import * as CustomAuthApiErrorCode from "../../../core/network_client/custom_auth_api/types/ApiErrorCodes.js";
export class ResetPasswordError extends AuthActionErrorBase {
/**
* Checks if the error is due to the user not being found.
* @returns true if the error is due to the user not being found, false otherwise.
*/
isUserNotFound(): boolean {
return this.isUserNotFoundError();
}
/**
* Checks if the error is due to the username being invalid.
* @returns true if the error is due to the username being invalid, false otherwise.
*/
isInvalidUsername(): boolean {
return this.isUserInvalidError();
}
/**
* Checks if the error is due to the provided challenge type is not supported.
* @returns {boolean} True if the error is due to the provided challenge type is not supported, false otherwise.
*/
isUnsupportedChallengeType(): boolean {
return this.isUnsupportedChallengeTypeError();
}
}
export class ResetPasswordSubmitPasswordError extends AuthActionErrorBase {
/**
* Checks if the new password is invalid or incorrect.
* @returns {boolean} True if the new password is invalid, false otherwise.
*/
isInvalidPassword(): boolean {
return (
this.isInvalidNewPasswordError() || this.isPasswordIncorrectError()
);
}
/**
* Checks if the password reset failed due to reset timeout or password change failed.
* @returns {boolean} True if the password reset failed, false otherwise.
*/
isPasswordResetFailed(): boolean {
return (
this.errorData instanceof CustomAuthApiError &&
(this.errorData.error ===
CustomAuthApiErrorCode.PASSWORD_RESET_TIMEOUT ||
this.errorData.error ===
CustomAuthApiErrorCode.PASSWORD_CHANGE_FAILED)
);
}
}
export class ResetPasswordSubmitCodeError extends AuthActionErrorBase {
/**
* Checks if the provided code is invalid.
* @returns {boolean} True if the provided code is invalid, false otherwise.
*/
isInvalidCode(): boolean {
return this.isInvalidCodeError();
}
}
export class ResetPasswordResendCodeError extends AuthActionErrorBase {}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js";
import { ResetPasswordResendCodeError } from "../error_type/ResetPasswordError.js";
import type { ResetPasswordCodeRequiredState } from "../state/ResetPasswordCodeRequiredState.js";
import { ResetPasswordFailedState } from "../state/ResetPasswordFailedState.js";
import {
RESET_PASSWORD_FAILED_STATE_TYPE,
RESET_PASSWORD_CODE_REQUIRED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Result of resending code in a reset password operation.
*/
export class ResetPasswordResendCodeResult extends AuthFlowResultBase<
ResetPasswordResendCodeResultState,
ResetPasswordResendCodeError,
void
> {
/**
* Creates a new instance of ResetPasswordResendCodeResult.
* @param state The state of the result.
*/
constructor(state: ResetPasswordResendCodeResultState) {
super(state);
}
/**
* Creates a new instance of ResetPasswordResendCodeResult with an error.
* @param error The error that occurred.
* @returns {ResetPasswordResendCodeResult} A new instance of ResetPasswordResendCodeResult with the error set.
*/
static createWithError(error: unknown): ResetPasswordResendCodeResult {
const result = new ResetPasswordResendCodeResult(
new ResetPasswordFailedState()
);
result.error = new ResetPasswordResendCodeError(
ResetPasswordResendCodeResult.createErrorData(error)
);
return result;
}
/**
* Checks if the result is in a failed state.
*/
isFailed(): this is ResetPasswordResendCodeResult & {
state: ResetPasswordFailedState;
} {
return this.state.stateType === RESET_PASSWORD_FAILED_STATE_TYPE;
}
/**
* Checks if the result is in a code required state.
*/
isCodeRequired(): this is ResetPasswordResendCodeResult & {
state: ResetPasswordCodeRequiredState;
} {
return this.state.stateType === RESET_PASSWORD_CODE_REQUIRED_STATE_TYPE;
}
}
/**
* The possible states for the ResetPasswordResendCodeResult.
* This includes:
* - ResetPasswordCodeRequiredState: The reset password process requires a code.
* - ResetPasswordFailedState: The reset password process has failed.
*/
export type ResetPasswordResendCodeResultState =
| ResetPasswordCodeRequiredState
| ResetPasswordFailedState;

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js";
import { ResetPasswordError } from "../error_type/ResetPasswordError.js";
import { ResetPasswordCodeRequiredState } from "../state/ResetPasswordCodeRequiredState.js";
import { ResetPasswordFailedState } from "../state/ResetPasswordFailedState.js";
import {
RESET_PASSWORD_FAILED_STATE_TYPE,
RESET_PASSWORD_CODE_REQUIRED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Result of a reset password operation.
*/
export class ResetPasswordStartResult extends AuthFlowResultBase<
ResetPasswordStartResultState,
ResetPasswordError,
void
> {
/**
* Creates a new instance of ResetPasswordStartResult.
* @param state The state of the result.
*/
constructor(state: ResetPasswordStartResultState) {
super(state);
}
/**
* Creates a new instance of ResetPasswordStartResult with an error.
* @param error The error that occurred.
* @returns {ResetPasswordStartResult} A new instance of ResetPasswordStartResult with the error set.
*/
static createWithError(error: unknown): ResetPasswordStartResult {
const result = new ResetPasswordStartResult(
new ResetPasswordFailedState()
);
result.error = new ResetPasswordError(
ResetPasswordStartResult.createErrorData(error)
);
return result;
}
/**
* Checks if the result is in a failed state.
*/
isFailed(): this is ResetPasswordStartResult & {
state: ResetPasswordFailedState;
} {
return this.state.stateType === RESET_PASSWORD_FAILED_STATE_TYPE;
}
/**
* Checks if the result is in a code required state.
*/
isCodeRequired(): this is ResetPasswordStartResult & {
state: ResetPasswordCodeRequiredState;
} {
return this.state.stateType === RESET_PASSWORD_CODE_REQUIRED_STATE_TYPE;
}
}
/**
* The possible states for the ResetPasswordStartResult.
* This includes:
* - ResetPasswordCodeRequiredState: The reset password process requires a code.
* - ResetPasswordFailedState: The reset password process has failed.
*/
export type ResetPasswordStartResultState =
| ResetPasswordCodeRequiredState
| ResetPasswordFailedState;

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js";
import { ResetPasswordSubmitCodeError } from "../error_type/ResetPasswordError.js";
import { ResetPasswordFailedState } from "../state/ResetPasswordFailedState.js";
import { ResetPasswordPasswordRequiredState } from "../state/ResetPasswordPasswordRequiredState.js";
import {
RESET_PASSWORD_FAILED_STATE_TYPE,
RESET_PASSWORD_PASSWORD_REQUIRED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Result of a reset password operation that requires a code.
*/
export class ResetPasswordSubmitCodeResult extends AuthFlowResultBase<
ResetPasswordSubmitCodeResultState,
ResetPasswordSubmitCodeError,
void
> {
/**
* Creates a new instance of ResetPasswordSubmitCodeResult.
* @param state The state of the result.
*/
constructor(state: ResetPasswordSubmitCodeResultState) {
super(state);
}
/**
* Creates a new instance of ResetPasswordSubmitCodeResult with an error.
* @param error The error that occurred.
* @returns {ResetPasswordSubmitCodeResult} A new instance of ResetPasswordSubmitCodeResult with the error set.
*/
static createWithError(error: unknown): ResetPasswordSubmitCodeResult {
const result = new ResetPasswordSubmitCodeResult(
new ResetPasswordFailedState()
);
result.error = new ResetPasswordSubmitCodeError(
ResetPasswordSubmitCodeResult.createErrorData(error)
);
return result;
}
/**
* Checks if the result is in a failed state.
*/
isFailed(): this is ResetPasswordSubmitCodeResult & {
state: ResetPasswordFailedState;
} {
return this.state.stateType === RESET_PASSWORD_FAILED_STATE_TYPE;
}
/**
* Checks if the result is in a password required state.
*/
isPasswordRequired(): this is ResetPasswordSubmitCodeResult & {
state: ResetPasswordPasswordRequiredState;
} {
return (
this.state.stateType === RESET_PASSWORD_PASSWORD_REQUIRED_STATE_TYPE
);
}
}
/**
* The possible states for the ResetPasswordSubmitCodeResult.
* This includes:
* - ResetPasswordPasswordRequiredState: The reset password process requires a password.
* - ResetPasswordFailedState: The reset password process has failed.
*/
export type ResetPasswordSubmitCodeResultState =
| ResetPasswordPasswordRequiredState
| ResetPasswordFailedState;

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js";
import { ResetPasswordSubmitPasswordError } from "../error_type/ResetPasswordError.js";
import { ResetPasswordCompletedState } from "../state/ResetPasswordCompletedState.js";
import { ResetPasswordFailedState } from "../state/ResetPasswordFailedState.js";
import {
RESET_PASSWORD_FAILED_STATE_TYPE,
RESET_PASSWORD_COMPLETED_STATE_TYPE,
} from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Result of a reset password operation that requires a password.
*/
export class ResetPasswordSubmitPasswordResult extends AuthFlowResultBase<
ResetPasswordSubmitPasswordResultState,
ResetPasswordSubmitPasswordError,
void
> {
/**
* Creates a new instance of ResetPasswordSubmitPasswordResult.
* @param state The state of the result.
*/
constructor(state: ResetPasswordSubmitPasswordResultState) {
super(state);
}
static createWithError(error: unknown): ResetPasswordSubmitPasswordResult {
const result = new ResetPasswordSubmitPasswordResult(
new ResetPasswordFailedState()
);
result.error = new ResetPasswordSubmitPasswordError(
ResetPasswordSubmitPasswordResult.createErrorData(error)
);
return result;
}
/**
* Checks if the result is in a failed state.
*/
isFailed(): this is ResetPasswordSubmitPasswordResult & {
state: ResetPasswordFailedState;
} {
return this.state.stateType === RESET_PASSWORD_FAILED_STATE_TYPE;
}
/**
* Checks if the result is in a completed state.
*/
isCompleted(): this is ResetPasswordSubmitPasswordResult & {
state: ResetPasswordCompletedState;
} {
return this.state.stateType === RESET_PASSWORD_COMPLETED_STATE_TYPE;
}
}
/**
* The possible states for the ResetPasswordSubmitPasswordResult.
* This includes:
* - ResetPasswordCompletedState: The reset password process has completed successfully.
* - ResetPasswordFailedState: The reset password process has failed.
*/
export type ResetPasswordSubmitPasswordResultState =
| ResetPasswordCompletedState
| ResetPasswordFailedState;

View File

@ -0,0 +1,140 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ResetPasswordResendCodeResult } from "../result/ResetPasswordResendCodeResult.js";
import { ResetPasswordSubmitCodeResult } from "../result/ResetPasswordSubmitCodeResult.js";
import { ResetPasswordCodeRequiredStateParameters } from "./ResetPasswordStateParameters.js";
import { ResetPasswordState } from "./ResetPasswordState.js";
import { ResetPasswordPasswordRequiredState } from "./ResetPasswordPasswordRequiredState.js";
import { RESET_PASSWORD_CODE_REQUIRED_STATE_TYPE } from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Reset password code required state.
*/
export class ResetPasswordCodeRequiredState extends ResetPasswordState<ResetPasswordCodeRequiredStateParameters> {
/**
* The type of the state.
*/
stateType = RESET_PASSWORD_CODE_REQUIRED_STATE_TYPE;
/**
* Submits a one-time passcode that the customer user received in their email in order to continue password reset flow.
* @param {string} code - The code to submit.
* @returns {Promise<ResetPasswordSubmitCodeResult>} The result of the operation.
*/
async submitCode(code: string): Promise<ResetPasswordSubmitCodeResult> {
try {
this.ensureCodeIsValid(code, this.stateParameters.codeLength);
this.stateParameters.logger.verbose(
"Submitting code for password reset.",
this.stateParameters.correlationId
);
const result =
await this.stateParameters.resetPasswordClient.submitCode({
clientId: this.stateParameters.config.auth.clientId,
correlationId: this.stateParameters.correlationId,
challengeType:
this.stateParameters.config.customAuth.challengeTypes ??
[],
continuationToken:
this.stateParameters.continuationToken ?? "",
code: code,
username: this.stateParameters.username,
});
this.stateParameters.logger.verbose(
"Code is submitted for password reset.",
this.stateParameters.correlationId
);
return new ResetPasswordSubmitCodeResult(
new ResetPasswordPasswordRequiredState({
correlationId: result.correlationId,
continuationToken: result.continuationToken,
logger: this.stateParameters.logger,
config: this.stateParameters.config,
resetPasswordClient:
this.stateParameters.resetPasswordClient,
signInClient: this.stateParameters.signInClient,
cacheClient: this.stateParameters.cacheClient,
jitClient: this.stateParameters.jitClient,
mfaClient: this.stateParameters.mfaClient,
username: this.stateParameters.username,
})
);
} catch (error) {
this.stateParameters.logger.errorPii(
`Failed to submit code for password reset. Error: '${error}'.`,
this.stateParameters.correlationId
);
return ResetPasswordSubmitCodeResult.createWithError(error);
}
}
/**
* Resends another one-time passcode if the previous one hasn't been verified
* @returns {Promise<ResetPasswordResendCodeResult>} The result of the operation.
*/
async resendCode(): Promise<ResetPasswordResendCodeResult> {
try {
this.stateParameters.logger.verbose(
"Resending code for password reset.",
this.stateParameters.correlationId
);
const result =
await this.stateParameters.resetPasswordClient.resendCode({
clientId: this.stateParameters.config.auth.clientId,
challengeType:
this.stateParameters.config.customAuth.challengeTypes ??
[],
username: this.stateParameters.username,
correlationId: this.stateParameters.correlationId,
continuationToken:
this.stateParameters.continuationToken ?? "",
});
this.stateParameters.logger.verbose(
"Code is resent for password reset.",
this.stateParameters.correlationId
);
return new ResetPasswordResendCodeResult(
new ResetPasswordCodeRequiredState({
correlationId: result.correlationId,
continuationToken: result.continuationToken,
logger: this.stateParameters.logger,
config: this.stateParameters.config,
resetPasswordClient:
this.stateParameters.resetPasswordClient,
signInClient: this.stateParameters.signInClient,
cacheClient: this.stateParameters.cacheClient,
jitClient: this.stateParameters.jitClient,
mfaClient: this.stateParameters.mfaClient,
username: this.stateParameters.username,
codeLength: result.codeLength,
})
);
} catch (error) {
this.stateParameters.logger.errorPii(
`Failed to resend code for password reset. Error: '${error}'.`,
this.stateParameters.correlationId
);
return ResetPasswordResendCodeResult.createWithError(error);
}
}
/**
* Gets the sent code length.
* @returns {number} The length of the code.
*/
getCodeLength(): number {
return this.stateParameters.codeLength;
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { SignInContinuationState } from "../../../sign_in/auth_flow/state/SignInContinuationState.js";
import { RESET_PASSWORD_COMPLETED_STATE_TYPE } from "../../../core/auth_flow/AuthFlowStateTypes.js";
/**
* Represents the state that indicates the successful completion of a password reset operation.
*/
export class ResetPasswordCompletedState extends SignInContinuationState {
/**
* The type of the state.
*/
stateType = RESET_PASSWORD_COMPLETED_STATE_TYPE;
}

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js";
import { RESET_PASSWORD_FAILED_STATE_TYPE } from "../../../core/auth_flow/AuthFlowStateTypes.js";
/**
* State of a reset password operation that has failed.
*/
export class ResetPasswordFailedState extends AuthFlowStateBase {
/**
* The type of the state.
*/
stateType = RESET_PASSWORD_FAILED_STATE_TYPE;
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ResetPasswordSubmitPasswordResult } from "../result/ResetPasswordSubmitPasswordResult.js";
import { ResetPasswordState } from "./ResetPasswordState.js";
import { ResetPasswordPasswordRequiredStateParameters } from "./ResetPasswordStateParameters.js";
import { ResetPasswordCompletedState } from "./ResetPasswordCompletedState.js";
import { SignInScenario } from "../../../sign_in/auth_flow/SignInScenario.js";
import { RESET_PASSWORD_PASSWORD_REQUIRED_STATE_TYPE } from "../../../core/auth_flow/AuthFlowStateTypes.js";
/*
* Reset password password required state.
*/
export class ResetPasswordPasswordRequiredState extends ResetPasswordState<ResetPasswordPasswordRequiredStateParameters> {
/**
* The type of the state.
*/
stateType = RESET_PASSWORD_PASSWORD_REQUIRED_STATE_TYPE;
/**
* Submits a new password for reset password flow.
* @param {string} password - The password to submit.
* @returns {Promise<ResetPasswordSubmitPasswordResult>} The result of the operation.
*/
async submitNewPassword(
password: string
): Promise<ResetPasswordSubmitPasswordResult> {
try {
this.ensurePasswordIsNotEmpty(password);
this.stateParameters.logger.verbose(
"Submitting new password for password reset.",
this.stateParameters.correlationId
);
const result =
await this.stateParameters.resetPasswordClient.submitNewPassword(
{
clientId: this.stateParameters.config.auth.clientId,
correlationId: this.stateParameters.correlationId,
challengeType:
this.stateParameters.config.customAuth
.challengeTypes ?? [],
continuationToken:
this.stateParameters.continuationToken ?? "",
newPassword: password,
username: this.stateParameters.username,
}
);
this.stateParameters.logger.verbose(
"New password is submitted for sign-up.",
this.stateParameters.correlationId
);
return new ResetPasswordSubmitPasswordResult(
new ResetPasswordCompletedState({
correlationId: result.correlationId,
continuationToken: result.continuationToken,
logger: this.stateParameters.logger,
config: this.stateParameters.config,
username: this.stateParameters.username,
signInClient: this.stateParameters.signInClient,
cacheClient: this.stateParameters.cacheClient,
jitClient: this.stateParameters.jitClient,
mfaClient: this.stateParameters.mfaClient,
signInScenario: SignInScenario.SignInAfterPasswordReset,
})
);
} catch (error) {
this.stateParameters.logger.errorPii(
`Failed to submit password for password reset. Error: '${error}'.`,
this.stateParameters.correlationId
);
return ResetPasswordSubmitPasswordResult.createWithError(error);
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthFlowActionRequiredStateBase } from "../../../core/auth_flow/AuthFlowState.js";
import { ensureArgumentIsNotEmptyString } from "../../../core/utils/ArgumentValidator.js";
import { ResetPasswordStateParameters } from "./ResetPasswordStateParameters.js";
/*
* Base state handler for reset password operation.
*/
export abstract class ResetPasswordState<
TParameters extends ResetPasswordStateParameters
> extends AuthFlowActionRequiredStateBase<TParameters> {
/*
* Creates a new state for reset password operation.
* @param stateParameters - The state parameters for reset-password.
*/
constructor(stateParameters: TParameters) {
super(stateParameters);
ensureArgumentIsNotEmptyString(
"username",
this.stateParameters.username,
this.stateParameters.correlationId
);
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ResetPasswordClient } from "../../interaction_client/ResetPasswordClient.js";
import { SignInClient } from "../../../sign_in/interaction_client/SignInClient.js";
import { CustomAuthSilentCacheClient } from "../../../get_account/interaction_client/CustomAuthSilentCacheClient.js";
import { AuthFlowActionRequiredStateParameters } from "../../../core/auth_flow/AuthFlowState.js";
import { JitClient } from "../../../core/interaction_client/jit/JitClient.js";
import { MfaClient } from "../../../core/interaction_client/mfa/MfaClient.js";
export interface ResetPasswordStateParameters
extends AuthFlowActionRequiredStateParameters {
username: string;
resetPasswordClient: ResetPasswordClient;
signInClient: SignInClient;
cacheClient: CustomAuthSilentCacheClient;
jitClient: JitClient;
mfaClient: MfaClient;
}
export type ResetPasswordPasswordRequiredStateParameters =
ResetPasswordStateParameters;
export interface ResetPasswordCodeRequiredStateParameters
extends ResetPasswordStateParameters {
codeLength: number;
}

View File

@ -0,0 +1,337 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ServerTelemetryManager } from "@azure/msal-common/browser";
import { CustomAuthApiError } from "../../core/error/CustomAuthApiError.js";
import { CustomAuthInteractionClientBase } from "../../core/interaction_client/CustomAuthInteractionClientBase.js";
import * as CustomAuthApiErrorCode from "../../core/network_client/custom_auth_api/types/ApiErrorCodes.js";
import {
ResetPasswordChallengeRequest,
ResetPasswordContinueRequest,
ResetPasswordPollCompletionRequest,
ResetPasswordStartRequest,
ResetPasswordSubmitRequest,
} from "../../core/network_client/custom_auth_api/types/ApiRequestTypes.js";
import * as PublicApiId from "../../core/telemetry/PublicApiId.js";
import {
ChallengeType,
DefaultCustomAuthApiCodeLength,
PasswordResetPollingTimeoutInMs,
ResetPasswordPollStatus,
} from "../../CustomAuthConstants.js";
import {
ResetPasswordResendCodeParams,
ResetPasswordStartParams,
ResetPasswordSubmitCodeParams,
ResetPasswordSubmitNewPasswordParams,
} from "./parameter/ResetPasswordParams.js";
import {
ResetPasswordCodeRequiredResult,
ResetPasswordCompletedResult,
ResetPasswordPasswordRequiredResult,
} from "./result/ResetPasswordActionResult.js";
import { ensureArgumentIsNotEmptyString } from "../../core/utils/ArgumentValidator.js";
import { initializeServerTelemetryManager } from "../../../interaction_client/BaseInteractionClient.js";
export class ResetPasswordClient extends CustomAuthInteractionClientBase {
/**
* Starts the password reset flow.
* @param parameters The parameters for starting the password reset flow.
* @returns The result of password reset start operation.
*/
async start(
parameters: ResetPasswordStartParams
): Promise<ResetPasswordCodeRequiredResult> {
const correlationId = parameters.correlationId || this.correlationId;
const apiId = PublicApiId.PASSWORD_RESET_START;
const telemetryManager = initializeServerTelemetryManager(
apiId,
this.config.auth.clientId,
correlationId,
this.browserStorage,
this.logger
);
const startRequest: ResetPasswordStartRequest = {
challenge_type: this.getChallengeTypes(parameters.challengeType),
username: parameters.username,
correlationId: correlationId,
telemetryManager: telemetryManager,
};
this.logger.verbose(
"Calling start endpoint for password reset flow.",
correlationId
);
const startResponse =
await this.customAuthApiClient.resetPasswordApi.start(startRequest);
this.logger.verbose(
"Start endpoint for password reset returned successfully.",
correlationId
);
const challengeRequest: ResetPasswordChallengeRequest = {
continuation_token: startResponse.continuation_token ?? "",
challenge_type: this.getChallengeTypes(parameters.challengeType),
correlationId: startResponse.correlation_id || correlationId,
telemetryManager: telemetryManager,
};
return this.performChallengeRequest(challengeRequest);
}
/**
* Submits the code for password reset.
* @param parameters The parameters for submitting the code for password reset.
* @returns The result of submitting the code for password reset.
*/
async submitCode(
parameters: ResetPasswordSubmitCodeParams
): Promise<ResetPasswordPasswordRequiredResult> {
const correlationId = parameters.correlationId || this.correlationId;
ensureArgumentIsNotEmptyString(
"parameters.code",
parameters.code,
correlationId
);
const apiId = PublicApiId.PASSWORD_RESET_SUBMIT_CODE;
const telemetryManager = initializeServerTelemetryManager(
apiId,
this.config.auth.clientId,
correlationId,
this.browserStorage,
this.logger
);
const continueRequest: ResetPasswordContinueRequest = {
continuation_token: parameters.continuationToken,
oob: parameters.code,
correlationId: correlationId,
telemetryManager: telemetryManager,
};
this.logger.verbose(
"Calling continue endpoint with code for password reset.",
correlationId
);
const response =
await this.customAuthApiClient.resetPasswordApi.continueWithCode(
continueRequest
);
this.logger.verbose(
"Continue endpoint called successfully with code for password reset.",
response.correlation_id
);
return {
correlationId: response.correlation_id || correlationId,
continuationToken: response.continuation_token ?? "",
};
}
/**
* Resends the another one-time passcode if the previous one hasn't been verified
* @param parameters The parameters for resending the code for password reset.
* @returns The result of resending the code for password reset.
*/
async resendCode(
parameters: ResetPasswordResendCodeParams
): Promise<ResetPasswordCodeRequiredResult> {
const correlationId = parameters.correlationId || this.correlationId;
const apiId = PublicApiId.PASSWORD_RESET_RESEND_CODE;
const telemetryManager = initializeServerTelemetryManager(
apiId,
this.config.auth.clientId,
correlationId,
this.browserStorage,
this.logger
);
const challengeRequest: ResetPasswordChallengeRequest = {
continuation_token: parameters.continuationToken,
challenge_type: this.getChallengeTypes(parameters.challengeType),
correlationId: correlationId,
telemetryManager: telemetryManager,
};
return this.performChallengeRequest(challengeRequest);
}
/**
* Submits the new password for password reset.
* @param parameters The parameters for submitting the new password for password reset.
* @returns The result of submitting the new password for password reset.
*/
async submitNewPassword(
parameters: ResetPasswordSubmitNewPasswordParams
): Promise<ResetPasswordCompletedResult> {
const correlationId = parameters.correlationId || this.correlationId;
ensureArgumentIsNotEmptyString(
"parameters.newPassword",
parameters.newPassword,
correlationId
);
const apiId = PublicApiId.PASSWORD_RESET_SUBMIT_PASSWORD;
const telemetryManager = initializeServerTelemetryManager(
apiId,
this.config.auth.clientId,
correlationId,
this.browserStorage,
this.logger
);
const submitRequest: ResetPasswordSubmitRequest = {
continuation_token: parameters.continuationToken,
new_password: parameters.newPassword,
correlationId: correlationId,
telemetryManager: telemetryManager,
};
this.logger.verbose(
"Calling submit endpoint with new password for password reset.",
correlationId
);
const submitResponse =
await this.customAuthApiClient.resetPasswordApi.submitNewPassword(
submitRequest
);
this.logger.verbose(
"Submit endpoint called successfully with new password for password reset.",
submitResponse.correlation_id || correlationId
);
return this.performPollCompletionRequest(
submitResponse.continuation_token ?? "",
submitResponse.poll_interval,
submitResponse.correlation_id || correlationId,
telemetryManager
);
}
private async performChallengeRequest(
request: ResetPasswordChallengeRequest
): Promise<ResetPasswordCodeRequiredResult> {
const correlationId = request.correlationId;
this.logger.verbose(
"Calling challenge endpoint for password reset flow.",
correlationId
);
const response =
await this.customAuthApiClient.resetPasswordApi.requestChallenge(
request
);
this.logger.verbose(
"Challenge endpoint for password reset returned successfully.",
response.correlation_id || correlationId
);
if (response.challenge_type === ChallengeType.OOB) {
// Code is required
this.logger.verbose(
"Code is required for password reset flow.",
response.correlation_id || correlationId
);
return {
correlationId: response.correlation_id || correlationId,
continuationToken: response.continuation_token ?? "",
challengeChannel: response.challenge_channel ?? "",
challengeTargetLabel: response.challenge_target_label ?? "",
codeLength:
response.code_length ?? DefaultCustomAuthApiCodeLength,
bindingMethod: response.binding_method ?? "",
};
}
this.logger.error(
`Unsupported challenge type '${response.challenge_type}' returned from challenge endpoint for password reset.`,
response.correlation_id || correlationId
);
throw new CustomAuthApiError(
CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE,
`Unsupported challenge type '${response.challenge_type}'.`,
response.correlation_id || correlationId
);
}
private async performPollCompletionRequest(
continuationToken: string,
pollInterval: number,
correlationId: string,
telemetryManager: ServerTelemetryManager
): Promise<ResetPasswordCompletedResult> {
const startTime = performance.now();
while (
performance.now() - startTime <
PasswordResetPollingTimeoutInMs
) {
const pollRequest: ResetPasswordPollCompletionRequest = {
continuation_token: continuationToken,
correlationId: correlationId,
telemetryManager: telemetryManager,
};
this.logger.verbose(
"Calling the poll completion endpoint for password reset flow.",
correlationId
);
const pollResponse =
await this.customAuthApiClient.resetPasswordApi.pollCompletion(
pollRequest
);
this.logger.verbose(
"Poll completion endpoint for password reset returned successfully.",
correlationId
);
if (pollResponse.status === ResetPasswordPollStatus.SUCCEEDED) {
return {
correlationId: pollResponse.correlation_id,
continuationToken: pollResponse.continuation_token ?? "",
};
} else if (pollResponse.status === ResetPasswordPollStatus.FAILED) {
throw new CustomAuthApiError(
CustomAuthApiErrorCode.PASSWORD_CHANGE_FAILED,
"Password is failed to be reset.",
pollResponse.correlation_id
);
}
this.logger.verbose(
`Poll completion endpoint for password reset is not started or in progress, waiting '${pollInterval}' seconds for next check.`,
correlationId
);
await this.delay(pollInterval * 1000);
}
this.logger.error("Password reset flow has timed out.", correlationId);
throw new CustomAuthApiError(
CustomAuthApiErrorCode.PASSWORD_RESET_TIMEOUT,
"Password reset flow has timed out.",
correlationId
);
}
private async delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface ResetPasswordParamsBase {
clientId: string;
challengeType: Array<string>;
username: string;
correlationId: string;
}
export type ResetPasswordStartParams = ResetPasswordParamsBase;
export interface ResetPasswordResendCodeParams extends ResetPasswordParamsBase {
continuationToken: string;
}
export interface ResetPasswordSubmitCodeParams extends ResetPasswordParamsBase {
continuationToken: string;
code: string;
}
export interface ResetPasswordSubmitNewPasswordParams
extends ResetPasswordParamsBase {
continuationToken: string;
newPassword: string;
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
interface ResetPasswordActionResult {
correlationId: string;
continuationToken: string;
}
export interface ResetPasswordCodeRequiredResult
extends ResetPasswordActionResult {
challengeChannel: string;
challengeTargetLabel: string;
codeLength: number;
bindingMethod: string;
}
export type ResetPasswordPasswordRequiredResult = ResetPasswordActionResult;
export type ResetPasswordCompletedResult = ResetPasswordActionResult;

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const SignInScenario = {
SignInAfterSignUp: "SignInAfterSignUp",
SignInAfterPasswordReset: "SignInAfterPasswordReset",
} as const;
export type SignInScenarioType =
(typeof SignInScenario)[keyof typeof SignInScenario];

Some files were not shown because too many files have changed in this diff Show More