diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..e11b45c43c --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,178 @@ +# AI Context File for MSAL Custom Authentication Project + +## πŸ“¦ Description + +MSAL Custom Authentication is a TypeScript-based library that extends Microsoft's Authentication Library (MSAL) to enable customized authentication experiences in modern web applications. Key features include: + +- **Flexible Authentication Flows:** + + - Sign-in with username/password or passwordless options + - Sign-up with customizable user attributes + - Password reset and account recovery + - Account management and token handling + +- **UI Framework Agnostic:** + + - Built as a headless authentication library + - Complete control over UI implementation + - Separation of authentication logic from presentation + - Seamless integration with any UI framework (React, Angular, Vue, vanilla JavaScript) + +- **Developer-Friendly:** + - Strongly typed APIs for enhanced development experience + - Comprehensive error handling and logging + - Built-in state management for auth flows + - Clear separation of concerns for better maintainability + +## 🧱 Technology Stack + +- **Language & Runtime:** + - TypeScript 5.7+ + - Node.js 10+ +- **Core Dependencies:** + - @azure/msal-browser - Core MSAL browser library +- **Build System:** + - Rollup.js - Module bundling + - TypeScript compiler + - Babel - JavaScript compilation +- **Testing & Quality:** + - Jest - Testing framework with JSDOM environment + - ESLint - Code linting + - Prettier - Code formatting +- **Documentation:** + - TypeDoc - API documentation + - API Extractor - API report generation + +## πŸ—‚οΈ Structure + +``` +lib/msal-custom-auth/ + β”œβ”€β”€ src/ β†’ Core library implementation + β”‚ β”œβ”€β”€ configuration/ β†’ Configuration related files + β”‚ β”œβ”€β”€ controller/ β†’ Controller implementation + β”‚ β”œβ”€β”€ core/ β†’ Core functionality and errors + β”‚ β”‚ β”œβ”€β”€ auth_flow/ β†’ Base authentication flow classes + β”‚ β”‚ β”œβ”€β”€ error/ β†’ Error type definitions + β”‚ β”‚ β”œβ”€β”€ interaction_client/ β†’ Base interaction clients + β”‚ β”‚ β”œβ”€β”€ network_client/ β†’ HTTP and API client implementations + β”‚ β”‚ β”‚ β”œβ”€β”€ custom_auth_api/ β†’ Custom auth API clients + β”‚ β”‚ β”‚ └── http_client/ β†’ HTTP client implementation + β”‚ β”‚ β”œβ”€β”€ telemetry/ β†’ Telemetry implementations + β”‚ β”‚ └── utils/ β†’ Utility functions + β”‚ β”œβ”€β”€ get_account/ β†’ Account retrieval functionality + β”‚ β”œβ”€β”€ operating_context/ β†’ Operating context implementation + β”‚ β”œβ”€β”€ reset_password/ β†’ Password reset flow + β”‚ β”œβ”€β”€ sign_in/ β†’ Sign-in flow implementation + β”‚ β”‚ β”œβ”€β”€ auth_flow/ β†’ Sign-in authentication flow + β”‚ β”‚ β”‚ β”œβ”€β”€ error_type/ β†’ Sign-in specific errors + β”‚ β”‚ β”‚ β”œβ”€β”€ result/ β†’ Sign-in operation results + β”‚ β”‚ β”‚ └── state/ β†’ Sign-in flow states + β”‚ β”‚ └── interaction_client/ β†’ Sign-in interaction handling + β”‚ β”œβ”€β”€ sign_up/ β†’ Sign-up flow implementation + β”‚ β”‚ β”œβ”€β”€ auth_flow/ β†’ Sign-up authentication flow + β”‚ β”‚ β”‚ β”œβ”€β”€ error_type/ β†’ Sign-up specific errors + β”‚ β”‚ β”‚ β”œβ”€β”€ result/ β†’ Sign-up operation results + β”‚ β”‚ β”‚ └── state/ β†’ Sign-up flow states + β”‚ β”‚ └── interaction_client/ β†’ Sign-up interaction handling + β”‚ β”œβ”€β”€ index.ts β†’ Library entry point + β”‚ β”œβ”€β”€ CustomAuthPublicClientApplication.ts β†’ Main application class + β”‚ β”œβ”€β”€ CustomAuthActionInputs.ts β†’ Action input type definitions + β”‚ β”œβ”€β”€ CustomAuthConstants.ts β†’ Constants definitions + β”‚ β”œβ”€β”€ ICustomAuthPublicClientApplication.ts β†’ Interface definitions + β”‚ β”œβ”€β”€ packageMetadata.ts β†’ Package version information + β”‚ └── UserAccountAttributes.ts β†’ User account attributes + β”œβ”€β”€ test/ β†’ Unit tests + β”œβ”€β”€ package.json β†’ Project dependencies and scripts + β”œβ”€β”€ rollup.config.js β†’ Build configuration + β”œβ”€β”€ jest.config.cjs β†’ Testing configuration + └── typedoc.json β†’ Documentation generation config +``` + +## πŸ” Patterns & Conventions + +### πŸ“Œ General + +- Always define classes and significant logic in separate TypeScript files under `src/core/`. +- Public-facing APIs should be exposed through `src/index.ts`. +- Avoid embedding complex logic directly within entry points or utility scripts. + +### πŸ“Œ Async Method Handling + +- All methods interacting with network or authentication flows must be marked `async`. +- Properly await promises and handle exceptions internally using try-catch. + +```typescript +async signInUser(credentials): Promise { + try { + const result = await this.performAuthentication(credentials); + return result; + } catch (error) { + logger.error("Sign-in error:", error); + throw error; + } +} +``` + +### πŸ“Œ Error Handling + +- Use centralized logging for all caught exceptions (`logger.error()` for errors). +- Return standardized error objects to maintain consistency across API responses. + +### πŸ“Œ Testing + +- Unit tests must be located under `/test` directory, following `.spec.ts` naming convention. +- Maintain high test coverage, especially for core authentication flows. + +## πŸ—οΈ Key Files + +- `src/index.ts`: Exports all public classes, interfaces, types, and constants. +- `src/CustomAuthPublicClientApplication.ts`: Main application class implementing custom authentication flows. +- `src/ICustomAuthPublicClientApplication.ts`: Interface definition for the main application. +- `src/CustomAuthActionInputs.ts`: Type definitions for authentication flow inputs. +- `src/CustomAuthConstants.ts`: Shared constants used throughout the library. +- `src/UserAccountAttributes.ts`: User account attribute management. +- `src/configuration/`: Configuration and initialization related files. +- `src/controller/`: Authentication flow controllers. +- `src/core/`: Core error handling and utilities. +- `src/get_account/`: Account retrieval and management. +- `src/sign_in/`, `src/sign_up/`, `src/reset_password/`: Authentication flow implementations. +- `package.json`: Manage dependencies and npm scripts. +- `rollup.config.js`: Defines build steps. +- `jest.config.cjs`: Testing framework setup. + +## 🎯 Goals for Copilot / AI Tools + +When modifying or extending authentication functionality: + +- Keep main application logic in `CustomAuthPublicClientApplication.ts` focused on high-level flow coordination. +- Implement specific authentication flows in their dedicated directories (`sign_in/`, `sign_up/`, `reset_password/`). +- Place shared utilities and error handling in `src/core/`. +- Implement flow controllers in `src/controller/` to manage authentication state and operations. +- Define new action inputs in `CustomAuthActionInputs.ts`. +- Add any new constants to `CustomAuthConstants.ts`. +- Export all public APIs through `src/index.ts`. +- Write corresponding tests under `/test` folder immediately. +- Ensure proper error handling using the error types defined in `src/core/error/`. +- Follow established async patterns for all network and authentication operations. +- Use type-safe interfaces and maintain strict type checking throughout the codebase. + +## πŸ“š Reference Files + +- **Entry Point & Exports:** `src/index.ts` +- **Main Application:** `src/CustomAuthPublicClientApplication.ts`, `src/ICustomAuthPublicClientApplication.ts` +- **Authentication Flows:** + - `src/sign_in/`: Sign-in implementation + - `src/sign_up/`: Sign-up implementation + - `src/reset_password/`: Password reset implementation + - `src/get_account/`: Account management +- **Core Implementation:** + - `src/controller/`: Authentication flow controllers + - `src/core/`: Error handling and utilities + - `src/configuration/`: Configuration management + - `src/operating_context/`: Operating context implementation +- **Type Definitions & Constants:** + - `src/CustomAuthActionInputs.ts`: Action input types + - `src/UserAccountAttributes.ts`: User account attributes + - `src/CustomAuthConstants.ts`: Shared constants + +> This context file is optimized for guidance of AI-driven coding tools like GitHub Copilot or ChatGPT, ensuring consistent implementation aligned with project standards. diff --git a/.gitignore b/.gitignore index 152b238714..0ff8bf21c1 100644 --- a/.gitignore +++ b/.gitignore @@ -286,4 +286,5 @@ temp-cache.json junit.xml # ApiExtractor -temp/ \ No newline at end of file +temp/ +samples/msal-custom-auth-samples/react-sample-nextjs/.next/ diff --git a/lib/msal-custom-auth/src/sign_in/auth_flow/SignInScenario.ts b/lib/msal-custom-auth/src/sign_in/auth_flow/SignInScenario.ts deleted file mode 100644 index 3afa9658e8..0000000000 --- a/lib/msal-custom-auth/src/sign_in/auth_flow/SignInScenario.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -export enum SignInScenario { - SignInAfterSignUp, - SignInAfterPasswordReset, -} diff --git a/lib/msal-custom-auth/src/sign_in/auth_flow/error_type/SignInError.ts b/lib/msal-custom-auth/src/sign_in/auth_flow/error_type/SignInError.ts index 00098aff23..8a78e6f0bf 100644 --- a/lib/msal-custom-auth/src/sign_in/auth_flow/error_type/SignInError.ts +++ b/lib/msal-custom-auth/src/sign_in/auth_flow/error_type/SignInError.ts @@ -4,36 +4,35 @@ */ import { AuthFlowErrorBase } from "../../../core/auth_flow/AuthFlowErrorBase.js"; -import { CustomAuthApiErrorCode } from "../../../core/network_client/custom_auth_api/types/ApiErrorResponseTypes.js"; export class SignInError extends AuthFlowErrorBase { /** - * 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. + * Checks if the error is due to the user not found. + * @returns {boolean} True if the error is due to the user not found, false otherwise. */ isUserNotFound(): boolean { - return this.errorData.error === CustomAuthApiErrorCode.USER_NOT_FOUND; + 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. + * @returns {boolean} 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 password being incorrect. - * @returns true if the error is due to the provided password being incorrect, false otherwise. + * Checks if the error is due to the password being incorrect. + * @returns {boolean} True if the error is due to the password being incorrect, false otherwise. */ - isPasswordIncorrect(): boolean { + isIncorrectPassword(): boolean { return this.isPasswordIncorrectError(); } /** - * 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. + * Checks if the error is due to the provided challenge type not being supported. + * @returns {boolean} True if the error is due to the provided challenge type not being supported, false otherwise. */ isUnsupportedChallengeType(): boolean { return this.isUnsupportedChallengeTypeError(); @@ -41,7 +40,7 @@ export class SignInError extends AuthFlowErrorBase { /** * Check if client app supports the challenge type configured in Entra. - * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + * @returns {boolean} True if "loginPopup" function is required to continue the operation. */ isRedirectRequired(): boolean { return this.isRedirectError(); @@ -50,28 +49,44 @@ export class SignInError extends AuthFlowErrorBase { export class SignInSubmitPasswordError extends AuthFlowErrorBase { /** - * Checks if the password submitted during sign-in is incorrect. - * @returns {boolean} True if the error is due to the password being invalid, false otherwise. + * Checks if the error is due to the password being incorrect. + * @returns {boolean} True if the error is due to the password being incorrect, false otherwise. */ - isInvalidPassword(): boolean { + isIncorrectPassword(): boolean { return this.isPasswordIncorrectError(); } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue the operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } } export class SignInSubmitCodeError extends AuthFlowErrorBase { /** - * Checks if the code submitted during sign-in is invalid. - * @returns {boolean} True if the error is due to the code being invalid, false otherwise. + * Checks if the provided code is invalid. + * @returns {boolean} True if the provided code is invalid, false otherwise. */ isInvalidCode(): boolean { return this.isInvalidCodeError(); } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue the operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } } export class SignInResendCodeError extends AuthFlowErrorBase { /** * Check if client app supports the challenge type configured in Entra. - * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + * @returns {boolean} True if "loginPopup" function is required to continue the operation. */ isRedirectRequired(): boolean { return this.isRedirectError(); diff --git a/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInResendCodeResult.ts b/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInResendCodeResult.ts index 7eb350afcc..6466da9df2 100644 --- a/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInResendCodeResult.ts +++ b/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInResendCodeResult.ts @@ -8,6 +8,9 @@ import { SignInResendCodeError } from "../error_type/SignInError.js"; import { SignInCodeRequiredState } from "../state/SignInCodeRequiredState.js"; import { SignInFailedState } from "../state/SignInFailedState.js"; +/* + * Result of resending code in a sign-in operation. + */ export class SignInResendCodeResult extends AuthFlowResultBase< SignInResendCodeResultState, SignInResendCodeError, @@ -44,11 +47,7 @@ export class SignInResendCodeResult extends AuthFlowResultBase< * Checks if the result is in a code required state. */ isCodeRequired(): this is SignInResendCodeResult & { state: SignInCodeRequiredState } { - /* - * The instanceof operator couldn't be used here to check the state type since the circular dependency issue. - * So we are using the constructor name to check the state type. - */ - return this.state.constructor?.name === "SignInCodeRequiredState"; + return this.state instanceof SignInCodeRequiredState; } } diff --git a/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInResult.ts b/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInResult.ts deleted file mode 100644 index aef5d4ebfb..0000000000 --- a/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInResult.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { CustomAuthAccountData } from "../../../get_account/auth_flow/CustomAuthAccountData.js"; -import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; -import { SignInError } from "../error_type/SignInError.js"; -import { SignInCodeRequiredState } from "../state/SignInCodeRequiredState.js"; -import { SignInPasswordRequiredState } from "../state/SignInPasswordRequiredState.js"; -import { SignInFailedState } from "../state/SignInFailedState.js"; -import { SignInCompletedState } from "../state/SignInCompletedState.js"; - -/* - * Result of a sign-in operation. - */ -export class SignInResult extends AuthFlowResultBase { - /** - * Creates a new instance of SignInResultState. - * @param state The state of the result. - */ - constructor(state: SignInResultState, resultData?: CustomAuthAccountData) { - super(state, resultData); - } - - /** - * Creates a new instance of SignInResult with an error. - * @param error The error that occurred. - * @returns {SignInResult} A new instance of SignInResult with the error set. - */ - static createWithError(error: unknown): SignInResult { - const result = new SignInResult(new SignInFailedState()); - result.error = new SignInError(SignInResult.createErrorData(error)); - - return result; - } - - /** - * Checks if the result is in a failed state. - */ - isFailed(): this is SignInResult & { state: SignInFailedState } { - return this.state instanceof SignInFailedState; - } - - /** - * Checks if the result is in a code required state. - */ - isCodeRequired(): this is SignInResult & { state: SignInCodeRequiredState } { - return this.state instanceof SignInCodeRequiredState; - } - - /** - * Checks if the result is in a password required state. - */ - isPasswordRequired(): this is SignInResult & { state: SignInPasswordRequiredState } { - return this.state instanceof SignInPasswordRequiredState; - } - - /** - * Checks if the result is in a completed state. - */ - isCompleted(): this is SignInResult & { state: SignInCompletedState } { - return this.state instanceof SignInCompletedState; - } -} - -/** - * The possible states for the SignInResult. - * This includes: - * - SignInCodeRequiredState: The sign-in process requires a code. - * - SignInPasswordRequiredState: The sign-in process requires a password. - * - SignInFailedState: The sign-in process has failed. - * - SignInCompletedState: The sign-in process is completed. - */ -export type SignInResultState = - | SignInCodeRequiredState - | SignInPasswordRequiredState - | SignInFailedState - | SignInCompletedState; diff --git a/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInSubmitCodeResult.ts b/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInSubmitCodeResult.ts index 82fe1222f5..48e3a74f3d 100644 --- a/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInSubmitCodeResult.ts +++ b/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInSubmitCodeResult.ts @@ -3,17 +3,29 @@ * Licensed under the MIT License. */ +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; import { SignInSubmitCodeError } from "../error_type/SignInError.js"; import { SignInCompletedState } from "../state/SignInCompletedState.js"; import { SignInFailedState } from "../state/SignInFailedState.js"; -import { SignInSubmitCredentialResult } from "./SignInSubmitCredentialResult.js"; /* - * Result of a sign-in submit code operation. + * Result of a sign-in operation that requires a code. */ -export class SignInSubmitCodeResult extends SignInSubmitCredentialResult { +export class SignInSubmitCodeResult extends AuthFlowResultBase< + SignInSubmitCodeResultState, + SignInSubmitCodeError, + void +> { /** - * Creates a new instance of SignInSubmitCodeResult with error data. + * Creates a new instance of SignInSubmitCodeResult. + * @param state The state of the result. + */ + constructor(state: SignInSubmitCodeResultState) { + super(state); + } + + /** + * Creates a new instance of SignInSubmitCodeResult with an error. * @param error The error that occurred. * @returns {SignInSubmitCodeResult} A new instance of SignInSubmitCodeResult with the error set. */ @@ -38,3 +50,11 @@ export class SignInSubmitCodeResult extends SignInSubmitCredentialResult extends AuthFlowResultBase< - SignInSubmitCredentialResultState, - TError, - CustomAuthAccountData -> { - /** - * Creates a new instance of SignInSubmitCredentialResult. - * @param state The state of the result. - * @param resultData The result data. - */ - constructor(state: SignInSubmitCredentialResultState, resultData?: CustomAuthAccountData) { - super(state, resultData); - } -} - -/** - * The possible states of the SignInSubmitCredentialResult. - * This includes: - * - SignInCompletedState: The sign-in process has completed successfully. - * - SignInFailedState: The sign-in process has failed. - */ -export type SignInSubmitCredentialResultState = SignInCompletedState | SignInFailedState; diff --git a/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInSubmitPasswordResult.ts b/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInSubmitPasswordResult.ts index 1e46a1e9a0..217503442d 100644 --- a/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInSubmitPasswordResult.ts +++ b/lib/msal-custom-auth/src/sign_in/auth_flow/result/SignInSubmitPasswordResult.ts @@ -3,15 +3,33 @@ * Licensed under the MIT License. */ +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; import { SignInSubmitPasswordError } from "../error_type/SignInError.js"; +import { SignInCodeRequiredState } from "../state/SignInCodeRequiredState.js"; import { SignInCompletedState } from "../state/SignInCompletedState.js"; import { SignInFailedState } from "../state/SignInFailedState.js"; -import { SignInSubmitCredentialResult } from "./SignInSubmitCredentialResult.js"; /* - * Result of a sign-in submit password operation. + * Result of a sign-in operation that requires a password. */ -export class SignInSubmitPasswordResult extends SignInSubmitCredentialResult { +export class SignInSubmitPasswordResult extends AuthFlowResultBase< + SignInSubmitPasswordResultState, + SignInSubmitPasswordError, + void +> { + /** + * Creates a new instance of SignInSubmitPasswordResult. + * @param state The state of the result. + */ + constructor(state: SignInSubmitPasswordResultState) { + super(state); + } + + /** + * Creates a new instance of SignInSubmitPasswordResult with an error. + * @param error The error that occurred. + * @returns {SignInSubmitPasswordResult} A new instance of SignInSubmitPasswordResult with the error set. + */ static createWithError(error: unknown): SignInSubmitPasswordResult { const result = new SignInSubmitPasswordResult(new SignInFailedState()); result.error = new SignInSubmitPasswordError(SignInSubmitPasswordResult.createErrorData(error)); @@ -26,6 +44,13 @@ export class SignInSubmitPasswordResult extends SignInSubmitCredentialResult { /** - * Once user configures email one-time passcode as a authentication method in Microsoft Entra, a one-time passcode will be sent to the user’s email. - * Submit this one-time passcode to continue sign-in flow. + * Submit one-time passcode to continue sign-in flow. * @param {string} code - The code to submit. * @returns {Promise} The result of the operation. */ @@ -25,34 +24,42 @@ export class SignInCodeRequiredState extends SignInState} The result of the operation. */ async resendCode(): Promise { try { - const submitCodeParams: SignInResendCodeParams = { + this.stateParameters.logger.verbose("Resending code for sign-in.", this.stateParameters.correlationId); + + const result = await this.stateParameters.signInClient.resendCode({ clientId: this.stateParameters.config.auth.clientId, - correlationId: this.stateParameters.correlationId, challengeType: this.stateParameters.config.customAuth.challengeTypes ?? [], - continuationToken: this.stateParameters.continuationToken ?? "", username: this.stateParameters.username, - }; - - this.stateParameters.logger.verbose("Resending code for sign-in.", this.stateParameters.correlationId); - - const result = await this.stateParameters.signInClient.resendCode(submitCodeParams); + correlationId: this.stateParameters.correlationId, + continuationToken: this.stateParameters.continuationToken ?? "", + }); this.stateParameters.logger.verbose("Code resent for sign-in.", this.stateParameters.correlationId); @@ -90,10 +95,15 @@ export class SignInCodeRequiredState extends SignInState { - /** - * Initiates the sign-in flow with continuation token. - * @param {SignInWithContinuationTokenInputs} signInWithContinuationTokenInputs - The result of the operation. - * @returns {Promise} The result of the operation. - */ - async signIn(signInWithContinuationTokenInputs?: SignInWithContinuationTokenInputs): Promise { - try { - const continuationTokenParams: SignInContinuationTokenParams = { - clientId: this.stateParameters.config.auth.clientId, - correlationId: this.stateParameters.correlationId, - challengeType: this.stateParameters.config.customAuth.challengeTypes ?? [], - scopes: signInWithContinuationTokenInputs?.scopes ?? [], - continuationToken: this.stateParameters.continuationToken ?? "", - username: this.stateParameters.username, - signInScenario: this.stateParameters.signInScenario, - }; - - this.stateParameters.logger.verbose( - "Signing in with continuation token.", - this.stateParameters.correlationId, - ); - - const completedResult = - await this.stateParameters.signInClient.signInWithContinuationToken(continuationTokenParams); - - this.stateParameters.logger.verbose( - "Signed in with continuation token.", - this.stateParameters.correlationId, - ); - - const accountInfo = new CustomAuthAccountData( - completedResult.authenticationResult.account, - this.stateParameters.config, - this.stateParameters.cacheClient, - this.stateParameters.logger, - this.stateParameters.correlationId, - ); - - return new SignInResult(new SignInCompletedState(), accountInfo); - } catch (error) { - this.stateParameters.logger.errorPii( - `Failed to sign in with continuation token. Error: ${error}.`, - this.stateParameters.correlationId, - ); - - return SignInResult.createWithError(error); - } - } -} diff --git a/lib/msal-custom-auth/src/sign_in/auth_flow/state/SignInFailedState.ts b/lib/msal-custom-auth/src/sign_in/auth_flow/state/SignInFailedState.ts index e80641e575..1382acde61 100644 --- a/lib/msal-custom-auth/src/sign_in/auth_flow/state/SignInFailedState.ts +++ b/lib/msal-custom-auth/src/sign_in/auth_flow/state/SignInFailedState.ts @@ -6,6 +6,6 @@ import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js"; /** - * Represents the state of a sign-in operation that has been failed. + * Represents the state of a sign-in operation that has failed. */ export class SignInFailedState extends AuthFlowStateBase {} diff --git a/lib/msal-custom-auth/src/sign_in/auth_flow/state/SignInPasswordRequiredState.ts b/lib/msal-custom-auth/src/sign_in/auth_flow/state/SignInPasswordRequiredState.ts index 86987314c4..197fbf111d 100644 --- a/lib/msal-custom-auth/src/sign_in/auth_flow/state/SignInPasswordRequiredState.ts +++ b/lib/msal-custom-auth/src/sign_in/auth_flow/state/SignInPasswordRequiredState.ts @@ -3,19 +3,20 @@ * Licensed under the MIT License. */ -import { CustomAuthAccountData } from "../../../get_account/auth_flow/CustomAuthAccountData.js"; -import { SignInSubmitPasswordParams } from "../../interaction_client/parameter/SignInParams.js"; -import { SignInSubmitPasswordResult } from "../result/SignInSubmitPasswordResult.js"; -import { SignInCompletedState } from "./SignInCompletedState.js"; +import { UnexpectedError } from "../../../core/error/UnexpectedError.js"; import { SignInState } from "./SignInState.js"; import { SignInPasswordRequiredStateParameters } from "./SignInStateParameters.js"; +import { SignInCompletedState } from "./SignInCompletedState.js"; +import { SignInCodeRequiredState } from "./SignInCodeRequiredState.js"; +import { SignInSubmitPasswordResult } from "../result/SignInSubmitPasswordResult.js"; +import { SignInCompletedResult, SignInCodeRequiredResult } from "../../interaction_client/result/SignInActionResult.js"; /* * Sign-in password required state. */ export class SignInPasswordRequiredState extends SignInState { /** - * Once user configures email with password as a authentication method in Microsoft Entra, user submits a password to continue sign-in flow. + * Submits a password for sign-in. * @param {string} password - The password to submit. * @returns {Promise} The result of the operation. */ @@ -23,46 +24,63 @@ export class SignInPasswordRequiredState extends SignInState; } diff --git a/lib/msal-custom-auth/src/sign_in/interaction_client/SignInClient.ts b/lib/msal-custom-auth/src/sign_in/interaction_client/SignInClient.ts index 9fbef74353..b60931a1b0 100644 --- a/lib/msal-custom-auth/src/sign_in/interaction_client/SignInClient.ts +++ b/lib/msal-custom-auth/src/sign_in/interaction_client/SignInClient.ts @@ -3,91 +3,39 @@ * Licensed under the MIT License. */ -import { ChallengeType, DefaultCustomAuthApiCodeLength } from "../../CustomAuthConstants.js"; +import { ServerTelemetryManager } from "@azure/msal-browser"; import { CustomAuthApiError } from "../../core/error/CustomAuthApiError.js"; import { CustomAuthApiErrorCode } from "../../core/network_client/custom_auth_api/types/ApiErrorResponseTypes.js"; - +import { UnexpectedError } from "../../core/error/UnexpectedError.js"; import { CustomAuthInteractionClientBase } from "../../core/interaction_client/CustomAuthInteractionClientBase.js"; +import { PublicApiId } from "../../core/telemetry/PublicApiId.js"; +import { ArgumentValidator } from "../../core/utils/ArgumentValidator.js"; +import { + ChallengeType, + DefaultCustomAuthApiCodeLength, + DefaultCustomAuthApiCodeResendIntervalInSec, +} from "../../CustomAuthConstants.js"; import { + SignInParamsBase, SignInStartParams, - SignInResendCodeParams, SignInSubmitCodeParams, SignInSubmitPasswordParams, - SignInContinuationTokenParams, + SignInResendCodeParams, } from "./parameter/SignInParams.js"; import { - SignInCodeSendResult, + SignInAttributesRequiredResult, + SignInCodeRequiredResult, SignInCompletedResult, SignInPasswordRequiredResult, } from "./result/SignInActionResult.js"; -import { PublicApiId } from "../../core/telemetry/PublicApiId.js"; -import { ArgumentValidator } from "../../core/utils/ArgumentValidator.js"; -import { - SignInChallengeRequest, - SignInContinuationTokenRequest, - SignInInitiateRequest, - SignInOobTokenRequest, - SignInPasswordTokenRequest, -} from "../../core/network_client/custom_auth_api/types/ApiRequestTypes.js"; -import { SignInTokenResponse } from "../../core/network_client/custom_auth_api/types/ApiResponseTypes.js"; -import { SignInScenario } from "../auth_flow/SignInScenario.js"; -import { UnexpectedError } from "../../core/error/UnexpectedError.js"; -import { - AuthenticationResult, - BrowserCacheManager, - BrowserConfiguration, - EventHandler, - ICrypto, - INavigationClient, - IPerformanceClient, - Logger, - ResponseHandler, -} from "@azure/msal-browser"; -import { ICustomAuthApiClient } from "../../core/network_client/custom_auth_api/ICustomAuthApiClient.js"; -import { CustomAuthAuthority } from "../../core/CustomAuthAuthority.js"; export class SignInClient extends CustomAuthInteractionClientBase { - private readonly tokenResponseHandler: ResponseHandler; - - constructor( - config: BrowserConfiguration, - storageImpl: BrowserCacheManager, - browserCrypto: ICrypto, - logger: Logger, - eventHandler: EventHandler, - navigationClient: INavigationClient, - performanceClient: IPerformanceClient, - customAuthApiClient: ICustomAuthApiClient, - customAuthAuthority: CustomAuthAuthority, - ) { - super( - config, - storageImpl, - browserCrypto, - logger, - eventHandler, - navigationClient, - performanceClient, - customAuthApiClient, - customAuthAuthority, - ); - - this.tokenResponseHandler = new ResponseHandler( - this.config.auth.clientId, - this.browserStorage, - this.browserCrypto, - this.logger, - null, - null, - ); - } - /** - * Starts the signin flow. - * @param parameters The parameters required to start the sign-in flow. - * @returns The result of the sign-in start operation. + * Starts the sign in flow. + * @param parameters The parameters for the sign in start action. + * @returns The result of the sign in start action. */ - async start(parameters: SignInStartParams): Promise { + async start(parameters: SignInStartParams): Promise { ArgumentValidator.ensureArgumentIsNotNullOrUndefined("parameters", parameters, parameters.correlationId); const apiId = !parameters.password @@ -95,59 +43,64 @@ export class SignInClient extends CustomAuthInteractionClientBase { : PublicApiId.SIGN_IN_WITH_PASSWORD_START; const telemetryManager = this.initializeServerTelemetryManager(apiId); - this.logger.verbose("Calling initiate endpoint for sign in.", parameters.correlationId); - - const initReq: SignInInitiateRequest = { - challenge_type: this.getChallengeTypes(parameters.challengeType), + const startRequest = { username: parameters.username, + password: parameters.password, + challenge_type: this.getChallengeTypes(parameters.challengeType), + telemetryManager, correlationId: parameters.correlationId, - telemetryManager: telemetryManager, }; - const initiateResponse = await this.customAuthApiClient.signInApi.initiate(initReq); + this.logger.verbose("Initiating sign in.", parameters.correlationId); - this.logger.verbose("Initiate endpoint called for sign in.", parameters.correlationId); + const startResponse = await this.customAuthApiClient.signInApi.initiate(startRequest); - const challengeReq: SignInChallengeRequest = { + this.logger.verbose("Sign in initiated.", parameters.correlationId); + + const challengeRequest = { + continuation_token: startResponse.continuation_token ?? "", challenge_type: this.getChallengeTypes(parameters.challengeType), - continuation_token: initiateResponse.continuation_token ?? "", - correlationId: initiateResponse.correlation_id, - telemetryManager: telemetryManager, + telemetryManager, + correlationId: startResponse.correlation_id, }; - return this.performChallengeRequest(challengeReq); + return this.performChallengeRequest(challengeRequest); } /** - * Resends the code for sign-in flow. - * @param parameters The parameters required to resend the code. - * @returns The result of the sign-in resend code action. + * Submits the code for the sign in flow. + * @param parameters The parameters for the sign in submit code action. + * @returns The result of the sign in submit code action. */ - async resendCode(parameters: SignInResendCodeParams): Promise { + async submitCode( + parameters: SignInSubmitCodeParams, + ): Promise { ArgumentValidator.ensureArgumentIsNotNullOrUndefined("parameters", parameters, parameters.correlationId); - const apiId = PublicApiId.SIGN_IN_RESEND_CODE; + const apiId = PublicApiId.SIGN_IN_SUBMIT_CODE; const telemetryManager = this.initializeServerTelemetryManager(apiId); - const challengeReq: SignInChallengeRequest = { - challenge_type: this.getChallengeTypes(parameters.challengeType), - continuation_token: parameters.continuationToken ?? "", + const requestSubmitCode = { + continuation_token: parameters.continuationToken, + oob: parameters.code, + scope: "openid profile", // Default scopes required for sign-in + telemetryManager, correlationId: parameters.correlationId, - telemetryManager: telemetryManager, }; - const result = await this.performChallengeRequest(challengeReq); - - if (result instanceof SignInPasswordRequiredResult) { - this.logger.error( - "Resend code operation failed due to the challenge type 'password' is not supported.", - parameters.correlationId, - ); + const result = await this.performContinueRequest( + "SignInClient.submitCode", + parameters, + telemetryManager, + () => this.customAuthApiClient.signInApi.requestTokensWithOob(requestSubmitCode), + parameters.correlationId, + ); + if (result instanceof SignInCodeRequiredResult) { throw new CustomAuthApiError( CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, - "Unsupported challenge type 'password'.", - result.correlationId, + "The challenge type 'oob' is invalid after submitting code for sign in.", + parameters.correlationId, ); } @@ -155,122 +108,71 @@ export class SignInClient extends CustomAuthInteractionClientBase { } /** - * Submits the code for sign-in flow. - * @param parameters The parameters required to submit the code. - * @returns The result of the sign-in submit code action. + * Submits the password for the sign in flow. + * @param parameters The parameters for the sign in submit password action. + * @returns The result of the sign in submit password action. */ - async submitCode(parameters: SignInSubmitCodeParams): Promise { + async submitPassword( + parameters: SignInSubmitPasswordParams, + ): Promise { ArgumentValidator.ensureArgumentIsNotNullOrUndefined("parameters", parameters, parameters.correlationId); - ArgumentValidator.ensureArgumentIsNotEmptyString("parameters.code", parameters.code, parameters.correlationId); - - const apiId = PublicApiId.SIGN_IN_SUBMIT_CODE; - const telemetryManager = this.initializeServerTelemetryManager(apiId); - const scopes = this.getScopes(parameters.scopes); - - const request: SignInOobTokenRequest = { - continuation_token: parameters.continuationToken, - oob: parameters.code, - scope: scopes.join(" "), - correlationId: parameters.correlationId, - telemetryManager: telemetryManager, - }; - - return this.performTokenRequest(() => this.customAuthApiClient.signInApi.requestTokensWithOob(request), scopes); - } - - /** - * Submits the password for sign-in flow. - * @param parameters The parameters required to submit the password. - * @returns The result of the sign-in submit password action. - */ - async submitPassword(parameters: SignInSubmitPasswordParams): Promise { - ArgumentValidator.ensureArgumentIsNotNullOrUndefined("parameters", parameters, parameters.correlationId); - ArgumentValidator.ensureArgumentIsNotEmptyString( - "parameters.password", - parameters.password, - parameters.correlationId, - ); const apiId = PublicApiId.SIGN_IN_SUBMIT_PASSWORD; const telemetryManager = this.initializeServerTelemetryManager(apiId); - const scopes = this.getScopes(parameters.scopes); - const request: SignInPasswordTokenRequest = { + const requestSubmitPassword = { continuation_token: parameters.continuationToken, password: parameters.password, - scope: scopes.join(" "), + scope: "openid profile", // Default scopes required for sign-in + telemetryManager, correlationId: parameters.correlationId, - telemetryManager: telemetryManager, }; - return this.performTokenRequest( - () => this.customAuthApiClient.signInApi.requestTokensWithPassword(request), - scopes, + const result = await this.performContinueRequest( + "SignInClient.submitPassword", + parameters, + telemetryManager, + () => this.customAuthApiClient.signInApi.requestTokensWithPassword(requestSubmitPassword), + parameters.correlationId, ); + + return result; } /** - * Signs in with continuation token. - * @param parameters The parameters required to sign in with continuation token. - * @returns The result of the sign-in complete action. + * Resends the code for the sign in flow. + * @param parameters The parameters for the sign in resend code action. + * @returns The result of the sign in resend code action. */ - async signInWithContinuationToken(parameters: SignInContinuationTokenParams): Promise { + async resendCode(parameters: SignInResendCodeParams): Promise { ArgumentValidator.ensureArgumentIsNotNullOrUndefined("parameters", parameters, parameters.correlationId); - const apiId = this.getPublicApiIdBySignInScenario(parameters.signInScenario, parameters.correlationId); + const apiId = PublicApiId.SIGN_IN_RESEND_CODE; const telemetryManager = this.initializeServerTelemetryManager(apiId); - const scopes = this.getScopes(parameters.scopes); - // Create token request. - const request: SignInContinuationTokenRequest = { - continuation_token: parameters.continuationToken, - username: parameters.username, + const challengeRequest = { + continuation_token: parameters.continuationToken ?? "", + challenge_type: this.getChallengeTypes(parameters.challengeType), + telemetryManager, correlationId: parameters.correlationId, - telemetryManager: telemetryManager, - scope: scopes.join(" "), }; - // Call token endpoint. - return this.performTokenRequest( - () => this.customAuthApiClient.signInApi.requestTokenWithContinuationToken(request), - scopes, - ); - } + const result = await this.performChallengeRequest(challengeRequest); - private async performTokenRequest( - tokenEndpointCaller: () => Promise, - requestScopes: string[], - ): Promise { - this.logger.verbose("Calling token endpoint for sign in.", this.correlationId); - - const requestTimestamp = Math.round(new Date().getTime() / 1000.0); - const tokenResponse = await tokenEndpointCaller(); - - this.logger.verbose("Token endpoint called for sign in.", this.correlationId); - - // Save tokens and create authentication result. - const result = await this.tokenResponseHandler.handleServerTokenResponse( - tokenResponse, - this.customAuthAuthority, - requestTimestamp, - { - authority: this.customAuthAuthority.canonicalAuthority, - correlationId: tokenResponse.correlation_id ?? "", - scopes: requestScopes, - storeInCache: { - idToken: true, - accessToken: true, - refreshToken: true, - }, - }, - ); + if (result instanceof SignInPasswordRequiredResult) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + "The challenge type 'password' is invalid after resending code for sign in.", + parameters.correlationId, + ); + } - return new SignInCompletedResult(tokenResponse.correlation_id ?? "", result as AuthenticationResult); + return result; } private async performChallengeRequest( - request: SignInChallengeRequest, - ): Promise { + request: any, + ): Promise { this.logger.verbose("Calling challenge endpoint for sign in.", request.correlationId); const challengeResponse = await this.customAuthApiClient.signInApi.requestChallenge(request); @@ -281,12 +183,13 @@ export class SignInClient extends CustomAuthInteractionClientBase { // Code is required this.logger.verbose("Challenge type is oob for sign in.", request.correlationId); - return new SignInCodeSendResult( + return new SignInCodeRequiredResult( challengeResponse.correlation_id, challengeResponse.continuation_token ?? "", challengeResponse.challenge_channel ?? "", challengeResponse.challenge_target_label ?? "", challengeResponse.code_length ?? DefaultCustomAuthApiCodeLength, + challengeResponse.code_length ?? DefaultCustomAuthApiCodeLength, challengeResponse.binding_method ?? "", ); } @@ -309,18 +212,83 @@ export class SignInClient extends CustomAuthInteractionClientBase { throw new CustomAuthApiError( CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, `Unsupported challenge type '${challengeResponse.challenge_type}'.`, - challengeResponse.correlation_id, + request.correlationId, ); } - private getPublicApiIdBySignInScenario(scenario: SignInScenario, correlationId: string): number { - switch (scenario) { - case SignInScenario.SignInAfterSignUp: - return PublicApiId.SIGN_IN_AFTER_SIGN_UP; - case SignInScenario.SignInAfterPasswordReset: - return PublicApiId.SIGN_IN_AFTER_PASSWORD_RESET; - default: - throw new UnexpectedError(`nsupported sign-in scenario '${scenario}'.`, correlationId); + private async performContinueRequest( + callerName: string, + requestParams: SignInParamsBase, + telemetryManager: ServerTelemetryManager, + responseGetter: () => Promise, + requestCorrelationId: string, + ): Promise { + this.logger.verbose(`${callerName} is calling continue endpoint for sign in.`, requestCorrelationId); + + try { + const response = await responseGetter(); + + this.logger.verbose(`Continue endpoint called by ${callerName} for sign in.`, requestCorrelationId); + + return new SignInCompletedResult(requestCorrelationId, response.continuation_token ?? ""); + } catch (error) { + if (error instanceof CustomAuthApiError) { + return this.handleContinueResponseError( + error, + error.correlationId ?? requestCorrelationId, + requestParams, + telemetryManager, + ); + } else { + this.logger.errorPii( + `${callerName} failed to call continue endpoint for sign in. Error: ${error}`, + requestCorrelationId, + ); + + throw new UnexpectedError(error, requestCorrelationId); + } } } + + private async handleContinueResponseError( + responseError: CustomAuthApiError, + correlationId: string, + requestParams: SignInParamsBase, + telemetryManager: ServerTelemetryManager, + ): Promise { + if ( + responseError.error === CustomAuthApiErrorCode.CREDENTIAL_REQUIRED && + !!responseError.errorCodes && + responseError.errorCodes.includes(55103) + ) { + // Credential is required + this.logger.verbose("The credential is required in the sign in flow.", correlationId); + + const continuationToken = this.readContinuationTokenFromResponeError(responseError); + + // Call the challenge endpoint to ensure the password challenge type is supported. + const challengeRequest = { + continuation_token: continuationToken, + challenge_type: this.getChallengeTypes(requestParams.challengeType), + telemetryManager, + correlationId, + }; + + return this.performChallengeRequest(challengeRequest); + } + + throw responseError; + } + + private readContinuationTokenFromResponeError(responseError: CustomAuthApiError): string { + if (!responseError.continuationToken) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.CONTINUATION_TOKEN_MISSING, + "Continuation token is missing in the response body", + responseError.correlationId, + ); + } + + return responseError.continuationToken; + } } diff --git a/lib/msal-custom-auth/src/sign_in/interaction_client/parameter/SignInParams.ts b/lib/msal-custom-auth/src/sign_in/interaction_client/parameter/SignInParams.ts index f9b320c037..6785d5ed80 100644 --- a/lib/msal-custom-auth/src/sign_in/interaction_client/parameter/SignInParams.ts +++ b/lib/msal-custom-auth/src/sign_in/interaction_client/parameter/SignInParams.ts @@ -3,37 +3,29 @@ * Licensed under the MIT License. */ -import { SignInScenario } from "../../auth_flow/SignInScenario.js"; - export interface SignInParamsBase { clientId: string; - correlationId: string; challengeType: Array; username: string; -} - -export interface SignInResendCodeParams extends SignInParamsBase { - continuationToken: string; + correlationId: string; } export interface SignInStartParams extends SignInParamsBase { password?: string; } -export interface SignInSubmitCodeParams extends SignInParamsBase { +export interface SignInResendCodeParams extends SignInParamsBase { continuationToken: string; - code: string; - scopes: Array; } -export interface SignInSubmitPasswordParams extends SignInParamsBase { +export interface SignInContinueParams extends SignInParamsBase { continuationToken: string; - password: string; - scopes: Array; } -export interface SignInContinuationTokenParams extends SignInParamsBase { - continuationToken: string; - signInScenario: SignInScenario; - scopes: Array; +export interface SignInSubmitCodeParams extends SignInContinueParams { + code: string; +} + +export interface SignInSubmitPasswordParams extends SignInContinueParams { + password: string; } diff --git a/lib/msal-custom-auth/src/sign_in/interaction_client/result/SignInActionResult.ts b/lib/msal-custom-auth/src/sign_in/interaction_client/result/SignInActionResult.ts index f1996f5bf6..8cb406195f 100644 --- a/lib/msal-custom-auth/src/sign_in/interaction_client/result/SignInActionResult.ts +++ b/lib/msal-custom-auth/src/sign_in/interaction_client/result/SignInActionResult.ts @@ -3,33 +3,39 @@ * Licensed under the MIT License. */ -import { AuthenticationResult } from "@azure/msal-browser"; +import { UserAttribute } from "../../../core/network_client/custom_auth_api/types/ApiErrorResponseTypes.js"; -export class SignInCompletedResult { - constructor( - public correlationId: string, - public authenticationResult: AuthenticationResult, - ) {} -} - -class SignInContinuationTokenResult { +class SignInResultBase { constructor( public correlationId: string, public continuationToken: string, ) {} } -export class SignInPasswordRequiredResult extends SignInContinuationTokenResult {} +export class SignInCompletedResult extends SignInResultBase {} + +export class SignInPasswordRequiredResult extends SignInResultBase {} -export class SignInCodeSendResult extends SignInContinuationTokenResult { +export class SignInCodeRequiredResult extends SignInResultBase { constructor( correlationId: string, continuationToken: string, public challengeChannel: string, public challengeTargetLabel: string, public codeLength: number, + public interval: number, public bindingMethod: string, ) { super(correlationId, continuationToken); } } + +export class SignInAttributesRequiredResult extends SignInResultBase { + constructor( + correlationId: string, + continuationToken: string, + public requiredAttributes: Array, + ) { + super(correlationId, continuationToken); + } +}