@azure/msal-browser
Version:
Microsoft Authentication Library for js
518 lines (458 loc) • 20.5 kB
text/typescript
/*
* 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,
} 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 { 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 { 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 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.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.authority
);
this.signInClient = interactionClientFactory.create(SignInClient);
this.signUpClient = interactionClientFactory.create(SignUpClient);
this.resetPasswordClient =
interactionClientFactory.create(ResetPasswordClient);
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,
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,
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 completedResult = await this.signInClient.submitPassword(
submitPasswordParams
);
this.logger.verbose("Sign-in flow completed.", correlationId);
const accountInfo = new CustomAuthAccountData(
completedResult.authenticationResult.account,
this.customAuthConfig,
this.cacheClient,
this.logger,
correlationId
);
return new SignInResult(
new SignInCompletedState(),
accountInfo
);
}
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,
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,
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,
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);
}
}
}