diff --git a/change/@azure-msal-angular-46ce52bc-2e6e-40e0-acc8-1bccfd4ed63b.json b/change/@azure-msal-angular-46ce52bc-2e6e-40e0-acc8-1bccfd4ed63b.json new file mode 100644 index 0000000000..95e01e6b52 --- /dev/null +++ b/change/@azure-msal-angular-46ce52bc-2e6e-40e0-acc8-1bccfd4ed63b.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Adjust InjectionToken usage so that properly-typed tokens can be used with new `inject(TOKEN)` syntax.", + "packageName": "@azure/msal-angular", + "email": "bradkovach@gmail.com", + "dependentChangeType": "major" +} diff --git a/change/@azure-msal-browser-35e00def-ebba-4cd3-b2f8-3b05c57b2f58.json b/change/@azure-msal-browser-35e00def-ebba-4cd3-b2f8-3b05c57b2f58.json new file mode 100644 index 0000000000..41453dc766 --- /dev/null +++ b/change/@azure-msal-browser-35e00def-ebba-4cd3-b2f8-3b05c57b2f58.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix expires_in format & double brokering errors #7646", + "packageName": "@azure/msal-browser", + "email": "sameera.gajjarapu@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-browser-81b8d13b-0d90-4a64-bf38-09f56a789bd2.json b/change/@azure-msal-browser-81b8d13b-0d90-4a64-bf38-09f56a789bd2.json new file mode 100644 index 0000000000..748469d401 --- /dev/null +++ b/change/@azure-msal-browser-81b8d13b-0d90-4a64-bf38-09f56a789bd2.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Refactor /authorize request generation", + "packageName": "@azure/msal-browser", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-common-057c3170-8c50-4695-bfb0-876cc6b0c419.json b/change/@azure-msal-common-057c3170-8c50-4695-bfb0-876cc6b0c419.json new file mode 100644 index 0000000000..c9ed6b261b --- /dev/null +++ b/change/@azure-msal-common-057c3170-8c50-4695-bfb0-876cc6b0c419.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "encodeURIComponent during QS generation", + "packageName": "@azure/msal-common", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-common-05af4dd2-f51f-46cd-a212-5e0e5b64288b.json b/change/@azure-msal-common-05af4dd2-f51f-46cd-a212-5e0e5b64288b.json new file mode 100644 index 0000000000..62c0467481 --- /dev/null +++ b/change/@azure-msal-common-05af4dd2-f51f-46cd-a212-5e0e5b64288b.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Refactor /authorize request generation", + "packageName": "@azure/msal-common", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-01d4e500-04b2-448b-a836-44f80546e11b.json b/change/@azure-msal-node-01d4e500-04b2-448b-a836-44f80546e11b.json new file mode 100644 index 0000000000..b4fc4b7d76 --- /dev/null +++ b/change/@azure-msal-node-01d4e500-04b2-448b-a836-44f80546e11b.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Test changes", + "packageName": "@azure/msal-node", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@azure-msal-node-c5c3efce-d2b0-47b3-ac24-867539845fb6.json b/change/@azure-msal-node-c5c3efce-d2b0-47b3-ac24-867539845fb6.json new file mode 100644 index 0000000000..78d8f1fe1d --- /dev/null +++ b/change/@azure-msal-node-c5c3efce-d2b0-47b3-ac24-867539845fb6.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Refactor /authorize request generation", + "packageName": "@azure/msal-node", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-angular/src/constants.ts b/lib/msal-angular/src/constants.ts index dbb44746a5..57fea5f227 100644 --- a/lib/msal-angular/src/constants.ts +++ b/lib/msal-angular/src/constants.ts @@ -4,17 +4,21 @@ */ import { InjectionToken } from "@angular/core"; +import { type IPublicClientApplication } from "@azure/msal-browser"; +import { type MsalBroadcastConfiguration } from "./msal.broadcast.config"; +import { type MsalGuardConfiguration } from "./msal.guard.config"; +import { type MsalInterceptorConfiguration } from "./msal.interceptor.config"; -export const MSAL_INSTANCE = new InjectionToken("MSAL_INSTANCE"); +export const MSAL_INSTANCE = new InjectionToken( + "MSAL_INSTANCE" +); -export const MSAL_GUARD_CONFIG = new InjectionToken( +export const MSAL_GUARD_CONFIG = new InjectionToken( "MSAL_GUARD_CONFIG" ); -export const MSAL_INTERCEPTOR_CONFIG = new InjectionToken( - "MSAL_INTERCEPTOR_CONFIG" -); +export const MSAL_INTERCEPTOR_CONFIG = + new InjectionToken("MSAL_INTERCEPTOR_CONFIG"); -export const MSAL_BROADCAST_CONFIG = new InjectionToken( - "MSAL_BROADCAST_CONFIG" -); +export const MSAL_BROADCAST_CONFIG = + new InjectionToken("MSAL_BROADCAST_CONFIG"); diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts index 59ae769391..8bb4d96b3d 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts @@ -498,7 +498,15 @@ export class NativeInteractionClient extends BaseInteractionClient { nativeAccountId: request.accountId, })?.homeAccountId; + // add exception for double brokering, please note this is temporary and will be fortified in future if ( + request.extraParameters?.child_client_id && + response.account.id !== request.accountId + ) { + this.logger.info( + "handleNativeServerResponse: Double broker flow detected, ignoring accountId mismatch" + ); + } else if ( homeAccountIdentifier !== cachedhomeAccountId && response.account.id !== request.accountId ) { @@ -525,6 +533,9 @@ export class NativeInteractionClient extends BaseInteractionClient { this.logger ); + // Ensure expires_in is in number format + response.expires_in = Number(response.expires_in); + // generate authenticationResult const result = await this.generateAuthenticationResult( response, diff --git a/lib/msal-browser/src/interaction_client/PopupClient.ts b/lib/msal-browser/src/interaction_client/PopupClient.ts index c94c4019e2..45000e201d 100644 --- a/lib/msal-browser/src/interaction_client/PopupClient.ts +++ b/lib/msal-browser/src/interaction_client/PopupClient.ts @@ -48,6 +48,8 @@ import { PopupWindowAttributes } from "../request/PopupWindowAttributes.js"; import { EventError } from "../event/EventMessage.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import * as ResponseHandler from "../response/ResponseHandler.js"; +import { getAuthCodeRequestUrl } from "../protocol/Authorize.js"; +import { generatePkceCodes } from "../crypto/PkceGenerator.js"; export type PopupParams = { popup?: Window | null; @@ -217,6 +219,17 @@ export class PopupClient extends StandardInteractionClient { this.correlationId )(request, InteractionType.Popup); + const pkce = + pkceCodes || + (await invokeAsync( + generatePkceCodes, + PerformanceEvents.GeneratePkceCodes, + this.logger, + this.performanceClient, + this.correlationId + )(this.performanceClient, this.logger, this.correlationId)); + validRequest.codeChallenge = pkce.challenge; + /* * Skip pre-connect for async popups to reduce time between user interaction and popup window creation to avoid * popup from being blocked by browsers with shorter popup timers @@ -226,16 +239,6 @@ export class PopupClient extends StandardInteractionClient { } try { - // Create auth code request and generate PKCE params - const authCodeRequest: CommonAuthorizationCodeRequest = - await invokeAsync( - this.initializeAuthorizationCodeRequest.bind(this), - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - this.logger, - this.performanceClient, - this.correlationId - )(validRequest, pkceCodes); - // Initialize the client const authClient: AuthorizationCodeClient = await invokeAsync( this.createAuthCodeClient.bind(this), @@ -269,16 +272,19 @@ export class PopupClient extends StandardInteractionClient { } // Create acquire token url. - const navigateUrl = await authClient.getAuthCodeUrl({ - ...validRequest, - platformBroker: isPlatformBroker, - }); - - // Create popup interaction handler. - const interactionHandler = new InteractionHandler( - authClient, - this.browserStorage, - authCodeRequest, + const navigateUrl = await invokeAsync( + getAuthCodeRequestUrl, + PerformanceEvents.GetAuthCodeUrl, + this.logger, + this.performanceClient, + validRequest.correlationId + )( + this.config, + authClient.authority, + { + ...validRequest, + platformBroker: isPlatformBroker, + }, this.logger, this.performanceClient ); @@ -316,7 +322,7 @@ export class PopupClient extends StandardInteractionClient { ThrottlingUtils.removeThrottle( this.browserStorage, this.config.auth.clientId, - authCodeRequest + validRequest ); if (serverParams.accountId) { @@ -361,6 +367,19 @@ export class PopupClient extends StandardInteractionClient { }); } + const authCodeRequest: CommonAuthorizationCodeRequest = { + ...validRequest, + code: serverParams.code || "", + codeVerifier: pkce.verifier, + }; + // Create popup interaction handler. + const interactionHandler = new InteractionHandler( + authClient, + this.browserStorage, + authCodeRequest, + this.logger, + this.performanceClient + ); // Handle response from hash string. const result = await interactionHandler.handleCodeResponse( serverParams, diff --git a/lib/msal-browser/src/interaction_client/RedirectClient.ts b/lib/msal-browser/src/interaction_client/RedirectClient.ts index 0d18134bd9..c858b52f97 100644 --- a/lib/msal-browser/src/interaction_client/RedirectClient.ts +++ b/lib/msal-browser/src/interaction_client/RedirectClient.ts @@ -11,7 +11,7 @@ import { ServerTelemetryManager, Constants, ProtocolUtils, - ServerAuthorizationCodeResponse, + AuthorizeResponse, ThrottlingUtils, ICrypto, Logger, @@ -48,6 +48,8 @@ import { INavigationClient } from "../navigation/INavigationClient.js"; import { EventError } from "../event/EventMessage.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import * as ResponseHandler from "../response/ResponseHandler.js"; +import { getAuthCodeRequestUrl } from "../protocol/Authorize.js"; +import { generatePkceCodes } from "../crypto/PkceGenerator.js"; function getNavigationType(): NavigationTimingType | undefined { if ( @@ -107,6 +109,15 @@ export class RedirectClient extends StandardInteractionClient { this.correlationId )(request, InteractionType.Redirect); + const pkceCodes = await invokeAsync( + generatePkceCodes, + PerformanceEvents.GeneratePkceCodes, + this.logger, + this.performanceClient, + this.correlationId + )(this.performanceClient, this.logger, this.correlationId); + validRequest.codeChallenge = pkceCodes.challenge; + this.browserStorage.updateCacheEntries( validRequest.state, validRequest.nonce, @@ -133,16 +144,6 @@ export class RedirectClient extends StandardInteractionClient { }; try { - // Create auth code request and generate PKCE params - const authCodeRequest: CommonAuthorizationCodeRequest = - await invokeAsync( - this.initializeAuthorizationCodeRequest.bind(this), - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - this.logger, - this.performanceClient, - this.correlationId - )(validRequest); - // Initialize the client const authClient: AuthorizationCodeClient = await invokeAsync( this.createAuthCodeClient.bind(this), @@ -158,6 +159,11 @@ export class RedirectClient extends StandardInteractionClient { account: validRequest.account, }); + const authCodeRequest: CommonAuthorizationCodeRequest = { + ...validRequest, + code: "", // Will get filled in after the redirect + codeVerifier: pkceCodes.verifier, + }; // Create redirect interaction handler. const interactionHandler = new RedirectHandler( authClient, @@ -168,15 +174,28 @@ export class RedirectClient extends StandardInteractionClient { ); // Create acquire token url. - const navigateUrl = await authClient.getAuthCodeUrl({ - ...validRequest, - platformBroker: NativeMessageHandler.isPlatformBrokerAvailable( - this.config, - this.logger, - this.nativeMessageHandler, - request.authenticationScheme - ), - }); + const navigateUrl = await invokeAsync( + getAuthCodeRequestUrl, + PerformanceEvents.GetAuthCodeUrl, + this.logger, + this.performanceClient, + validRequest.correlationId + )( + this.config, + authClient.authority, + { + ...validRequest, + platformBroker: + NativeMessageHandler.isPlatformBrokerAvailable( + this.config, + this.logger, + this.nativeMessageHandler, + request.authenticationScheme + ), + }, + this.logger, + this.performanceClient + ); const redirectStartPage = this.getRedirectStartPage( request.redirectStartPage @@ -373,7 +392,7 @@ export class RedirectClient extends StandardInteractionClient { */ protected getRedirectResponse( userProvidedResponse: string - ): [ServerAuthorizationCodeResponse | null, string] { + ): [AuthorizeResponse | null, string] { this.logger.verbose("getRedirectResponseHash called"); // Get current location hash from window or cache. let responseString = userProvidedResponse; @@ -439,7 +458,7 @@ export class RedirectClient extends StandardInteractionClient { * @param state */ protected async handleResponse( - serverParams: ServerAuthorizationCodeResponse, + serverParams: AuthorizeResponse, serverTelemetryManager: ServerTelemetryManager ): Promise { const state = serverParams.state; diff --git a/lib/msal-browser/src/interaction_client/SilentIframeClient.ts b/lib/msal-browser/src/interaction_client/SilentIframeClient.ts index 7229e782ca..7a937bd60b 100644 --- a/lib/msal-browser/src/interaction_client/SilentIframeClient.ts +++ b/lib/msal-browser/src/interaction_client/SilentIframeClient.ts @@ -42,6 +42,8 @@ import { AuthenticationResult } from "../response/AuthenticationResult.js"; import { InteractionHandler } from "../interaction_handler/InteractionHandler.js"; import * as BrowserUtils from "../utils/BrowserUtils.js"; import * as ResponseHandler from "../response/ResponseHandler.js"; +import { getAuthCodeRequestUrl } from "../protocol/Authorize.js"; +import { generatePkceCodes } from "../crypto/PkceGenerator.js"; export class SilentIframeClient extends StandardInteractionClient { protected apiId: ApiId; @@ -212,49 +214,47 @@ export class SilentIframeClient extends StandardInteractionClient { */ protected async silentTokenHelper( authClient: AuthorizationCodeClient, - silentRequest: AuthorizationUrlRequest + request: AuthorizationUrlRequest ): Promise { - const correlationId = silentRequest.correlationId; + const correlationId = request.correlationId; this.performanceClient.addQueueMeasurement( PerformanceEvents.SilentIframeClientTokenHelper, correlationId ); - - // Create auth code request and generate PKCE params - const authCodeRequest: CommonAuthorizationCodeRequest = - await invokeAsync( - this.initializeAuthorizationCodeRequest.bind(this), - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - this.logger, - this.performanceClient, - correlationId - )(silentRequest); - + const pkceCodes = await invokeAsync( + generatePkceCodes, + PerformanceEvents.GeneratePkceCodes, + this.logger, + this.performanceClient, + this.correlationId + )(this.performanceClient, this.logger, this.correlationId); + const silentRequest = { + ...request, + codeChallenge: pkceCodes.challenge, + }; // Create authorize request url const navigateUrl = await invokeAsync( - authClient.getAuthCodeUrl.bind(authClient), + getAuthCodeRequestUrl, PerformanceEvents.GetAuthCodeUrl, this.logger, this.performanceClient, correlationId - )({ - ...silentRequest, - platformBroker: NativeMessageHandler.isPlatformBrokerAvailable( - this.config, - this.logger, - this.nativeMessageHandler, - silentRequest.authenticationScheme - ), - }); - - // Create silent handler - const interactionHandler = new InteractionHandler( - authClient, - this.browserStorage, - authCodeRequest, + )( + this.config, + authClient.authority, + { + ...silentRequest, + platformBroker: NativeMessageHandler.isPlatformBrokerAvailable( + this.config, + this.logger, + this.nativeMessageHandler, + silentRequest.authenticationScheme + ), + }, this.logger, this.performanceClient ); + // Get the frame handle for the silent request const msalFrame = await invokeAsync( initiateAuthRequest, @@ -335,6 +335,19 @@ export class SilentIframeClient extends StandardInteractionClient { prompt: silentRequest.prompt || PromptValue.NONE, }); } + const authCodeRequest: CommonAuthorizationCodeRequest = { + ...silentRequest, + code: serverParams.code || "", + codeVerifier: pkceCodes.verifier, + }; + // Create silent handler + const interactionHandler = new InteractionHandler( + authClient, + this.browserStorage, + authCodeRequest, + this.logger, + this.performanceClient + ); // Handle response from hash string return invokeAsync( diff --git a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts index 5ac536e11b..15bce42dae 100644 --- a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts @@ -5,7 +5,6 @@ import { ServerTelemetryManager, - CommonAuthorizationCodeRequest, Constants, AuthorizationCodeClient, ClientConfiguration, @@ -20,7 +19,6 @@ import { invokeAsync, BaseAuthRequest, StringDict, - PkceCodes, } from "@azure/msal-common/browser"; import { BaseInteractionClient } from "./BaseInteractionClient.js"; import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js"; @@ -35,7 +33,6 @@ import * as BrowserUtils from "../utils/BrowserUtils.js"; import { RedirectRequest } from "../request/RedirectRequest.js"; import { PopupRequest } from "../request/PopupRequest.js"; import { SsoSilentRequest } from "../request/SsoSilentRequest.js"; -import { generatePkceCodes } from "../crypto/PkceGenerator.js"; import { createNewGuid } from "../crypto/BrowserCrypto.js"; import { initializeBaseRequest } from "../request/RequestHelpers.js"; @@ -43,43 +40,6 @@ import { initializeBaseRequest } from "../request/RequestHelpers.js"; * Defines the class structure and helper functions used by the "standard", non-brokered auth flows (popup, redirect, silent (RT), silent (iframe)) */ export abstract class StandardInteractionClient extends BaseInteractionClient { - /** - * Generates an auth code request tied to the url request. - * @param request - * @param pkceCodes - */ - protected async initializeAuthorizationCodeRequest( - request: AuthorizationUrlRequest, - pkceCodes?: PkceCodes - ): Promise { - this.performanceClient.addQueueMeasurement( - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - this.correlationId - ); - - const generatedPkceParams: PkceCodes = - pkceCodes || - (await invokeAsync( - generatePkceCodes, - PerformanceEvents.GeneratePkceCodes, - this.logger, - this.performanceClient, - this.correlationId - )(this.performanceClient, this.logger, this.correlationId)); - - const authCodeRequest: CommonAuthorizationCodeRequest = { - ...request, - redirectUri: request.redirectUri, - code: Constants.EMPTY_STRING, - codeVerifier: generatedPkceParams.verifier, - }; - - request.codeChallenge = generatedPkceParams.challenge; - request.codeChallengeMethod = Constants.S256_CODE_CHALLENGE_METHOD; - - return authCodeRequest; - } - /** * Initializer for the logout request. * @param logoutRequest diff --git a/lib/msal-browser/src/interaction_handler/InteractionHandler.ts b/lib/msal-browser/src/interaction_handler/InteractionHandler.ts index efb8850341..f93aa8d053 100644 --- a/lib/msal-browser/src/interaction_handler/InteractionHandler.ts +++ b/lib/msal-browser/src/interaction_handler/InteractionHandler.ts @@ -14,7 +14,8 @@ import { PerformanceEvents, invokeAsync, CcsCredentialType, - ServerAuthorizationCodeResponse, + AuthorizeResponse, + AuthorizeProtocol, } from "@azure/msal-common/browser"; import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; @@ -54,7 +55,7 @@ export class InteractionHandler { * @param locationHash */ async handleCodeResponse( - response: ServerAuthorizationCodeResponse, + response: AuthorizeResponse, request: AuthorizationUrlRequest ): Promise { this.performanceClient.addQueueMeasurement( @@ -64,7 +65,7 @@ export class InteractionHandler { let authCodeResponse; try { - authCodeResponse = this.authModule.handleFragmentResponse( + authCodeResponse = AuthorizeProtocol.getAuthorizationCodePayload( response, request.state ); diff --git a/lib/msal-browser/src/interaction_handler/RedirectHandler.ts b/lib/msal-browser/src/interaction_handler/RedirectHandler.ts index 8362eaddd1..055297bb63 100644 --- a/lib/msal-browser/src/interaction_handler/RedirectHandler.ts +++ b/lib/msal-browser/src/interaction_handler/RedirectHandler.ts @@ -14,7 +14,8 @@ import { CcsCredential, invokeAsync, PerformanceEvents, - ServerAuthorizationCodeResponse, + AuthorizeResponse, + AuthorizeProtocol, } from "@azure/msal-common/browser"; import { createBrowserAuthError, @@ -143,7 +144,7 @@ export class RedirectHandler { * @param hash */ async handleCodeResponse( - response: ServerAuthorizationCodeResponse, + response: AuthorizeResponse, state: string ): Promise { this.logger.verbose("RedirectHandler.handleCodeResponse called"); @@ -163,7 +164,7 @@ export class RedirectHandler { let authCodeResponse; try { - authCodeResponse = this.authModule.handleFragmentResponse( + authCodeResponse = AuthorizeProtocol.getAuthorizationCodePayload( response, requestState ); diff --git a/lib/msal-browser/src/protocol/Authorize.ts b/lib/msal-browser/src/protocol/Authorize.ts new file mode 100644 index 0000000000..a62554f6ee --- /dev/null +++ b/lib/msal-browser/src/protocol/Authorize.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + AuthenticationScheme, + Authority, + AuthorizeProtocol, + ClientConfigurationErrorCodes, + CommonAuthorizationUrlRequest, + createClientConfigurationError, + invokeAsync, + IPerformanceClient, + Logger, + PerformanceEvents, + PopTokenGenerator, + ProtocolMode, + RequestParameterBuilder, + Constants, +} from "@azure/msal-common/browser"; +import { BrowserConfiguration } from "../config/Configuration.js"; +import { BrowserConstants } from "../utils/BrowserConstants.js"; +import { version } from "../packageMetadata.js"; +import { CryptoOps } from "../crypto/CryptoOps.js"; + +/** + * Returns map of parameters that are applicable to all calls to /authorize whether using PKCE or EAR + * @param config + * @param authority + * @param request + * @param logger + * @param performanceClient + * @returns + */ +async function getStandardParameters( + config: BrowserConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient: IPerformanceClient +): Promise> { + const parameters = AuthorizeProtocol.getStandardAuthorizeRequestParameters( + { ...config.auth, authority: authority }, + request, + logger, + performanceClient + ); + RequestParameterBuilder.addLibraryInfo(parameters, { + sku: BrowserConstants.MSAL_SKU, + version: version, + os: "", + cpu: "", + }); + if (config.auth.protocolMode !== ProtocolMode.OIDC) { + RequestParameterBuilder.addApplicationTelemetry( + parameters, + config.telemetry.application + ); + } + + if (request.platformBroker) { + // signal ests that this is a WAM call + RequestParameterBuilder.addNativeBroker(parameters); + + // pass the req_cnf for POP + if (request.authenticationScheme === AuthenticationScheme.POP) { + const cryptoOps = new CryptoOps(logger, performanceClient); + const popTokenGenerator = new PopTokenGenerator(cryptoOps); + + // req_cnf is always sent as a string for SPAs + let reqCnfData; + if (!request.popKid) { + const generatedReqCnfData = await invokeAsync( + popTokenGenerator.generateCnf.bind(popTokenGenerator), + PerformanceEvents.PopTokenGenerateCnf, + logger, + performanceClient, + request.correlationId + )(request, logger); + reqCnfData = generatedReqCnfData.reqCnfString; + } else { + reqCnfData = cryptoOps.encodeKid(request.popKid); + } + RequestParameterBuilder.addPopToken(parameters, reqCnfData); + } + } + + RequestParameterBuilder.instrumentBrokerParams( + parameters, + request.correlationId, + performanceClient + ); + + return parameters; +} + +/** + * Gets the full /authorize URL with request parameters when using Auth Code + PKCE + * @param config + * @param authority + * @param request + * @param logger + * @param performanceClient + * @returns + */ +export async function getAuthCodeRequestUrl( + config: BrowserConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient: IPerformanceClient +): Promise { + if (!request.codeChallenge) { + throw createClientConfigurationError( + ClientConfigurationErrorCodes.pkceParamsMissing + ); + } + + const parameters = await invokeAsync( + getStandardParameters, + PerformanceEvents.GetStandardParams, + logger, + performanceClient, + request.correlationId + )(config, authority, request, logger, performanceClient); + RequestParameterBuilder.addResponseTypeCode(parameters); + + RequestParameterBuilder.addCodeChallengeParams( + parameters, + request.codeChallenge, + Constants.S256_CODE_CHALLENGE_METHOD + ); + + return AuthorizeProtocol.getAuthorizeUrl(authority, parameters); +} diff --git a/lib/msal-browser/src/response/ResponseHandler.ts b/lib/msal-browser/src/response/ResponseHandler.ts index 84e405f343..b71a456360 100644 --- a/lib/msal-browser/src/response/ResponseHandler.ts +++ b/lib/msal-browser/src/response/ResponseHandler.ts @@ -6,7 +6,7 @@ import { ICrypto, Logger, - ServerAuthorizationCodeResponse, + AuthorizeResponse, UrlUtils, } from "@azure/msal-common/browser"; import { @@ -20,7 +20,7 @@ export function deserializeResponse( responseString: string, responseLocation: string, logger: Logger -): ServerAuthorizationCodeResponse { +): AuthorizeResponse { // Deserialize hash fragment response parameters. const serverParams = UrlUtils.getDeserializedResponse(responseString); if (!serverParams) { @@ -49,7 +49,7 @@ export function deserializeResponse( * Returns the interaction type that the response object belongs to */ export function validateInteractionType( - response: ServerAuthorizationCodeResponse, + response: AuthorizeResponse, browserCrypto: ICrypto, interactionType: InteractionType ): void { diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 94946767a6..311c156827 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -5,8 +5,6 @@ import { PublicClientApplication } from "../../src/app/PublicClientApplication.js"; import { - DEFAULT_OPENID_CONFIG_RESPONSE, - DEFAULT_TENANT_DISCOVERY_RESPONSE, ID_TOKEN_ALT_CLAIMS, ID_TOKEN_CLAIMS, RANDOM_TEST_GUID, @@ -17,11 +15,10 @@ import { TEST_SSH_VALUES, TEST_STATE_VALUES, TEST_TOKEN_LIFETIMES, - TEST_TOKEN_RESPONSE, TEST_TOKENS, TEST_URIS, testLogoutUrl, - testNavUrlNoRequest, + verifyUrl, } from "../utils/StringConstants.js"; import { AccountEntity, @@ -57,6 +54,7 @@ import { ServerTelemetryEntity, TokenClaims, StubPerformanceClient, + OIDC_DEFAULT_SCOPES, } from "@azure/msal-common/browser"; import { ApiId, @@ -1289,9 +1287,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { RedirectHandler.prototype, "initiateAuthRequest" ).mockImplementation((navigateUrl): Promise => { - expect( - navigateUrl.startsWith(testNavUrlNoRequest) - ).toBeTruthy(); + verifyUrl(navigateUrl); return Promise.resolve(done()); }); jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ diff --git a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts index bac564dc61..b8ff3ffde7 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts @@ -65,6 +65,18 @@ const MOCK_WAM_RESPONSE = { properties: {}, }; +const MOCK_WAM_RESPONSE_STRING_EXPIRES_IN = { + access_token: TEST_TOKENS.ACCESS_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2, + scope: "User.Read", + expires_in: "3600", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + account: { + id: "nativeAccountId", + }, + properties: {}, +}; + const testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims( ID_TOKEN_CLAIMS, undefined, @@ -252,6 +264,35 @@ describe("NativeInteractionClient Tests", () => { expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); + it("acquires token successfully with string expires_in", async () => { + jest.spyOn( + NativeMessageHandler.prototype, + "sendMessage" + ).mockImplementation((): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE_STRING_EXPIRES_IN); + }); + const response = await nativeInteractionClient.acquireToken({ + scopes: ["User.Read"], + }); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual( + MOCK_WAM_RESPONSE_STRING_EXPIRES_IN.id_token + ); + expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); + expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); + expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + expect(response.authority).toEqual(TEST_CONFIG.validAuthority); + expect(response.scopes).toContain( + MOCK_WAM_RESPONSE_STRING_EXPIRES_IN.scope + ); + expect(response.correlationId).toEqual(RANDOM_TEST_GUID); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); + expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); + expect(response.expiresOn).toBeDefined(); + }); + it("throws if prompt: select_account", (done) => { nativeInteractionClient .acquireToken({ @@ -444,6 +485,59 @@ describe("NativeInteractionClient Tests", () => { }); }); + it("does not throw error on user switch for double brokering", (done) => { + const raw_client_info = + "eyJ1aWQiOiAiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwgInV0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcifQ=="; + + const mockWamResponse = { + access_token: TEST_TOKENS.ACCESS_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2_ALT, + scope: "User.Read", + expires_in: 3600, + client_info: raw_client_info, + account: { + id: "different-nativeAccountId", + }, + properties: {}, + }; + + jest.spyOn( + CacheManager.prototype, + "getAccountInfoFilteredBy" + ).mockReturnValue(TEST_ACCOUNT_INFO); + + jest.spyOn( + NativeMessageHandler.prototype, + "sendMessage" + ).mockImplementation((): Promise => { + return Promise.resolve(mockWamResponse); + }); + + nativeInteractionClient + .acquireToken({ + scopes: ["User.Read"], + redirectUri: "localhost", + extraQueryParameters: { + brk_client_id: "broker_client_id", + brk_redirect_uri: "https://broker_redirect_uri.com", + client_id: "parent_client_id", + }, + }) + .catch((e) => { + console.error( + "User switch error should not have been thrown." + ); + expect(e.errorCode).not.toBe( + NativeAuthErrorCodes.userSwitch + ); + expect(e.errorMessage).not.toBe( + NativeAuthErrorMessages[NativeAuthErrorCodes.userSwitch] + ); + done(); + }); + done(); + }); + it("ssoSilent overwrites prompt to be 'none' and succeeds", async () => { jest.spyOn( NativeMessageHandler.prototype, diff --git a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts index ac1410bdf5..30e46e192e 100644 --- a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts @@ -43,6 +43,7 @@ import { } from "../../src/utils/BrowserConstants.js"; import * as BrowserCrypto from "../../src/crypto/BrowserCrypto.js"; import * as PkceGenerator from "../../src/crypto/PkceGenerator.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { NavigationClient } from "../../src/navigation/NavigationClient.js"; import { EndSessionPopupRequest } from "../../src/request/EndSessionPopupRequest.js"; import { PopupClient } from "../../src/interaction_client/PopupClient.js"; @@ -369,8 +370,8 @@ describe("PopupClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn( PopupClient.prototype, @@ -496,8 +497,8 @@ describe("PopupClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn( PopupClient.prototype, @@ -604,8 +605,8 @@ describe("PopupClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(PopupClient.prototype, "initiateAuthRequest") .mockClear() @@ -786,8 +787,8 @@ describe("PopupClient", () => { "Error in creating a login url" ); jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn( PopupClient.prototype, diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index f13014801d..783d959057 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -20,6 +20,7 @@ import { TEST_SSH_VALUES, ID_TOKEN_CLAIMS, TEST_TOKEN_RESPONSE, + verifyUrl, } from "../utils/StringConstants.js"; import { ServerError, @@ -47,7 +48,6 @@ import { createClientConfigurationError, ClientConfigurationErrorCodes, IdTokenEntity, - CredentialType, InProgressPerformanceEvent, StubPerformanceClient, } from "@azure/msal-common"; @@ -69,6 +69,7 @@ import { RedirectHandler } from "../../src/interaction_handler/RedirectHandler.j import { CryptoOps } from "../../src/crypto/CryptoOps.js"; import * as BrowserCrypto from "../../src/crypto/BrowserCrypto.js"; import * as PkceGenerator from "../../src/crypto/PkceGenerator.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; import { RedirectRequest } from "../../src/request/RedirectRequest.js"; import { NavigationClient } from "../../src/navigation/NavigationClient.js"; @@ -1929,7 +1930,7 @@ describe("RedirectClient", () => { "initiateAuthRequest" ).mockImplementation(async (navigateUrl): Promise => { try { - expect(navigateUrl).toEqual(testNavUrl); + verifyUrl(navigateUrl, ["user.read"]); return Promise.resolve(done()); } catch (err) { Promise.reject(err); @@ -2382,8 +2383,8 @@ describe("RedirectClient", () => { correlationId: TEST_CONFIG.CORRELATION_ID, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockRejectedValue(createBrowserAuthError(testError.errorCode)); try { await redirectClient.acquireToken(emptyRequest); @@ -2409,7 +2410,7 @@ describe("RedirectClient", () => { RedirectHandler.prototype, "initiateAuthRequest" ).mockImplementation((navigateUrl): Promise => { - expect(navigateUrl).toEqual(testNavUrl); + verifyUrl(navigateUrl, ["user.read"]); return Promise.resolve(done()); }); jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ @@ -2431,7 +2432,7 @@ describe("RedirectClient", () => { it("passes onRedirectNavigate callback", (done) => { const onRedirectNavigate = (url: string) => { - expect(url).toEqual(testNavUrl); + verifyUrl(url, ["user.read"]); done(); }; @@ -2448,7 +2449,7 @@ describe("RedirectClient", () => { } ): Promise => { expect(onRedirectNavigateCb).toEqual(onRedirectNavigate); - expect(navigateUrl).toEqual(testNavUrl); + verifyUrl(navigateUrl, ["user.read"]); onRedirectNavigate(navigateUrl); return Promise.resolve(); } @@ -2625,8 +2626,8 @@ describe("RedirectClient", () => { "Error in creating a login url" ); jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockRejectedValue(testError); try { await redirectClient.acquireToken(emptyRequest); diff --git a/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts index bed7fde446..0ab8d1c13e 100644 --- a/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts @@ -34,6 +34,7 @@ import { } from "../../src/index.js"; import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler.js"; import { FetchClient } from "../../src/network/FetchClient.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { TestTimeUtils } from "msal-test-utils"; describe("SilentAuthCodeClient", () => { @@ -137,8 +138,8 @@ describe("SilentAuthCodeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); const handleCodeSpy = jest .spyOn( diff --git a/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts index 8755c3f01e..d05fc5ee0b 100644 --- a/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts @@ -36,6 +36,7 @@ import { import * as SilentHandler from "../../src/interaction_handler/SilentHandler.js"; import * as BrowserCrypto from "../../src/crypto/BrowserCrypto.js"; import * as PkceGenerator from "../../src/crypto/PkceGenerator.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { SilentIframeClient } from "../../src/interaction_client/SilentIframeClient.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; import { ApiId, AuthenticationResult } from "../../src/index.js"; @@ -145,8 +146,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -186,8 +187,8 @@ describe("SilentIframeClient", () => { it("Errors thrown during token acquisition are cached for telemetry and browserStorage is cleaned", (done) => { jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockRejectedValue( createBrowserAuthError( @@ -224,8 +225,8 @@ describe("SilentIframeClient", () => { it("Unexpected non-msal errors do not add correlationId and browserStorage is cleaned", (done) => { jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); const testError = { errorCode: "Unexpected error", @@ -298,8 +299,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -367,8 +368,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -478,8 +479,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_NATIVE_ACCOUNT_ID_SILENT @@ -580,8 +581,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_NATIVE_ACCOUNT_ID_SILENT @@ -727,8 +728,8 @@ describe("SilentIframeClient", () => { cloudGraphHostName: "", }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -779,8 +780,8 @@ describe("SilentIframeClient", () => { status: 200, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -988,8 +989,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -1098,8 +1099,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT diff --git a/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts index 204e3e0b56..d2968d7921 100644 --- a/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts @@ -39,10 +39,6 @@ class testStandardInteractionClient extends StandardInteractionClient { return Promise.resolve(); } - async initializeAuthorizationCodeRequest(request: AuthorizationUrlRequest) { - return super.initializeAuthorizationCodeRequest(request); - } - async initializeAuthorizationRequest( request: RedirectRequest, interactionType: InteractionType @@ -127,81 +123,6 @@ describe("StandardInteractionClient", () => { jest.restoreAllMocks(); }); - it("initializeAuthorizationCodeRequest", async () => { - const request: AuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: ["scope"], - loginHint: "AbeLi@microsoft.com", - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - const authCodeRequest = - await testClient.initializeAuthorizationCodeRequest(request); - expect(request.codeChallenge).toBe(TEST_CONFIG.TEST_CHALLENGE); - expect(authCodeRequest.codeVerifier).toBe(TEST_CONFIG.TEST_VERIFIER); - expect(authCodeRequest.popKid).toBeUndefined; - }); - - it("initializeAuthorizationCodeRequest validates the request and does not influence undefined popKid param", async () => { - const request: AuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: ["scope"], - loginHint: "AbeLi@microsoft.com", - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - const authCodeRequest = - await testClient.initializeAuthorizationCodeRequest(request); - expect(authCodeRequest.popKid).toBeUndefined; - }); - - it("initializeAuthorizationCodeRequest validates the request and adds reqCnf param when user defined", async () => { - const request: AuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: ["scope"], - loginHint: "AbeLi@microsoft.com", - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - popKid: TEST_REQ_CNF_DATA.kid, - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - const authCodeRequest = - await testClient.initializeAuthorizationCodeRequest(request); - expect(authCodeRequest.popKid).toEqual(TEST_REQ_CNF_DATA.kid); - }); - it("getDiscoveredAuthority - request authority only", async () => { const requestAuthority = TEST_CONFIG.validAuthority; diff --git a/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts b/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts index 3ff4bc34f6..d4ac69261d 100644 --- a/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts +++ b/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts @@ -34,23 +34,15 @@ import { TEST_DATA_CLIENT_INFO, TEST_TOKENS, TEST_TOKEN_LIFETIMES, - TEST_HASHES, TEST_POP_VALUES, TEST_STATE_VALUES, RANDOM_TEST_GUID, TEST_CRYPTO_VALUES, } from "../utils/StringConstants.js"; -import { - createBrowserAuthError, - BrowserAuthErrorCodes, -} from "../../src/error/BrowserAuthError.js"; import { CryptoOps } from "../../src/crypto/CryptoOps.js"; import { TestStorageManager } from "../cache/TestStorageManager.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; -import { - TemporaryCacheKeys, - BrowserConstants, -} from "../../src/utils/BrowserConstants.js"; +import { TemporaryCacheKeys } from "../../src/utils/BrowserConstants.js"; import { EventHandler } from "../../src/event/EventHandler.js"; import { TestTimeUtils } from "msal-test-utils"; @@ -417,10 +409,6 @@ describe("InteractionHandler.ts Unit Tests", () => { ), idTokenClaims.nonce ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "handleFragmentResponse" - ).mockReturnValue(testCodeResponse); const updateAuthoritySpy = jest.spyOn( AuthorizationCodeClient.prototype, "updateAuthority" @@ -434,10 +422,7 @@ describe("InteractionHandler.ts Unit Tests", () => { ); await interactionHandler.initiateAuthRequest("testNavUrl"); const tokenResponse = await interactionHandler.handleCodeResponse( - { - code: "authCode", - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }, + testCodeResponse, { authority: TEST_CONFIG.validAuthority, scopes: ["User.Read"], @@ -522,10 +507,6 @@ describe("InteractionHandler.ts Unit Tests", () => { TemporaryCacheKeys.CCS_CREDENTIAL, CcsCredentialType.UPN ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "handleFragmentResponse" - ).mockReturnValue(testCodeResponse); const acquireTokenSpy = jest .spyOn(AuthorizationCodeClient.prototype, "acquireToken") .mockResolvedValue(testTokenResponse); @@ -535,10 +516,7 @@ describe("InteractionHandler.ts Unit Tests", () => { ); await interactionHandler.initiateAuthRequest("testNavUrl"); const tokenResponse = await interactionHandler.handleCodeResponse( - { - code: "authCode", - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }, + testCodeResponse, { authority: TEST_CONFIG.validAuthority, scopes: ["User.Read"], diff --git a/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts b/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts index 6ad8f15f19..d09510859b 100644 --- a/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts +++ b/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts @@ -7,7 +7,6 @@ import { PkceCodes, NetworkRequestOptions, AccountInfo, - AuthorityFactory, CommonAuthorizationCodeRequest, Constants, AuthenticationResult, @@ -413,10 +412,6 @@ describe("RedirectHandler.ts Unit Tests", () => { browserStorage.generateCacheKey(TemporaryCacheKeys.URL_HASH), TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "handleFragmentResponse" - ).mockReturnValue(testCodeResponse); jest.spyOn( AuthorizationCodeClient.prototype, "acquireToken" @@ -463,11 +458,6 @@ describe("RedirectHandler.ts Unit Tests", () => { tid: "3338040d-6c67-4c5b-b112-36a304b66dad", nonce: "123523", }; - const testCodeResponse: AuthorizationCodePayload = { - code: "authcode", - nonce: idTokenClaims.nonce, - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }; const testAccount: AccountInfo = { homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID_ENCODED, @@ -535,10 +525,6 @@ describe("RedirectHandler.ts Unit Tests", () => { TemporaryCacheKeys.CCS_CREDENTIAL, JSON.stringify(testCcsCred) ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "handleFragmentResponse" - ).mockReturnValue(testCodeResponse); jest.spyOn( AuthorizationCodeClient.prototype, "acquireToken" diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index 6109a4ad14..7af9ba1cbd 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -139,6 +139,7 @@ export const ID_TOKEN_ALT_CLAIMS = { // Test Expiration Vals export const TEST_TOKEN_LIFETIMES = { DEFAULT_EXPIRES_IN: 3599, + STRING_EXPIRES_IN: "3599", TEST_ID_TOKEN_EXP: 1536361411, TEST_ACCESS_TOKEN_EXP: 1537234948, }; @@ -422,17 +423,54 @@ export const testNavUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/ `${RANDOM_TEST_GUID}` )}&state=${encodeURIComponent(`${TEST_STATE_VALUES.TEST_STATE_REDIRECT}`)}`; -export const testNavUrlNoRequest = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${encodeURIComponent( - `${TEST_CONFIG.MSAL_CLIENT_ID}` -)}&scope=openid%20profile%20offline_access&redirect_uri=https%3A%2F%2Flocalhost%3A8081%2Findex.html&client-request-id=${encodeURIComponent( - `${RANDOM_TEST_GUID}` -)}&response_mode=fragment&response_type=code&x-client-SKU=msal.js.browser&x-client-VER=${version}&x-app-name=${ - TEST_CONFIG.applicationName -}&x-app-ver=${ - TEST_CONFIG.applicationVersion -}&client_info=1&code_challenge=JsjesZmxJwehdhNY9kvyr0QOeSMEvryY_EHZo3BKrqg&code_challenge_method=S256&nonce=${encodeURIComponent( - `${RANDOM_TEST_GUID}` -)}&state=`; +export function verifyUrl( + url: string, + scopes: Array = [], + state?: string +) { + scopes.push("openid", "profile", "offline_access"); // Add default scopes + expect( + url.startsWith( + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" + ) + ).toBe(true); + expect( + url.includes( + `client_id=${encodeURIComponent(TEST_CONFIG.MSAL_CLIENT_ID)}` + ) + ).toBe(true); + expect( + url.includes(`redirect_uri=${encodeURIComponent(window.location.href)}`) + ).toBe(true); + expect(url.includes(`scope=${scopes.join("%20")}`)).toBe(true); + expect( + url.includes( + `client-request-id=${encodeURIComponent(RANDOM_TEST_GUID)}` + ) + ); + expect(url.includes(`response_mode=fragment`)).toBe(true); + expect(url.includes("x-client-SKU=msal.js.browser")).toBe(true); + expect(url.includes(`x-client-VER=${version}`)).toBe(true); + expect(url.includes(`x-app-name=${TEST_CONFIG.applicationName}`)).toBe( + true + ); + expect(url.includes(`x-app-ver=${TEST_CONFIG.applicationVersion}`)).toBe( + true + ); + expect(url.includes("client_info=1")).toBe(true); + expect( + url.includes( + "code_challenge=JsjesZmxJwehdhNY9kvyr0QOeSMEvryY_EHZo3BKrqg" + ) + ).toBe(true); + expect(url.includes("code_challenge_method=S256")).toBe(true); + expect(url.includes(`nonce=${encodeURIComponent(RANDOM_TEST_GUID)}`)).toBe( + true + ); + expect( + url.includes(state ? `state=${encodeURIComponent(state)}` : "state=") + ).toBe(true); +} export const testLogoutUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=${encodeURIComponent( `${TEST_URIS.TEST_REDIR_URI}` diff --git a/lib/msal-common/apiReview/msal-common.api.md b/lib/msal-common/apiReview/msal-common.api.md index deb6d13f15..aa050ee78c 100644 --- a/lib/msal-common/apiReview/msal-common.api.md +++ b/lib/msal-common/apiReview/msal-common.api.md @@ -302,7 +302,7 @@ function addClientSecret(parameters: Map, clientSecret: string): // Warning: (ae-missing-release-tag) "addCodeChallengeParams" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function addCodeChallengeParams(parameters: Map, codeChallenge: string, codeChallengeMethod: string): void; +function addCodeChallengeParams(parameters: Map, codeChallenge?: string, codeChallengeMethod?: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "addCodeVerifier" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -759,11 +759,7 @@ export class AuthorizationCodeClient extends BaseClient { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen acquireToken(request: CommonAuthorizationCodeRequest, authCodePayload?: AuthorizationCodePayload): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - getAuthCodeUrl(request: CommonAuthorizationUrlRequest): Promise; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen getLogoutUri(logoutRequest: CommonEndSessionRequest): string; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - handleFragmentResponse(serverParams: ServerAuthorizationCodeResponse, cachedState: string): AuthorizationCodePayload; // (undocumented) protected includeRedirectUri: boolean; } @@ -787,6 +783,37 @@ export type AuthorizationCodePayload = { client_info?: string; }; +declare namespace AuthorizeProtocol { + export { + getStandardAuthorizeRequestParameters, + getAuthorizeUrl, + getAuthorizationCodePayload, + validateAuthorizationResponse + } +} + +// Warning: (ae-missing-release-tag) "AuthorizeResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type AuthorizeResponse = { + code?: string; + client_info?: string; + state?: string; + cloud_instance_name?: string; + cloud_instance_host_name?: string; + cloud_graph_host_name?: string; + msgraph_host?: string; + error?: string; + error_uri?: string; + error_description?: string; + suberror?: string; + timestamp?: string; + trace_id?: string; + correlation_id?: string; + claims?: string; + accountId?: string; +}; + // Warning: (ae-missing-release-tag) "authTimeNotFound" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2257,6 +2284,22 @@ function generateAuthorityMetadataExpiresAt(): number; // @public function generateCredentialKey(credentialEntity: CredentialEntity): string; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-missing-release-tag) "getAuthorizationCodePayload" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function getAuthorizationCodePayload(serverParams: AuthorizeResponse, cachedState: string): AuthorizationCodePayload; + +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-incompatible-release-tags) The symbol "getAuthorizeUrl" is marked as @public, but its signature references "Authority" which is marked as @internal +// Warning: (ae-incompatible-release-tags) The symbol "getAuthorizeUrl" is marked as @public, but its signature references "Authority" which is marked as @internal +// Warning: (ae-missing-release-tag) "getAuthorizeUrl" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function getAuthorizeUrl(authority: Authority, requestParameters: Map): string; + // Warning: (ae-missing-release-tag) "getClientAssertion" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2265,7 +2308,7 @@ export function getClientAssertion(clientAssertion: string | ClientAssertionCall // Warning: (ae-missing-release-tag) "getDeserializedResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function getDeserializedResponse(responseString: string): ServerAuthorizationCodeResponse | null; +function getDeserializedResponse(responseString: string): AuthorizeResponse | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "getJWSPayload" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2278,6 +2321,17 @@ function getJWSPayload(authToken: string): string; // @public (undocumented) export function getRequestThumbprint(clientId: string, request: BaseAuthRequest, homeAccountId?: string): RequestThumbprint; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-incompatible-release-tags) The symbol "getStandardAuthorizeRequestParameters" is marked as @public, but its signature references "AuthOptions" which is marked as @internal +// Warning: (ae-incompatible-release-tags) The symbol "getStandardAuthorizeRequestParameters" is marked as @public, but its signature references "AuthOptions" which is marked as @internal +// Warning: (ae-missing-release-tag) "getStandardAuthorizeRequestParameters" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function getStandardAuthorizeRequestParameters(authOptions: AuthOptions, request: CommonAuthorizationUrlRequest, logger: Logger, performanceClient?: IPerformanceClient): Map; + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "getTenantIdFromIdTokenClaims" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3406,15 +3460,14 @@ export const PerformanceEvents: { readonly StandardInteractionClientCreateAuthCodeClient: "standardInteractionClientCreateAuthCodeClient"; readonly StandardInteractionClientGetClientConfiguration: "standardInteractionClientGetClientConfiguration"; readonly StandardInteractionClientInitializeAuthorizationRequest: "standardInteractionClientInitializeAuthorizationRequest"; - readonly StandardInteractionClientInitializeAuthorizationCodeRequest: "standardInteractionClientInitializeAuthorizationCodeRequest"; readonly GetAuthCodeUrl: "getAuthCodeUrl"; + readonly GetStandardParams: "getStandardParams"; readonly HandleCodeResponseFromServer: "handleCodeResponseFromServer"; readonly HandleCodeResponse: "handleCodeResponse"; readonly UpdateTokenEndpointAuthority: "updateTokenEndpointAuthority"; readonly AuthClientAcquireToken: "authClientAcquireToken"; readonly AuthClientExecuteTokenRequest: "authClientExecuteTokenRequest"; readonly AuthClientCreateTokenRequestBody: "authClientCreateTokenRequestBody"; - readonly AuthClientCreateQueryString: "authClientCreateQueryString"; readonly PopTokenGenerateCnf: "popTokenGenerateCnf"; readonly PopTokenGenerateKid: "popTokenGenerateKid"; readonly HandleServerTokenResponse: "handleServerTokenResponse"; @@ -3763,10 +3816,6 @@ export class ResponseHandler { handleServerTokenResponse(serverTokenResponse: ServerAuthorizationTokenResponse, authority: Authority, reqTimestamp: number, request: BaseAuthRequest, authCodePayload?: AuthorizationCodePayload, userAssertionHash?: string, handlingRefreshTokenResponse?: boolean, forceCacheRefreshTokenResponse?: boolean, serverRequestId?: string): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - validateServerAuthorizationCodeResponse(serverResponse: ServerAuthorizationCodeResponse, requestState: string): void; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen validateTokenResponse(serverResponse: ServerAuthorizationTokenResponse, refreshAccessToken?: boolean): void; } @@ -3826,28 +3875,6 @@ export class ScopeSet { unionScopeSets(otherScopes: ScopeSet): Set; } -// Warning: (ae-missing-release-tag) "ServerAuthorizationCodeResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export type ServerAuthorizationCodeResponse = { - code?: string; - client_info?: string; - state?: string; - cloud_instance_name?: string; - cloud_instance_host_name?: string; - cloud_graph_host_name?: string; - msgraph_host?: string; - error?: string; - error_uri?: string; - error_description?: string; - suberror?: string; - timestamp?: string; - trace_id?: string; - correlation_id?: string; - claims?: string; - accountId?: string; -}; - // Warning: (ae-missing-release-tag) "ServerAuthorizationTokenResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -4403,6 +4430,13 @@ const userCanceled = "user_canceled"; // @public (undocumented) const userTimeoutReached = "user_timeout_reached"; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-missing-release-tag) "validateAuthorizationResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function validateAuthorizationResponse(serverResponse: AuthorizeResponse, requestState: string): void; + // Warning: (ae-internal-missing-underscore) The name "ValidCacheType" should be prefixed with an underscore because the declaration is marked as @internal // // @internal @@ -4532,12 +4566,10 @@ const X_MS_LIB_CAPABILITY = "x-ms-lib-capability"; // src/cache/CacheManager.ts:1826:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/cache/utils/CacheTypes.ts:94:53 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag // src/cache/utils/CacheTypes.ts:94:43 - (tsdoc-malformed-html-name) Invalid HTML element: An HTML name must be an ASCII letter followed by zero or more letters, digits, or hyphens -// src/client/AuthorizationCodeClient.ts:229:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:230:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:299:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:531:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:844:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:899:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:158:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:159:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:228:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:460:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/RefreshTokenClient.ts:194:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/RefreshTokenClient.ts:287:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/RefreshTokenClient.ts:288:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -4548,102 +4580,102 @@ const X_MS_LIB_CAPABILITY = "x-ms-lib-capability"; // src/index.ts:8:12 - (tsdoc-characters-after-block-tag) The token "@azure" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" // src/index.ts:8:4 - (tsdoc-undefined-tag) The TSDoc tag "@module" is not defined in this configuration // src/request/AuthenticationHeaderParser.ts:74:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/response/ResponseHandler.ts:431:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/response/ResponseHandler.ts:432:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/response/ResponseHandler.ts:433:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:333:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:334:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:335:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/telemetry/performance/PerformanceClient.ts:916:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/telemetry/performance/PerformanceClient.ts:916:15 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // src/telemetry/performance/PerformanceClient.ts:928:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/telemetry/performance/PerformanceClient.ts:928:27 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // src/telemetry/performance/PerformanceClient.ts:929:24 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // src/telemetry/performance/PerformanceClient.ts:929:17 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:586:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:586:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:586:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:593:37 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:593:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:593:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:600:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:600:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:600:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:607:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:607:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:607:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:614:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:614:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:614:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:620:8 - (tsdoc-undefined-tag) The TSDoc tag "@date" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:622:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:622:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:622:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:630:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:630:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:630:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:637:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:637:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:637:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:644:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:644:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:644:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:652:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:652:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:652:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:659:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:659:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:659:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:666:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:666:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:666:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:673:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:673:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:673:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:680:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:680:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:680:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:692:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:692:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:692:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:699:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:699:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:699:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:706:23 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:706:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:706:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:713:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:713:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:713:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:720:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:720:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:720:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:726:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:726:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:726:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:733:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:733:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:733:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:752:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:752:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:752:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:758:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:758:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:758:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:765:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:765:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:765:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:773:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:773:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:773:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:782:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:782:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:782:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:790:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:790:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:790:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:797:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:797:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:797:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:867:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:867:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:867:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:576:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:576:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:576:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:583:37 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:583:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:583:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:590:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:590:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:590:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:597:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:597:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:597:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:604:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:604:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:604:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:610:8 - (tsdoc-undefined-tag) The TSDoc tag "@date" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:612:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:612:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:612:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:620:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:620:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:620:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:627:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:627:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:627:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:634:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:634:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:634:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:642:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:642:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:642:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:649:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:649:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:649:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:656:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:656:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:656:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:663:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:663:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:663:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:670:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:670:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:670:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:682:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:682:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:682:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:689:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:689:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:689:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:696:23 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:696:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:696:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:703:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:703:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:703:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:710:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:710:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:710:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:716:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:716:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:716:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:723:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:723:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:723:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:742:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:742:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:742:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:748:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:748:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:748:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:755:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:755:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:755:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:763:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:763:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:763:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:772:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:772:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:772:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:780:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:780:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:780:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:787:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:787:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:787:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:857:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:857:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:857:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration ``` diff --git a/lib/msal-common/src/client/AuthorizationCodeClient.ts b/lib/msal-common/src/client/AuthorizationCodeClient.ts index 1e40e7b0c0..ce00200387 100644 --- a/lib/msal-common/src/client/AuthorizationCodeClient.ts +++ b/lib/msal-common/src/client/AuthorizationCodeClient.ts @@ -4,7 +4,6 @@ */ import { BaseClient } from "./BaseClient.js"; -import { CommonAuthorizationUrlRequest } from "../request/CommonAuthorizationUrlRequest.js"; import { CommonAuthorizationCodeRequest } from "../request/CommonAuthorizationCodeRequest.js"; import { Authority } from "../authority/Authority.js"; import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js"; @@ -12,7 +11,6 @@ import * as UrlUtils from "../utils/UrlUtils.js"; import { GrantType, AuthenticationScheme, - PromptValue, Separators, HeaderNames, } from "../utils/Constants.js"; @@ -31,12 +29,10 @@ import { createClientAuthError, } from "../error/ClientAuthError.js"; import { UrlString } from "../url/UrlString.js"; -import { ServerAuthorizationCodeResponse } from "../response/ServerAuthorizationCodeResponse.js"; import { CommonEndSessionRequest } from "../request/CommonEndSessionRequest.js"; import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js"; import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js"; import * as TimeUtils from "../utils/TimeUtils.js"; -import { AccountInfo } from "../account/AccountInfo.js"; import { buildClientInfoFromHomeAccountId, buildClientInfo, @@ -72,38 +68,6 @@ export class AuthorizationCodeClient extends BaseClient { this.config.authOptions.authority.options.OIDCOptions?.defaultScopes; } - /** - * Creates the URL of the authorization request letting the user input credentials and consent to the - * application. The URL target the /authorize endpoint of the authority configured in the - * application object. - * - * Once the user inputs their credentials and consents, the authority will send a response to the redirect URI - * sent in the request and should contain an authorization code, which can then be used to acquire tokens via - * acquireToken(AuthorizationCodeRequest) - * @param request - */ - async getAuthCodeUrl( - request: CommonAuthorizationUrlRequest - ): Promise { - this.performanceClient?.addQueueMeasurement( - PerformanceEvents.GetAuthCodeUrl, - request.correlationId - ); - - const queryString = await invokeAsync( - this.createAuthCodeUrlQueryString.bind(this), - PerformanceEvents.AuthClientCreateQueryString, - this.logger, - this.performanceClient, - request.correlationId - )(request); - - return UrlString.appendQueryString( - this.authority.authorizationEndpoint, - queryString - ); - } - /** * API to acquire a token in exchange of 'authorization_code` acquired by the user in the first leg of the * authorization_code_grant @@ -168,41 +132,6 @@ export class AuthorizationCodeClient extends BaseClient { ); } - /** - * Handles the hash fragment response from public client code request. Returns a code response used by - * the client to exchange for a token in acquireToken. - * @param hashFragment - */ - handleFragmentResponse( - serverParams: ServerAuthorizationCodeResponse, - cachedState: string - ): AuthorizationCodePayload { - // Handle responses. - const responseHandler = new ResponseHandler( - this.config.authOptions.clientId, - this.cacheManager, - this.cryptoUtils, - this.logger, - null, - null - ); - - // Get code response - responseHandler.validateServerAuthorizationCodeResponse( - serverParams, - cachedState - ); - - // throw when there is no auth code in the response - if (!serverParams.code) { - throw createClientAuthError( - ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse - ); - } - - return serverParams as AuthorizationCodePayload; - } - /** * Used to log out the current user, and redirect the user to the postLogoutRedirectUri. * Default behaviour is to redirect the user to `window.location.href`. @@ -526,319 +455,6 @@ export class AuthorizationCodeClient extends BaseClient { return UrlUtils.mapToQueryString(parameters); } - /** - * This API validates the `AuthorizationCodeUrlRequest` and creates a URL - * @param request - */ - private async createAuthCodeUrlQueryString( - request: CommonAuthorizationUrlRequest - ): Promise { - // generate the correlationId if not set by the user and add - const correlationId = - request.correlationId || - this.config.cryptoInterface.createNewGuid(); - - this.performanceClient?.addQueueMeasurement( - PerformanceEvents.AuthClientCreateQueryString, - correlationId - ); - - const parameters = new Map(); - - RequestParameterBuilder.addClientId( - parameters, - request.embeddedClientId || - request.extraQueryParameters?.[AADServerParamKeys.CLIENT_ID] || - this.config.authOptions.clientId - ); - - const requestScopes = [ - ...(request.scopes || []), - ...(request.extraScopesToConsent || []), - ]; - RequestParameterBuilder.addScopes( - parameters, - requestScopes, - true, - this.oidcDefaultScopes - ); - - // validate the redirectUri (to be a non null value) - RequestParameterBuilder.addRedirectUri(parameters, request.redirectUri); - - RequestParameterBuilder.addCorrelationId(parameters, correlationId); - - // add response_mode. If not passed in it defaults to query. - RequestParameterBuilder.addResponseMode( - parameters, - request.responseMode - ); - - // add response_type = code - RequestParameterBuilder.addResponseTypeCode(parameters); - - // add library info parameters - RequestParameterBuilder.addLibraryInfo( - parameters, - this.config.libraryInfo - ); - if (!isOidcProtocolMode(this.config)) { - RequestParameterBuilder.addApplicationTelemetry( - parameters, - this.config.telemetry.application - ); - } - - // add client_info=1 - RequestParameterBuilder.addClientInfo(parameters); - - if (request.codeChallenge && request.codeChallengeMethod) { - RequestParameterBuilder.addCodeChallengeParams( - parameters, - request.codeChallenge, - request.codeChallengeMethod - ); - } - - if (request.prompt) { - RequestParameterBuilder.addPrompt(parameters, request.prompt); - } - - if (request.domainHint) { - RequestParameterBuilder.addDomainHint( - parameters, - request.domainHint - ); - this.performanceClient?.addFields( - { domainHintFromRequest: true }, - correlationId - ); - } - - this.performanceClient?.addFields( - { prompt: request.prompt }, - correlationId - ); - - // Add sid or loginHint with preference for login_hint claim (in request) -> sid -> loginHint (upn/email) -> username of AccountInfo object - if (request.prompt !== PromptValue.SELECT_ACCOUNT) { - // AAD will throw if prompt=select_account is passed with an account hint - if (request.sid && request.prompt === PromptValue.NONE) { - // SessionID is only used in silent calls - this.logger.verbose( - "createAuthCodeUrlQueryString: Prompt is none, adding sid from request" - ); - RequestParameterBuilder.addSid(parameters, request.sid); - this.performanceClient?.addFields( - { sidFromRequest: true }, - correlationId - ); - } else if (request.account) { - const accountSid = this.extractAccountSid(request.account); - let accountLoginHintClaim = this.extractLoginHint( - request.account - ); - - if (accountLoginHintClaim && request.domainHint) { - this.logger.warning( - `AuthorizationCodeClient.createAuthCodeUrlQueryString: "domainHint" param is set, skipping opaque "login_hint" claim. Please consider not passing domainHint` - ); - accountLoginHintClaim = null; - } - - // If login_hint claim is present, use it over sid/username - if (accountLoginHintClaim) { - this.logger.verbose( - "createAuthCodeUrlQueryString: login_hint claim present on account" - ); - RequestParameterBuilder.addLoginHint( - parameters, - accountLoginHintClaim - ); - this.performanceClient?.addFields( - { loginHintFromClaim: true }, - correlationId - ); - try { - const clientInfo = buildClientInfoFromHomeAccountId( - request.account.homeAccountId - ); - RequestParameterBuilder.addCcsOid( - parameters, - clientInfo - ); - } catch (e) { - this.logger.verbose( - "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" - ); - } - } else if (accountSid && request.prompt === PromptValue.NONE) { - /* - * If account and loginHint are provided, we will check account first for sid before adding loginHint - * SessionId is only used in silent calls - */ - this.logger.verbose( - "createAuthCodeUrlQueryString: Prompt is none, adding sid from account" - ); - RequestParameterBuilder.addSid(parameters, accountSid); - this.performanceClient?.addFields( - { sidFromClaim: true }, - correlationId - ); - try { - const clientInfo = buildClientInfoFromHomeAccountId( - request.account.homeAccountId - ); - RequestParameterBuilder.addCcsOid( - parameters, - clientInfo - ); - } catch (e) { - this.logger.verbose( - "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" - ); - } - } else if (request.loginHint) { - this.logger.verbose( - "createAuthCodeUrlQueryString: Adding login_hint from request" - ); - RequestParameterBuilder.addLoginHint( - parameters, - request.loginHint - ); - RequestParameterBuilder.addCcsUpn( - parameters, - request.loginHint - ); - this.performanceClient?.addFields( - { loginHintFromRequest: true }, - correlationId - ); - } else if (request.account.username) { - // Fallback to account username if provided - this.logger.verbose( - "createAuthCodeUrlQueryString: Adding login_hint from account" - ); - RequestParameterBuilder.addLoginHint( - parameters, - request.account.username - ); - this.performanceClient?.addFields( - { loginHintFromUpn: true }, - correlationId - ); - try { - const clientInfo = buildClientInfoFromHomeAccountId( - request.account.homeAccountId - ); - RequestParameterBuilder.addCcsOid( - parameters, - clientInfo - ); - } catch (e) { - this.logger.verbose( - "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" - ); - } - } - } else if (request.loginHint) { - this.logger.verbose( - "createAuthCodeUrlQueryString: No account, adding login_hint from request" - ); - RequestParameterBuilder.addLoginHint( - parameters, - request.loginHint - ); - RequestParameterBuilder.addCcsUpn( - parameters, - request.loginHint - ); - this.performanceClient?.addFields( - { loginHintFromRequest: true }, - correlationId - ); - } - } else { - this.logger.verbose( - "createAuthCodeUrlQueryString: Prompt is select_account, ignoring account hints" - ); - } - - if (request.nonce) { - RequestParameterBuilder.addNonce(parameters, request.nonce); - } - - if (request.state) { - RequestParameterBuilder.addState(parameters, request.state); - } - - if ( - request.claims || - (this.config.authOptions.clientCapabilities && - this.config.authOptions.clientCapabilities.length > 0) - ) { - RequestParameterBuilder.addClaims( - parameters, - request.claims, - this.config.authOptions.clientCapabilities - ); - } - - if (request.embeddedClientId) { - RequestParameterBuilder.addBrokerParameters( - parameters, - this.config.authOptions.clientId, - this.config.authOptions.redirectUri - ); - } - - if (request.extraQueryParameters) { - RequestParameterBuilder.addExtraQueryParameters( - parameters, - request.extraQueryParameters - ); - } - - if (this.config.authOptions.instanceAware) { - RequestParameterBuilder.addInstanceAware(parameters); - } - - if (request.platformBroker) { - // signal ests that this is a WAM call - RequestParameterBuilder.addNativeBroker(parameters); - - // pass the req_cnf for POP - if (request.authenticationScheme === AuthenticationScheme.POP) { - const popTokenGenerator = new PopTokenGenerator( - this.cryptoUtils - ); - - // req_cnf is always sent as a string for SPAs - let reqCnfData; - if (!request.popKid) { - const generatedReqCnfData = await invokeAsync( - popTokenGenerator.generateCnf.bind(popTokenGenerator), - PerformanceEvents.PopTokenGenerateCnf, - this.logger, - this.performanceClient, - request.correlationId - )(request, this.logger); - reqCnfData = generatedReqCnfData.reqCnfString; - } else { - reqCnfData = this.cryptoUtils.encodeKid(request.popKid); - } - RequestParameterBuilder.addPopToken(parameters, reqCnfData); - } - } - - RequestParameterBuilder.instrumentBrokerParams( - parameters, - request.correlationId, - this.performanceClient - ); - return UrlUtils.mapToQueryString(parameters); - } - /** * This API validates the `EndSessionRequest` and creates a URL * @param request @@ -893,16 +509,4 @@ export class AuthorizationCodeClient extends BaseClient { return UrlUtils.mapToQueryString(parameters); } - - /** - * Helper to get sid from account. Returns null if idTokenClaims are not present or sid is not present. - * @param account - */ - private extractAccountSid(account: AccountInfo): string | null { - return account.idTokenClaims?.sid || null; - } - - private extractLoginHint(account: AccountInfo): string | null { - return account.idTokenClaims?.login_hint || null; - } } diff --git a/lib/msal-common/src/exports-common.ts b/lib/msal-common/src/exports-common.ts index 6c3b191e41..f90a0d60d4 100644 --- a/lib/msal-common/src/exports-common.ts +++ b/lib/msal-common/src/exports-common.ts @@ -105,6 +105,8 @@ export { DEFAULT_CRYPTO_IMPLEMENTATION, SignedHttpRequestParameters, } from "./crypto/ICrypto.js"; + +export * as AuthorizeProtocol from "./protocol/Authorize.js"; export { BaseAuthRequest } from "./request/BaseAuthRequest.js"; export { CommonAuthorizationUrlRequest } from "./request/CommonAuthorizationUrlRequest.js"; export { CommonAuthorizationCodeRequest } from "./request/CommonAuthorizationCodeRequest.js"; @@ -117,7 +119,7 @@ export { AzureRegion } from "./authority/AzureRegion.js"; export { AzureRegionConfiguration } from "./authority/AzureRegionConfiguration.js"; export { AuthenticationResult } from "./response/AuthenticationResult.js"; export { AuthorizationCodePayload } from "./response/AuthorizationCodePayload.js"; -export { ServerAuthorizationCodeResponse } from "./response/ServerAuthorizationCodeResponse.js"; +export { AuthorizeResponse } from "./response/AuthorizeResponse.js"; export { ServerAuthorizationTokenResponse } from "./response/ServerAuthorizationTokenResponse.js"; export { ResponseHandler, diff --git a/lib/msal-common/src/protocol/Authorize.ts b/lib/msal-common/src/protocol/Authorize.ts new file mode 100644 index 0000000000..8787a26e87 --- /dev/null +++ b/lib/msal-common/src/protocol/Authorize.ts @@ -0,0 +1,409 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CommonAuthorizationUrlRequest } from "../request/CommonAuthorizationUrlRequest.js"; +import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js"; +import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js"; +import * as AADServerParamKeys from "../constants/AADServerParamKeys.js"; +import { AuthOptions } from "../config/ClientConfiguration.js"; +import { PromptValue } from "../utils/Constants.js"; +import { AccountInfo } from "../account/AccountInfo.js"; +import { Logger } from "../logger/Logger.js"; +import { buildClientInfoFromHomeAccountId } from "../account/ClientInfo.js"; +import { Authority } from "../authority/Authority.js"; +import { mapToQueryString } from "../utils/UrlUtils.js"; +import { UrlString } from "../url/UrlString.js"; +import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js"; +import { AuthorizeResponse } from "../response/AuthorizeResponse.js"; +import { + ClientAuthErrorCodes, + createClientAuthError, +} from "../error/ClientAuthError.js"; +import { + InteractionRequiredAuthError, + isInteractionRequiredError, +} from "../error/InteractionRequiredAuthError.js"; +import { ServerError } from "../error/ServerError.js"; + +/** + * Returns map of parameters that are applicable to all calls to /authorize whether using PKCE or EAR + * @param config + * @param request + * @param logger + * @param performanceClient + * @returns + */ +export function getStandardAuthorizeRequestParameters( + authOptions: AuthOptions, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient?: IPerformanceClient +): Map { + // generate the correlationId if not set by the user and add + const correlationId = request.correlationId; + + const parameters = new Map(); + + RequestParameterBuilder.addClientId( + parameters, + request.embeddedClientId || + request.extraQueryParameters?.[AADServerParamKeys.CLIENT_ID] || + authOptions.clientId + ); + + const requestScopes = [ + ...(request.scopes || []), + ...(request.extraScopesToConsent || []), + ]; + RequestParameterBuilder.addScopes( + parameters, + requestScopes, + true, + authOptions.authority.options.OIDCOptions?.defaultScopes + ); + + RequestParameterBuilder.addRedirectUri(parameters, request.redirectUri); + + RequestParameterBuilder.addCorrelationId(parameters, correlationId); + + // add response_mode. If not passed in it defaults to query. + RequestParameterBuilder.addResponseMode(parameters, request.responseMode); + + // add client_info=1 + RequestParameterBuilder.addClientInfo(parameters); + + if (request.prompt) { + RequestParameterBuilder.addPrompt(parameters, request.prompt); + performanceClient?.addFields({ prompt: request.prompt }, correlationId); + } + + if (request.domainHint) { + RequestParameterBuilder.addDomainHint(parameters, request.domainHint); + performanceClient?.addFields( + { domainHintFromRequest: true }, + correlationId + ); + } + + // Add sid or loginHint with preference for login_hint claim (in request) -> sid -> loginHint (upn/email) -> username of AccountInfo object + if (request.prompt !== PromptValue.SELECT_ACCOUNT) { + // AAD will throw if prompt=select_account is passed with an account hint + if (request.sid && request.prompt === PromptValue.NONE) { + // SessionID is only used in silent calls + logger.verbose( + "createAuthCodeUrlQueryString: Prompt is none, adding sid from request" + ); + RequestParameterBuilder.addSid(parameters, request.sid); + performanceClient?.addFields( + { sidFromRequest: true }, + correlationId + ); + } else if (request.account) { + const accountSid = extractAccountSid(request.account); + let accountLoginHintClaim = extractLoginHint(request.account); + + if (accountLoginHintClaim && request.domainHint) { + logger.warning( + `AuthorizationCodeClient.createAuthCodeUrlQueryString: "domainHint" param is set, skipping opaque "login_hint" claim. Please consider not passing domainHint` + ); + accountLoginHintClaim = null; + } + + // If login_hint claim is present, use it over sid/username + if (accountLoginHintClaim) { + logger.verbose( + "createAuthCodeUrlQueryString: login_hint claim present on account" + ); + RequestParameterBuilder.addLoginHint( + parameters, + accountLoginHintClaim + ); + performanceClient?.addFields( + { loginHintFromClaim: true }, + correlationId + ); + try { + const clientInfo = buildClientInfoFromHomeAccountId( + request.account.homeAccountId + ); + RequestParameterBuilder.addCcsOid(parameters, clientInfo); + } catch (e) { + logger.verbose( + "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" + ); + } + } else if (accountSid && request.prompt === PromptValue.NONE) { + /* + * If account and loginHint are provided, we will check account first for sid before adding loginHint + * SessionId is only used in silent calls + */ + logger.verbose( + "createAuthCodeUrlQueryString: Prompt is none, adding sid from account" + ); + RequestParameterBuilder.addSid(parameters, accountSid); + performanceClient?.addFields( + { sidFromClaim: true }, + correlationId + ); + try { + const clientInfo = buildClientInfoFromHomeAccountId( + request.account.homeAccountId + ); + RequestParameterBuilder.addCcsOid(parameters, clientInfo); + } catch (e) { + logger.verbose( + "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" + ); + } + } else if (request.loginHint) { + logger.verbose( + "createAuthCodeUrlQueryString: Adding login_hint from request" + ); + RequestParameterBuilder.addLoginHint( + parameters, + request.loginHint + ); + RequestParameterBuilder.addCcsUpn( + parameters, + request.loginHint + ); + performanceClient?.addFields( + { loginHintFromRequest: true }, + correlationId + ); + } else if (request.account.username) { + // Fallback to account username if provided + logger.verbose( + "createAuthCodeUrlQueryString: Adding login_hint from account" + ); + RequestParameterBuilder.addLoginHint( + parameters, + request.account.username + ); + performanceClient?.addFields( + { loginHintFromUpn: true }, + correlationId + ); + try { + const clientInfo = buildClientInfoFromHomeAccountId( + request.account.homeAccountId + ); + RequestParameterBuilder.addCcsOid(parameters, clientInfo); + } catch (e) { + logger.verbose( + "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" + ); + } + } + } else if (request.loginHint) { + logger.verbose( + "createAuthCodeUrlQueryString: No account, adding login_hint from request" + ); + RequestParameterBuilder.addLoginHint(parameters, request.loginHint); + RequestParameterBuilder.addCcsUpn(parameters, request.loginHint); + performanceClient?.addFields( + { loginHintFromRequest: true }, + correlationId + ); + } + } else { + logger.verbose( + "createAuthCodeUrlQueryString: Prompt is select_account, ignoring account hints" + ); + } + + if (request.nonce) { + RequestParameterBuilder.addNonce(parameters, request.nonce); + } + + if (request.state) { + RequestParameterBuilder.addState(parameters, request.state); + } + + if ( + request.claims || + (authOptions.clientCapabilities && + authOptions.clientCapabilities.length > 0) + ) { + RequestParameterBuilder.addClaims( + parameters, + request.claims, + authOptions.clientCapabilities + ); + } + + if (request.embeddedClientId) { + RequestParameterBuilder.addBrokerParameters( + parameters, + authOptions.clientId, + authOptions.redirectUri + ); + } + + if (request.extraQueryParameters) { + RequestParameterBuilder.addExtraQueryParameters( + parameters, + request.extraQueryParameters + ); + } + + if (authOptions.instanceAware) { + RequestParameterBuilder.addInstanceAware(parameters); + } + + return parameters; +} + +/** + * Returns authorize endpoint with given request parameters in the query string + * @param authority + * @param requestParameters + * @returns + */ +export function getAuthorizeUrl( + authority: Authority, + requestParameters: Map +): string { + const queryString = mapToQueryString(requestParameters); + + return UrlString.appendQueryString( + authority.authorizationEndpoint, + queryString + ); +} + +/** + * Handles the hash fragment response from public client code request. Returns a code response used by + * the client to exchange for a token in acquireToken. + * @param serverParams + * @param cachedState + */ +export function getAuthorizationCodePayload( + serverParams: AuthorizeResponse, + cachedState: string +): AuthorizationCodePayload { + // Get code response + validateAuthorizationResponse(serverParams, cachedState); + + // throw when there is no auth code in the response + if (!serverParams.code) { + throw createClientAuthError( + ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse + ); + } + + return serverParams as AuthorizationCodePayload; +} + +/** + * Function which validates server authorization code response. + * @param serverResponseHash + * @param requestState + */ +export function validateAuthorizationResponse( + serverResponse: AuthorizeResponse, + requestState: string +): void { + if (!serverResponse.state || !requestState) { + throw serverResponse.state + ? createClientAuthError( + ClientAuthErrorCodes.stateNotFound, + "Cached State" + ) + : createClientAuthError( + ClientAuthErrorCodes.stateNotFound, + "Server State" + ); + } + + let decodedServerResponseState: string; + let decodedRequestState: string; + + try { + decodedServerResponseState = decodeURIComponent(serverResponse.state); + } catch (e) { + throw createClientAuthError( + ClientAuthErrorCodes.invalidState, + serverResponse.state + ); + } + + try { + decodedRequestState = decodeURIComponent(requestState); + } catch (e) { + throw createClientAuthError( + ClientAuthErrorCodes.invalidState, + serverResponse.state + ); + } + + if (decodedServerResponseState !== decodedRequestState) { + throw createClientAuthError(ClientAuthErrorCodes.stateMismatch); + } + + // Check for error + if ( + serverResponse.error || + serverResponse.error_description || + serverResponse.suberror + ) { + const serverErrorNo = parseServerErrorNo(serverResponse); + if ( + isInteractionRequiredError( + serverResponse.error, + serverResponse.error_description, + serverResponse.suberror + ) + ) { + throw new InteractionRequiredAuthError( + serverResponse.error || "", + serverResponse.error_description, + serverResponse.suberror, + serverResponse.timestamp || "", + serverResponse.trace_id || "", + serverResponse.correlation_id || "", + serverResponse.claims || "", + serverErrorNo + ); + } + + throw new ServerError( + serverResponse.error || "", + serverResponse.error_description, + serverResponse.suberror, + serverErrorNo + ); + } +} + +/** + * Get server error No from the error_uri + * @param serverResponse + * @returns + */ +function parseServerErrorNo( + serverResponse: AuthorizeResponse +): string | undefined { + const errorCodePrefix = "code="; + const errorCodePrefixIndex = + serverResponse.error_uri?.lastIndexOf(errorCodePrefix); + return errorCodePrefixIndex && errorCodePrefixIndex >= 0 + ? serverResponse.error_uri?.substring( + errorCodePrefixIndex + errorCodePrefix.length + ) + : undefined; +} + +/** + * Helper to get sid from account. Returns null if idTokenClaims are not present or sid is not present. + * @param account + */ +function extractAccountSid(account: AccountInfo): string | null { + return account.idTokenClaims?.sid || null; +} + +function extractLoginHint(account: AccountInfo): string | null { + return account.idTokenClaims?.login_hint || null; +} diff --git a/lib/msal-common/src/request/RequestParameterBuilder.ts b/lib/msal-common/src/request/RequestParameterBuilder.ts index ee91695e67..10512b5f48 100644 --- a/lib/msal-common/src/request/RequestParameterBuilder.ts +++ b/lib/msal-common/src/request/RequestParameterBuilder.ts @@ -58,7 +58,7 @@ export function instrumentBrokerParams( export function addResponseTypeCode(parameters: Map): void { parameters.set( AADServerParamKeys.RESPONSE_TYPE, - encodeURIComponent(Constants.CODE_RESPONSE_TYPE) + Constants.CODE_RESPONSE_TYPE ); } @@ -70,9 +70,7 @@ export function addResponseTypeForTokenAndIdToken( ): void { parameters.set( AADServerParamKeys.RESPONSE_TYPE, - encodeURIComponent( - `${Constants.TOKEN_RESPONSE_TYPE} ${Constants.ID_TOKEN_RESPONSE_TYPE}` - ) + `${Constants.TOKEN_RESPONSE_TYPE} ${Constants.ID_TOKEN_RESPONSE_TYPE}` ); } @@ -86,7 +84,7 @@ export function addResponseMode( ): void { parameters.set( AADServerParamKeys.RESPONSE_MODE, - encodeURIComponent(responseMode ? responseMode : ResponseMode.QUERY) + responseMode ? responseMode : ResponseMode.QUERY ); } @@ -94,7 +92,7 @@ export function addResponseMode( * Add flag to indicate STS should attempt to use WAM if available */ export function addNativeBroker(parameters: Map): void { - parameters.set(AADServerParamKeys.NATIVE_BROKER, encodeURIComponent("1")); + parameters.set(AADServerParamKeys.NATIVE_BROKER, "1"); } /** @@ -120,10 +118,7 @@ export function addScopes( ? [...(scopes || []), ...defaultScopes] : scopes || []; const scopeSet = new ScopeSet(requestScopes); - parameters.set( - AADServerParamKeys.SCOPE, - encodeURIComponent(scopeSet.printScopes()) - ); + parameters.set(AADServerParamKeys.SCOPE, scopeSet.printScopes()); } /** @@ -134,7 +129,7 @@ export function addClientId( parameters: Map, clientId: string ): void { - parameters.set(AADServerParamKeys.CLIENT_ID, encodeURIComponent(clientId)); + parameters.set(AADServerParamKeys.CLIENT_ID, clientId); } /** @@ -145,10 +140,7 @@ export function addRedirectUri( parameters: Map, redirectUri: string ): void { - parameters.set( - AADServerParamKeys.REDIRECT_URI, - encodeURIComponent(redirectUri) - ); + parameters.set(AADServerParamKeys.REDIRECT_URI, redirectUri); } /** @@ -159,10 +151,7 @@ export function addPostLogoutRedirectUri( parameters: Map, redirectUri: string ): void { - parameters.set( - AADServerParamKeys.POST_LOGOUT_URI, - encodeURIComponent(redirectUri) - ); + parameters.set(AADServerParamKeys.POST_LOGOUT_URI, redirectUri); } /** @@ -173,10 +162,7 @@ export function addIdTokenHint( parameters: Map, idTokenHint: string ): void { - parameters.set( - AADServerParamKeys.ID_TOKEN_HINT, - encodeURIComponent(idTokenHint) - ); + parameters.set(AADServerParamKeys.ID_TOKEN_HINT, idTokenHint); } /** @@ -187,10 +173,7 @@ export function addDomainHint( parameters: Map, domainHint: string ): void { - parameters.set( - AADServerParamKeys.DOMAIN_HINT, - encodeURIComponent(domainHint) - ); + parameters.set(AADServerParamKeys.DOMAIN_HINT, domainHint); } /** @@ -201,10 +184,7 @@ export function addLoginHint( parameters: Map, loginHint: string ): void { - parameters.set( - AADServerParamKeys.LOGIN_HINT, - encodeURIComponent(loginHint) - ); + parameters.set(AADServerParamKeys.LOGIN_HINT, loginHint); } /** @@ -215,10 +195,7 @@ export function addCcsUpn( parameters: Map, loginHint: string ): void { - parameters.set( - HeaderNames.CCS_HEADER, - encodeURIComponent(`UPN:${loginHint}`) - ); + parameters.set(HeaderNames.CCS_HEADER, `UPN:${loginHint}`); } /** @@ -231,7 +208,7 @@ export function addCcsOid( ): void { parameters.set( HeaderNames.CCS_HEADER, - encodeURIComponent(`Oid:${clientInfo.uid}@${clientInfo.utid}`) + `Oid:${clientInfo.uid}@${clientInfo.utid}` ); } @@ -240,7 +217,7 @@ export function addCcsOid( * @param sid */ export function addSid(parameters: Map, sid: string): void { - parameters.set(AADServerParamKeys.SID, encodeURIComponent(sid)); + parameters.set(AADServerParamKeys.SID, sid); } /** @@ -263,7 +240,7 @@ export function addClaims( ClientConfigurationErrorCodes.invalidClaims ); } - parameters.set(AADServerParamKeys.CLAIMS, encodeURIComponent(mergedClaims)); + parameters.set(AADServerParamKeys.CLAIMS, mergedClaims); } /** @@ -274,10 +251,7 @@ export function addCorrelationId( parameters: Map, correlationId: string ): void { - parameters.set( - AADServerParamKeys.CLIENT_REQUEST_ID, - encodeURIComponent(correlationId) - ); + parameters.set(AADServerParamKeys.CLIENT_REQUEST_ID, correlationId); } /** @@ -324,7 +298,7 @@ export function addPrompt( parameters: Map, prompt: string ): void { - parameters.set(`${AADServerParamKeys.PROMPT}`, encodeURIComponent(prompt)); + parameters.set(AADServerParamKeys.PROMPT, prompt); } /** @@ -333,7 +307,7 @@ export function addPrompt( */ export function addState(parameters: Map, state: string): void { if (state) { - parameters.set(AADServerParamKeys.STATE, encodeURIComponent(state)); + parameters.set(AADServerParamKeys.STATE, state); } } @@ -342,7 +316,7 @@ export function addState(parameters: Map, state: string): void { * @param nonce */ export function addNonce(parameters: Map, nonce: string): void { - parameters.set(AADServerParamKeys.NONCE, encodeURIComponent(nonce)); + parameters.set(AADServerParamKeys.NONCE, nonce); } /** @@ -353,17 +327,14 @@ export function addNonce(parameters: Map, nonce: string): void { */ export function addCodeChallengeParams( parameters: Map, - codeChallenge: string, - codeChallengeMethod: string + codeChallenge?: string, + codeChallengeMethod?: string ): void { if (codeChallenge && codeChallengeMethod) { - parameters.set( - AADServerParamKeys.CODE_CHALLENGE, - encodeURIComponent(codeChallenge) - ); + parameters.set(AADServerParamKeys.CODE_CHALLENGE, codeChallenge); parameters.set( AADServerParamKeys.CODE_CHALLENGE_METHOD, - encodeURIComponent(codeChallengeMethod) + codeChallengeMethod ); } else { throw createClientConfigurationError( @@ -380,7 +351,7 @@ export function addAuthorizationCode( parameters: Map, code: string ): void { - parameters.set(AADServerParamKeys.CODE, encodeURIComponent(code)); + parameters.set(AADServerParamKeys.CODE, code); } /** @@ -391,7 +362,7 @@ export function addDeviceCode( parameters: Map, code: string ): void { - parameters.set(AADServerParamKeys.DEVICE_CODE, encodeURIComponent(code)); + parameters.set(AADServerParamKeys.DEVICE_CODE, code); } /** @@ -402,10 +373,7 @@ export function addRefreshToken( parameters: Map, refreshToken: string ): void { - parameters.set( - AADServerParamKeys.REFRESH_TOKEN, - encodeURIComponent(refreshToken) - ); + parameters.set(AADServerParamKeys.REFRESH_TOKEN, refreshToken); } /** @@ -416,10 +384,7 @@ export function addCodeVerifier( parameters: Map, codeVerifier: string ): void { - parameters.set( - AADServerParamKeys.CODE_VERIFIER, - encodeURIComponent(codeVerifier) - ); + parameters.set(AADServerParamKeys.CODE_VERIFIER, codeVerifier); } /** @@ -430,10 +395,7 @@ export function addClientSecret( parameters: Map, clientSecret: string ): void { - parameters.set( - AADServerParamKeys.CLIENT_SECRET, - encodeURIComponent(clientSecret) - ); + parameters.set(AADServerParamKeys.CLIENT_SECRET, clientSecret); } /** @@ -445,10 +407,7 @@ export function addClientAssertion( clientAssertion: string ): void { if (clientAssertion) { - parameters.set( - AADServerParamKeys.CLIENT_ASSERTION, - encodeURIComponent(clientAssertion) - ); + parameters.set(AADServerParamKeys.CLIENT_ASSERTION, clientAssertion); } } @@ -463,7 +422,7 @@ export function addClientAssertionType( if (clientAssertionType) { parameters.set( AADServerParamKeys.CLIENT_ASSERTION_TYPE, - encodeURIComponent(clientAssertionType) + clientAssertionType ); } } @@ -476,10 +435,7 @@ export function addOboAssertion( parameters: Map, oboAssertion: string ): void { - parameters.set( - AADServerParamKeys.OBO_ASSERTION, - encodeURIComponent(oboAssertion) - ); + parameters.set(AADServerParamKeys.OBO_ASSERTION, oboAssertion); } /** @@ -490,10 +446,7 @@ export function addRequestTokenUse( parameters: Map, tokenUse: string ): void { - parameters.set( - AADServerParamKeys.REQUESTED_TOKEN_USE, - encodeURIComponent(tokenUse) - ); + parameters.set(AADServerParamKeys.REQUESTED_TOKEN_USE, tokenUse); } /** @@ -504,10 +457,7 @@ export function addGrantType( parameters: Map, grantType: string ): void { - parameters.set( - AADServerParamKeys.GRANT_TYPE, - encodeURIComponent(grantType) - ); + parameters.set(AADServerParamKeys.GRANT_TYPE, grantType); } /** @@ -582,10 +532,7 @@ export function addUsername( parameters: Map, username: string ): void { - parameters.set( - PasswordGrantConstants.username, - encodeURIComponent(username) - ); + parameters.set(PasswordGrantConstants.username, username); } /** @@ -596,10 +543,7 @@ export function addPassword( parameters: Map, password: string ): void { - parameters.set( - PasswordGrantConstants.password, - encodeURIComponent(password) - ); + parameters.set(PasswordGrantConstants.password, password); } /** @@ -612,10 +556,7 @@ export function addPopToken( ): void { if (cnfString) { parameters.set(AADServerParamKeys.TOKEN_TYPE, AuthenticationScheme.POP); - parameters.set( - AADServerParamKeys.REQ_CNF, - encodeURIComponent(cnfString) - ); + parameters.set(AADServerParamKeys.REQ_CNF, cnfString); } } @@ -628,10 +569,7 @@ export function addSshJwk( ): void { if (sshJwkString) { parameters.set(AADServerParamKeys.TOKEN_TYPE, AuthenticationScheme.SSH); - parameters.set( - AADServerParamKeys.REQ_CNF, - encodeURIComponent(sshJwkString) - ); + parameters.set(AADServerParamKeys.REQ_CNF, sshJwkString); } } @@ -670,10 +608,7 @@ export function addLogoutHint( parameters: Map, logoutHint: string ): void { - parameters.set( - AADServerParamKeys.LOGOUT_HINT, - encodeURIComponent(logoutHint) - ); + parameters.set(AADServerParamKeys.LOGOUT_HINT, logoutHint); } export function addBrokerParameters( @@ -682,15 +617,12 @@ export function addBrokerParameters( brokerRedirectUri: string ): void { if (!parameters.has(AADServerParamKeys.BROKER_CLIENT_ID)) { - parameters.set( - AADServerParamKeys.BROKER_CLIENT_ID, - encodeURIComponent(brokerClientId) - ); + parameters.set(AADServerParamKeys.BROKER_CLIENT_ID, brokerClientId); } if (!parameters.has(AADServerParamKeys.BROKER_REDIRECT_URI)) { parameters.set( AADServerParamKeys.BROKER_REDIRECT_URI, - encodeURIComponent(brokerRedirectUri) + brokerRedirectUri ); } } diff --git a/lib/msal-common/src/response/AuthorizeResponse.ts b/lib/msal-common/src/response/AuthorizeResponse.ts new file mode 100644 index 0000000000..f6e9d1e702 --- /dev/null +++ b/lib/msal-common/src/response/AuthorizeResponse.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Response properties that may be returned by the /authorize endpoint + */ +export type AuthorizeResponse = { + /** + * Authorization Code to be exchanged for tokens + */ + code?: string; + /** + * Client info object containing UserId and TenantId + */ + client_info?: string; + /** + * State string, should match what was sent on request + */ + state?: string; + /** + * Cloud instance returned when application is instance aware + */ + cloud_instance_name?: string; + /** + * Cloud instance hostname returned when application is instance aware + */ + cloud_instance_host_name?: string; + /** + * AAD Graph hostname returned when application is instance aware + * https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api + */ + cloud_graph_host_name?: string; + /** + * Microsoft Graph hostname returned when application is instance aware + * https://docs.microsoft.com/en-us/graph/overview + */ + msgraph_host?: string; + /** + * Server error code + */ + error?: string; + /** + * Server error URI + */ + error_uri?: string; + /** + * Server error description + */ + error_description?: string; + /** + * Server Sub-Error + */ + suberror?: string; + /** + * Timestamp of request + */ + timestamp?: string; + /** + * Trace Id used to look up request in logs + */ + trace_id?: string; + /** + * Correlation ID use to look up request in logs + */ + correlation_id?: string; + /** + * Claims + */ + claims?: string; + /** + * AccountId for the user, returned when platform broker is available to use + */ + accountId?: string; +}; diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index ff4b1a18ae..1a90b755f0 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -9,7 +9,6 @@ import { ClientAuthErrorCodes, createClientAuthError, } from "../error/ClientAuthError.js"; -import { ServerAuthorizationCodeResponse } from "./ServerAuthorizationCodeResponse.js"; import { Logger } from "../logger/Logger.js"; import { ServerError } from "../error/ServerError.js"; import { ScopeSet } from "../request/ScopeSet.js"; @@ -54,19 +53,6 @@ import { import * as CacheHelpers from "../cache/utils/CacheHelpers.js"; import * as TimeUtils from "../utils/TimeUtils.js"; -function parseServerErrorNo( - serverResponse: ServerAuthorizationCodeResponse -): string | undefined { - const errorCodePrefix = "code="; - const errorCodePrefixIndex = - serverResponse.error_uri?.lastIndexOf(errorCodePrefix); - return errorCodePrefixIndex && errorCodePrefixIndex >= 0 - ? serverResponse.error_uri?.substring( - errorCodePrefixIndex + errorCodePrefix.length - ) - : undefined; -} - /** * Class that handles response parsing. * @internal @@ -99,90 +85,6 @@ export class ResponseHandler { this.performanceClient = performanceClient; } - /** - * Function which validates server authorization code response. - * @param serverResponseHash - * @param requestState - * @param cryptoObj - */ - validateServerAuthorizationCodeResponse( - serverResponse: ServerAuthorizationCodeResponse, - requestState: string - ): void { - if (!serverResponse.state || !requestState) { - throw serverResponse.state - ? createClientAuthError( - ClientAuthErrorCodes.stateNotFound, - "Cached State" - ) - : createClientAuthError( - ClientAuthErrorCodes.stateNotFound, - "Server State" - ); - } - - let decodedServerResponseState: string; - let decodedRequestState: string; - - try { - decodedServerResponseState = decodeURIComponent( - serverResponse.state - ); - } catch (e) { - throw createClientAuthError( - ClientAuthErrorCodes.invalidState, - serverResponse.state - ); - } - - try { - decodedRequestState = decodeURIComponent(requestState); - } catch (e) { - throw createClientAuthError( - ClientAuthErrorCodes.invalidState, - serverResponse.state - ); - } - - if (decodedServerResponseState !== decodedRequestState) { - throw createClientAuthError(ClientAuthErrorCodes.stateMismatch); - } - - // Check for error - if ( - serverResponse.error || - serverResponse.error_description || - serverResponse.suberror - ) { - const serverErrorNo = parseServerErrorNo(serverResponse); - if ( - isInteractionRequiredError( - serverResponse.error, - serverResponse.error_description, - serverResponse.suberror - ) - ) { - throw new InteractionRequiredAuthError( - serverResponse.error || "", - serverResponse.error_description, - serverResponse.suberror, - serverResponse.timestamp || "", - serverResponse.trace_id || "", - serverResponse.correlation_id || "", - serverResponse.claims || "", - serverErrorNo - ); - } - - throw new ServerError( - serverResponse.error || "", - serverResponse.error_description, - serverResponse.suberror, - serverErrorNo - ); - } - } - /** * Function which validates server authorization token response. * @param serverResponse diff --git a/lib/msal-common/src/response/ServerAuthorizationCodeResponse.ts b/lib/msal-common/src/response/ServerAuthorizationCodeResponse.ts deleted file mode 100644 index 26468c8fc8..0000000000 --- a/lib/msal-common/src/response/ServerAuthorizationCodeResponse.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * Deserialized response object from server authorization code request. - * - code: authorization code from server - * - client_info: client info object - * - state: OAuth2 request state - * - error: error sent back in hash - * - error: description - */ -export type ServerAuthorizationCodeResponse = { - // Success case - code?: string; - client_info?: string; - state?: string; - cloud_instance_name?: string; - cloud_instance_host_name?: string; - cloud_graph_host_name?: string; - msgraph_host?: string; - // Error case - error?: string; - error_uri?: string; - error_description?: string; - suberror?: string; - timestamp?: string; - trace_id?: string; - correlation_id?: string; - claims?: string; - // Native Account ID - accountId?: string; -}; diff --git a/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts b/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts index a8171a02c4..f4700c65a4 100644 --- a/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts +++ b/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts @@ -214,13 +214,12 @@ export const PerformanceEvents = { "standardInteractionClientGetClientConfiguration", StandardInteractionClientInitializeAuthorizationRequest: "standardInteractionClientInitializeAuthorizationRequest", - StandardInteractionClientInitializeAuthorizationCodeRequest: - "standardInteractionClientInitializeAuthorizationCodeRequest", /** * getAuthCodeUrl API (msal-browser and msal-node). */ GetAuthCodeUrl: "getAuthCodeUrl", + GetStandardParams: "getStandardParams", /** * Functions from InteractionHandler (msal-browser) @@ -235,7 +234,6 @@ export const PerformanceEvents = { AuthClientAcquireToken: "authClientAcquireToken", AuthClientExecuteTokenRequest: "authClientExecuteTokenRequest", AuthClientCreateTokenRequestBody: "authClientCreateTokenRequestBody", - AuthClientCreateQueryString: "authClientCreateQueryString", /** * Generate functions in PopTokenGenerator (msal-common) @@ -433,10 +431,6 @@ export const PerformanceEventAbbreviations: ReadonlyMap = PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest, "StdIntClientInitAuthReq", ], - [ - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - "StdIntClientInitAuthCodeReq", - ], [PerformanceEvents.GetAuthCodeUrl, "GetAuthCodeUrl"], @@ -453,10 +447,6 @@ export const PerformanceEventAbbreviations: ReadonlyMap = PerformanceEvents.AuthClientCreateTokenRequestBody, "AuthClientCreateTReqBody", ], - [ - PerformanceEvents.AuthClientCreateQueryString, - "AuthClientCreateQueryStr", - ], [PerformanceEvents.PopTokenGenerateCnf, "PopTGenCnf"], [PerformanceEvents.PopTokenGenerateKid, "PopTGenKid"], [PerformanceEvents.HandleServerTokenResponse, "HandleServerTRes"], diff --git a/lib/msal-common/src/utils/UrlUtils.ts b/lib/msal-common/src/utils/UrlUtils.ts index d3e219f1fb..77031434bc 100644 --- a/lib/msal-common/src/utils/UrlUtils.ts +++ b/lib/msal-common/src/utils/UrlUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { ServerAuthorizationCodeResponse } from "../response/ServerAuthorizationCodeResponse.js"; +import { AuthorizeResponse } from "../response/AuthorizeResponse.js"; import { ClientAuthErrorCodes, createClientAuthError, @@ -31,7 +31,7 @@ export function stripLeadingHashOrQuery(responseString: string): string { */ export function getDeserializedResponse( responseString: string -): ServerAuthorizationCodeResponse | null { +): AuthorizeResponse | null { // Check if given hash is empty if (!responseString || responseString.indexOf("=") < 0) { return null; @@ -40,8 +40,9 @@ export function getDeserializedResponse( // Strip the # or ? symbol if present const normalizedResponse = stripLeadingHashOrQuery(responseString); // If # symbol was not present, above will return empty string, so give original hash value - const deserializedHash: ServerAuthorizationCodeResponse = - Object.fromEntries(new URLSearchParams(normalizedResponse)); + const deserializedHash: AuthorizeResponse = Object.fromEntries( + new URLSearchParams(normalizedResponse) + ); // Check for known response properties if ( @@ -66,7 +67,7 @@ export function mapToQueryString(parameters: Map): string { const queryParameterArray: Array = new Array(); parameters.forEach((value, key) => { - queryParameterArray.push(`${key}=${value}`); + queryParameterArray.push(`${key}=${encodeURIComponent(value)}`); }); return queryParameterArray.join("&"); diff --git a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts index 3b2856cb1a..154db781c1 100644 --- a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts +++ b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts @@ -1,5 +1,4 @@ import { - ALTERNATE_OPENID_CONFIG_RESPONSE, AUTHENTICATION_RESULT, DEFAULT_OPENID_CONFIG_RESPONSE, TEST_CONFIG, @@ -7,10 +6,8 @@ import { TEST_URIS, TEST_DATA_CLIENT_INFO, RANDOM_TEST_GUID, - TEST_STATE_VALUES, TEST_POP_VALUES, POP_AUTHENTICATION_RESULT, - TEST_ACCOUNT_INFO, CORS_SIMPLE_REQUEST_HEADERS, TEST_SSH_VALUES, AUTHENTICATION_RESULT_WITH_HEADERS, @@ -19,8 +16,6 @@ import { import { ClientConfiguration } from "../../src/config/ClientConfiguration.js"; import { BaseClient } from "../../src/client/BaseClient.js"; import { - PromptValue, - ResponseMode, AuthenticationScheme, ThrottlingConstants, Constants, @@ -28,13 +23,11 @@ import { ONE_DAY_IN_MS, } from "../../src/utils/Constants.js"; import * as AADServerParamKeys from "../../src/constants/AADServerParamKeys.js"; -import { ClientTestUtils, MockStorageClass } from "./ClientTestUtils.js"; +import { ClientTestUtils } from "./ClientTestUtils.js"; import { TestError } from "../test_kit/TestErrors.js"; import { Authority } from "../../src/authority/Authority.js"; import { AuthorizationCodeClient } from "../../src/client/AuthorizationCodeClient.js"; -import { CommonAuthorizationUrlRequest } from "../../src/request/CommonAuthorizationUrlRequest.js"; import { TokenClaims } from "../../src/account/TokenClaims.js"; -import { ServerError } from "../../src/error/ServerError.js"; import { CommonAuthorizationCodeRequest } from "../../src/request/CommonAuthorizationCodeRequest.js"; import * as AuthToken from "../../src/account/AuthToken.js"; import { @@ -42,13 +35,11 @@ import { createClientAuthError, } from "../../src/error/ClientAuthError.js"; import { - AuthError, CcsCredentialType, ClientConfigurationErrorCodes, createClientConfigurationError, } from "../../src/index.js"; import { ProtocolMode } from "../../src/authority/ProtocolMode.js"; -import { MockPerformanceClient } from "../telemetry/PerformanceClient.spec.js"; describe("AuthorizationCodeClient unit tests", () => { afterEach(() => { @@ -70,1388 +61,6 @@ describe("AuthorizationCodeClient unit tests", () => { }); }); - describe("Authorization url creation", () => { - it("Creates an authorization url with default parameters", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.QUERY, - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: TEST_CONFIG.DEFAULT_SCOPES, - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(Constants.DEFAULT_AUTHORITY)).toBe(true); - expect( - loginUrl.includes( - DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( - "{tenant}", - "common" - ) - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.SCOPE}=${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.CODE_RESPONSE_TYPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( - TEST_URIS.TEST_REDIRECT_URI_LOCALHOST - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( - ResponseMode.QUERY - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CODE_CHALLENGE}=${encodeURIComponent( - TEST_CONFIG.TEST_CHALLENGE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${ - AADServerParamKeys.CODE_CHALLENGE_METHOD - }=${encodeURIComponent( - Constants.S256_CODE_CHALLENGE_METHOD - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.X_APP_NAME}=${TEST_CONFIG.applicationName}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.X_APP_VER}=${TEST_CONFIG.applicationVersion}` - ) - ).toBe(true); - }); - - it("Creates an authorization url passing in optional parameters", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FORM_POST, - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: TEST_CONFIG.CODE_CHALLENGE_METHOD, - state: TEST_CONFIG.STATE, - prompt: PromptValue.LOGIN, - loginHint: TEST_CONFIG.LOGIN_HINT, - domainHint: TEST_CONFIG.DOMAIN_HINT, - claims: TEST_CONFIG.CLAIMS, - nonce: TEST_CONFIG.NONCE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(TEST_CONFIG.validAuthority)).toBe(true); - expect( - loginUrl.includes( - DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( - "{tenant}", - "common" - ) - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.SCOPE}=${TEST_CONFIG.DEFAULT_GRAPH_SCOPE}%20${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.CODE_RESPONSE_TYPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( - TEST_URIS.TEST_REDIRECT_URI_LOCALHOST - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( - ResponseMode.FORM_POST - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.STATE}=${encodeURIComponent( - TEST_CONFIG.STATE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.PROMPT}=${PromptValue.LOGIN}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.NONCE}=${encodeURIComponent( - TEST_CONFIG.NONCE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CODE_CHALLENGE}=${encodeURIComponent( - TEST_CONFIG.TEST_CHALLENGE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${ - AADServerParamKeys.CODE_CHALLENGE_METHOD - }=${encodeURIComponent(TEST_CONFIG.CODE_CHALLENGE_METHOD)}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( - TEST_CONFIG.DOMAIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CLAIMS}=${encodeURIComponent( - TEST_CONFIG.CLAIMS - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.X_APP_NAME}=${TEST_CONFIG.applicationName}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.X_APP_VER}=${TEST_CONFIG.applicationVersion}` - ) - ).toBe(true); - }); - - it("Adds CCS entry if loginHint is provided", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const mockPerfClient = new MockPerformanceClient(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.LOGIN, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${HeaderNames.CCS_HEADER}=${encodeURIComponent( - `UPN:${TEST_CONFIG.LOGIN_HINT}` - )}` - ) - ).toBe(true); - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromRequest).toBeTruthy(); - expect(event.loginHintFromUpn).toBeFalsy(); - expect(event.loginHintFromClaim).toBeFalsy(); - }); - - it("Adds CCS entry if account is provided", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - | "login_hint" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - prompt: PromptValue.NONE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${HeaderNames.CCS_HEADER}=${encodeURIComponent( - `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` - )}` - ) - ).toBe(true); - }); - - it("prefers login_hint claim over sid/upn if both provided", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const mockPerfClient = new MockPerformanceClient(); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - login_hint: "opaque-login-hint-claim", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - prompt: PromptValue.NONE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(false); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - testTokenClaims.login_hint - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${HeaderNames.CCS_HEADER}=${encodeURIComponent( - `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` - )}` - ) - ).toBe(true); - - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromUpn).toBeFalsy(); - expect(event.loginHintFromClaim).toBeTruthy(); - expect(event.loginHintFromRequest).toBeFalsy(); - expect(event.domainHintFromRequest).toBeFalsy(); - expect(event.sidFromClaim).toBeFalsy(); - expect(event.sidFromRequest).toBeFalsy(); - }); - - it("skips login_hint claim if domainHint param is set", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const mockPerfClient = new MockPerformanceClient(); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - login_hint: "opaque-login-hint-claim", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - domainHint: TEST_CONFIG.DOMAIN_HINT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(false); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - testAccount.username - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( - TEST_CONFIG.DOMAIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${HeaderNames.CCS_HEADER}=${encodeURIComponent( - `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` - )}` - ) - ).toBe(true); - - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromUpn).toBeTruthy(); - expect(event.loginHintFromClaim).toBeFalsy(); - expect(event.loginHintFromRequest).toBeFalsy(); - expect(event.domainHintFromRequest).toBeTruthy(); - expect(event.sidFromClaim).toBeFalsy(); - expect(event.sidFromRequest).toBeFalsy(); - }); - - it("picks up both loginHint and domainHint params", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - login_hint: "opaque-login-hint-claim", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - domainHint: TEST_CONFIG.DOMAIN_HINT, - loginHint: TEST_CONFIG.LOGIN_HINT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(false); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( - TEST_CONFIG.DOMAIN_HINT - )}` - ) - ).toBe(true); - }); - - it("Prefers sid over loginHint if both provided and prompt=None", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const mockPerfClient = new MockPerformanceClient(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.NONE, - sid: TEST_CONFIG.SID, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl).toEqual( - expect.not.arrayContaining([ - `${AADServerParamKeys.LOGIN_HINT}=`, - ]) - ); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - TEST_CONFIG.SID - )}` - ) - ).toBe(true); - - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromRequest).toBeFalsy(); - expect(event.loginHintFromClaim).toBeFalsy(); - expect(event.loginHintFromUpn).toBeFalsy(); - expect(event.domainHintFromRequest).toBeFalsy(); - expect(event.sidFromRequest).toBeTruthy(); - expect(event.sidFromRequest).toBeTruthy(); - expect(event.prompt).toEqual(PromptValue.NONE); - }); - - it("Prefers loginHint over sid if both provided and prompt!=None", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.LOGIN, - sid: TEST_CONFIG.SID, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Ignores sid if prompt!=None", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const mockPerfClient = new MockPerformanceClient(); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - prompt: PromptValue.LOGIN, - sid: TEST_CONFIG.SID, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromUpn).toBeFalsy(); - expect(event.loginHintFromClaim).toBeFalsy(); - expect(event.loginHintFromRequest).toBeFalsy(); - expect(event.domainHintFromRequest).toBeFalsy(); - expect(event.sidFromClaim).toBeFalsy(); - expect(event.sidFromRequest).toBeFalsy(); - expect(event.prompt).toEqual(PromptValue.LOGIN); - }); - - it("Prefers loginHint over Account if both provided and account does not have token claims", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: TEST_CONFIG.LOGIN_HINT, - account: TEST_ACCOUNT_INFO, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_ACCOUNT_INFO.username - )}` - ) - ).toBe(false); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Uses sid from account if not provided in request and prompt=None, overrides login_hint", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.NONE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(true); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - }); - - it("Uses loginHint instead of sid from account prompt!=None", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - | "iat" - | "x5c_ca" - | "ts" - | "at" - | "u" - | "p" - | "m" - | "login_hint" - | "aud" - | "nbf" - | "roles" - | "amr" - | "idp" - | "auth_time" - | "tfp" - | "acr" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - tenant_region_scope: "test_tenant_region_scope", - tenant_region_sub_scope: "test_tenant_region_sub_scope", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.LOGIN, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - }); - - it("Uses login_hint instead of username if sid is not present in token claims for account or request", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - | "sid" - | "iat" - | "x5c_ca" - | "ts" - | "at" - | "u" - | "p" - | "m" - | "login_hint" - | "aud" - | "nbf" - | "roles" - | "amr" - | "idp" - | "auth_time" - | "tfp" - | "acr" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - tenant_region_scope: "test_tenant_region_scope", - tenant_region_sub_scope: "test_tenant_region_sub_scope", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - loginHint: TEST_CONFIG.LOGIN_HINT, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Sets login_hint to Account.username if login_hint and sid are not provided", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: TEST_ACCOUNT_INFO, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_ACCOUNT_INFO.username - )}` - ) - ).toBe(true); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Ignores Account if prompt is select_account", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: TEST_ACCOUNT_INFO, - prompt: "select_account", - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Ignores loginHint if prompt is select_account", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: "testaccount@microsoft.com", - prompt: "select_account", - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Ignores sid if prompt is select_account", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - sid: "testsid", - prompt: "select_account", - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Creates a login URL with scopes from given token request", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const testScope1 = "testscope1"; - const testScope2 = "testscope2"; - const loginRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [testScope1, testScope2], - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - - const loginUrl = await client.getAuthCodeUrl(loginRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SCOPE}=${encodeURIComponent( - `${testScope1} ${testScope2}` - )}` - ) - ).toBe(true); - }); - - it("Does not append an extra '?' when the authorization endpoint already contains a query string", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromHardcodedValues" - ).mockReturnValue({ - token_endpoint: - "https://login.windows.net/common/oauth2/v2.0/token?param1=value1", - issuer: "https://login.windows.net/{tenantid}/v2.0", - userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo", - authorization_endpoint: - "https://login.windows.net/common/oauth2/v2.0/authorize?param1=value1", - end_session_endpoint: - "https://login.windows.net/common/oauth2/v2.0/logout?param1=value1", - }); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.QUERY, - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: TEST_CONFIG.DEFAULT_SCOPES, - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.split("?").length).toEqual(2); - expect(loginUrl.includes(`param1=value1`)).toBe(true); - expect( - loginUrl.includes( - ALTERNATE_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( - "{tenant}", - "common" - ) - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.SCOPE}=${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.CODE_RESPONSE_TYPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( - TEST_URIS.TEST_REDIRECT_URI_LOCALHOST - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( - ResponseMode.QUERY - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CODE_CHALLENGE}=${encodeURIComponent( - TEST_CONFIG.TEST_CHALLENGE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${ - AADServerParamKeys.CODE_CHALLENGE_METHOD - }=${encodeURIComponent( - Constants.S256_CODE_CHALLENGE_METHOD - )}` - ) - ).toBe(true); - }); - }); - - it("Adds req-cnf as needed", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - if (!config.cryptoInterface) { - throw TestError.createTestSetupError( - "configuration cryptoInterface not initialized correctly." - ); - } - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FORM_POST, - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: TEST_CONFIG.CODE_CHALLENGE_METHOD, - state: TEST_CONFIG.STATE, - prompt: PromptValue.LOGIN, - loginHint: TEST_CONFIG.LOGIN_HINT, - domainHint: TEST_CONFIG.DOMAIN_HINT, - claims: TEST_CONFIG.CLAIMS, - nonce: TEST_CONFIG.NONCE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.POP, - platformBroker: true, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.NATIVE_BROKER}=${encodeURIComponent("1")}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.REQ_CNF}=${encodeURIComponent( - TEST_POP_VALUES.ENCODED_REQ_CNF - )}` - ) - ).toBe(true); - }); - - describe("handleFragmentResponse()", () => { - it("returns valid server code response", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - - const client: AuthorizationCodeClient = new AuthorizationCodeClient( - config - ); - const authCodePayload = client.handleFragmentResponse( - { - code: "thisIsATestCode", - state: TEST_STATE_VALUES.ENCODED_LIB_STATE, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - }, - TEST_STATE_VALUES.ENCODED_LIB_STATE - ); - expect(authCodePayload.code).toBe("thisIsATestCode"); - expect(authCodePayload.state).toBe( - TEST_STATE_VALUES.ENCODED_LIB_STATE - ); - }); - - it("throws server error when error is in hash", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client: AuthorizationCodeClient = new AuthorizationCodeClient( - config - ); - const cacheStorageMock = - config.storageInterface as MockStorageClass; - - let error: AuthError | null = null; - try { - client.handleFragmentResponse( - { - error: "error_code", - error_description: "msal error description", - state: TEST_STATE_VALUES.ENCODED_LIB_STATE, - }, - TEST_STATE_VALUES.ENCODED_LIB_STATE - ); - } catch (e) { - error = e as AuthError; - } - expect(error).toBeInstanceOf(ServerError); - expect(error?.errorCode).toEqual("error_code"); - expect(error?.errorMessage).toEqual("msal error description"); - expect(cacheStorageMock.getKeys().length).toBe(1); - expect(cacheStorageMock.getAuthorityMetadataKeys().length).toBe(1); - }); - }); - describe("Acquire a token", () => { let config: ClientConfiguration; beforeEach(async () => { @@ -2235,7 +844,11 @@ describe("AuthorizationCodeClient unit tests", () => { ).toBe(true); expect( returnVal.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -2416,7 +1029,11 @@ describe("AuthorizationCodeClient unit tests", () => { ).toBe(true); expect( returnVal.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -3957,168 +2574,6 @@ describe("AuthorizationCodeClient unit tests", () => { }); }); - describe("createAuthCodeUrlQueryString tests", () => { - it("pick up default client_id", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - }); - - expect(queryString).toContain( - `client_id=${TEST_CONFIG.MSAL_CLIENT_ID}` - ); - }); - - it("pick up extra query client_id param", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - extraQueryParameters: { - client_id: "child_client_id", - }, - }); - - expect(queryString).toContain(`client_id=child_client_id`); - }); - - it("pick up instance_aware config param when set to true", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - config.authOptions.instanceAware = true; - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - }); - - expect(queryString).toContain(`instance_aware=true`); - }); - - it("do not pick up instance_aware config param when set to false", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - config.authOptions.instanceAware = false; - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - }); - - expect(queryString.includes("instance_aware")).toBeFalsy(); - }); - - it("pick up instance_aware EQ param when config is set to false", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - config.authOptions.instanceAware = false; - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - extraQueryParameters: { - instance_aware: "true", - }, - }); - - expect(queryString).toContain(`instance_aware=true`); - }); - - it("pick up instance_aware EQ param when config is set to true", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - config.authOptions.instanceAware = true; - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - extraQueryParameters: { - instance_aware: "false", - }, - }); - - expect(queryString).toContain(`instance_aware=false`); - }); - - it("pick up broker params", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - redirectUri: "localhost", - embeddedClientId: "child_client_id_1", - }); - - expect(queryString).toContain(`client_id=child_client_id_1`); - expect(queryString).toContain( - `brk_client_id=${config.authOptions.clientId}` - ); - expect(queryString).toContain( - `brk_redirect_uri=${encodeURIComponent("https://localhost")}` - ); - }); - - it("broker params take precedence over extra query params", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - redirectUri: "localhost", - embeddedClientId: "child_client_id_1", - extraQueryParameters: { - client_id: "child_client_id_2", - brk_client_id: "broker_client_id_2", - brk_redirect_uri: "broker_redirect_uri_2", - }, - }); - - expect(queryString).toContain(`client_id=child_client_id_1`); - expect(queryString).toContain( - `brk_client_id=${config.authOptions.clientId}` - ); - expect(queryString).toContain( - `brk_redirect_uri=${encodeURIComponent("https://localhost")}` - ); - }); - }); - describe("createTokenRequestBody tests", () => { it("pick up default client_id", async () => { const config: ClientConfiguration = diff --git a/lib/msal-common/test/client/ClientTestUtils.ts b/lib/msal-common/test/client/ClientTestUtils.ts index 620a3e9017..3d014bef3d 100644 --- a/lib/msal-common/test/client/ClientTestUtils.ts +++ b/lib/msal-common/test/client/ClientTestUtils.ts @@ -253,35 +253,11 @@ export class ClientTestUtils { }, }; - const authorityOptions: AuthorityOptions = { - protocolMode: protocolMode, - knownAuthorities: [TEST_CONFIG.validAuthority], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - }; - - const loggerOptions = { - loggerCallback: (): void => {}, - piiLoggingEnabled: true, - logLevel: LogLevel.Verbose, - }; - const logger = new Logger(loggerOptions); - - const authority = new Authority( - TEST_CONFIG.validAuthority, - mockHttpClient, - mockStorage, - authorityOptions, - logger, - TEST_CONFIG.CORRELATION_ID + const authority = await getDiscoveredAuthority( + protocolMode, + mockStorage ); - await authority.resolveEndpointsAsync().catch((error) => { - throw createClientAuthError( - ClientAuthErrorCodes.endpointResolutionError - ); - }); - let serverTelemetryManager = null; if (telem) { @@ -330,3 +306,55 @@ export class ClientTestUtils { }; } } + +export async function getDiscoveredAuthority( + protocolMode: ProtocolMode = ProtocolMode.AAD, + mockStorage: MockStorageClass = new MockStorageClass( + TEST_CONFIG.MSAL_CLIENT_ID, + mockCrypto, + new Logger({}), + { + canonicalAuthority: TEST_CONFIG.validAuthority, + } + ) +): Promise { + const mockHttpClient = { + sendGetRequestAsync(): T { + return {} as T; + }, + sendPostRequestAsync(): T { + return {} as T; + }, + }; + + const authorityOptions: AuthorityOptions = { + protocolMode: protocolMode, + knownAuthorities: [TEST_CONFIG.validAuthority], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + + const loggerOptions = { + loggerCallback: (): void => {}, + piiLoggingEnabled: true, + logLevel: LogLevel.Verbose, + }; + const logger = new Logger(loggerOptions); + + const authority = new Authority( + TEST_CONFIG.validAuthority, + mockHttpClient, + mockStorage, + authorityOptions, + logger, + TEST_CONFIG.CORRELATION_ID + ); + + await authority.resolveEndpointsAsync().catch((error) => { + throw createClientAuthError( + ClientAuthErrorCodes.endpointResolutionError + ); + }); + + return authority; +} diff --git a/lib/msal-common/test/client/RefreshTokenClient.spec.ts b/lib/msal-common/test/client/RefreshTokenClient.spec.ts index 95ed518136..a79301aab5 100644 --- a/lib/msal-common/test/client/RefreshTokenClient.spec.ts +++ b/lib/msal-common/test/client/RefreshTokenClient.spec.ts @@ -519,7 +519,11 @@ describe("RefreshTokenClient unit tests", () => { ).toBe(true); expect( result.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -781,7 +785,11 @@ describe("RefreshTokenClient unit tests", () => { ).toBe(true); expect( result.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -899,7 +907,11 @@ describe("RefreshTokenClient unit tests", () => { ).toBe(true); expect( result.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); diff --git a/lib/msal-common/test/protocol/Authorize.spec.ts b/lib/msal-common/test/protocol/Authorize.spec.ts new file mode 100644 index 0000000000..bb04cfbe43 --- /dev/null +++ b/lib/msal-common/test/protocol/Authorize.spec.ts @@ -0,0 +1,1678 @@ +import { Authority } from "../../src/authority/Authority.js"; +import { AuthOptions } from "../../src/config/ClientConfiguration.js"; +import { CommonAuthorizationUrlRequest } from "../../src/request/CommonAuthorizationUrlRequest.js"; +import { + AuthenticationScheme, + Constants, + HeaderNames, + PromptValue, + ResponseMode, +} from "../../src/utils/Constants.js"; +import { getDiscoveredAuthority } from "../client/ClientTestUtils.js"; +import { + DEFAULT_OPENID_CONFIG_RESPONSE, + RANDOM_TEST_GUID, + TEST_ACCOUNT_INFO, + TEST_CONFIG, + TEST_DATA_CLIENT_INFO, + TEST_STATE_VALUES, + TEST_URIS, +} from "../test_kit/StringConstants.js"; +import * as AADServerParamKeys from "../../src/constants/AADServerParamKeys.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; +import * as UrlUtils from "../../src/utils/UrlUtils.js"; +import { MockPerformanceClient } from "../telemetry/PerformanceClient.spec.js"; +import { TokenClaims } from "../../src/account/TokenClaims.js"; +import { Logger } from "../../src/logger/Logger.js"; +import { AuthError } from "../../src/error/AuthError.js"; +import { ServerError } from "../../src/error/ServerError.js"; +import { AuthorizeResponse } from "../../src/response/AuthorizeResponse.js"; +import { InteractionRequiredAuthError } from "../../src/error/InteractionRequiredAuthError.js"; +import { + ClientAuthError, + ClientAuthErrorCodes, +} from "../../src/error/ClientAuthError.js"; + +describe("Authorize Protocol Tests", () => { + let authOptions: AuthOptions; + let authority: Authority; + + beforeEach(async () => { + jest.spyOn( + Authority.prototype, + "getEndpointMetadataFromNetwork" + ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); + authority = await getDiscoveredAuthority(); + authOptions = { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: authority, + redirectUri: "https://localhost", + }; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + describe("Authorization url creation", () => { + it("Creates an authorization url with default parameters", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.QUERY, + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + codeChallenge: TEST_CONFIG.TEST_CHALLENGE, + codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(Constants.DEFAULT_AUTHORITY)).toBe(true); + expect( + loginUrl.includes( + DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( + "{tenant}", + "common" + ) + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.SCOPE}=${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( + TEST_URIS.TEST_REDIRECT_URI_LOCALHOST + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( + ResponseMode.QUERY + )}` + ) + ).toBe(true); + }); + + it("Creates an authorization url passing in optional parameters", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FORM_POST, + codeChallenge: TEST_CONFIG.TEST_CHALLENGE, + codeChallengeMethod: TEST_CONFIG.CODE_CHALLENGE_METHOD, + state: TEST_CONFIG.STATE, + prompt: PromptValue.LOGIN, + loginHint: TEST_CONFIG.LOGIN_HINT, + domainHint: TEST_CONFIG.DOMAIN_HINT, + claims: TEST_CONFIG.CLAIMS, + nonce: TEST_CONFIG.NONCE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(TEST_CONFIG.validAuthority)).toBe(true); + expect( + loginUrl.includes( + DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( + "{tenant}", + "common" + ) + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.SCOPE}=${TEST_CONFIG.DEFAULT_GRAPH_SCOPE}%20${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( + TEST_URIS.TEST_REDIRECT_URI_LOCALHOST + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( + ResponseMode.FORM_POST + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.STATE}=${encodeURIComponent( + TEST_CONFIG.STATE + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.PROMPT}=${PromptValue.LOGIN}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.NONCE}=${encodeURIComponent( + TEST_CONFIG.NONCE + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( + TEST_CONFIG.DOMAIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.CLAIMS}=${encodeURIComponent( + TEST_CONFIG.CLAIMS + )}` + ) + ).toBe(true); + }); + + it("Adds CCS entry if loginHint is provided", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.LOGIN, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${HeaderNames.CCS_HEADER}=${encodeURIComponent( + `UPN:${TEST_CONFIG.LOGIN_HINT}` + )}` + ) + ).toBe(true); + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromRequest).toBeTruthy(); + expect(event.loginHintFromUpn).toBeFalsy(); + expect(event.loginHintFromClaim).toBeFalsy(); + }); + + it("Adds CCS entry if account is provided", async () => { + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + | "login_hint" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + prompt: PromptValue.NONE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${HeaderNames.CCS_HEADER}=${encodeURIComponent( + `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` + )}` + ) + ).toBe(true); + }); + + it("prefers login_hint claim over sid/upn if both provided", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + login_hint: "opaque-login-hint-claim", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + prompt: PromptValue.NONE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(false); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + testTokenClaims.login_hint + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${HeaderNames.CCS_HEADER}=${encodeURIComponent( + `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` + )}` + ) + ).toBe(true); + + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromUpn).toBeFalsy(); + expect(event.loginHintFromClaim).toBeTruthy(); + expect(event.loginHintFromRequest).toBeFalsy(); + expect(event.domainHintFromRequest).toBeFalsy(); + expect(event.sidFromClaim).toBeFalsy(); + expect(event.sidFromRequest).toBeFalsy(); + }); + + it("skips login_hint claim if domainHint param is set", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + login_hint: "opaque-login-hint-claim", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + domainHint: TEST_CONFIG.DOMAIN_HINT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(false); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + testAccount.username + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( + TEST_CONFIG.DOMAIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${HeaderNames.CCS_HEADER}=${encodeURIComponent( + `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` + )}` + ) + ).toBe(true); + + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromUpn).toBeTruthy(); + expect(event.loginHintFromClaim).toBeFalsy(); + expect(event.loginHintFromRequest).toBeFalsy(); + expect(event.domainHintFromRequest).toBeTruthy(); + expect(event.sidFromClaim).toBeFalsy(); + expect(event.sidFromRequest).toBeFalsy(); + }); + + it("picks up both loginHint and domainHint params", async () => { + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + login_hint: "opaque-login-hint-claim", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + domainHint: TEST_CONFIG.DOMAIN_HINT, + loginHint: TEST_CONFIG.LOGIN_HINT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(false); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( + TEST_CONFIG.DOMAIN_HINT + )}` + ) + ).toBe(true); + }); + + it("Prefers sid over loginHint if both provided and prompt=None", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.NONE, + sid: TEST_CONFIG.SID, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl).toEqual( + expect.not.arrayContaining([ + `${AADServerParamKeys.LOGIN_HINT}=`, + ]) + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + TEST_CONFIG.SID + )}` + ) + ).toBe(true); + + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromRequest).toBeFalsy(); + expect(event.loginHintFromClaim).toBeFalsy(); + expect(event.loginHintFromUpn).toBeFalsy(); + expect(event.domainHintFromRequest).toBeFalsy(); + expect(event.sidFromRequest).toBeTruthy(); + expect(event.prompt).toEqual(PromptValue.NONE); + }); + + it("Prefers loginHint over sid if both provided and prompt!=None", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.LOGIN, + sid: TEST_CONFIG.SID, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Ignores sid if prompt!=None", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + prompt: PromptValue.LOGIN, + sid: TEST_CONFIG.SID, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromUpn).toBeFalsy(); + expect(event.loginHintFromClaim).toBeFalsy(); + expect(event.loginHintFromRequest).toBeFalsy(); + expect(event.domainHintFromRequest).toBeFalsy(); + expect(event.sidFromClaim).toBeFalsy(); + expect(event.sidFromRequest).toBeFalsy(); + expect(event.prompt).toEqual(PromptValue.LOGIN); + }); + + it("Prefers loginHint over Account if both provided and account does not have token claims", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + loginHint: TEST_CONFIG.LOGIN_HINT, + account: TEST_ACCOUNT_INFO, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_ACCOUNT_INFO.username + )}` + ) + ).toBe(false); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Uses sid from account if not provided in request and prompt=None, overrides login_hint", async () => { + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.NONE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + }); + + it("Uses loginHint instead of sid from account prompt!=None", async () => { + const testAccount = TEST_ACCOUNT_INFO; + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + | "iat" + | "x5c_ca" + | "ts" + | "at" + | "u" + | "p" + | "m" + | "login_hint" + | "aud" + | "nbf" + | "roles" + | "amr" + | "idp" + | "auth_time" + | "tfp" + | "acr" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + tenant_region_scope: "test_tenant_region_scope", + tenant_region_sub_scope: "test_tenant_region_sub_scope", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.LOGIN, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + }); + + it("Uses login_hint instead of username if sid is not present in token claims for account or request", async () => { + const testAccount = TEST_ACCOUNT_INFO; + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + | "sid" + | "iat" + | "x5c_ca" + | "ts" + | "at" + | "u" + | "p" + | "m" + | "login_hint" + | "aud" + | "nbf" + | "roles" + | "amr" + | "idp" + | "auth_time" + | "tfp" + | "acr" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + tenant_region_scope: "test_tenant_region_scope", + tenant_region_sub_scope: "test_tenant_region_sub_scope", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + loginHint: TEST_CONFIG.LOGIN_HINT, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Sets login_hint to Account.username if login_hint and sid are not provided", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: TEST_ACCOUNT_INFO, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_ACCOUNT_INFO.username + )}` + ) + ).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Ignores Account if prompt is select_account", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: TEST_ACCOUNT_INFO, + prompt: "select_account", + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Ignores loginHint if prompt is select_account", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + loginHint: "testaccount@microsoft.com", + prompt: "select_account", + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Ignores sid if prompt is select_account", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + sid: "testsid", + prompt: "select_account", + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Creates a login URL with scopes from given token request", async () => { + const testScope1 = "testscope1"; + const testScope2 = "testscope2"; + const loginRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: [testScope1, testScope2], + codeChallenge: TEST_CONFIG.TEST_CHALLENGE, + codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + loginRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SCOPE}=${encodeURIComponent( + `${testScope1} ${testScope2}` + )}` + ) + ).toBe(true); + }); + }); + + describe("createAuthCodeUrlQueryString tests", () => { + it("pick up default client_id", async () => { + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain( + `client_id=${TEST_CONFIG.MSAL_CLIENT_ID}` + ); + }); + + it("pick up extra query client_id param", async () => { + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + extraQueryParameters: { + client_id: "child_client_id", + }, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain(`client_id=child_client_id`); + }); + + it("pick up instance_aware config param when set to true", async () => { + authOptions.instanceAware = true; + + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain(`instance_aware=true`); + }); + + it("do not pick up instance_aware config param when set to false", async () => { + authOptions.instanceAware = false; + + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString.includes("instance_aware")).toBeFalsy(); + }); + + it("pick up instance_aware EQ param when config is set to false", async () => { + authOptions.instanceAware = false; + + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + extraQueryParameters: { + instance_aware: "true", + }, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain(`instance_aware=true`); + }); + + it("pick up instance_aware EQ param when config is set to true", async () => { + authOptions.instanceAware = true; + + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + extraQueryParameters: { + instance_aware: "false", + }, + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain(`instance_aware=false`); + }); + + it("pick up broker params", async () => { + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + redirectUri: "localhost", + embeddedClientId: "child_client_id_1", + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + expect(queryString).toContain(`client_id=child_client_id_1`); + expect(queryString).toContain( + `brk_client_id=${TEST_CONFIG.MSAL_CLIENT_ID}` + ); + expect(queryString).toContain( + `brk_redirect_uri=${encodeURIComponent("https://localhost")}` + ); + }); + + it("broker params take precedence over extra query params", async () => { + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + redirectUri: "localhost", + embeddedClientId: "child_client_id_1", + extraQueryParameters: { + client_id: "child_client_id_2", + brk_client_id: "broker_client_id_2", + brk_redirect_uri: "broker_redirect_uri_2", + }, + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + expect(queryString).toContain(`client_id=child_client_id_1`); + expect(queryString).toContain( + `brk_client_id=${TEST_CONFIG.MSAL_CLIENT_ID}` + ); + expect(queryString).toContain( + `brk_redirect_uri=${encodeURIComponent("https://localhost")}` + ); + }); + }); + + describe("getAuthorizationCodePayload", () => { + it("returns valid server code response", () => { + const authCodePayload = + AuthorizeProtocol.getAuthorizationCodePayload( + { + code: "thisIsATestCode", + state: TEST_STATE_VALUES.ENCODED_LIB_STATE, + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + }, + TEST_STATE_VALUES.ENCODED_LIB_STATE + ); + expect(authCodePayload.code).toBe("thisIsATestCode"); + expect(authCodePayload.state).toBe( + TEST_STATE_VALUES.ENCODED_LIB_STATE + ); + }); + + it("throws server error when error is in hash", () => { + let error: AuthError | null = null; + try { + AuthorizeProtocol.getAuthorizationCodePayload( + { + error: "error_code", + error_description: "msal error description", + state: TEST_STATE_VALUES.ENCODED_LIB_STATE, + }, + TEST_STATE_VALUES.ENCODED_LIB_STATE + ); + } catch (e) { + error = e as AuthError; + } + expect(error).toBeInstanceOf(ServerError); + expect(error?.errorCode).toEqual("error_code"); + expect(error?.errorMessage).toEqual("msal error description"); + }); + }); + + describe("validateAuthorizationResponse", () => { + it("throws state mismatch error", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + "differentState" + ); + } catch (e) { + expect(e).toBeInstanceOf(ClientAuthError); + // @ts-ignore + expect(e.errorCode).toBe(ClientAuthErrorCodes.stateMismatch); + done(); + } + }); + + it("Does not throw state mismatch error when states match", () => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + }); + + it("Does not throw state mismatch error when Uri encoded characters have different casing", () => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + const testAltState = + "eyJpZCI6IjExNTUzYTliLTcxMTYtNDhiMS05ZDQ4LWY2ZDRhOGZmODM3MSIsInRzIjoxNTkyODQ2NDgyfQ%3d%3d"; + + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + testAltState + ); + }); + + it("throws interactionRequiredError", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "interaction_required", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(InteractionRequiredAuthError); + done(); + } + }); + + it("thows ServerError if error in response", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + done(); + } + }); + + it("throws ServerError if error_description in response", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error_description: "test_error", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + done(); + } + }); + + it("throws ServerError if suberror in response", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + suberror: "test_error", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + done(); + } + }); + + it("throws invalid state error", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + "dummy-state-%20%%%30%%%%%40" + ); + } catch (e) { + expect(e).toBeInstanceOf(ClientAuthError); + const err = e as ClientAuthError; + expect(err.errorCode).toBe(ClientAuthErrorCodes.invalidState); + done(); + } + }); + + it("throws ServerError and parser error no", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + error_uri: + "https://login.microsoftonline.com/error_code=500011", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + const serverError = e as ServerError; + expect(serverError.errorNo).toEqual("500011"); + done(); + } + }); + + it("throws InteractionRequiredAuthError and parser error no", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "interaction_required", + error_uri: + "https://login.microsoftonline.com/error_code=500011", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(InteractionRequiredAuthError); + const serverError = e as InteractionRequiredAuthError; + expect(serverError.errorNo).toEqual("500011"); + done(); + } + }); + + it("throws ServerError and skips invalid error uri", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + error_uri: "https://login.microsoftonline.com/500011", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + const serverError = e as ServerError; + expect(serverError.errorNo).toBeUndefined(); + done(); + } + }); + + it("throws ServerError and skips undefined error uri", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + error_uri: undefined, + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + const serverError = e as ServerError; + expect(serverError.errorNo).toBeUndefined(); + done(); + } + }); + + it("throws ServerError and skips empty error uri", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + error_uri: "", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + const serverError = e as ServerError; + expect(serverError.errorNo).toBeUndefined(); + done(); + } + }); + }); +}); diff --git a/lib/msal-common/test/response/ResponseHandler.spec.ts b/lib/msal-common/test/response/ResponseHandler.spec.ts index f0a069816d..6aa4b3550c 100644 --- a/lib/msal-common/test/response/ResponseHandler.spec.ts +++ b/lib/msal-common/test/response/ResponseHandler.spec.ts @@ -9,7 +9,6 @@ import { TEST_CRYPTO_VALUES, TEST_DATA_CLIENT_INFO, TEST_POP_VALUES, - TEST_STATE_VALUES, TEST_TOKEN_LIFETIMES, TEST_TOKENS, TEST_URIS, @@ -20,7 +19,6 @@ import { NetworkRequestOptions, } from "../../src/network/INetworkModule.js"; import { ICrypto } from "../../src/crypto/ICrypto.js"; -import { ServerAuthorizationCodeResponse } from "../../src/response/ServerAuthorizationCodeResponse.js"; import { MockStorageClass } from "../client/ClientTestUtils.js"; import { TokenClaims } from "../../src/account/TokenClaims.js"; import { AccountInfo } from "../../src/account/AccountInfo.js"; @@ -725,396 +723,6 @@ describe("ResponseHandler.ts", () => { }); }); - describe("validateServerAuthorizationCodeResponse", () => { - it("throws state mismatch error", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - "differentState" - ); - } catch (e) { - expect(e).toBeInstanceOf(ClientAuthError); - // @ts-ignore - expect(e.errorCode).toBe(ClientAuthErrorCodes.stateMismatch); - done(); - } - }); - - it("Does not throw state mismatch error when states match", () => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - }); - - it("Does not throw state mismatch error when Uri encoded characters have different casing", () => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - - const testAltState = - "eyJpZCI6IjExNTUzYTliLTcxMTYtNDhiMS05ZDQ4LWY2ZDRhOGZmODM3MSIsInRzIjoxNTkyODQ2NDgyfQ%3d%3d"; - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - testAltState - ); - }); - - it("throws interactionRequiredError", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "interaction_required", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(InteractionRequiredAuthError); - done(); - } - }); - - it("thows ServerError if error in response", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - done(); - } - }); - - it("throws ServerError if error_description in response", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error_description: "test_error", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - done(); - } - }); - - it("throws ServerError if suberror in response", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - suberror: "test_error", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - done(); - } - }); - - it("does not call buildClientInfo if clientInfo not in response", () => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - // Can't spy on buildClientInfo, spy on one of its function calls instead - const buildClientInfoSpy = jest.spyOn( - cryptoInterface, - "base64Decode" - ); - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - expect(buildClientInfoSpy).not.toHaveBeenCalled(); - }); - - it("throws invalid state error", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - "dummy-state-%20%%%30%%%%%40" - ); - } catch (e) { - expect(e).toBeInstanceOf(ClientAuthError); - const err = e as ClientAuthError; - expect(err.errorCode).toBe(ClientAuthErrorCodes.invalidState); - done(); - } - }); - - it("throws ServerError and parser error no", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - error_uri: - "https://login.microsoftonline.com/error_code=500011", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - const serverError = e as ServerError; - expect(serverError.errorNo).toEqual("500011"); - done(); - } - }); - - it("throws InteractionRequiredAuthError and parser error no", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "interaction_required", - error_uri: - "https://login.microsoftonline.com/error_code=500011", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(InteractionRequiredAuthError); - const serverError = e as InteractionRequiredAuthError; - expect(serverError.errorNo).toEqual("500011"); - done(); - } - }); - - it("throws ServerError and skips invalid error uri", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - error_uri: "https://login.microsoftonline.com/500011", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - const serverError = e as ServerError; - expect(serverError.errorNo).toBeUndefined(); - done(); - } - }); - - it("throws ServerError and skips undefined error uri", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - error_uri: undefined, - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - const serverError = e as ServerError; - expect(serverError.errorNo).toBeUndefined(); - done(); - } - }); - - it("throws ServerError and skips empty error uri", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - error_uri: "", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - const serverError = e as ServerError; - expect(serverError.errorNo).toBeUndefined(); - done(); - } - }); - }); - describe("validateTokenResponse", () => { it("captures server error no", (done) => { const testTokenResponse: ServerAuthorizationTokenResponse = { diff --git a/lib/msal-common/test/telemetry/PerformanceClient.spec.ts b/lib/msal-common/test/telemetry/PerformanceClient.spec.ts index 6ba199fbdc..c859174c3c 100644 --- a/lib/msal-common/test/telemetry/PerformanceClient.spec.ts +++ b/lib/msal-common/test/telemetry/PerformanceClient.spec.ts @@ -17,8 +17,8 @@ import crypto from "crypto"; import { compactStack, compactStackLine, -} from "../../src/telemetry/performance/PerformanceClient"; -import * as PerformanceClient from "../../src/telemetry/performance/PerformanceClient"; +} from "../../src/telemetry/performance/PerformanceClient.js"; +import * as PerformanceClient from "../../src/telemetry/performance/PerformanceClient.js"; import { PerformanceEventAbbreviations } from "../../src/telemetry/performance/PerformanceEvent"; import { AuthError } from "../../src/error/AuthError.js"; @@ -944,7 +944,7 @@ describe("PerformanceClient.spec.ts", () => { ); const thirdChildEventChild = mockPerfClient.startMeasurement( - PerformanceEvents.AuthClientCreateQueryString, + PerformanceEvents.GetAuthCodeUrl, correlationId ); thirdChildEventChild.end({ success: true }); diff --git a/lib/msal-node/apiReview/msal-node.api.md b/lib/msal-node/apiReview/msal-node.api.md index 0967a62b97..bc9bf0ea39 100644 --- a/lib/msal-node/apiReview/msal-node.api.md +++ b/lib/msal-node/apiReview/msal-node.api.md @@ -23,6 +23,7 @@ import { AuthErrorMessage } from '@azure/msal-common/node'; import { Authority } from '@azure/msal-common/node'; import { AuthorityMetadataEntity } from '@azure/msal-common/node'; import { AuthorizationCodePayload } from '@azure/msal-common/node'; +import { AuthorizeResponse } from '@azure/msal-common/node'; import { AzureCloudInstance } from '@azure/msal-common/node'; import { AzureCloudOptions } from '@azure/msal-common/node'; import { AzureRegionConfiguration } from '@azure/msal-common/node'; @@ -72,7 +73,6 @@ import { ProtocolMode } from '@azure/msal-common/node'; import { RefreshTokenCache } from '@azure/msal-common/node'; import { RefreshTokenEntity } from '@azure/msal-common/node'; import { ResponseMode } from '@azure/msal-common/node'; -import { ServerAuthorizationCodeResponse } from '@azure/msal-common/node'; import { ServerError } from '@azure/msal-common/node'; import { ServerTelemetryEntity } from '@azure/msal-common/node'; import { ServerTelemetryManager } from '@azure/msal-common/node'; @@ -113,6 +113,9 @@ export type AuthorizationUrlRequest = Partial; acquireTokenSilent(request: SilentFlowRequest): Promise; - protected buildOauthClientConfiguration(authority: string, requestCorrelationId: string, redirectUri: string, serverTelemetryManager?: ServerTelemetryManager, azureRegionConfiguration?: AzureRegionConfiguration, azureCloudOptions?: AzureCloudOptions): Promise; + protected buildOauthClientConfiguration(discoveredAuthority: Authority, requestCorrelationId: string, redirectUri: string, serverTelemetryManager?: ServerTelemetryManager): Promise; clearCache(): void; protected clientAssertion: ClientAssertion; protected clientSecret: string; // Warning: (ae-forgotten-export) The symbol "NodeConfiguration" needs to be exported by the entry point index.d.ts protected config: NodeConfiguration; + protected createAuthority(authorityString: string, requestCorrelationId: string, azureRegionConfiguration?: AzureRegionConfiguration, azureCloudOptions?: AzureCloudOptions): Promise; // (undocumented) protected readonly cryptoProvider: CryptoProvider; // (undocumented) @@ -297,7 +301,7 @@ export interface ILoopbackClient { // (undocumented) getRedirectUri(): string; // (undocumented) - listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise; + listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise; } export { INativeBrokerPlugin } @@ -574,8 +578,6 @@ class Serializer { static serializeRefreshTokens(rtCache: RefreshTokenCache): Record; } -export { ServerAuthorizationCodeResponse } - export { ServerError } // @public (undocumented) diff --git a/lib/msal-node/src/client/ClientApplication.ts b/lib/msal-node/src/client/ClientApplication.ts index d38064ee7e..32741e73c5 100644 --- a/lib/msal-node/src/client/ClientApplication.ts +++ b/lib/msal-node/src/client/ClientApplication.ts @@ -57,6 +57,7 @@ import { version, name } from "../packageMetadata.js"; import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest.js"; import { NodeAuthError } from "../error/NodeAuthError.js"; import { UsernamePasswordClient } from "./UsernamePasswordClient.js"; +import { getAuthCodeRequestUrl } from "../protocol/Authorize.js"; /** * Base abstract class for all ClientApplications - public and confidential @@ -132,22 +133,18 @@ export abstract class ClientApplication { authenticationScheme: AuthenticationScheme.BEARER, }; - const authClientConfig = await this.buildOauthClientConfiguration( + const discoveredAuthority = await this.createAuthority( validRequest.authority, validRequest.correlationId, - validRequest.redirectUri, - undefined, undefined, request.azureCloudOptions ); - const authorizationCodeClient = new AuthorizationCodeClient( - authClientConfig - ); - this.logger.verbose( - "Auth code client created", - validRequest.correlationId + return getAuthCodeRequestUrl( + this.config, + discoveredAuthority, + validRequest, + this.logger ); - return authorizationCodeClient.getAuthCodeUrl(validRequest); } /** @@ -180,14 +177,18 @@ export abstract class ClientApplication { validRequest.correlationId ); try { - const authClientConfig = await this.buildOauthClientConfiguration( + const discoveredAuthority = await this.createAuthority( validRequest.authority, validRequest.correlationId, - validRequest.redirectUri, - serverTelemetryManager, undefined, request.azureCloudOptions ); + const authClientConfig = await this.buildOauthClientConfiguration( + discoveredAuthority, + validRequest.correlationId, + validRequest.redirectUri, + serverTelemetryManager + ); const authorizationCodeClient = new AuthorizationCodeClient( authClientConfig ); @@ -233,14 +234,18 @@ export abstract class ClientApplication { validRequest.correlationId ); try { + const discoveredAuthority = await this.createAuthority( + validRequest.authority, + validRequest.correlationId, + undefined, + request.azureCloudOptions + ); const refreshTokenClientConfig = await this.buildOauthClientConfiguration( - validRequest.authority, + discoveredAuthority, validRequest.correlationId, validRequest.redirectUri || "", - serverTelemetryManager, - undefined, - request.azureCloudOptions + serverTelemetryManager ); const refreshTokenClient = new RefreshTokenClient( refreshTokenClientConfig @@ -283,14 +288,18 @@ export abstract class ClientApplication { ); try { + const discoveredAuthority = await this.createAuthority( + validRequest.authority, + validRequest.correlationId, + undefined, + request.azureCloudOptions + ); const clientConfiguration = await this.buildOauthClientConfiguration( - validRequest.authority, + discoveredAuthority, validRequest.correlationId, validRequest.redirectUri || "", - serverTelemetryManager, - undefined, - validRequest.azureCloudOptions + serverTelemetryManager ); const silentFlowClient = new SilentFlowClient(clientConfiguration); this.logger.verbose( @@ -391,14 +400,18 @@ export abstract class ClientApplication { validRequest.correlationId ); try { + const discoveredAuthority = await this.createAuthority( + validRequest.authority, + validRequest.correlationId, + undefined, + request.azureCloudOptions + ); const usernamePasswordClientConfig = await this.buildOauthClientConfiguration( - validRequest.authority, + discoveredAuthority, validRequest.correlationId, "", - serverTelemetryManager, - undefined, - request.azureCloudOptions + serverTelemetryManager ); const usernamePasswordClient = new UsernamePasswordClient( usernamePasswordClientConfig @@ -465,31 +478,16 @@ export abstract class ClientApplication { * @param serverTelemetryManager - initializes servertelemetry if passed */ protected async buildOauthClientConfiguration( - authority: string, + discoveredAuthority: Authority, requestCorrelationId: string, redirectUri: string, - serverTelemetryManager?: ServerTelemetryManager, - azureRegionConfiguration?: AzureRegionConfiguration, - azureCloudOptions?: AzureCloudOptions + serverTelemetryManager?: ServerTelemetryManager ): Promise { this.logger.verbose( "buildOauthClientConfiguration called", requestCorrelationId ); - // precedence - azureCloudInstance + tenant >> authority and request >> config - const userAzureCloudOptions = azureCloudOptions - ? azureCloudOptions - : this.config.auth.azureCloudOptions; - - // using null assertion operator as we ensure that all config values have default values in buildConfiguration() - const discoveredAuthority = await this.createAuthority( - authority, - requestCorrelationId, - azureRegionConfiguration, - userAzureCloudOptions - ); - this.logger.info( `Building oauth client configuration with the following authority: ${discoveredAuthority.tokenEndpoint}.`, requestCorrelationId @@ -640,7 +638,7 @@ export abstract class ClientApplication { * object. If no authority set in application object, then default to common authority. * @param authorityString - authority from user configuration */ - private async createAuthority( + protected async createAuthority( authorityString: string, requestCorrelationId: string, azureRegionConfiguration?: AzureRegionConfiguration, @@ -651,7 +649,7 @@ export abstract class ClientApplication { // build authority string based on auth params - azureCloudInstance is prioritized if provided const authorityUrl = Authority.generateAuthority( authorityString, - azureCloudOptions + azureCloudOptions || this.config.auth.azureCloudOptions ); const authorityOptions: AuthorityOptions = { diff --git a/lib/msal-node/src/client/ConfidentialClientApplication.ts b/lib/msal-node/src/client/ConfidentialClientApplication.ts index 8ea240a22e..eb341cf28a 100644 --- a/lib/msal-node/src/client/ConfidentialClientApplication.ts +++ b/lib/msal-node/src/client/ConfidentialClientApplication.ts @@ -221,14 +221,18 @@ export class ConfidentialClientApplication validRequest.skipCache ); try { + const discoveredAuthority = await this.createAuthority( + validRequest.authority, + validRequest.correlationId, + azureRegionConfiguration, + request.azureCloudOptions + ); const clientCredentialConfig = await this.buildOauthClientConfiguration( - validRequest.authority, + discoveredAuthority, validRequest.correlationId, "", - serverTelemetryManager, - azureRegionConfiguration, - request.azureCloudOptions + serverTelemetryManager ); const clientCredentialClient = new ClientCredentialClient( clientCredentialConfig, @@ -271,14 +275,18 @@ export class ConfidentialClientApplication ...(await this.initializeBaseRequest(request)), }; try { - const onBehalfOfConfig = await this.buildOauthClientConfiguration( + const discoveredAuthority = await this.createAuthority( validRequest.authority, validRequest.correlationId, - "", - undefined, undefined, request.azureCloudOptions ); + const onBehalfOfConfig = await this.buildOauthClientConfiguration( + discoveredAuthority, + validRequest.correlationId, + "", + undefined + ); const oboClient = new OnBehalfOfClient(onBehalfOfConfig); this.logger.verbose( "On behalf of client created", diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index 1f81ec06fb..2fa0a0b09f 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -21,7 +21,7 @@ import { NativeSignOutRequest, AccountInfo, INativeBrokerPlugin, - ServerAuthorizationCodeResponse, + AuthorizeResponse, AADServerParamKeys, ServerTelemetryManager, } from "@azure/msal-common/node"; @@ -113,14 +113,18 @@ export class PublicClientApplication validRequest.correlationId ); try { - const deviceCodeConfig = await this.buildOauthClientConfiguration( + const discoveredAuthority = await this.createAuthority( validRequest.authority, validRequest.correlationId, - "", - serverTelemetryManager, undefined, request.azureCloudOptions ); + const deviceCodeConfig = await this.buildOauthClientConfiguration( + discoveredAuthority, + validRequest.correlationId, + "", + serverTelemetryManager + ); const deviceCodeClient = new DeviceCodeClient(deviceCodeConfig); this.logger.verbose( "Device code client created", @@ -181,7 +185,7 @@ export class PublicClientApplication const loopbackClient: ILoopbackClient = customLoopbackClient || new LoopbackClient(); - let authCodeResponse: ServerAuthorizationCodeResponse = {}; + let authCodeResponse: AuthorizeResponse = {}; let authCodeListenerError: AuthError | null = null; try { const authCodeListener = loopbackClient diff --git a/lib/msal-node/src/index.ts b/lib/msal-node/src/index.ts index 04418ea898..a8f134838c 100644 --- a/lib/msal-node/src/index.ts +++ b/lib/msal-node/src/index.ts @@ -12,7 +12,6 @@ * Warning: This set of exports is purely intended to be used by other MSAL libraries, and should be considered potentially unstable. We strongly discourage using them directly, you do so at your own risk. * Breaking changes to these APIs will be shipped under a minor version, instead of a major version. */ - import * as internals from "./internals.js"; export { internals }; @@ -87,7 +86,11 @@ export { AuthorizationCodePayload, // Response AuthenticationResult, - ServerAuthorizationCodeResponse, + AuthorizeResponse, + /** + * @deprecated Use AuthorizeResponse instead + */ + AuthorizeResponse as ServerAuthorizationCodeResponse, IdTokenClaims, // Cache AccountInfo, diff --git a/lib/msal-node/src/network/ILoopbackClient.ts b/lib/msal-node/src/network/ILoopbackClient.ts index 2933b787d3..31971e5870 100644 --- a/lib/msal-node/src/network/ILoopbackClient.ts +++ b/lib/msal-node/src/network/ILoopbackClient.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { ServerAuthorizationCodeResponse } from "@azure/msal-common/node"; +import { AuthorizeResponse } from "@azure/msal-common/node"; /** * Interface for LoopbackClient allowing to replace the default loopback server with a custom implementation. @@ -13,7 +13,7 @@ export interface ILoopbackClient { listenForAuthCode( successTemplate?: string, errorTemplate?: string - ): Promise; + ): Promise; getRedirectUri(): string; closeServer(): void; } diff --git a/lib/msal-node/src/network/LoopbackClient.ts b/lib/msal-node/src/network/LoopbackClient.ts index fc6877a56c..52d3277b49 100644 --- a/lib/msal-node/src/network/LoopbackClient.ts +++ b/lib/msal-node/src/network/LoopbackClient.ts @@ -5,7 +5,7 @@ import { Constants as CommonConstants, - ServerAuthorizationCodeResponse, + AuthorizeResponse, HttpStatus, UrlUtils, } from "@azure/msal-common/node"; @@ -26,57 +26,54 @@ export class LoopbackClient implements ILoopbackClient { async listenForAuthCode( successTemplate?: string, errorTemplate?: string - ): Promise { + ): Promise { if (this.server) { throw NodeAuthError.createLoopbackServerAlreadyExistsError(); } - return new Promise( - (resolve, reject) => { - this.server = http.createServer( - (req: http.IncomingMessage, res: http.ServerResponse) => { - const url = req.url; - if (!url) { - res.end( - errorTemplate || - "Error occurred loading redirectUrl" - ); - reject( - NodeAuthError.createUnableToLoadRedirectUrlError() - ); - return; - } else if (url === CommonConstants.FORWARD_SLASH) { - res.end( - successTemplate || - "Auth code was successfully acquired. You can close this window now." - ); - return; - } + return new Promise((resolve, reject) => { + this.server = http.createServer( + (req: http.IncomingMessage, res: http.ServerResponse) => { + const url = req.url; + if (!url) { + res.end( + errorTemplate || + "Error occurred loading redirectUrl" + ); + reject( + NodeAuthError.createUnableToLoadRedirectUrlError() + ); + return; + } else if (url === CommonConstants.FORWARD_SLASH) { + res.end( + successTemplate || + "Auth code was successfully acquired. You can close this window now." + ); + return; + } - const redirectUri = this.getRedirectUri(); - const parsedUrl = new URL(url, redirectUri); - const authCodeResponse = - UrlUtils.getDeserializedResponse( - parsedUrl.search - ) || {}; - if (authCodeResponse.code) { - res.writeHead(HttpStatus.REDIRECT, { - location: redirectUri, - }); // Prevent auth code from being saved in the browser history - res.end(); - } - if (authCodeResponse.error) { - res.end( - errorTemplate || - `Error occurred: ${authCodeResponse.error}` - ); - } - resolve(authCodeResponse); + const redirectUri = this.getRedirectUri(); + const parsedUrl = new URL(url, redirectUri); + const authCodeResponse = + UrlUtils.getDeserializedResponse(parsedUrl.search) || + {}; + if (authCodeResponse.code) { + res.writeHead(HttpStatus.REDIRECT, { + location: redirectUri, + }); // Prevent auth code from being saved in the browser history + res.end(); } - ); - this.server.listen(0, "127.0.0.1"); // Listen on any available port - } - ); + if (authCodeResponse.error) { + res.end( + errorTemplate || + `Error occurred: ${authCodeResponse.error}` + ); + } + resolve(authCodeResponse); + } + ); + this.server.listen(0, "127.0.0.1"); // Listen on any available port + }); } /** diff --git a/lib/msal-node/src/protocol/Authorize.ts b/lib/msal-node/src/protocol/Authorize.ts new file mode 100644 index 0000000000..14b94a0deb --- /dev/null +++ b/lib/msal-node/src/protocol/Authorize.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Authority, + AuthorizeProtocol, + CommonAuthorizationUrlRequest, + Logger, + ProtocolMode, + RequestParameterBuilder, +} from "@azure/msal-common/node"; +import { NodeConfiguration } from "../config/Configuration.js"; +import { Constants as NodeConstants } from "../utils/Constants.js"; +import { version } from "../packageMetadata.js"; + +/** + * Constructs the full /authorize URL with request parameters + * @param config + * @param authority + * @param request + * @param logger + * @returns + */ +export function getAuthCodeRequestUrl( + config: NodeConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger +): string { + const parameters = AuthorizeProtocol.getStandardAuthorizeRequestParameters( + { + ...config.auth, + authority: authority, + redirectUri: request.redirectUri || "", + }, + request, + logger + ); + RequestParameterBuilder.addLibraryInfo(parameters, { + sku: NodeConstants.MSAL_SKU, + version: version, + cpu: process.arch || "", + os: process.platform || "", + }); + if (config.auth.protocolMode !== ProtocolMode.OIDC) { + RequestParameterBuilder.addApplicationTelemetry( + parameters, + config.telemetry.application + ); + } + RequestParameterBuilder.addResponseTypeCode(parameters); + if (request.codeChallenge && request.codeChallengeMethod) { + RequestParameterBuilder.addCodeChallengeParams( + parameters, + request.codeChallenge, + request.codeChallengeMethod + ); + } + + return AuthorizeProtocol.getAuthorizeUrl(authority, parameters); +} diff --git a/lib/msal-node/test/client/ClientTestUtils.ts b/lib/msal-node/test/client/ClientTestUtils.ts index 754dc9a15e..7f90b9641d 100644 --- a/lib/msal-node/test/client/ClientTestUtils.ts +++ b/lib/msal-node/test/client/ClientTestUtils.ts @@ -526,7 +526,9 @@ export const checkMockedNetworkRequest = ( if (checks.msLibraryCapability !== undefined) { expect( returnVal.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(checks.msLibraryCapability); } diff --git a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts index fd93da9748..6461e73927 100644 --- a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts +++ b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts @@ -360,7 +360,7 @@ describe("ConfidentialClientApplication", () => { }; let acquireTokenByClientCredentialSpy: jest.SpyInstance; - let buildOauthClientConfigurationSpy: jest.SpyInstance; + let createAuthoritySpy: jest.SpyInstance; let sendPostRequestAsyncSpy: jest.SpyInstance; let client: ConfidentialClientApplication; let request: ClientCredentialRequest; @@ -370,9 +370,9 @@ describe("ConfidentialClientApplication", () => { "acquireTokenByClientCredential" ); - buildOauthClientConfigurationSpy = jest.spyOn( + createAuthoritySpy = jest.spyOn( ConfidentialClientApplication.prototype, - "buildOauthClientConfiguration" + "createAuthority" ); sendPostRequestAsyncSpy = jest.spyOn( @@ -404,10 +404,9 @@ describe("ConfidentialClientApplication", () => { expect(acquireTokenByClientCredentialSpy).toHaveBeenCalledTimes( 1 ); - expect( - buildOauthClientConfigurationSpy.mock.lastCall[4] - .azureRegion - ).toEqual(process.env[MSAL_FORCE_REGION]); + expect(createAuthoritySpy.mock.lastCall[2].azureRegion).toEqual( + process.env[MSAL_FORCE_REGION] + ); checkRegion( sendPostRequestAsyncSpy.mock.lastCall[0], @@ -427,10 +426,9 @@ describe("ConfidentialClientApplication", () => { expect(acquireTokenByClientCredentialSpy).toHaveBeenCalledTimes( 1 ); - expect( - buildOauthClientConfigurationSpy.mock.lastCall[4] - .azureRegion - ).toEqual(region); + expect(createAuthoritySpy.mock.lastCall[2].azureRegion).toEqual( + region + ); checkRegion(sendPostRequestAsyncSpy.mock.lastCall[0], region); }); @@ -446,8 +444,7 @@ describe("ConfidentialClientApplication", () => { 1 ); expect( - buildOauthClientConfigurationSpy.mock.lastCall[4] - .azureRegion + createAuthoritySpy.mock.lastCall[2].azureRegion ).toBeUndefined(); const endpoint: string = diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts index 52f1eb6a54..2b72f84435 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts @@ -236,7 +236,9 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = ManagedIdentityEnvironmentVariableNames .IDENTITY_ENDPOINT ] - }?api-version=${ARC_API_VERSION}&resource=${MANAGED_IDENTITY_RESOURCE_BASE}`, + }?api-version=${ARC_API_VERSION}&resource=${encodeURIComponent( + MANAGED_IDENTITY_RESOURCE_BASE + )}`, { headers: { Authorization: diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index 507562bae7..e03dc265f2 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -21,7 +21,7 @@ import { Logger, LogLevel, AccountInfo, - ServerAuthorizationCodeResponse, + AuthorizeResponse, InteractionRequiredAuthError, AccountEntity, AuthToken, @@ -61,7 +61,7 @@ import * as msalNode from "../../src/index.js"; import { setupServerTelemetryManagerMock } from "./test-fixtures.js"; import { getMsalCommonAutoMock, MSALCommonModule } from "../utils/MockUtils.js"; -import { version, name } from "../../package.json"; +import { version, name } from "../../src/packageMetadata.js"; import { MockNativeBrokerPlugin } from "../utils/MockNativeBrokerPlugin.js"; import { SignOutRequest } from "../../src/request/SignOutRequest.js"; import { LoopbackClient } from "../../src/network/LoopbackClient.js"; @@ -80,6 +80,7 @@ import { Constants } from "../../src/utils/Constants.js"; import { NodeStorage } from "../../src/cache/NodeStorage.js"; import { TokenCache } from "../../src/index.js"; import { buildAccountFromIdTokenClaims } from "msal-test-utils"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; const msalCommon: MSALCommonModule = jest.requireActual( "@azure/msal-common/node" @@ -652,11 +653,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { redirectUri = req.redirectUri; - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); jest.spyOn( @@ -724,11 +725,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { redirectUri = req.redirectUri; - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); jest.spyOn( @@ -760,18 +761,16 @@ describe("PublicClientApplication", () => { return Promise.resolve(); }; - const testServerCodeResponse: ServerAuthorizationCodeResponse = { + const testServerCodeResponse: AuthorizeResponse = { code: TEST_CONSTANTS.AUTHORIZATION_CODE, client_info: TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO, state: "123", }; const mockListenForAuthCode = jest.fn(() => { - return new Promise( - (resolve) => { - resolve(testServerCodeResponse); - } - ); + return new Promise((resolve) => { + resolve(testServerCodeResponse); + }); }); const mockGetRedirectUri = jest.fn( () => TEST_CONSTANTS.REDIRECT_URI @@ -800,11 +799,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { expect(req.redirectUri).toEqual(TEST_CONSTANTS.REDIRECT_URI); - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); jest.spyOn( @@ -913,7 +912,7 @@ describe("PublicClientApplication", () => { return Promise.reject("Browser open error"); }; - const testServerCodeResponse: ServerAuthorizationCodeResponse = { + const testServerCodeResponse: AuthorizeResponse = { code: TEST_CONSTANTS.AUTHORIZATION_CODE, client_info: TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO, state: "123", @@ -923,11 +922,9 @@ describe("PublicClientApplication", () => { LoopbackClient.prototype, "listenForAuthCode" ).mockImplementation(() => { - return new Promise( - (resolve) => { - resolve(testServerCodeResponse); - } - ); + return new Promise((resolve) => { + resolve(testServerCodeResponse); + }); }); jest.spyOn( LoopbackClient.prototype, @@ -953,11 +950,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { expect(req.redirectUri).toEqual(TEST_CONSTANTS.REDIRECT_URI); - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); authApp.acquireTokenInteractive(request).catch((e) => { @@ -1018,11 +1015,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { expect(req.redirectUri).toEqual(TEST_CONSTANTS.REDIRECT_URI); - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); authApp.acquireTokenInteractive(request).catch((e) => {