Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "[Native Auth] Enable the MFA and JIT (SMS) in the public interfaces #8069",
"packageName": "@azure/msal-browser",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
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";
Expand All @@ -43,6 +44,7 @@ import { CustomAuthApiClient } from "../core/network_client/custom_auth_api/Cust
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";
Expand All @@ -52,6 +54,7 @@ import { SignInCodeRequiredState } from "../sign_in/auth_flow/state/SignInCodeRe
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";
Expand All @@ -68,6 +71,7 @@ export class CustomAuthStandardController
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;
Expand Down Expand Up @@ -129,6 +133,7 @@ export class CustomAuthStandardController
this.resetPasswordClient =
interactionClientFactory.create(ResetPasswordClient);
this.jitClient = interactionClientFactory.create(JitClient);
this.mfaClient = interactionClientFactory.create(MfaClient);
this.cacheClient = interactionClientFactory.create(
CustomAuthSilentCacheClient
);
Expand Down Expand Up @@ -242,6 +247,7 @@ export class CustomAuthStandardController
signInClient: this.signInClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: signInInputs.username,
codeLength: startResult.codeLength,
scopes: signInInputs.scopes ?? [],
Expand Down Expand Up @@ -272,6 +278,7 @@ export class CustomAuthStandardController
signInClient: this.signInClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: signInInputs.username,
scopes: signInInputs.scopes ?? [],
claims: signInInputs.claims,
Expand Down Expand Up @@ -344,6 +351,29 @@ export class CustomAuthStandardController
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 };
Expand Down Expand Up @@ -437,6 +467,7 @@ export class CustomAuthStandardController
signUpClient: this.signUpClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: signUpInputs.username,
codeLength: startResult.codeLength,
codeResendInterval: startResult.interval,
Expand All @@ -461,6 +492,7 @@ export class CustomAuthStandardController
signUpClient: this.signUpClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: signUpInputs.username,
})
);
Expand Down Expand Up @@ -531,6 +563,7 @@ export class CustomAuthStandardController
resetPasswordClient: this.resetPasswordClient,
cacheClient: this.cacheClient,
jitClient: this.jitClient,
mfaClient: this.mfaClient,
username: resetPasswordInputs.username,
codeLength: startResult.codeLength,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ export class MfaRequestChallengeError extends AuthActionErrorBase {
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();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export class CustomAuthApiClient implements ICustomAuthApiClient {
this.registerApi = new RegisterApiClient(
customAuthApiBaseUrl,
clientId,
httpClient
httpClient,
customAuthApiQueryParams
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
override async acquireToken(
silentRequest: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
const correlationId = silentRequest.correlationId || this.correlationId;
const telemetryManager = this.initializeServerTelemetryManager(
PublicApiId.ACCOUNT_GET_ACCESS_TOKEN
);
const clientConfig = this.getCustomAuthClientConfiguration(
telemetryManager,
this.customAuthAuthority
this.customAuthAuthority,
correlationId
);
const silentFlowClient = new SilentFlowClient(
clientConfig,
Expand All @@ -52,7 +54,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
try {
this.logger.verbose(
"Starting silent flow to acquire token from cache",
this.correlationId
correlationId
);

const result = await silentFlowClient.acquireCachedToken(
Expand All @@ -61,7 +63,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase

this.logger.verbose(
"Silent flow to acquire token from cache is completed and token is found",
this.correlationId
correlationId
);

return result[0] as AuthenticationResult;
Expand All @@ -72,7 +74,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
) {
this.logger.verbose(
"Token refresh is required to acquire token silently",
this.correlationId
correlationId
);

const refreshTokenClient = new RefreshTokenClient(
Expand All @@ -82,7 +84,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase

this.logger.verbose(
"Starting refresh flow to refresh token",
this.correlationId
correlationId
);

const refreshTokenResult =
Expand All @@ -92,7 +94,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase

this.logger.verbose(
"Refresh flow to refresh token is completed",
this.correlationId
correlationId
);

return refreshTokenResult as AuthenticationResult;
Expand All @@ -103,18 +105,17 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
}

override async logout(logoutRequest?: ClearCacheRequest): Promise<void> {
const correlationId =
logoutRequest?.correlationId || this.correlationId;
const validLogoutRequest = this.initializeLogoutRequest(logoutRequest);

// Clear the cache
this.logger.verbose(
"Start to clear the cache",
logoutRequest?.correlationId
);
this.logger.verbose("Start to clear the cache", correlationId);
await this.clearCacheOnLogout(
validLogoutRequest.correlationId,
correlationId,
validLogoutRequest?.account
);
this.logger.verbose("Cache cleared", logoutRequest?.correlationId);
this.logger.verbose("Cache cleared", correlationId);

const postLogoutRedirectUri = this.config.auth.postLogoutRedirectUri;

Expand All @@ -126,7 +127,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase

this.logger.verbose(
"Post logout redirect uri is set, redirecting to uri",
logoutRequest?.correlationId
correlationId
);

// Redirect to post logout redirect uri
Expand Down Expand Up @@ -173,7 +174,8 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase

private getCustomAuthClientConfiguration(
serverTelemetryManager: ServerTelemetryManager,
customAuthAuthority: CustomAuthAuthority
customAuthAuthority: CustomAuthAuthority,
correlationId: string
): ClientConfiguration {
const logger = this.config.system.loggerOptions;

Expand All @@ -193,7 +195,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
loggerCallback: logger.loggerCallback,
piiLoggingEnabled: logger.piiLoggingEnabled,
logLevel: logger.logLevel,
correlationId: this.correlationId,
correlationId: correlationId,
},
cacheOptions: {
claimsBasedCachingEnabled:
Expand Down
22 changes: 22 additions & 0 deletions lib/msal-browser/src/custom_auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,27 @@ export {
// 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";
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class ResetPasswordCodeRequiredState extends ResetPasswordState<ResetPass
signInClient: this.stateParameters.signInClient,
cacheClient: this.stateParameters.cacheClient,
jitClient: this.stateParameters.jitClient,
mfaClient: this.stateParameters.mfaClient,
username: this.stateParameters.username,
})
);
Expand Down Expand Up @@ -108,6 +109,7 @@ export class ResetPasswordCodeRequiredState extends ResetPasswordState<ResetPass
signInClient: this.stateParameters.signInClient,
cacheClient: this.stateParameters.cacheClient,
jitClient: this.stateParameters.jitClient,
mfaClient: this.stateParameters.mfaClient,
username: this.stateParameters.username,
codeLength: result.codeLength,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class ResetPasswordPasswordRequiredState extends ResetPasswordState<Reset
signInClient: this.stateParameters.signInClient,
cacheClient: this.stateParameters.cacheClient,
jitClient: this.stateParameters.jitClient,
mfaClient: this.stateParameters.mfaClient,
signInScenario: SignInScenario.SignInAfterPasswordReset,
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SignInClient } from "../../../sign_in/interaction_client/SignInClient.j
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 {
Expand All @@ -16,6 +17,7 @@ export interface ResetPasswordStateParameters
signInClient: SignInClient;
cacheClient: CustomAuthSilentCacheClient;
jitClient: JitClient;
mfaClient: MfaClient;
}

export type ResetPasswordPasswordRequiredStateParameters =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SignInPasswordRequiredState } from "../state/SignInPasswordRequiredStat
import { SignInFailedState } from "../state/SignInFailedState.js";
import { SignInCompletedState } from "../state/SignInCompletedState.js";
import { AuthMethodRegistrationRequiredState } from "../../../core/auth_flow/jit/state/AuthMethodRegistrationState.js";
import { MfaAwaitingState } from "../../../core/auth_flow/mfa/state/MfaState.js";

/*
* Result of a sign-in operation.
Expand Down Expand Up @@ -81,6 +82,14 @@ export class SignInResult extends AuthFlowResultBase<
} {
return this.state instanceof AuthMethodRegistrationRequiredState;
}

/**
* Checks if the result requires MFA.
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
*/
isMfaRequired(): this is SignInResult & { state: MfaAwaitingState } {
return this.state instanceof MfaAwaitingState;
}
}

/**
Expand All @@ -91,10 +100,12 @@ export class SignInResult extends AuthFlowResultBase<
* - SignInFailedState: The sign-in process has failed.
* - SignInCompletedState: The sign-in process is completed.
* - AuthMethodRegistrationRequiredState: The sign-in process requires authentication method registration.
* - MfaAwaitingState: The sign-in process requires MFA.
*/
export type SignInResultState =
| SignInCodeRequiredState
| SignInPasswordRequiredState
| SignInFailedState
| SignInCompletedState
| AuthMethodRegistrationRequiredState;
| AuthMethodRegistrationRequiredState
| MfaAwaitingState;
Loading