Skip to content

Commit b08a46c

Browse files
authored
[Native Auth] Enable MFA and JIT for SMS in public interfaces (#8069)
1 parent 0dae241 commit b08a46c

34 files changed

+4126
-231
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "[Native Auth] Enable the MFA and JIT (SMS) in the public interfaces #8069",
4+
"packageName": "@azure/msal-browser",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-browser/src/custom_auth/controller/CustomAuthStandardController.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE,
3232
SIGN_IN_COMPLETED_RESULT_TYPE,
3333
SIGN_IN_JIT_REQUIRED_RESULT_TYPE,
34+
SIGN_IN_MFA_REQUIRED_RESULT_TYPE,
3435
} from "../sign_in/interaction_client/result/SignInActionResult.js";
3536
import { SignUpClient } from "../sign_up/interaction_client/SignUpClient.js";
3637
import { CustomAuthInterationClientFactory } from "../core/interaction_client/CustomAuthInterationClientFactory.js";
@@ -43,6 +44,7 @@ import { CustomAuthApiClient } from "../core/network_client/custom_auth_api/Cust
4344
import { FetchHttpClient } from "../core/network_client/http_client/FetchHttpClient.js";
4445
import { ResetPasswordClient } from "../reset_password/interaction_client/ResetPasswordClient.js";
4546
import { JitClient } from "../core/interaction_client/jit/JitClient.js";
47+
import { MfaClient } from "../core/interaction_client/mfa/MfaClient.js";
4648
import { NoCachedAccountFoundError } from "../core/error/NoCachedAccountFoundError.js";
4749
import * as ArgumentValidator from "../core/utils/ArgumentValidator.js";
4850
import { UserAlreadySignedInError } from "../core/error/UserAlreadySignedInError.js";
@@ -52,6 +54,7 @@ import { SignInCodeRequiredState } from "../sign_in/auth_flow/state/SignInCodeRe
5254
import { SignInPasswordRequiredState } from "../sign_in/auth_flow/state/SignInPasswordRequiredState.js";
5355
import { SignInCompletedState } from "../sign_in/auth_flow/state/SignInCompletedState.js";
5456
import { AuthMethodRegistrationRequiredState } from "../core/auth_flow/jit/state/AuthMethodRegistrationState.js";
57+
import { MfaAwaitingState } from "../core/auth_flow/mfa/state/MfaState.js";
5558
import { SignUpCodeRequiredState } from "../sign_up/auth_flow/state/SignUpCodeRequiredState.js";
5659
import { SignUpPasswordRequiredState } from "../sign_up/auth_flow/state/SignUpPasswordRequiredState.js";
5760
import { ResetPasswordCodeRequiredState } from "../reset_password/auth_flow/state/ResetPasswordCodeRequiredState.js";
@@ -68,6 +71,7 @@ export class CustomAuthStandardController
6871
private readonly signUpClient: SignUpClient;
6972
private readonly resetPasswordClient: ResetPasswordClient;
7073
private readonly jitClient: JitClient;
74+
private readonly mfaClient: MfaClient;
7175
private readonly cacheClient: CustomAuthSilentCacheClient;
7276
private readonly customAuthConfig: CustomAuthBrowserConfiguration;
7377
private readonly authority: CustomAuthAuthority;
@@ -129,6 +133,7 @@ export class CustomAuthStandardController
129133
this.resetPasswordClient =
130134
interactionClientFactory.create(ResetPasswordClient);
131135
this.jitClient = interactionClientFactory.create(JitClient);
136+
this.mfaClient = interactionClientFactory.create(MfaClient);
132137
this.cacheClient = interactionClientFactory.create(
133138
CustomAuthSilentCacheClient
134139
);
@@ -242,6 +247,7 @@ export class CustomAuthStandardController
242247
signInClient: this.signInClient,
243248
cacheClient: this.cacheClient,
244249
jitClient: this.jitClient,
250+
mfaClient: this.mfaClient,
245251
username: signInInputs.username,
246252
codeLength: startResult.codeLength,
247253
scopes: signInInputs.scopes ?? [],
@@ -272,6 +278,7 @@ export class CustomAuthStandardController
272278
signInClient: this.signInClient,
273279
cacheClient: this.cacheClient,
274280
jitClient: this.jitClient,
281+
mfaClient: this.mfaClient,
275282
username: signInInputs.username,
276283
scopes: signInInputs.scopes ?? [],
277284
claims: signInInputs.claims,
@@ -344,6 +351,29 @@ export class CustomAuthStandardController
344351
claims: signInInputs.claims,
345352
})
346353
);
354+
} else if (
355+
submitPasswordResult.type ===
356+
SIGN_IN_MFA_REQUIRED_RESULT_TYPE
357+
) {
358+
// MFA is required - create MfaAwaitingState
359+
this.logger.verbose(
360+
"MFA required for sign-in.",
361+
correlationId
362+
);
363+
364+
return new SignInResult(
365+
new MfaAwaitingState({
366+
correlationId: correlationId,
367+
continuationToken:
368+
submitPasswordResult.continuationToken,
369+
logger: this.logger,
370+
config: this.customAuthConfig,
371+
mfaClient: this.mfaClient,
372+
cacheClient: this.cacheClient,
373+
scopes: signInInputs.scopes ?? [],
374+
authMethods: submitPasswordResult.authMethods ?? [],
375+
})
376+
);
347377
} else {
348378
// Unexpected result type
349379
const result = submitPasswordResult as { type: string };
@@ -437,6 +467,7 @@ export class CustomAuthStandardController
437467
signUpClient: this.signUpClient,
438468
cacheClient: this.cacheClient,
439469
jitClient: this.jitClient,
470+
mfaClient: this.mfaClient,
440471
username: signUpInputs.username,
441472
codeLength: startResult.codeLength,
442473
codeResendInterval: startResult.interval,
@@ -461,6 +492,7 @@ export class CustomAuthStandardController
461492
signUpClient: this.signUpClient,
462493
cacheClient: this.cacheClient,
463494
jitClient: this.jitClient,
495+
mfaClient: this.mfaClient,
464496
username: signUpInputs.username,
465497
})
466498
);
@@ -531,6 +563,7 @@ export class CustomAuthStandardController
531563
resetPasswordClient: this.resetPasswordClient,
532564
cacheClient: this.cacheClient,
533565
jitClient: this.jitClient,
566+
mfaClient: this.mfaClient,
534567
username: resetPasswordInputs.username,
535568
codeLength: startResult.codeLength,
536569
})

lib/msal-browser/src/custom_auth/core/auth_flow/mfa/error_type/MfaError.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export class MfaRequestChallengeError extends AuthActionErrorBase {
1616
isInvalidInput(): boolean {
1717
return this.isInvalidInputError();
1818
}
19+
20+
/**
21+
* Checks if the error is due to the verification contact (e.g., phone number or email) being blocked. Consider contacting customer support for assistance.
22+
* @returns true if the error is due to the verification contact being blocked, false otherwise.
23+
*/
24+
isVerificationContactBlocked(): boolean {
25+
return this.isVerificationContactBlockedError();
26+
}
1927
}
2028

2129
/**

lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ export class CustomAuthApiClient implements ICustomAuthApiClient {
4747
this.registerApi = new RegisterApiClient(
4848
customAuthApiBaseUrl,
4949
clientId,
50-
httpClient
50+
httpClient,
51+
customAuthApiQueryParams
5152
);
5253
}
5354
}

lib/msal-browser/src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
3737
override async acquireToken(
3838
silentRequest: CommonSilentFlowRequest
3939
): Promise<AuthenticationResult> {
40+
const correlationId = silentRequest.correlationId || this.correlationId;
4041
const telemetryManager = this.initializeServerTelemetryManager(
4142
PublicApiId.ACCOUNT_GET_ACCESS_TOKEN
4243
);
4344
const clientConfig = this.getCustomAuthClientConfiguration(
4445
telemetryManager,
45-
this.customAuthAuthority
46+
this.customAuthAuthority,
47+
correlationId
4648
);
4749
const silentFlowClient = new SilentFlowClient(
4850
clientConfig,
@@ -52,7 +54,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
5254
try {
5355
this.logger.verbose(
5456
"Starting silent flow to acquire token from cache",
55-
this.correlationId
57+
correlationId
5658
);
5759

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

6264
this.logger.verbose(
6365
"Silent flow to acquire token from cache is completed and token is found",
64-
this.correlationId
66+
correlationId
6567
);
6668

6769
return result[0] as AuthenticationResult;
@@ -72,7 +74,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
7274
) {
7375
this.logger.verbose(
7476
"Token refresh is required to acquire token silently",
75-
this.correlationId
77+
correlationId
7678
);
7779

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

8385
this.logger.verbose(
8486
"Starting refresh flow to refresh token",
85-
this.correlationId
87+
correlationId
8688
);
8789

8890
const refreshTokenResult =
@@ -92,7 +94,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
9294

9395
this.logger.verbose(
9496
"Refresh flow to refresh token is completed",
95-
this.correlationId
97+
correlationId
9698
);
9799

98100
return refreshTokenResult as AuthenticationResult;
@@ -103,18 +105,17 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
103105
}
104106

105107
override async logout(logoutRequest?: ClearCacheRequest): Promise<void> {
108+
const correlationId =
109+
logoutRequest?.correlationId || this.correlationId;
106110
const validLogoutRequest = this.initializeLogoutRequest(logoutRequest);
107111

108112
// Clear the cache
109-
this.logger.verbose(
110-
"Start to clear the cache",
111-
logoutRequest?.correlationId
112-
);
113+
this.logger.verbose("Start to clear the cache", correlationId);
113114
await this.clearCacheOnLogout(
114-
validLogoutRequest.correlationId,
115+
correlationId,
115116
validLogoutRequest?.account
116117
);
117-
this.logger.verbose("Cache cleared", logoutRequest?.correlationId);
118+
this.logger.verbose("Cache cleared", correlationId);
118119

119120
const postLogoutRedirectUri = this.config.auth.postLogoutRedirectUri;
120121

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

127128
this.logger.verbose(
128129
"Post logout redirect uri is set, redirecting to uri",
129-
logoutRequest?.correlationId
130+
correlationId
130131
);
131132

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

174175
private getCustomAuthClientConfiguration(
175176
serverTelemetryManager: ServerTelemetryManager,
176-
customAuthAuthority: CustomAuthAuthority
177+
customAuthAuthority: CustomAuthAuthority,
178+
correlationId: string
177179
): ClientConfiguration {
178180
const logger = this.config.system.loggerOptions;
179181

@@ -193,7 +195,7 @@ export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase
193195
loggerCallback: logger.loggerCallback,
194196
piiLoggingEnabled: logger.piiLoggingEnabled,
195197
logLevel: logger.logLevel,
196-
correlationId: this.correlationId,
198+
correlationId: correlationId,
197199
},
198200
cacheOptions: {
199201
claimsBasedCachingEnabled:

lib/msal-browser/src/custom_auth/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,27 @@ export {
212212
// Auth Method Registration Types
213213
export { AuthMethodDetails } from "./core/auth_flow/jit/AuthMethodDetails.js";
214214

215+
// MFA State
216+
export { MfaAwaitingState } from "./core/auth_flow/mfa/state/MfaState.js";
217+
export { MfaVerificationRequiredState } from "./core/auth_flow/mfa/state/MfaState.js";
218+
export { MfaCompletedState } from "./core/auth_flow/mfa/state/MfaCompletedState.js";
219+
export { MfaFailedState } from "./core/auth_flow/mfa/state/MfaFailedState.js";
220+
221+
// MFA Results
222+
export {
223+
MfaRequestChallengeResult,
224+
MfaRequestChallengeResultState,
225+
} from "./core/auth_flow/mfa/result/MfaRequestChallengeResult.js";
226+
export {
227+
MfaSubmitChallengeResult,
228+
MfaSubmitChallengeResultState,
229+
} from "./core/auth_flow/mfa/result/MfaSubmitChallengeResult.js";
230+
231+
// MFA Errors
232+
export {
233+
MfaRequestChallengeError,
234+
MfaSubmitChallengeError,
235+
} from "./core/auth_flow/mfa/error_type/MfaError.js";
236+
215237
// Components from msal_browser
216238
export { LogLevel } from "@azure/msal-common/browser";

lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class ResetPasswordCodeRequiredState extends ResetPasswordState<ResetPass
5656
signInClient: this.stateParameters.signInClient,
5757
cacheClient: this.stateParameters.cacheClient,
5858
jitClient: this.stateParameters.jitClient,
59+
mfaClient: this.stateParameters.mfaClient,
5960
username: this.stateParameters.username,
6061
})
6162
);
@@ -108,6 +109,7 @@ export class ResetPasswordCodeRequiredState extends ResetPasswordState<ResetPass
108109
signInClient: this.stateParameters.signInClient,
109110
cacheClient: this.stateParameters.cacheClient,
110111
jitClient: this.stateParameters.jitClient,
112+
mfaClient: this.stateParameters.mfaClient,
111113
username: this.stateParameters.username,
112114
codeLength: result.codeLength,
113115
})

lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class ResetPasswordPasswordRequiredState extends ResetPasswordState<Reset
5959
signInClient: this.stateParameters.signInClient,
6060
cacheClient: this.stateParameters.cacheClient,
6161
jitClient: this.stateParameters.jitClient,
62+
mfaClient: this.stateParameters.mfaClient,
6263
signInScenario: SignInScenario.SignInAfterPasswordReset,
6364
})
6465
);

lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordStateParameters.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { SignInClient } from "../../../sign_in/interaction_client/SignInClient.j
88
import { CustomAuthSilentCacheClient } from "../../../get_account/interaction_client/CustomAuthSilentCacheClient.js";
99
import { AuthFlowActionRequiredStateParameters } from "../../../core/auth_flow/AuthFlowState.js";
1010
import { JitClient } from "../../../core/interaction_client/jit/JitClient.js";
11+
import { MfaClient } from "../../../core/interaction_client/mfa/MfaClient.js";
1112

1213
export interface ResetPasswordStateParameters
1314
extends AuthFlowActionRequiredStateParameters {
@@ -16,6 +17,7 @@ export interface ResetPasswordStateParameters
1617
signInClient: SignInClient;
1718
cacheClient: CustomAuthSilentCacheClient;
1819
jitClient: JitClient;
20+
mfaClient: MfaClient;
1921
}
2022

2123
export type ResetPasswordPasswordRequiredStateParameters =

lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResult.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { SignInPasswordRequiredState } from "../state/SignInPasswordRequiredStat
1111
import { SignInFailedState } from "../state/SignInFailedState.js";
1212
import { SignInCompletedState } from "../state/SignInCompletedState.js";
1313
import { AuthMethodRegistrationRequiredState } from "../../../core/auth_flow/jit/state/AuthMethodRegistrationState.js";
14+
import { MfaAwaitingState } from "../../../core/auth_flow/mfa/state/MfaState.js";
1415

1516
/*
1617
* Result of a sign-in operation.
@@ -81,6 +82,14 @@ export class SignInResult extends AuthFlowResultBase<
8182
} {
8283
return this.state instanceof AuthMethodRegistrationRequiredState;
8384
}
85+
86+
/**
87+
* Checks if the result requires MFA.
88+
* @warning This API is experimental. It may be changed in the future without notice. Do not use in production applications.
89+
*/
90+
isMfaRequired(): this is SignInResult & { state: MfaAwaitingState } {
91+
return this.state instanceof MfaAwaitingState;
92+
}
8493
}
8594

8695
/**
@@ -91,10 +100,12 @@ export class SignInResult extends AuthFlowResultBase<
91100
* - SignInFailedState: The sign-in process has failed.
92101
* - SignInCompletedState: The sign-in process is completed.
93102
* - AuthMethodRegistrationRequiredState: The sign-in process requires authentication method registration.
103+
* - MfaAwaitingState: The sign-in process requires MFA.
94104
*/
95105
export type SignInResultState =
96106
| SignInCodeRequiredState
97107
| SignInPasswordRequiredState
98108
| SignInFailedState
99109
| SignInCompletedState
100-
| AuthMethodRegistrationRequiredState;
110+
| AuthMethodRegistrationRequiredState
111+
| MfaAwaitingState;

0 commit comments

Comments
 (0)