UNPKG

@azure/msal-common

Version:
833 lines (744 loc) 31.1 kB
/* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { BaseClient } from "./BaseClient.js"; import { CommonAuthorizationUrlRequest } from "../request/CommonAuthorizationUrlRequest.js"; import { CommonAuthorizationCodeRequest } from "../request/CommonAuthorizationCodeRequest.js"; import { Authority } from "../authority/Authority.js"; import { RequestParameterBuilder } from "../request/RequestParameterBuilder.js"; import { GrantType, AuthenticationScheme, PromptValue, Separators, HeaderNames, } from "../utils/Constants.js"; import * as AADServerParamKeys from "../constants/AADServerParamKeys.js"; import { ClientConfiguration, isOidcProtocolMode, } from "../config/ClientConfiguration.js"; import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js"; import { NetworkResponse } from "../network/NetworkResponse.js"; import { ResponseHandler } from "../response/ResponseHandler.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import { StringUtils } from "../utils/StringUtils.js"; import { ClientAuthErrorCodes, 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 { RequestThumbprint } from "../network/RequestThumbprint.js"; import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js"; import * as TimeUtils from "../utils/TimeUtils.js"; import { AccountInfo } from "../account/AccountInfo.js"; import { buildClientInfoFromHomeAccountId, buildClientInfo, } from "../account/ClientInfo.js"; import { CcsCredentialType, CcsCredential } from "../account/CcsCredential.js"; import { createClientConfigurationError, ClientConfigurationErrorCodes, } from "../error/ClientConfigurationError.js"; import { RequestValidator } from "../request/RequestValidator.js"; import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js"; import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js"; import { invokeAsync } from "../utils/FunctionWrappers.js"; import { ClientAssertion } from "../account/ClientCredentials.js"; import { getClientAssertion } from "../utils/ClientAssertionUtils.js"; /** * Oauth2.0 Authorization Code client * @internal */ export class AuthorizationCodeClient extends BaseClient { // Flag to indicate if client is for hybrid spa auth code redemption protected includeRedirectUri: boolean = true; private oidcDefaultScopes; constructor( configuration: ClientConfiguration, performanceClient?: IPerformanceClient ) { super(configuration, performanceClient); this.oidcDefaultScopes = 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<string> { 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 * @param request */ async acquireToken( request: CommonAuthorizationCodeRequest, authCodePayload?: AuthorizationCodePayload ): Promise<AuthenticationResult> { this.performanceClient?.addQueueMeasurement( PerformanceEvents.AuthClientAcquireToken, request.correlationId ); if (!request.code) { throw createClientAuthError( ClientAuthErrorCodes.requestCannotBeMade ); } const reqTimestamp = TimeUtils.nowSeconds(); const response = await invokeAsync( this.executeTokenRequest.bind(this), PerformanceEvents.AuthClientExecuteTokenRequest, this.logger, this.performanceClient, request.correlationId )(this.authority, request); // Retrieve requestId from response headers const requestId = response.headers?.[HeaderNames.X_MS_REQUEST_ID]; const responseHandler = new ResponseHandler( this.config.authOptions.clientId, this.cacheManager, this.cryptoUtils, this.logger, this.config.serializableCache, this.config.persistencePlugin, this.performanceClient ); // Validate response. This function throws a server error if an error is returned by the server. responseHandler.validateTokenResponse(response.body); return invokeAsync( responseHandler.handleServerTokenResponse.bind(responseHandler), PerformanceEvents.HandleServerTokenResponse, this.logger, this.performanceClient, request.correlationId )( response.body, this.authority, reqTimestamp, request, authCodePayload, undefined, undefined, undefined, requestId ); } /** * 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`. * @param authorityUri */ getLogoutUri(logoutRequest: CommonEndSessionRequest): string { // Throw error if logoutRequest is null/undefined if (!logoutRequest) { throw createClientConfigurationError( ClientConfigurationErrorCodes.logoutRequestEmpty ); } const queryString = this.createLogoutUrlQueryString(logoutRequest); // Construct logout URI return UrlString.appendQueryString( this.authority.endSessionEndpoint, queryString ); } /** * Executes POST request to token endpoint * @param authority * @param request */ private async executeTokenRequest( authority: Authority, request: CommonAuthorizationCodeRequest ): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> { this.performanceClient?.addQueueMeasurement( PerformanceEvents.AuthClientExecuteTokenRequest, request.correlationId ); const queryParametersString = this.createTokenQueryParameters(request); const endpoint = UrlString.appendQueryString( authority.tokenEndpoint, queryParametersString ); const requestBody = await invokeAsync( this.createTokenRequestBody.bind(this), PerformanceEvents.AuthClientCreateTokenRequestBody, this.logger, this.performanceClient, request.correlationId )(request); let ccsCredential: CcsCredential | undefined = undefined; if (request.clientInfo) { try { const clientInfo = buildClientInfo( request.clientInfo, this.cryptoUtils.base64Decode ); ccsCredential = { credential: `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`, type: CcsCredentialType.HOME_ACCOUNT_ID, }; } catch (e) { this.logger.verbose( "Could not parse client info for CCS Header: " + e ); } } const headers: Record<string, string> = this.createTokenRequestHeaders( ccsCredential || request.ccsCredential ); const thumbprint: RequestThumbprint = { clientId: request.tokenBodyParameters?.clientId || this.config.authOptions.clientId, authority: authority.canonicalAuthority, scopes: request.scopes, claims: request.claims, authenticationScheme: request.authenticationScheme, resourceRequestMethod: request.resourceRequestMethod, resourceRequestUri: request.resourceRequestUri, shrClaims: request.shrClaims, sshKid: request.sshKid, }; return invokeAsync( this.executePostToTokenEndpoint.bind(this), PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint, this.logger, this.performanceClient, request.correlationId )( endpoint, requestBody, headers, thumbprint, request.correlationId, PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint ); } /** * Generates a map for all the params to be sent to the service * @param request */ private async createTokenRequestBody( request: CommonAuthorizationCodeRequest ): Promise<string> { this.performanceClient?.addQueueMeasurement( PerformanceEvents.AuthClientCreateTokenRequestBody, request.correlationId ); const parameterBuilder = new RequestParameterBuilder( request.correlationId, this.performanceClient ); parameterBuilder.addClientId( request.embeddedClientId || request.tokenBodyParameters?.[AADServerParamKeys.CLIENT_ID] || this.config.authOptions.clientId ); /* * For hybrid spa flow, there will be a code but no verifier * In this scenario, don't include redirect uri as auth code will not be bound to redirect URI */ if (!this.includeRedirectUri) { // Just validate RequestValidator.validateRedirectUri(request.redirectUri); } else { // Validate and include redirect uri parameterBuilder.addRedirectUri(request.redirectUri); } // Add scope array, parameter builder will add default scopes and dedupe parameterBuilder.addScopes( request.scopes, true, this.oidcDefaultScopes ); // add code: user set, not validated parameterBuilder.addAuthorizationCode(request.code); // Add library metadata parameterBuilder.addLibraryInfo(this.config.libraryInfo); parameterBuilder.addApplicationTelemetry( this.config.telemetry.application ); parameterBuilder.addThrottling(); if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) { parameterBuilder.addServerTelemetry(this.serverTelemetryManager); } // add code_verifier if passed if (request.codeVerifier) { parameterBuilder.addCodeVerifier(request.codeVerifier); } if (this.config.clientCredentials.clientSecret) { parameterBuilder.addClientSecret( this.config.clientCredentials.clientSecret ); } if (this.config.clientCredentials.clientAssertion) { const clientAssertion: ClientAssertion = this.config.clientCredentials.clientAssertion; parameterBuilder.addClientAssertion( await getClientAssertion( clientAssertion.assertion, this.config.authOptions.clientId, request.resourceRequestUri ) ); parameterBuilder.addClientAssertionType( clientAssertion.assertionType ); } parameterBuilder.addGrantType(GrantType.AUTHORIZATION_CODE_GRANT); parameterBuilder.addClientInfo(); if (request.authenticationScheme === AuthenticationScheme.POP) { const popTokenGenerator = new PopTokenGenerator( this.cryptoUtils, this.performanceClient ); 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); } // SPA PoP requires full Base64Url encoded req_cnf string (unhashed) parameterBuilder.addPopToken(reqCnfData); } else if (request.authenticationScheme === AuthenticationScheme.SSH) { if (request.sshJwk) { parameterBuilder.addSshJwk(request.sshJwk); } else { throw createClientConfigurationError( ClientConfigurationErrorCodes.missingSshJwk ); } } if ( !StringUtils.isEmptyObj(request.claims) || (this.config.authOptions.clientCapabilities && this.config.authOptions.clientCapabilities.length > 0) ) { parameterBuilder.addClaims( request.claims, this.config.authOptions.clientCapabilities ); } let ccsCred: CcsCredential | undefined = undefined; if (request.clientInfo) { try { const clientInfo = buildClientInfo( request.clientInfo, this.cryptoUtils.base64Decode ); ccsCred = { credential: `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`, type: CcsCredentialType.HOME_ACCOUNT_ID, }; } catch (e) { this.logger.verbose( "Could not parse client info for CCS Header: " + e ); } } else { ccsCred = request.ccsCredential; } // Adds these as parameters in the request instead of headers to prevent CORS preflight request if (this.config.systemOptions.preventCorsPreflight && ccsCred) { switch (ccsCred.type) { case CcsCredentialType.HOME_ACCOUNT_ID: try { const clientInfo = buildClientInfoFromHomeAccountId( ccsCred.credential ); parameterBuilder.addCcsOid(clientInfo); } catch (e) { this.logger.verbose( "Could not parse home account ID for CCS Header: " + e ); } break; case CcsCredentialType.UPN: parameterBuilder.addCcsUpn(ccsCred.credential); break; } } if (request.embeddedClientId) { parameterBuilder.addBrokerParameters({ brokerClientId: this.config.authOptions.clientId, brokerRedirectUri: this.config.authOptions.redirectUri, }); } if (request.tokenBodyParameters) { parameterBuilder.addExtraQueryParameters( request.tokenBodyParameters ); } // Add hybrid spa parameters if not already provided if ( request.enableSpaAuthorizationCode && (!request.tokenBodyParameters || !request.tokenBodyParameters[ AADServerParamKeys.RETURN_SPA_CODE ]) ) { parameterBuilder.addExtraQueryParameters({ [AADServerParamKeys.RETURN_SPA_CODE]: "1", }); } return parameterBuilder.createQueryString(); } /** * This API validates the `AuthorizationCodeUrlRequest` and creates a URL * @param request */ private async createAuthCodeUrlQueryString( request: CommonAuthorizationUrlRequest ): Promise<string> { // 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 parameterBuilder = new RequestParameterBuilder( correlationId, this.performanceClient ); parameterBuilder.addClientId( request.embeddedClientId || request.extraQueryParameters?.[AADServerParamKeys.CLIENT_ID] || this.config.authOptions.clientId ); const requestScopes = [ ...(request.scopes || []), ...(request.extraScopesToConsent || []), ]; parameterBuilder.addScopes(requestScopes, true, this.oidcDefaultScopes); // validate the redirectUri (to be a non null value) parameterBuilder.addRedirectUri(request.redirectUri); parameterBuilder.addCorrelationId(correlationId); // add response_mode. If not passed in it defaults to query. parameterBuilder.addResponseMode(request.responseMode); // add response_type = code parameterBuilder.addResponseTypeCode(); // add library info parameters parameterBuilder.addLibraryInfo(this.config.libraryInfo); if (!isOidcProtocolMode(this.config)) { parameterBuilder.addApplicationTelemetry( this.config.telemetry.application ); } // add client_info=1 parameterBuilder.addClientInfo(); if (request.codeChallenge && request.codeChallengeMethod) { parameterBuilder.addCodeChallengeParams( request.codeChallenge, request.codeChallengeMethod ); } if (request.prompt) { parameterBuilder.addPrompt(request.prompt); } if (request.domainHint) { parameterBuilder.addDomainHint(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" ); parameterBuilder.addSid(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" ); parameterBuilder.addLoginHint(accountLoginHintClaim); this.performanceClient?.addFields( { loginHintFromClaim: true }, correlationId ); try { const clientInfo = buildClientInfoFromHomeAccountId( request.account.homeAccountId ); parameterBuilder.addCcsOid(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" ); parameterBuilder.addSid(accountSid); this.performanceClient?.addFields( { sidFromClaim: true }, correlationId ); try { const clientInfo = buildClientInfoFromHomeAccountId( request.account.homeAccountId ); parameterBuilder.addCcsOid(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" ); parameterBuilder.addLoginHint(request.loginHint); parameterBuilder.addCcsUpn(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" ); parameterBuilder.addLoginHint(request.account.username); this.performanceClient?.addFields( { loginHintFromUpn: true }, correlationId ); try { const clientInfo = buildClientInfoFromHomeAccountId( request.account.homeAccountId ); parameterBuilder.addCcsOid(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" ); parameterBuilder.addLoginHint(request.loginHint); parameterBuilder.addCcsUpn(request.loginHint); this.performanceClient?.addFields( { loginHintFromRequest: true }, correlationId ); } } else { this.logger.verbose( "createAuthCodeUrlQueryString: Prompt is select_account, ignoring account hints" ); } if (request.nonce) { parameterBuilder.addNonce(request.nonce); } if (request.state) { parameterBuilder.addState(request.state); } if ( request.claims || (this.config.authOptions.clientCapabilities && this.config.authOptions.clientCapabilities.length > 0) ) { parameterBuilder.addClaims( request.claims, this.config.authOptions.clientCapabilities ); } if (request.embeddedClientId) { parameterBuilder.addBrokerParameters({ brokerClientId: this.config.authOptions.clientId, brokerRedirectUri: this.config.authOptions.redirectUri, }); } this.addExtraQueryParams(request, parameterBuilder); if (request.platformBroker) { // signal ests that this is a WAM call parameterBuilder.addNativeBroker(); // 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); } parameterBuilder.addPopToken(reqCnfData); } } return parameterBuilder.createQueryString(); } /** * This API validates the `EndSessionRequest` and creates a URL * @param request */ private createLogoutUrlQueryString( request: CommonEndSessionRequest ): string { const parameterBuilder = new RequestParameterBuilder( request.correlationId, this.performanceClient ); if (request.postLogoutRedirectUri) { parameterBuilder.addPostLogoutRedirectUri( request.postLogoutRedirectUri ); } if (request.correlationId) { parameterBuilder.addCorrelationId(request.correlationId); } if (request.idTokenHint) { parameterBuilder.addIdTokenHint(request.idTokenHint); } if (request.state) { parameterBuilder.addState(request.state); } if (request.logoutHint) { parameterBuilder.addLogoutHint(request.logoutHint); } this.addExtraQueryParams(request, parameterBuilder); return parameterBuilder.createQueryString(); } private addExtraQueryParams( request: CommonAuthorizationUrlRequest | CommonEndSessionRequest, parameterBuilder: RequestParameterBuilder ) { const hasRequestInstanceAware = request.extraQueryParameters && request.extraQueryParameters.hasOwnProperty("instance_aware"); // Set instance_aware flag if config auth param is set if (!hasRequestInstanceAware && this.config.authOptions.instanceAware) { request.extraQueryParameters = request.extraQueryParameters || {}; request.extraQueryParameters["instance_aware"] = "true"; } if (request.extraQueryParameters) { parameterBuilder.addExtraQueryParameters( request.extraQueryParameters ); } } /** * 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; } }