/*
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Licensed under the MIT License.
|
*/
|
|
import { CommonAuthorizationCodeRequest } from "../request/CommonAuthorizationCodeRequest.js";
|
import { Authority } from "../authority/Authority.js";
|
import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js";
|
import * as UrlUtils from "../utils/UrlUtils.js";
|
import * as Constants from "../utils/Constants.js";
|
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
|
import {
|
buildClientConfiguration,
|
ClientConfiguration,
|
CommonClientConfiguration,
|
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 {
|
ClientAuthErrorCodes,
|
createClientAuthError,
|
} from "../error/ClientAuthError.js";
|
import { UrlString } from "../url/UrlString.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 {
|
buildClientInfoFromHomeAccountId,
|
buildClientInfo,
|
} from "../account/ClientInfo.js";
|
import { CcsCredentialType, CcsCredential } from "../account/CcsCredential.js";
|
import {
|
createClientConfigurationError,
|
ClientConfigurationErrorCodes,
|
} from "../error/ClientConfigurationError.js";
|
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
|
import * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.js";
|
import { invokeAsync } from "../utils/FunctionWrappers.js";
|
import { ClientAssertion } from "../account/ClientCredentials.js";
|
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
|
import { getRequestThumbprint } from "../network/RequestThumbprint.js";
|
import {
|
createTokenQueryParameters,
|
createTokenRequestHeaders,
|
executePostToTokenEndpoint,
|
} from "../protocol/Token.js";
|
import { createDiscoveredInstance } from "../authority/AuthorityFactory.js";
|
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
|
import { Logger } from "../logger/Logger.js";
|
import { ICrypto } from "../crypto/ICrypto.js";
|
import { CacheManager } from "../cache/CacheManager.js";
|
import { INetworkModule } from "../network/INetworkModule.js";
|
import { version, name } from "../packageMetadata.js";
|
|
/**
|
* Oauth2.0 Authorization Code client
|
* @internal
|
*/
|
export class AuthorizationCodeClient {
|
// Flag to indicate if client is for hybrid spa auth code redemption
|
protected includeRedirectUri: boolean = true;
|
private oidcDefaultScopes;
|
|
// Logger object
|
public logger: Logger;
|
|
// Application config
|
protected config: CommonClientConfiguration;
|
|
// Crypto Interface
|
protected cryptoUtils: ICrypto;
|
|
// Storage Interface
|
protected cacheManager: CacheManager;
|
|
// Network Interface
|
protected networkClient: INetworkModule;
|
|
// Server Telemetry Manager
|
protected serverTelemetryManager: ServerTelemetryManager | null;
|
|
// Default authority object
|
public authority: Authority;
|
|
// Performance telemetry client
|
protected performanceClient: IPerformanceClient;
|
|
constructor(
|
configuration: ClientConfiguration,
|
performanceClient: IPerformanceClient
|
) {
|
// Set the configuration
|
this.config = buildClientConfiguration(configuration);
|
|
// Initialize the logger
|
this.logger = new Logger(this.config.loggerOptions, name, version);
|
|
// Initialize crypto
|
this.cryptoUtils = this.config.cryptoInterface;
|
|
// Initialize storage interface
|
this.cacheManager = this.config.storageInterface;
|
|
// Set the network interface
|
this.networkClient = this.config.networkInterface;
|
|
// Set TelemetryManager
|
this.serverTelemetryManager = this.config.serverTelemetryManager;
|
|
// set Authority
|
this.authority = this.config.authOptions.authority;
|
|
// set performance telemetry client
|
this.performanceClient = performanceClient;
|
this.oidcDefaultScopes =
|
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes;
|
}
|
|
/**
|
* 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,
|
apiId: number,
|
authCodePayload?: AuthorizationCodePayload
|
): Promise<AuthenticationResult> {
|
if (!request.code) {
|
throw createClientAuthError(
|
ClientAuthErrorCodes.requestCannotBeMade
|
);
|
}
|
|
// Check for new cloud instance
|
if (authCodePayload && authCodePayload.cloud_instance_host_name) {
|
await invokeAsync(
|
this.updateTokenEndpointAuthority.bind(this),
|
PerformanceEvents.UpdateTokenEndpointAuthority,
|
this.logger,
|
this.performanceClient,
|
request.correlationId
|
)(authCodePayload.cloud_instance_host_name, request.correlationId);
|
}
|
|
const reqTimestamp = TimeUtils.nowSeconds();
|
const response = await invokeAsync(
|
this.executeTokenRequest.bind(this),
|
PerformanceEvents.AuthClientExecuteTokenRequest,
|
this.logger,
|
this.performanceClient,
|
request.correlationId
|
)(this.authority, request, this.serverTelemetryManager);
|
|
// Retrieve requestId from response headers
|
const requestId =
|
response.headers?.[Constants.HeaderNames.X_MS_REQUEST_ID];
|
|
const responseHandler = new ResponseHandler(
|
this.config.authOptions.clientId,
|
this.cacheManager,
|
this.cryptoUtils,
|
this.logger,
|
this.performanceClient,
|
this.config.serializableCache,
|
this.config.persistencePlugin
|
);
|
|
// Validate response. This function throws a server error if an error is returned by the server.
|
responseHandler.validateTokenResponse(
|
response.body,
|
request.correlationId
|
);
|
|
return invokeAsync(
|
responseHandler.handleServerTokenResponse.bind(responseHandler),
|
PerformanceEvents.HandleServerTokenResponse,
|
this.logger,
|
this.performanceClient,
|
request.correlationId
|
)(
|
response.body,
|
this.authority,
|
reqTimestamp,
|
request,
|
apiId,
|
authCodePayload,
|
undefined,
|
undefined,
|
undefined,
|
requestId
|
);
|
}
|
|
/**
|
* 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,
|
serverTelemetryManager: ServerTelemetryManager | null
|
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
|
const queryParametersString = createTokenQueryParameters(
|
request,
|
this.config.authOptions.clientId,
|
this.config.authOptions.redirectUri,
|
this.performanceClient
|
);
|
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}${Constants.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
|
type: CcsCredentialType.HOME_ACCOUNT_ID,
|
};
|
} catch (e) {
|
this.logger.verbose(
|
`Could not parse client info for CCS Header: '${e}'`,
|
request.correlationId
|
);
|
}
|
}
|
const headers: Record<string, string> = createTokenRequestHeaders(
|
this.logger,
|
this.config.systemOptions.preventCorsPreflight,
|
ccsCredential || request.ccsCredential
|
);
|
|
const thumbprint = getRequestThumbprint(
|
this.config.authOptions.clientId,
|
request
|
);
|
|
return invokeAsync(
|
executePostToTokenEndpoint,
|
PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint,
|
this.logger,
|
this.performanceClient,
|
request.correlationId
|
)(
|
endpoint,
|
requestBody,
|
headers,
|
thumbprint,
|
request.correlationId,
|
this.cacheManager,
|
this.networkClient,
|
this.logger,
|
this.performanceClient,
|
serverTelemetryManager
|
);
|
}
|
|
/**
|
* Generates a map for all the params to be sent to the service
|
* @param request
|
*/
|
private async createTokenRequestBody(
|
request: CommonAuthorizationCodeRequest
|
): Promise<string> {
|
const parameters = new Map<string, string>();
|
|
RequestParameterBuilder.addClientId(
|
parameters,
|
request.embeddedClientId ||
|
request.extraParameters?.[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
|
if (!request.redirectUri) {
|
throw createClientConfigurationError(
|
ClientConfigurationErrorCodes.redirectUriEmpty
|
);
|
}
|
} else {
|
// Validate and include redirect uri
|
RequestParameterBuilder.addRedirectUri(
|
parameters,
|
request.redirectUri
|
);
|
}
|
|
// Add scope array, parameter builder will add default scopes and dedupe
|
RequestParameterBuilder.addScopes(
|
parameters,
|
request.scopes,
|
true,
|
this.oidcDefaultScopes
|
);
|
|
RequestParameterBuilder.addResource(parameters, request.resource);
|
|
// add code: user set, not validated
|
RequestParameterBuilder.addAuthorizationCode(parameters, request.code);
|
|
// Add library metadata
|
RequestParameterBuilder.addLibraryInfo(
|
parameters,
|
this.config.libraryInfo
|
);
|
RequestParameterBuilder.addApplicationTelemetry(
|
parameters,
|
this.config.telemetry.application
|
);
|
RequestParameterBuilder.addThrottling(parameters);
|
|
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
|
RequestParameterBuilder.addServerTelemetry(
|
parameters,
|
this.serverTelemetryManager
|
);
|
}
|
|
// add code_verifier if passed
|
if (request.codeVerifier) {
|
RequestParameterBuilder.addCodeVerifier(
|
parameters,
|
request.codeVerifier
|
);
|
}
|
|
if (this.config.clientCredentials.clientSecret) {
|
RequestParameterBuilder.addClientSecret(
|
parameters,
|
this.config.clientCredentials.clientSecret
|
);
|
}
|
|
if (this.config.clientCredentials.clientAssertion) {
|
const clientAssertion: ClientAssertion =
|
this.config.clientCredentials.clientAssertion;
|
|
RequestParameterBuilder.addClientAssertion(
|
parameters,
|
await getClientAssertion(
|
clientAssertion.assertion,
|
this.config.authOptions.clientId,
|
request.resourceRequestUri
|
)
|
);
|
RequestParameterBuilder.addClientAssertionType(
|
parameters,
|
clientAssertion.assertionType
|
);
|
}
|
|
RequestParameterBuilder.addGrantType(
|
parameters,
|
Constants.GrantType.AUTHORIZATION_CODE_GRANT
|
);
|
RequestParameterBuilder.addClientInfo(parameters);
|
|
if (
|
request.authenticationScheme === Constants.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)
|
RequestParameterBuilder.addPopToken(parameters, reqCnfData);
|
} else if (
|
request.authenticationScheme === Constants.AuthenticationScheme.SSH
|
) {
|
if (request.sshJwk) {
|
RequestParameterBuilder.addSshJwk(parameters, request.sshJwk);
|
} else {
|
throw createClientConfigurationError(
|
ClientConfigurationErrorCodes.missingSshJwk
|
);
|
}
|
}
|
|
let ccsCred: CcsCredential | undefined = undefined;
|
if (request.clientInfo) {
|
try {
|
const clientInfo = buildClientInfo(
|
request.clientInfo,
|
this.cryptoUtils.base64Decode
|
);
|
ccsCred = {
|
credential: `${clientInfo.uid}${Constants.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
|
type: CcsCredentialType.HOME_ACCOUNT_ID,
|
};
|
} catch (e) {
|
this.logger.verbose(
|
`Could not parse client info for CCS Header: '${e}'`,
|
request.correlationId
|
);
|
}
|
} 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
|
);
|
RequestParameterBuilder.addCcsOid(
|
parameters,
|
clientInfo
|
);
|
} catch (e) {
|
this.logger.verbose(
|
`Could not parse home account ID for CCS Header: '${e}'`,
|
request.correlationId
|
);
|
}
|
break;
|
case CcsCredentialType.UPN:
|
RequestParameterBuilder.addCcsUpn(
|
parameters,
|
ccsCred.credential
|
);
|
break;
|
}
|
}
|
|
if (request.embeddedClientId) {
|
RequestParameterBuilder.addBrokerParameters(
|
parameters,
|
this.config.authOptions.clientId,
|
this.config.authOptions.redirectUri
|
);
|
}
|
|
if (request.extraParameters) {
|
RequestParameterBuilder.addExtraParameters(
|
parameters,
|
request.extraParameters
|
);
|
}
|
|
// Add hybrid spa parameters if not already provided
|
if (
|
request.enableSpaAuthorizationCode &&
|
(!request.extraParameters ||
|
!request.extraParameters[AADServerParamKeys.RETURN_SPA_CODE])
|
) {
|
RequestParameterBuilder.addExtraParameters(parameters, {
|
[AADServerParamKeys.RETURN_SPA_CODE]: "1",
|
});
|
}
|
|
RequestParameterBuilder.instrumentBrokerParams(
|
parameters,
|
request.correlationId,
|
this.performanceClient
|
);
|
|
RequestParameterBuilder.addClaims(
|
parameters,
|
request.claims,
|
this.config.authOptions.clientCapabilities,
|
request.skipBrokerClaims
|
);
|
|
return UrlUtils.mapToQueryString(parameters);
|
}
|
|
/**
|
* This API validates the `EndSessionRequest` and creates a URL
|
* @param request
|
*/
|
private createLogoutUrlQueryString(
|
request: CommonEndSessionRequest
|
): string {
|
const parameters = new Map<string, string>();
|
|
if (request.postLogoutRedirectUri) {
|
RequestParameterBuilder.addPostLogoutRedirectUri(
|
parameters,
|
request.postLogoutRedirectUri
|
);
|
}
|
|
if (request.correlationId) {
|
RequestParameterBuilder.addCorrelationId(
|
parameters,
|
request.correlationId
|
);
|
}
|
|
if (request.idTokenHint) {
|
RequestParameterBuilder.addIdTokenHint(
|
parameters,
|
request.idTokenHint
|
);
|
}
|
|
if (request.state) {
|
RequestParameterBuilder.addState(parameters, request.state);
|
}
|
|
if (request.logoutHint) {
|
RequestParameterBuilder.addLogoutHint(
|
parameters,
|
request.logoutHint
|
);
|
}
|
|
if (request.extraQueryParameters) {
|
RequestParameterBuilder.addExtraParameters(
|
parameters,
|
request.extraQueryParameters
|
);
|
}
|
|
if (this.config.authOptions.instanceAware) {
|
RequestParameterBuilder.addInstanceAware(parameters);
|
}
|
|
return UrlUtils.mapToQueryString(parameters);
|
}
|
|
/**
|
* Updates the authority to the cloud instance provided in the authorization response
|
* @param cloudInstanceHostName - cloud instance host name from authorization code payload
|
* @param correlationId - request correlation id
|
*/
|
private async updateTokenEndpointAuthority(
|
cloudInstanceHostName: string,
|
correlationId: string
|
): Promise<void> {
|
const cloudInstanceAuthorityUri = `https://${cloudInstanceHostName}/${this.authority.tenant}/`;
|
const cloudInstanceAuthority = await createDiscoveredInstance(
|
cloudInstanceAuthorityUri,
|
this.networkClient,
|
this.cacheManager,
|
this.authority.options,
|
this.logger,
|
correlationId,
|
this.performanceClient
|
);
|
this.authority = cloudInstanceAuthority;
|
}
|
}
|