/*! @azure/msal-common v14.4.0 2023-11-07 */ 'use strict'; import { BaseClient } from './BaseClient.mjs'; import { RequestParameterBuilder } from '../request/RequestParameterBuilder.mjs'; import { Separators, AADServerParamKeys, GrantType, AuthenticationScheme, PromptValue, HeaderNames } from '../utils/Constants.mjs'; import { isOidcProtocolMode } from '../config/ClientConfiguration.mjs'; import { ResponseHandler } from '../response/ResponseHandler.mjs'; import { StringUtils } from '../utils/StringUtils.mjs'; import { createClientAuthError } from '../error/ClientAuthError.mjs'; import { UrlString } from '../url/UrlString.mjs'; import { PopTokenGenerator } from '../crypto/PopTokenGenerator.mjs'; import { TimeUtils } from '../utils/TimeUtils.mjs'; import { buildClientInfo, buildClientInfoFromHomeAccountId } from '../account/ClientInfo.mjs'; import { CcsCredentialType } from '../account/CcsCredential.mjs'; import { createClientConfigurationError } from '../error/ClientConfigurationError.mjs'; import { RequestValidator } from '../request/RequestValidator.mjs'; import { PerformanceEvents } from '../telemetry/performance/PerformanceEvent.mjs'; import { invokeAsync } from '../utils/FunctionWrappers.mjs'; import { requestCannotBeMade, authorizationCodeMissingFromServerResponse } from '../error/ClientAuthErrorCodes.mjs'; import { logoutRequestEmpty, missingSshJwk } from '../error/ClientConfigurationErrorCodes.mjs'; /* * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ /** * Oauth2.0 Authorization Code client * @internal */ class AuthorizationCodeClient extends BaseClient { constructor(configuration, performanceClient) { super(configuration, performanceClient); // Flag to indicate if client is for hybrid spa auth code redemption this.includeRedirectUri = true; 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) { 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, authCodePayload) { this.performanceClient?.addQueueMeasurement(PerformanceEvents.AuthClientAcquireToken, request.correlationId); if (!request.code) { throw createClientAuthError(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, cachedState) { // 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(authorizationCodeMissingFromServerResponse); } return serverParams; } /** * 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) { // Throw error if logoutRequest is null/undefined if (!logoutRequest) { throw createClientConfigurationError(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 */ async executeTokenRequest(authority, request) { 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 = undefined; if (request.clientInfo) { try { const clientInfo = buildClientInfo(request.clientInfo, this.cryptoUtils); 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 = this.createTokenRequestHeaders(ccsCredential || request.ccsCredential); const thumbprint = { 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 */ async createTokenRequestBody(request) { this.performanceClient?.addQueueMeasurement(PerformanceEvents.AuthClientCreateTokenRequestBody, request.correlationId); const parameterBuilder = new RequestParameterBuilder(); parameterBuilder.addClientId(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 = this.config.clientCredentials.clientAssertion; parameterBuilder.addClientAssertion(clientAssertion.assertion); 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); const reqCnfData = await invokeAsync(popTokenGenerator.generateCnf.bind(popTokenGenerator), PerformanceEvents.PopTokenGenerateCnf, this.logger, this.performanceClient, request.correlationId)(request, this.logger); // SPA PoP requires full Base64Url encoded req_cnf string (unhashed) parameterBuilder.addPopToken(reqCnfData.reqCnfString); } else if (request.authenticationScheme === AuthenticationScheme.SSH) { if (request.sshJwk) { parameterBuilder.addSshJwk(request.sshJwk); } else { throw createClientConfigurationError(missingSshJwk); } } const correlationId = request.correlationId || this.config.cryptoInterface.createNewGuid(); parameterBuilder.addCorrelationId(correlationId); 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 = undefined; if (request.clientInfo) { try { const clientInfo = buildClientInfo(request.clientInfo, this.cryptoUtils); 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.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 */ async createAuthCodeUrlQueryString(request) { this.performanceClient?.addQueueMeasurement(PerformanceEvents.AuthClientCreateQueryString, request.correlationId); const parameterBuilder = new RequestParameterBuilder(); parameterBuilder.addClientId(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); // generate the correlationId if not set by the user and add const correlationId = request.correlationId || this.config.cryptoInterface.createNewGuid(); 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); } // 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); } else if (request.account) { const accountSid = this.extractAccountSid(request.account); const accountLoginHintClaim = this.extractLoginHint(request.account); // 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); 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); 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); } 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); 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); } } 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.extraQueryParameters) { parameterBuilder.addExtraQueryParameters(request.extraQueryParameters); } if (request.nativeBroker) { // 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); // to reduce the URL length, it is recommended to send the hash of the req_cnf instead of the whole string const reqCnfData = await invokeAsync(popTokenGenerator.generateCnf.bind(popTokenGenerator), PerformanceEvents.PopTokenGenerateCnf, this.logger, this.performanceClient, request.correlationId)(request, this.logger); parameterBuilder.addPopToken(reqCnfData.reqCnfHash); } } return parameterBuilder.createQueryString(); } /** * This API validates the `EndSessionRequest` and creates a URL * @param request */ createLogoutUrlQueryString(request) { const parameterBuilder = new RequestParameterBuilder(); 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); } if (request.extraQueryParameters) { parameterBuilder.addExtraQueryParameters(request.extraQueryParameters); } return parameterBuilder.createQueryString(); } /** * Helper to get sid from account. Returns null if idTokenClaims are not present or sid is not present. * @param account */ extractAccountSid(account) { return account.idTokenClaims?.sid || null; } extractLoginHint(account) { return account.idTokenClaims?.login_hint || null; } } export { AuthorizationCodeClient }; //# sourceMappingURL=AuthorizationCodeClient.mjs.map