diff --git a/change/@azure-msal-common-d9223c68-d959-4da8-8a74-caa4f751c48a.json b/change/@azure-msal-common-d9223c68-d959-4da8-8a74-caa4f751c48a.json new file mode 100644 index 0000000000..7192c702eb --- /dev/null +++ b/change/@azure-msal-common-d9223c68-d959-4da8-8a74-caa4f751c48a.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Client Assertion Implementation now accepts a callback instead of a string argument", + "packageName": "@azure/msal-common", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-93a9e21c-c628-4ae1-ad55-2e1217792f32.json b/change/@azure-msal-node-93a9e21c-c628-4ae1-ad55-2e1217792f32.json new file mode 100644 index 0000000000..b18631984a --- /dev/null +++ b/change/@azure-msal-node-93a9e21c-c628-4ae1-ad55-2e1217792f32.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Client Assertion Implementation now accepts a callback instead of a string argument", + "packageName": "@azure/msal-node", + "email": "rginsburg@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-common/src/account/ClientCredentials.ts b/lib/msal-common/src/account/ClientCredentials.ts index 8d18446d0a..6c148cf4f0 100644 --- a/lib/msal-common/src/account/ClientCredentials.ts +++ b/lib/msal-common/src/account/ClientCredentials.ts @@ -3,11 +3,20 @@ * Licensed under the MIT License. */ +export type ClientAssertionConfig = { + clientId: string; + tokenEndpoint?: string; +}; + +export type ClientAssertionCallback = ( + config: ClientAssertionConfig +) => Promise; + /** * Client Assertion credential for Confidential Clients */ export type ClientAssertion = { - assertion: string; + assertion: string | ClientAssertionCallback; assertionType: string; }; diff --git a/lib/msal-common/src/client/AuthorizationCodeClient.ts b/lib/msal-common/src/client/AuthorizationCodeClient.ts index e83d477912..4e914da11a 100644 --- a/lib/msal-common/src/client/AuthorizationCodeClient.ts +++ b/lib/msal-common/src/client/AuthorizationCodeClient.ts @@ -50,6 +50,8 @@ import { RequestValidator } from "../request/RequestValidator"; import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient"; import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent"; import { invokeAsync } from "../utils/FunctionWrappers"; +import { ClientAssertion } from "../account/ClientCredentials"; +import { getClientAssertion } from "../utils/ClientAssertionUtils"; /** * Oauth2.0 Authorization Code client @@ -364,9 +366,16 @@ export class AuthorizationCodeClient extends BaseClient { } if (this.config.clientCredentials.clientAssertion) { - const clientAssertion = + const clientAssertion: ClientAssertion = this.config.clientCredentials.clientAssertion; - parameterBuilder.addClientAssertion(clientAssertion.assertion); + + parameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + this.config.authOptions.clientId, + request.resourceRequestUri + ) + ); parameterBuilder.addClientAssertionType( clientAssertion.assertionType ); diff --git a/lib/msal-common/src/client/RefreshTokenClient.ts b/lib/msal-common/src/client/RefreshTokenClient.ts index fd89c0a061..0a34396174 100644 --- a/lib/msal-common/src/client/RefreshTokenClient.ts +++ b/lib/msal-common/src/client/RefreshTokenClient.ts @@ -48,6 +48,8 @@ import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent"; import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient"; import { invoke, invokeAsync } from "../utils/FunctionWrappers"; import { generateCredentialKey } from "../cache/utils/CacheHelpers"; +import { ClientAssertion } from "../account/ClientCredentials"; +import { getClientAssertion } from "../utils/ClientAssertionUtils"; const DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS = 300; // 5 Minutes @@ -385,9 +387,16 @@ export class RefreshTokenClient extends BaseClient { } if (this.config.clientCredentials.clientAssertion) { - const clientAssertion = + const clientAssertion: ClientAssertion = this.config.clientCredentials.clientAssertion; - parameterBuilder.addClientAssertion(clientAssertion.assertion); + + parameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + this.config.authOptions.clientId, + request.resourceRequestUri + ) + ); parameterBuilder.addClientAssertionType( clientAssertion.assertionType ); diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index 78116fa752..2bb6254320 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -133,7 +133,11 @@ export { NativeRequest } from "./request/NativeRequest"; export { NativeSignOutRequest } from "./request/NativeSignOutRequest"; export { RequestParameterBuilder } from "./request/RequestParameterBuilder"; export { StoreInCache } from "./request/StoreInCache"; -export { ClientAssertion } from "./account/ClientCredentials"; +export { + ClientAssertion, + ClientAssertionConfig, + ClientAssertionCallback, +} from "./account/ClientCredentials"; // Response export { AzureRegion } from "./authority/AzureRegion"; export { AzureRegionConfiguration } from "./authority/AzureRegionConfiguration"; @@ -182,6 +186,7 @@ export { createClientConfigurationError, } from "./error/ClientConfigurationError"; // Constants and Utils +export { getClientAssertion } from "./utils/ClientAssertionUtils"; export { Constants, OIDC_DEFAULT_SCOPES, @@ -218,6 +223,7 @@ export { } from "./utils/ProtocolUtils"; export * as TimeUtils from "./utils/TimeUtils"; export * as UrlUtils from "./utils/UrlUtils"; +export * as ClientAssertionUtils from "./utils/ClientAssertionUtils"; export * from "./utils/FunctionWrappers"; // Server Telemetry export { ServerTelemetryManager } from "./telemetry/server/ServerTelemetryManager"; diff --git a/lib/msal-common/src/utils/ClientAssertionUtils.ts b/lib/msal-common/src/utils/ClientAssertionUtils.ts new file mode 100644 index 0000000000..cb9c68c618 --- /dev/null +++ b/lib/msal-common/src/utils/ClientAssertionUtils.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ClientAssertionCallback, + ClientAssertionConfig, +} from "../account/ClientCredentials"; + +export async function getClientAssertion( + clientAssertion: string | ClientAssertionCallback, + clientId: string, + tokenEndpoint?: string +): Promise { + if (typeof clientAssertion === "string") { + return clientAssertion; + } else { + const config: ClientAssertionConfig = { + clientId: clientId, + tokenEndpoint: tokenEndpoint, + }; + return clientAssertion(config); + } +} diff --git a/lib/msal-common/test/request/RequestParameterBuilder.spec.ts b/lib/msal-common/test/request/RequestParameterBuilder.spec.ts index dd7be85090..56a3134891 100644 --- a/lib/msal-common/test/request/RequestParameterBuilder.spec.ts +++ b/lib/msal-common/test/request/RequestParameterBuilder.spec.ts @@ -23,6 +23,9 @@ import { ClientConfigurationErrorMessage, createClientConfigurationError, } from "../../src/error/ClientConfigurationError"; +import { ClientAssertion, ClientAssertionCallback } from "../../src"; +import { getClientAssertion } from "../../src/utils/ClientAssertionUtils"; +import { ClientAssertionConfig } from "../../src/account/ClientCredentials"; describe("RequestParameterBuilder unit tests", () => { it("constructor", () => { @@ -379,14 +382,20 @@ describe("RequestParameterBuilder unit tests", () => { sinon.restore(); }); - it("adds clientAssertion and assertionType if they are passed in as strings", () => { - const clientAssertion = { + it("adds clientAssertion (string) and assertionType if they are provided by the developer", async () => { + const clientAssertion: ClientAssertion = { assertion: "testAssertion", assertionType: "jwt-bearer", }; const requestParameterBuilder = new RequestParameterBuilder(); - requestParameterBuilder.addClientAssertion(clientAssertion.assertion); + requestParameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + "client_id", + "optional_token_endpoint" + ) + ); requestParameterBuilder.addClientAssertionType( clientAssertion.assertionType ); @@ -407,14 +416,94 @@ describe("RequestParameterBuilder unit tests", () => { ).toBe(true); }); - it("doesn't add client assertion and client assertion type if they are empty strings", () => { - const clientAssertion = { + it("doesn't add client assertion (string) and client assertion type if they are empty strings", async () => { + const clientAssertion: ClientAssertion = { assertion: "", assertionType: "", }; const requestParameterBuilder = new RequestParameterBuilder(); - requestParameterBuilder.addClientAssertion(clientAssertion.assertion); + requestParameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + "client_id", + "optional_token_endpoint" + ) + ); + requestParameterBuilder.addClientAssertionType( + clientAssertion.assertionType + ); + const requestQueryString = requestParameterBuilder.createQueryString(); + expect( + requestQueryString.includes(AADServerParamKeys.CLIENT_ASSERTION) + ).toBe(false); + expect( + requestQueryString.includes( + AADServerParamKeys.CLIENT_ASSERTION_TYPE + ) + ).toBe(false); + }); + + it("adds clientAssertion (ClientAssertionCallback) and assertionType if they are provided by the developer", async () => { + const ClientAssertionCallback: ClientAssertionCallback = ( + _config: ClientAssertionConfig + ) => { + return Promise.resolve("testAssertion"); + }; + + const clientAssertion: ClientAssertion = { + assertion: ClientAssertionCallback, + assertionType: "jwt-bearer", + }; + + const requestParameterBuilder = new RequestParameterBuilder(); + requestParameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + "client_id", + "optional_token_endpoint" + ) + ); + requestParameterBuilder.addClientAssertionType( + clientAssertion.assertionType + ); + const requestQueryString = requestParameterBuilder.createQueryString(); + expect( + requestQueryString.includes( + `${AADServerParamKeys.CLIENT_ASSERTION}=${encodeURIComponent( + "testAssertion" + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${ + AADServerParamKeys.CLIENT_ASSERTION_TYPE + }=${encodeURIComponent("jwt-bearer")}` + ) + ).toBe(true); + }); + + it("doesn't add client assertion (ClientAssertionCallback) and client assertion type if they are empty strings", async () => { + const ClientAssertionCallback: ClientAssertionCallback = ( + _config: ClientAssertionConfig + ) => { + return Promise.resolve(""); + }; + + const clientAssertion: ClientAssertion = { + assertion: ClientAssertionCallback, + assertionType: "", + }; + + const requestParameterBuilder = new RequestParameterBuilder(); + requestParameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + "client_id", + "optional_token_endpoint" + ) + ); requestParameterBuilder.addClientAssertionType( clientAssertion.assertionType ); diff --git a/lib/msal-node/docs/initialize-confidential-client-application.md b/lib/msal-node/docs/initialize-confidential-client-application.md index 03cab92b07..9e4ae04568 100644 --- a/lib/msal-node/docs/initialize-confidential-client-application.md +++ b/lib/msal-node/docs/initialize-confidential-client-application.md @@ -29,6 +29,16 @@ See the MSAL sample: [auth-code-with-certs](../../../samples/msal-node-samples/a import * as msal from "@azure/msal-node"; import "dotenv/config"; // process.env now has the values defined in a .env file +const clientAssertionCallback: msal.ClientAssertionCallback = async ( + config: msal.ClientAssertionConfig +): Promise => { + // network request that uses config.clientId and (optionally) config.tokenEndpoint + const result: Promise = await Promise.resolve( + "network request which gets assertion" + ); + return result; +}; + const clientConfig = { auth: { clientId: "your_client_id", @@ -38,7 +48,7 @@ const clientConfig = { thumbprint: process.env.thumbprint, privateKey: process.env.privateKey, }, // OR - clientAssertion: "assertion", + clientAssertion: clientAssertionCallback, // or a predetermined clientAssertion string }, }; const pca = new msal.ConfidentialClientApplication(clientConfig); @@ -53,7 +63,7 @@ const pca = new msal.ConfidentialClientApplication(clientConfig); - A Client credential is mandatory for confidential clients. Client credential can be a: - `clientSecret` is secret string generated set on the app registration. - `clientCertificate` is a certificate set on the app registration. The `thumbprint` is a X.509 SHA-1 thumbprint of the certificate, and the `privateKey` is the PEM encoded private key. `x5c` is the optional X.509 certificate chain used in [subject name/issuer auth scenarios](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/sni.md). - - `clientAssertion` is string that the application uses when requesting a token. The certificate used to sign the assertion should be set on the app registration. Assertion should be of type urn:ietf:params:oauth:client-assertion-type:jwt-bearer. + - `clientAssertion` is a ClientAssertion object containing an assertion string or a callback function that returns an assertion string that the application uses when requesting a token, as well as the assertion's type (urn:ietf:params:oauth:client-assertion-type:jwt-bearer). The callback is invoked every time MSAL needs to acquire a token from the token issuer. App developers should generally use the callback because assertions expire and new assertions need to be created. App developers are responsible for the assertion lifetime. Use [this mechanism](https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust) to get tokens for a downstream API using a Federated Identity Credential. ## Configure Authority diff --git a/lib/msal-node/src/client/ClientApplication.ts b/lib/msal-node/src/client/ClientApplication.ts index 6015ef446d..67c045e355 100644 --- a/lib/msal-node/src/client/ClientApplication.ts +++ b/lib/msal-node/src/client/ClientApplication.ts @@ -33,6 +33,9 @@ import { createClientAuthError, ClientAuthErrorCodes, buildStaticAuthorityOptions, + ClientAssertion as ClientAssertionType, + getClientAssertion, + ClientAssertionCallback, } from "@azure/msal-common"; import { Configuration, @@ -77,6 +80,9 @@ export abstract class ClientApplication { * Client assertion passed by the user for confidential client flows */ protected clientAssertion: ClientAssertion; + protected developerProvidedClientAssertion: + | string + | ClientAssertionCallback; /** * Client secret passed by the user for confidential client flows */ @@ -451,8 +457,8 @@ export abstract class ClientApplication { serverTelemetryManager: serverTelemetryManager, clientCredentials: { clientSecret: this.clientSecret, - clientAssertion: this.clientAssertion - ? this.getClientAssertion(discoveredAuthority) + clientAssertion: this.developerProvidedClientAssertion + ? await this.getClientAssertion(discoveredAuthority) : undefined, }, libraryInfo: { @@ -469,10 +475,17 @@ export abstract class ClientApplication { return clientConfiguration; } - private getClientAssertion(authority: Authority): { - assertion: string; - assertionType: string; - } { + private async getClientAssertion( + authority: Authority + ): Promise { + this.clientAssertion = ClientAssertion.fromAssertion( + await getClientAssertion( + this.developerProvidedClientAssertion, + this.config.auth.clientId, + authority.tokenEndpoint + ) + ); + return { assertion: this.clientAssertion.getJwt( this.cryptoProvider, diff --git a/lib/msal-node/src/client/ClientCredentialClient.ts b/lib/msal-node/src/client/ClientCredentialClient.ts index 6fe7c2c0e7..2198db3b79 100644 --- a/lib/msal-node/src/client/ClientCredentialClient.ts +++ b/lib/msal-node/src/client/ClientCredentialClient.ts @@ -32,6 +32,8 @@ import { TokenCacheContext, UrlString, createClientAuthError, + ClientAssertion, + getClientAssertion, } from "@azure/msal-common"; import { ManagedIdentityConfiguration, @@ -270,7 +272,7 @@ export class ClientCredentialClient extends BaseClient { queryParametersString ); - const requestBody = this.createTokenRequestBody(request); + const requestBody = await this.createTokenRequestBody(request); const headers: Record = this.createTokenRequestHeaders(); const thumbprint: RequestThumbprint = { @@ -330,9 +332,9 @@ export class ClientCredentialClient extends BaseClient { * generate the request to the server in the acceptable format * @param request */ - private createTokenRequestBody( + private async createTokenRequestBody( request: CommonClientCredentialRequest - ): string { + ): Promise { const parameterBuilder = new RequestParameterBuilder(); parameterBuilder.addClientId(this.config.authOptions.clientId); @@ -364,12 +366,18 @@ export class ClientCredentialClient extends BaseClient { } // Use clientAssertion from request, fallback to client assertion in base configuration - const clientAssertion = + const clientAssertion: ClientAssertion | undefined = request.clientAssertion || this.config.clientCredentials.clientAssertion; if (clientAssertion) { - parameterBuilder.addClientAssertion(clientAssertion.assertion); + parameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + this.config.authOptions.clientId, + request.resourceRequestUri + ) + ); parameterBuilder.addClientAssertionType( clientAssertion.assertionType ); diff --git a/lib/msal-node/src/client/ConfidentialClientApplication.ts b/lib/msal-node/src/client/ConfidentialClientApplication.ts index 00fda6c110..26e3c7a197 100644 --- a/lib/msal-node/src/client/ConfidentialClientApplication.ts +++ b/lib/msal-node/src/client/ConfidentialClientApplication.ts @@ -26,6 +26,8 @@ import { AADAuthorityConstants, createClientAuthError, ClientAuthErrorCodes, + ClientAssertion as ClientAssertionType, + getClientAssertion, } from "@azure/msal-common"; import { IConfidentialClientApplication } from "./IConfidentialClientApplication.js"; import { OnBehalfOfRequest } from "../request/OnBehalfOfRequest.js"; @@ -91,10 +93,14 @@ export class ConfidentialClientApplication ); // If there is a client assertion present in the request, it overrides the one present in the client configuration - let clientAssertion; + let clientAssertion: ClientAssertionType | undefined; if (request.clientAssertion) { clientAssertion = { - assertion: request.clientAssertion, + assertion: await getClientAssertion( + request.clientAssertion, + this.config.auth.clientId + // tokenEndpoint will be undefined. resourceRequestUri is omitted in ClientCredentialRequest + ), assertionType: NodeConstants.JWT_BEARER_ASSERTION_TYPE, }; } @@ -247,9 +253,8 @@ export class ConfidentialClientApplication } if (configuration.auth.clientAssertion) { - this.clientAssertion = ClientAssertion.fromAssertion( - configuration.auth.clientAssertion - ); + this.developerProvidedClientAssertion = + configuration.auth.clientAssertion; return; } diff --git a/lib/msal-node/src/client/OnBehalfOfClient.ts b/lib/msal-node/src/client/OnBehalfOfClient.ts index af0705df6d..02f18b0fed 100644 --- a/lib/msal-node/src/client/OnBehalfOfClient.ts +++ b/lib/msal-node/src/client/OnBehalfOfClient.ts @@ -30,6 +30,8 @@ import { TimeUtils, TokenClaims, UrlString, + ClientAssertion, + getClientAssertion, } from "@azure/msal-common"; import { EncodingUtils } from "../utils/EncodingUtils"; @@ -257,7 +259,7 @@ export class OnBehalfOfClient extends BaseClient { authority.tokenEndpoint, queryParametersString ); - const requestBody = this.createTokenRequestBody(request); + const requestBody = await this.createTokenRequestBody(request); const headers: Record = this.createTokenRequestHeaders(); const thumbprint: RequestThumbprint = { @@ -307,7 +309,9 @@ export class OnBehalfOfClient extends BaseClient { * generate a server request in accepable format * @param request */ - private createTokenRequestBody(request: CommonOnBehalfOfRequest): string { + private async createTokenRequestBody( + request: CommonOnBehalfOfRequest + ): Promise { const parameterBuilder = new RequestParameterBuilder(); parameterBuilder.addClientId(this.config.authOptions.clientId); @@ -343,10 +347,17 @@ export class OnBehalfOfClient extends BaseClient { ); } - if (this.config.clientCredentials.clientAssertion) { - const clientAssertion = - this.config.clientCredentials.clientAssertion; - parameterBuilder.addClientAssertion(clientAssertion.assertion); + const clientAssertion: ClientAssertion | undefined = + this.config.clientCredentials.clientAssertion; + + if (clientAssertion) { + parameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + this.config.authOptions.clientId, + request.resourceRequestUri + ) + ); parameterBuilder.addClientAssertionType( clientAssertion.assertionType ); diff --git a/lib/msal-node/src/client/UsernamePasswordClient.ts b/lib/msal-node/src/client/UsernamePasswordClient.ts index 91f3f1a0bc..3a452aa3ef 100644 --- a/lib/msal-node/src/client/UsernamePasswordClient.ts +++ b/lib/msal-node/src/client/UsernamePasswordClient.ts @@ -8,6 +8,7 @@ import { Authority, BaseClient, CcsCredentialType, + ClientAssertion, ClientConfiguration, CommonUsernamePasswordRequest, GrantType, @@ -19,6 +20,7 @@ import { StringUtils, TimeUtils, UrlString, + getClientAssertion, } from "@azure/msal-common"; /** @@ -81,7 +83,7 @@ export class UsernamePasswordClient extends BaseClient { authority.tokenEndpoint, queryParametersString ); - const requestBody = this.createTokenRequestBody(request); + const requestBody = await this.createTokenRequestBody(request); const headers: Record = this.createTokenRequestHeaders({ credential: request.username, type: CcsCredentialType.UPN, @@ -111,9 +113,9 @@ export class UsernamePasswordClient extends BaseClient { * Generates a map for all the params to be sent to the service * @param request */ - private createTokenRequestBody( + private async createTokenRequestBody( request: CommonUsernamePasswordRequest - ): string { + ): Promise { const parameterBuilder = new RequestParameterBuilder(); parameterBuilder.addClientId(this.config.authOptions.clientId); @@ -148,10 +150,17 @@ export class UsernamePasswordClient extends BaseClient { ); } - if (this.config.clientCredentials.clientAssertion) { - const clientAssertion = - this.config.clientCredentials.clientAssertion; - parameterBuilder.addClientAssertion(clientAssertion.assertion); + const clientAssertion: ClientAssertion | undefined = + this.config.clientCredentials.clientAssertion; + + if (clientAssertion) { + parameterBuilder.addClientAssertion( + await getClientAssertion( + clientAssertion.assertion, + this.config.authOptions.clientId, + request.resourceRequestUri + ) + ); parameterBuilder.addClientAssertionType( clientAssertion.assertionType ); diff --git a/lib/msal-node/src/config/Configuration.ts b/lib/msal-node/src/config/Configuration.ts index ba57eb4118..2751b1f157 100644 --- a/lib/msal-node/src/config/Configuration.ts +++ b/lib/msal-node/src/config/Configuration.ts @@ -14,6 +14,7 @@ import { AzureCloudOptions, ApplicationTelemetry, INativeBrokerPlugin, + ClientAssertionCallback, } from "@azure/msal-common"; import { HttpClient } from "../network/HttpClient.js"; import http from "http"; @@ -32,7 +33,7 @@ import { HttpClientWithRetries } from "../network/HttpClientWithRetries.js"; * - authority - Url of the authority. If no value is set, defaults to https://login.microsoftonline.com/common. * - knownAuthorities - Needed for Azure B2C and ADFS. All authorities that will be used in the client application. Only the host of the authority should be passed in. * - clientSecret - Secret string that the application uses when requesting a token. Only used in confidential client applications. Can be created in the Azure app registration portal. - * - clientAssertion - Assertion string that the application uses when requesting a token. Only used in confidential client applications. Assertion should be of type urn:ietf:params:oauth:client-assertion-type:jwt-bearer. + * - clientAssertion - A ClientAssertion object containing an assertion string or a callback function that returns an assertion string that the application uses when requesting a token, as well as the assertion's type (urn:ietf:params:oauth:client-assertion-type:jwt-bearer). Only used in confidential client applications. * - clientCertificate - Certificate that the application uses when requesting a token. Only used in confidential client applications. Requires hex encoded X.509 SHA-1 thumbprint of the certificiate, and the PEM encoded private key (string should contain -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY----- ) * - protocolMode - Enum that represents the protocol that msal follows. Used for configuring proper endpoints. * - skipAuthorityMetadataCache - A flag to choose whether to use or not use the local metadata cache during authority initialization. Defaults to false. @@ -42,7 +43,7 @@ export type NodeAuthOptions = { clientId: string; authority?: string; clientSecret?: string; - clientAssertion?: string; + clientAssertion?: string | ClientAssertionCallback; clientCertificate?: { thumbprint: string; privateKey: string; diff --git a/lib/msal-node/src/index.ts b/lib/msal-node/src/index.ts index 441c0c48ec..b2ba3251c6 100644 --- a/lib/msal-node/src/index.ts +++ b/lib/msal-node/src/index.ts @@ -125,6 +125,7 @@ export { AppTokenProviderParameters, AppTokenProviderResult, INativeBrokerPlugin, + ClientAssertionCallback, } from "@azure/msal-common"; export { version } from "./packageMetadata.js"; diff --git a/lib/msal-node/src/request/ClientCredentialRequest.ts b/lib/msal-node/src/request/ClientCredentialRequest.ts index eabd045c8c..d50437789a 100644 --- a/lib/msal-node/src/request/ClientCredentialRequest.ts +++ b/lib/msal-node/src/request/ClientCredentialRequest.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. */ -import { CommonClientCredentialRequest } from "@azure/msal-common"; +import { + ClientAssertionCallback, + CommonClientCredentialRequest, +} from "@azure/msal-common"; /** * CommonClientCredentialRequest @@ -11,7 +14,7 @@ import { CommonClientCredentialRequest } from "@azure/msal-common"; * - authority - URL of the authority, the security token service (STS) from which MSAL will acquire tokens. * - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes. * - skipCache - Skip token cache lookup and force request to authority to get a a new token. Defaults to false. - * - clientAssertion - A Base64Url-encoded signed JWT assertion string used in the Client Credential flow + * - clientAssertion - An assertion string or a callback function that returns an assertion string (both are Base64Url-encoded signed JWTs) used in the Client Credential flow * - tokenQueryParameters - String to string map of custom query parameters added to the /token call * @public */ @@ -25,5 +28,5 @@ export type ClientCredentialRequest = Partial< | "storeInCache" > > & { - clientAssertion?: string; + clientAssertion?: string | ClientAssertionCallback; }; diff --git a/lib/msal-node/test/client/ClientAssertion.spec.ts b/lib/msal-node/test/client/ClientAssertion.spec.ts index f5b51af8ad..6e0fabea4f 100644 --- a/lib/msal-node/test/client/ClientAssertion.spec.ts +++ b/lib/msal-node/test/client/ClientAssertion.spec.ts @@ -1,8 +1,13 @@ import { ClientAssertion } from "../../src/client/ClientAssertion"; -import { TEST_CONSTANTS } from "../utils/TestConstants"; +import { + DEFAULT_OPENID_CONFIG_RESPONSE, + TEST_CONSTANTS, +} from "../utils/TestConstants"; import { CryptoProvider } from "../../src/crypto/CryptoProvider"; import { EncodingUtils } from "../../src/utils/EncodingUtils"; import { JwtConstants } from "../../src/utils/Constants"; +import { getClientAssertionCallback } from "./ClientTestUtils"; +import { getClientAssertion } from "@azure/msal-common"; const jsonwebtoken = require("jsonwebtoken"); @@ -13,7 +18,7 @@ describe("Client assertion test", () => { const issuer = "client_id"; const audience = "audience"; - test("creates ClientAssertion From assertion", () => { + test("creates ClientAssertion from assertion string", () => { const assertion = ClientAssertion.fromAssertion( TEST_CONSTANTS.CLIENT_ASSERTION ); @@ -22,6 +27,23 @@ describe("Client assertion test", () => { ); }); + test("creates ClientAssertion from assertion callback (which returns a string)", async () => { + const clientAssertionCallback = getClientAssertionCallback( + TEST_CONSTANTS.CLIENT_ASSERTION + ); + + const assertionFromCallback: string = await getClientAssertion( + clientAssertionCallback, + TEST_CONSTANTS.CLIENT_ID, // value doesn't matter, will be ignored in mock callback + DEFAULT_OPENID_CONFIG_RESPONSE.body.token_endpoint // value doesn't matter, will be ignored in mock callback + ); + + const assertion = ClientAssertion.fromAssertion(assertionFromCallback); + expect(assertion.getJwt(cryptoProvider, issuer, audience)).toEqual( + TEST_CONSTANTS.CLIENT_ASSERTION + ); + }); + test("creates ClientAssertion from certificate", () => { const expectedPayload = { [JwtConstants.AUDIENCE]: audience, diff --git a/lib/msal-node/test/client/ClientCredentialClient.spec.ts b/lib/msal-node/test/client/ClientCredentialClient.spec.ts index 6bbdc80a7a..ef770409fa 100644 --- a/lib/msal-node/test/client/ClientCredentialClient.spec.ts +++ b/lib/msal-node/test/client/ClientCredentialClient.spec.ts @@ -20,6 +20,7 @@ import { createClientAuthError, ClientAuthErrorCodes, CacheHelpers, + GrantType, } from "@azure/msal-common"; import { ClientCredentialClient, UsernamePasswordClient } from "../../src"; import { @@ -36,6 +37,7 @@ import { import { checkMockedNetworkRequest, ClientTestUtils, + getClientAssertionCallback, mockCrypto, } from "./ClientTestUtils"; import { mockNetworkClient } from "../utils/MockNetworkClient"; @@ -98,12 +100,12 @@ describe("ClientCredentialClient unit tests", () => { clientCredentialRequest ); - const returnVal: string = - createTokenRequestBodySpy.mock.results[0].value; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; const checks = { graphScope: true, clientId: true, - grantType: true, + grantType: GrantType.CLIENT_CREDENTIALS_GRANT, clientSecret: true, clientSku: true, clientVersion: true, @@ -281,12 +283,12 @@ describe("ClientCredentialClient unit tests", () => { clientCredentialRequest ); - const returnVal: string = - createTokenRequestBodySpy.mock.results[0].value; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; const checks = { dstsScope: true, clientId: true, - grantType: true, + grantType: GrantType.CLIENT_CREDENTIALS_GRANT, clientSecret: true, clientSku: true, clientVersion: true, @@ -363,12 +365,12 @@ describe("ClientCredentialClient unit tests", () => { clientCredentialRequest ); - const returnVal: string = - createTokenRequestBodySpy.mock.results[0].value; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; const checks = { graphScope: true, clientId: true, - grantType: true, + grantType: GrantType.CLIENT_CREDENTIALS_GRANT, clientSecret: true, clientSku: true, clientVersion: true, @@ -431,8 +433,8 @@ describe("ClientCredentialClient unit tests", () => { expect(authResult.fromCache).toBe(false); // verify that the client capabilities have been merged with the (empty) claims - const returnVal: string = createTokenRequestBodySpy.mock - .results[0].value as string; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; expect( decodeURIComponent( returnVal @@ -464,8 +466,8 @@ describe("ClientCredentialClient unit tests", () => { expect(authResult2.fromCache).toBe(false); // verify that the client capabilities have been merged with the claims - const returnVal2: string = createTokenRequestBodySpy.mock - .results[1].value as string; + const returnVal2: string = await createTokenRequestBodySpy.mock + .results[1].value; expect( decodeURIComponent( returnVal2 @@ -526,12 +528,12 @@ describe("ClientCredentialClient unit tests", () => { clientCredentialRequest ); - const returnVal: string = createTokenRequestBodySpy.mock.results[0] - .value as string; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; const checks = { graphScope: true, clientId: true, - grantType: true, + grantType: GrantType.CLIENT_CREDENTIALS_GRANT, clientSecret: true, clientSku: true, clientVersion: true, @@ -545,118 +547,127 @@ describe("ClientCredentialClient unit tests", () => { checkMockedNetworkRequest(returnVal, checks); }); - it("Uses clientAssertion from ClientConfiguration when no client assertion is added to request", async () => { - config.clientCredentials = { - ...config.clientCredentials, - clientAssertion: { - assertion: TEST_CONFIG.TEST_CONFIG_ASSERTION, - assertionType: TEST_CONFIG.TEST_ASSERTION_TYPE, - }, - }; - const client: ClientCredentialClient = new ClientCredentialClient( - config - ); - - const clientCredentialRequest: CommonClientCredentialRequest = { - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - claims: "{}", - }; + it.each([ + TEST_CONFIG.TEST_CONFIG_ASSERTION, + getClientAssertionCallback(TEST_CONFIG.TEST_CONFIG_ASSERTION), + ])( + "Uses clientAssertion from ClientConfiguration when no client assertion is added to request", + async (clientAssertion) => { + config.clientCredentials = { + ...config.clientCredentials, + clientAssertion: { + assertion: clientAssertion, + assertionType: TEST_CONFIG.TEST_ASSERTION_TYPE, + }, + }; + const client: ClientCredentialClient = new ClientCredentialClient( + config + ); - const authResult = (await client.acquireToken( - clientCredentialRequest - )) as AuthenticationResult; - const expectedScopes = [TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0]]; - expect(authResult.scopes).toEqual(expectedScopes); - expect(authResult.accessToken).toEqual( - CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT.body.access_token - ); - expect(authResult.state).toBe(""); + const clientCredentialRequest: CommonClientCredentialRequest = { + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + }; - expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( - clientCredentialRequest - ); + const authResult = (await client.acquireToken( + clientCredentialRequest + )) as AuthenticationResult; + const expectedScopes = [TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0]]; + expect(authResult.scopes).toEqual(expectedScopes); + expect(authResult.accessToken).toEqual( + CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT.body.access_token + ); + expect(authResult.state).toBe(""); - const returnVal: string = createTokenRequestBodySpy.mock.results[0] - .value as string; - const checks = { - graphScope: true, - clientId: true, - grantType: true, - clientSecret: true, - clientSku: true, - clientVersion: true, - clientOs: true, - clientCpu: true, - appName: true, - appVersion: true, - msLibraryCapability: true, - claims: false, - testConfigAssertion: true, - testAssertionType: true, - }; - checkMockedNetworkRequest(returnVal, checks); - }); + expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( + clientCredentialRequest + ); - it("Uses the clientAssertion included in the request instead of the one in ClientConfiguration", async () => { - config.clientCredentials = { - ...config.clientCredentials, - clientAssertion: { - assertion: TEST_CONFIG.TEST_CONFIG_ASSERTION, - assertionType: TEST_CONFIG.TEST_ASSERTION_TYPE, - }, - }; - const client: ClientCredentialClient = new ClientCredentialClient( - config - ); + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; + const checks = { + graphScope: true, + clientId: true, + grantType: GrantType.CLIENT_CREDENTIALS_GRANT, + clientSecret: true, + clientSku: true, + clientVersion: true, + clientOs: true, + clientCpu: true, + appName: true, + appVersion: true, + msLibraryCapability: true, + testConfigAssertion: true, + testAssertionType: true, + }; + checkMockedNetworkRequest(returnVal, checks); + } + ); + + it.each([ + TEST_CONFIG.TEST_REQUEST_ASSERTION, + getClientAssertionCallback(TEST_CONFIG.TEST_REQUEST_ASSERTION), + ])( + "Uses the clientAssertion included in the request instead of the one in ClientConfiguration", + async (clientAssertion) => { + config.clientCredentials = { + ...config.clientCredentials, + clientAssertion: { + assertion: + "config-assertion that will be overridden by request-assertion", + assertionType: TEST_CONFIG.TEST_ASSERTION_TYPE, + }, + }; + const client: ClientCredentialClient = new ClientCredentialClient( + config + ); - const clientCredentialRequest: CommonClientCredentialRequest = { - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - claims: "{}", - clientAssertion: { - assertion: TEST_CONFIG.TEST_REQUEST_ASSERTION, - assertionType: TEST_CONFIG.TEST_ASSERTION_TYPE, - }, - }; + const clientCredentialRequest: CommonClientCredentialRequest = { + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + clientAssertion: { + assertion: clientAssertion, + assertionType: TEST_CONFIG.TEST_ASSERTION_TYPE, + }, + }; - const authResult = (await client.acquireToken( - clientCredentialRequest - )) as AuthenticationResult; - const expectedScopes = [TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0]]; - expect(authResult.scopes).toEqual(expectedScopes); - expect(authResult.accessToken).toEqual( - CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT.body.access_token - ); - expect(authResult.state).toBe(""); + const authResult = (await client.acquireToken( + clientCredentialRequest + )) as AuthenticationResult; + const expectedScopes = [TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0]]; + expect(authResult.scopes).toEqual(expectedScopes); + expect(authResult.accessToken).toEqual( + CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT.body.access_token + ); + expect(authResult.state).toBe(""); - expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( - clientCredentialRequest - ); + expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( + clientCredentialRequest + ); - const returnVal: string = createTokenRequestBodySpy.mock.results[0] - .value as string; - const checks = { - graphScope: true, - clientId: true, - grantType: true, - clientSecret: true, - clientSku: true, - clientVersion: true, - clientOs: true, - clientCpu: true, - appName: true, - appVersion: true, - msLibraryCapability: true, - claims: false, - testConfigAssertion: false, - testRequestAssertion: true, - testAssertionType: true, - }; - checkMockedNetworkRequest(returnVal, checks); - }); + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; + const checks = { + graphScope: true, + clientId: true, + grantType: GrantType.CLIENT_CREDENTIALS_GRANT, + clientSecret: true, + clientSku: true, + clientVersion: true, + clientOs: true, + clientCpu: true, + appName: true, + appVersion: true, + msLibraryCapability: true, + testConfigAssertion: false, + testRequestAssertion: true, + testAssertionType: true, + }; + checkMockedNetworkRequest(returnVal, checks); + } + ); // For more information about this test see: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS it("Does not add headers that do not qualify for a simple request", async () => { @@ -840,12 +851,12 @@ describe("ClientCredentialClient unit tests", () => { clientCredentialRequest ); - const returnVal: string = createTokenRequestBodySpy.mock.results[0] - .value as string; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; const checks = { graphScope: true, clientId: true, - grantType: true, + grantType: GrantType.CLIENT_CREDENTIALS_GRANT, clientSecret: true, }; checkMockedNetworkRequest(returnVal, checks); @@ -877,12 +888,12 @@ describe("ClientCredentialClient unit tests", () => { clientCredentialRequest ); - const returnVal: string = createTokenRequestBodySpy.mock.results[0] - .value as string; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; const checks = { graphScope: true, clientId: true, - grantType: true, + grantType: GrantType.CLIENT_CREDENTIALS_GRANT, clientSecret: true, }; checkMockedNetworkRequest(returnVal, checks); diff --git a/lib/msal-node/test/client/ClientTestUtils.ts b/lib/msal-node/test/client/ClientTestUtils.ts index fb0fd4b4ef..85623f9db7 100644 --- a/lib/msal-node/test/client/ClientTestUtils.ts +++ b/lib/msal-node/test/client/ClientTestUtils.ts @@ -5,7 +5,6 @@ import { AADServerParamKeys, - GrantType, ThrottlingConstants, ServerTelemetryEntity, CacheManager, @@ -30,6 +29,9 @@ import { CacheHelpers, Authority, INetworkModule, + ClientAssertionCallback, + ClientAssertionConfig, + PasswordGrantConstants, } from "@azure/msal-common"; import { AUTHENTICATION_RESULT, @@ -361,7 +363,7 @@ interface checks { dstsScope?: boolean | undefined; graphScope?: boolean | undefined; clientId?: boolean | undefined; - grantType?: boolean | undefined; + grantType?: string | undefined; clientSecret?: boolean | undefined; clientSku?: boolean | undefined; clientVersion?: boolean | undefined; @@ -374,6 +376,9 @@ interface checks { testConfigAssertion?: boolean | undefined; testRequestAssertion?: boolean | undefined; testAssertionType?: boolean | undefined; + responseType?: boolean | undefined; + username?: string | undefined; + password?: string | undefined; } export const checkMockedNetworkRequest = ( @@ -405,9 +410,9 @@ export const checkMockedNetworkRequest = ( if (checks.grantType !== undefined) { expect( returnVal.includes( - `${AADServerParamKeys.GRANT_TYPE}=${GrantType.CLIENT_CREDENTIALS_GRANT}` + `${AADServerParamKeys.GRANT_TYPE}=${checks.grantType}` ) - ).toBe(checks.grantType); + ).toBe(true); } if (checks.clientSecret !== undefined) { @@ -513,4 +518,40 @@ export const checkMockedNetworkRequest = ( ) ).toBe(checks.testAssertionType); } + + if (checks.responseType !== undefined) { + expect( + returnVal.includes( + `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.TOKEN_RESPONSE_TYPE}%20${Constants.ID_TOKEN_RESPONSE_TYPE}` + ) + ).toBe(checks.responseType); + } + + if (checks.username !== undefined) { + expect( + returnVal.includes( + `${PasswordGrantConstants.username}=${checks.username}` + ) + ).toBe(true); + } + + if (checks.password !== undefined) { + expect( + returnVal.includes( + `${PasswordGrantConstants.password}=${checks.password}` + ) + ).toBe(true); + } +}; + +export const getClientAssertionCallback = ( + clientAssertion: string +): ClientAssertionCallback => { + const clientAssertionCallback: ClientAssertionCallback = async ( + _config: ClientAssertionConfig + ): Promise => { + return await Promise.resolve(clientAssertion); + }; + + return clientAssertionCallback; }; diff --git a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts index 41a2269a5a..5ed62f40f0 100644 --- a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts +++ b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts @@ -5,14 +5,17 @@ import { AuthorizationCodeClient, + SilentFlowClient, RefreshTokenClient, AuthenticationResult, OIDC_DEFAULT_SCOPES, CommonClientCredentialRequest, createClientAuthError, ClientAuthErrorCodes, + AccountEntity, + AccountInfo, } from "@azure/msal-common"; -import { TEST_CONSTANTS } from "../utils/TestConstants"; +import { ID_TOKEN_CLAIMS, TEST_CONSTANTS } from "../utils/TestConstants"; import { AuthError, ConfidentialClientApplication, @@ -23,6 +26,7 @@ import { AuthorizationCodeRequest, ClientCredentialClient, RefreshTokenRequest, + SilentFlowRequest, } from "../../src"; import * as msalNode from "../../src"; @@ -30,8 +34,12 @@ import { getMsalCommonAutoMock, MSALCommonModule } from "../utils/MockUtils"; import { CAE_CONSTANTS, CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT, + TEST_CONFIG, + TEST_TOKENS, } from "../test_kit/StringConstants"; import { mockNetworkClient } from "../utils/MockNetworkClient"; +import { getClientAssertionCallback } from "./ClientTestUtils"; +import { buildAccountFromIdTokenClaims } from "msal-test-utils"; const msalCommon: MSALCommonModule = jest.requireActual("@azure/msal-common"); @@ -82,6 +90,38 @@ describe("ConfidentialClientApplication", () => { expect(AuthorizationCodeClient).toHaveBeenCalledTimes(1); }); + test("acquireTokenBySilentFlow", async () => { + const testAccountEntity: AccountEntity = + buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); + + const testAccount: AccountInfo = { + ...testAccountEntity.getAccountInfo(), + idTokenClaims: ID_TOKEN_CLAIMS, + idToken: TEST_TOKENS.IDTOKEN_V2, + }; + + const request: SilentFlowRequest = { + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + account: testAccount, + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + forceRefresh: false, + }; + + const mockSilentFlowClientInstance = { + includeRedirectUri: false, + acquireToken: jest.fn(), + }; + jest.spyOn(msalCommon, "SilentFlowClient").mockImplementation( + () => + mockSilentFlowClientInstance as unknown as SilentFlowClient + ); + + const authApp = new ConfidentialClientApplication(appConfig); + await authApp.acquireTokenSilent(request); + expect(SilentFlowClient).toHaveBeenCalledTimes(1); + }); + describe("CAE, claims and client capabilities", () => { let createTokenRequestBodySpy: jest.SpyInstance; let client: ConfidentialClientApplication; @@ -250,29 +290,35 @@ describe("ConfidentialClientApplication", () => { expect(ClientCredentialClient).toHaveBeenCalledTimes(1); }); - test("acquireTokenByClientCredential with client assertion", async () => { - const request: ClientCredentialRequest = { - scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, - skipCache: false, - clientAssertion: "testAssertion", - }; + it.each([ + TEST_CONFIG.TEST_REQUEST_ASSERTION, + getClientAssertionCallback(TEST_CONFIG.TEST_REQUEST_ASSERTION), + ])( + "acquireTokenByClientCredential with client assertion", + async (clientAssertion) => { + const request: ClientCredentialRequest = { + scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, + skipCache: false, + clientAssertion: clientAssertion, + }; - ClientCredentialClient.prototype.acquireToken = jest.fn( - (request: CommonClientCredentialRequest) => { - expect(request.clientAssertion).not.toBe(undefined); - expect(request.clientAssertion?.assertion).toBe( - "testAssertion" - ); - expect(request.clientAssertion?.assertionType).toBe( - "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" - ); - return Promise.resolve(null); - } - ); + ClientCredentialClient.prototype.acquireToken = jest.fn( + (request: CommonClientCredentialRequest) => { + expect(request.clientAssertion).not.toBe(undefined); + expect(request.clientAssertion?.assertion).toBe( + TEST_CONFIG.TEST_REQUEST_ASSERTION + ); + expect(request.clientAssertion?.assertionType).toBe( + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + ); + return Promise.resolve(null); + } + ); - const authApp = new ConfidentialClientApplication(appConfig); - await authApp.acquireTokenByClientCredential(request); - }); + const authApp = new ConfidentialClientApplication(appConfig); + await authApp.acquireTokenByClientCredential(request); + } + ); test("acquireTokenOnBehalfOf", async () => { const request: OnBehalfOfRequest = { diff --git a/lib/msal-node/test/client/OnBehalfOfClient.spec.ts b/lib/msal-node/test/client/OnBehalfOfClient.spec.ts index ee670aecd9..33f50a3cdb 100644 --- a/lib/msal-node/test/client/OnBehalfOfClient.spec.ts +++ b/lib/msal-node/test/client/OnBehalfOfClient.spec.ts @@ -25,7 +25,11 @@ import { TEST_CONFIG, TEST_TOKENS, } from "../test_kit/StringConstants"; -import { checkMockedNetworkRequest, ClientTestUtils } from "./ClientTestUtils"; +import { + checkMockedNetworkRequest, + ClientTestUtils, + getClientAssertionCallback, +} from "./ClientTestUtils"; import { EncodingUtils } from "../../src/utils/EncodingUtils"; import { mockNetworkClient } from "../utils/MockNetworkClient"; @@ -86,8 +90,8 @@ describe("OnBehalfOf unit tests", () => { oboRequest ); - const returnVal: string = - createTokenRequestBodySpy.mock.results[0].value; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; const checks = { graphScope: true, clientId: true, @@ -153,8 +157,8 @@ describe("OnBehalfOf unit tests", () => { expect(authResult.fromCache).toBe(false); // verify that the client capabilities have been merged with the (empty) claims - const returnVal: string = createTokenRequestBodySpy.mock - .results[0].value as string; + const returnVal: string = await createTokenRequestBodySpy + .mock.results[0].value; expect( decodeURIComponent( returnVal @@ -188,8 +192,8 @@ describe("OnBehalfOf unit tests", () => { expect(authResult2.fromCache).toBe(false); // verify that the client capabilities have been merged with the claims - const returnVal2: string = createTokenRequestBodySpy.mock - .results[1].value as string; + const returnVal2: string = await createTokenRequestBodySpy + .mock.results[1].value; expect( decodeURIComponent( returnVal2 @@ -291,8 +295,8 @@ describe("OnBehalfOf unit tests", () => { oboRequest ); - const returnVal: string = - createTokenRequestBodySpy.mock.results[0].value; + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; const checks = { graphScope: true, clientId: true, @@ -402,5 +406,60 @@ describe("OnBehalfOf unit tests", () => { testAccessTokenEntity.homeAccountId ); }); + + it.each([ + TEST_CONFIG.TEST_CONFIG_ASSERTION, + getClientAssertionCallback(TEST_CONFIG.TEST_CONFIG_ASSERTION), + ])( + "Uses clientAssertion from ClientConfiguration when no client assertion is added to request", + async (clientAssertion) => { + config.clientCredentials = { + ...config.clientCredentials, + clientAssertion: { + assertion: clientAssertion, + assertionType: TEST_CONFIG.TEST_ASSERTION_TYPE, + }, + }; + const client: OnBehalfOfClient = new OnBehalfOfClient(config); + + const oboRequest: CommonOnBehalfOfRequest = { + scopes: [...TEST_CONFIG.DEFAULT_GRAPH_SCOPE], + authority: TEST_CONFIG.validAuthority, + correlationId: TEST_CONFIG.CORRELATION_ID, + oboAssertion: "user_assertion_hash", + skipCache: true, + }; + + const authResult = (await client.acquireToken( + oboRequest + )) as AuthenticationResult; + expect(authResult.accessToken).toEqual( + AUTHENTICATION_RESULT.body.access_token + ); + expect(authResult.state).toBe(""); + expect(authResult.fromCache).toBe(false); + + expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( + oboRequest + ); + + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; + const checks = { + graphScope: true, + clientId: true, + clientSecret: true, + clientSku: true, + clientVersion: true, + clientOs: true, + appName: true, + appVersion: true, + msLibraryCapability: true, + testConfigAssertion: true, + testAssertionType: true, + }; + checkMockedNetworkRequest(returnVal, checks); + } + ); }); }); diff --git a/lib/msal-node/test/client/UsernamePasswordClient.spec.ts b/lib/msal-node/test/client/UsernamePasswordClient.spec.ts index 8c07a523d6..d4ad46ebef 100644 --- a/lib/msal-node/test/client/UsernamePasswordClient.spec.ts +++ b/lib/msal-node/test/client/UsernamePasswordClient.spec.ts @@ -3,43 +3,53 @@ * Licensed under the MIT License. */ -import sinon from "sinon"; import { - AADServerParamKeys, AuthenticationResult, - Authority, BaseClient, ClientConfiguration, CommonUsernamePasswordRequest, Constants, GrantType, - PasswordGrantConstants, - ThrottlingConstants, } from "@azure/msal-common"; import { AUTHENTICATION_RESULT_DEFAULT_SCOPES, DEFAULT_OPENID_CONFIG_RESPONSE, + MOCK_PASSWORD, + MOCK_USERNAME, RANDOM_TEST_GUID, TEST_CONFIG, } from "../test_kit/StringConstants"; import { UsernamePasswordClient } from "../../src"; -import { ClientTestUtils } from "./ClientTestUtils"; +import { + ClientTestUtils, + checkMockedNetworkRequest, + getClientAssertionCallback, +} from "./ClientTestUtils"; +import { mockNetworkClient } from "../utils/MockNetworkClient"; describe("Username Password unit tests", () => { + let createTokenRequestBodySpy: jest.SpyInstance; let config: ClientConfiguration; - beforeEach(async () => { - sinon - .stub(Authority.prototype, "getEndpointMetadataFromNetwork") - .resolves(DEFAULT_OPENID_CONFIG_RESPONSE.body); - config = await ClientTestUtils.createTestClientConfiguration(); + createTokenRequestBodySpy = jest.spyOn( + UsernamePasswordClient.prototype, + "createTokenRequestBody" + ); + + config = await ClientTestUtils.createTestClientConfiguration( + undefined, + mockNetworkClient( + DEFAULT_OPENID_CONFIG_RESPONSE.body, + AUTHENTICATION_RESULT_DEFAULT_SCOPES + ) + ); if (config.systemOptions) { config.systemOptions.preventCorsPreflight = true; } }); afterEach(() => { - sinon.restore(); + jest.restoreAllMocks(); }); describe("Constructor", () => { @@ -52,24 +62,13 @@ describe("Username Password unit tests", () => { }); it("acquires a token", async () => { - sinon - .stub( - UsernamePasswordClient.prototype, - "executePostToTokenEndpoint" - ) - .resolves(AUTHENTICATION_RESULT_DEFAULT_SCOPES); - - const createTokenRequestBodySpy = sinon.spy( - UsernamePasswordClient.prototype, - "createTokenRequestBody" - ); - const client = new UsernamePasswordClient(config); + const usernamePasswordRequest: CommonUsernamePasswordRequest = { authority: Constants.DEFAULT_AUTHORITY, scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - username: "mock_name", - password: "mock_password", + username: MOCK_USERNAME, + password: MOCK_PASSWORD, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, }; @@ -92,107 +91,50 @@ describe("Username Password unit tests", () => { ); expect(authResult.state).toHaveLength(0); - expect( - createTokenRequestBodySpy.calledWith(usernamePasswordRequest) - ).toBe(true); + expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( + usernamePasswordRequest + ); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0]}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.CLIENT_ID}=${encodeURIComponent( - TEST_CONFIG.MSAL_CLIENT_ID - )}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.GRANT_TYPE}=${encodeURIComponent( - GrantType.RESOURCE_OWNER_PASSWORD_GRANT - )}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${PasswordGrantConstants.username}=mock_name` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${PasswordGrantConstants.password}=mock_password` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_CLIENT_SKU}=${Constants.SKU}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_CLIENT_VER}=${TEST_CONFIG.TEST_VERSION}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_CLIENT_OS}=${TEST_CONFIG.TEST_OS}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_CLIENT_CPU}=${TEST_CONFIG.TEST_CPU}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_APP_NAME}=${TEST_CONFIG.applicationName}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_APP_VER}=${TEST_CONFIG.applicationVersion}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.TOKEN_RESPONSE_TYPE}%20${Constants.ID_TOKEN_RESPONSE_TYPE}` - ) - ).toBe(true); + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; + const checks = { + graphScope: true, + clientId: true, + grantType: GrantType.RESOURCE_OWNER_PASSWORD_GRANT, + clientSecret: true, + clientSku: true, + clientVersion: true, + clientOs: true, + clientCpu: true, + appName: true, + appVersion: true, + msLibraryCapability: true, + claims: true, + responseType: true, + username: MOCK_USERNAME, + password: MOCK_PASSWORD, + }; + checkMockedNetworkRequest(returnVal, checks); }); - it("Adds tokenQueryParameters to the /token request", (done) => { - sinon - .stub( - UsernamePasswordClient.prototype, - "executePostToTokenEndpoint" - ) - .callsFake((url: string) => { - try { - expect( - url.includes( - "/token?testParam1=testValue1&testParam3=testValue3" - ) - ).toBeTruthy(); - expect(!url.includes("/token?testParam2=")).toBeTruthy(); - done(); - } catch (error) { - done(error); - } - }); + it("Adds tokenQueryParameters to the /token request", async () => { + const badExecutePostToTokenEndpointMock = jest.spyOn( + UsernamePasswordClient.prototype, + "executePostToTokenEndpoint" + ); + // no implementation has been mocked, the acquireToken call will fail + + const fakeConfig: ClientConfiguration = + await ClientTestUtils.createTestClientConfiguration(); + const client: UsernamePasswordClient = new UsernamePasswordClient( + fakeConfig + ); - const client = new UsernamePasswordClient(config); const usernamePasswordRequest: CommonUsernamePasswordRequest = { authority: Constants.DEFAULT_AUTHORITY, scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - username: "mock_name", - password: "mock_password", + username: MOCK_USERNAME, + password: MOCK_PASSWORD, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, tokenQueryParameters: { @@ -202,30 +144,29 @@ describe("Username Password unit tests", () => { }, }; - client.acquireToken(usernamePasswordRequest).catch(() => { - // Catch errors thrown after the function call this test is testing - }); + await expect( + client.acquireToken(usernamePasswordRequest) + ).rejects.toThrow(); + + if (!badExecutePostToTokenEndpointMock.mock.lastCall) { + fail("executePostToTokenEndpointMock was not called"); + } + const url: string = badExecutePostToTokenEndpointMock.mock + .lastCall[0] as string; + expect( + url.includes("/token?testParam1=testValue1&testParam3=testValue3") + ).toBeTruthy(); + expect(!url.includes("/token?testParam2=")).toBeTruthy(); }); it("properly encodes special characters in emails (usernames)", async () => { - sinon - .stub( - UsernamePasswordClient.prototype, - "executePostToTokenEndpoint" - ) - .resolves(AUTHENTICATION_RESULT_DEFAULT_SCOPES); - - const createTokenRequestBodySpy = sinon.spy( - UsernamePasswordClient.prototype, - "createTokenRequestBody" - ); - const client = new UsernamePasswordClient(config); + const usernamePasswordRequest: CommonUsernamePasswordRequest = { authority: Constants.DEFAULT_AUTHORITY, scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - username: "mock+name", - password: "mock_password", + username: `${MOCK_USERNAME}&+`, + password: MOCK_PASSWORD, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, }; @@ -248,36 +189,40 @@ describe("Username Password unit tests", () => { ); expect(authResult.state).toHaveLength(0); - expect( - createTokenRequestBodySpy.calledWith(usernamePasswordRequest) - ).toBe(true); + expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( + usernamePasswordRequest + ); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${PasswordGrantConstants.username}=mock%2Bname` - ) - ).toBe(true); + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; + const checks = { + graphScope: true, + clientId: true, + grantType: GrantType.RESOURCE_OWNER_PASSWORD_GRANT, + clientSecret: true, + clientSku: true, + clientVersion: true, + clientOs: true, + clientCpu: true, + appName: true, + appVersion: true, + msLibraryCapability: true, + claims: true, + responseType: true, + username: `${MOCK_USERNAME}%26%2B`, + password: MOCK_PASSWORD, + }; + checkMockedNetworkRequest(returnVal, checks); }); it("properly encodes special characters in passwords", async () => { - sinon - .stub( - UsernamePasswordClient.prototype, - "executePostToTokenEndpoint" - ) - .resolves(AUTHENTICATION_RESULT_DEFAULT_SCOPES); - - const createTokenRequestBodySpy = sinon.spy( - UsernamePasswordClient.prototype, - "createTokenRequestBody" - ); - const client = new UsernamePasswordClient(config); + const usernamePasswordRequest: CommonUsernamePasswordRequest = { authority: Constants.DEFAULT_AUTHORITY, scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - username: "mock_name", - password: "mock_password&+", + username: MOCK_USERNAME, + password: `${MOCK_PASSWORD}&+`, claims: TEST_CONFIG.CLAIMS, correlationId: RANDOM_TEST_GUID, }; @@ -300,36 +245,40 @@ describe("Username Password unit tests", () => { ); expect(authResult.state).toHaveLength(0); - expect( - createTokenRequestBodySpy.calledWith(usernamePasswordRequest) - ).toBe(true); + expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( + usernamePasswordRequest + ); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${PasswordGrantConstants.password}=mock_password%26%2B` - ) - ).toBe(true); + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; + const checks = { + graphScope: true, + clientId: true, + grantType: GrantType.RESOURCE_OWNER_PASSWORD_GRANT, + clientSecret: true, + clientSku: true, + clientVersion: true, + clientOs: true, + clientCpu: true, + appName: true, + appVersion: true, + msLibraryCapability: true, + claims: true, + responseType: true, + username: MOCK_USERNAME, + password: `${MOCK_PASSWORD}%26%2B`, + }; + checkMockedNetworkRequest(returnVal, checks); }); it("Does not include claims if empty object is passed", async () => { - sinon - .stub( - UsernamePasswordClient.prototype, - "executePostToTokenEndpoint" - ) - .resolves(AUTHENTICATION_RESULT_DEFAULT_SCOPES); - - const createTokenRequestBodySpy = sinon.spy( - UsernamePasswordClient.prototype, - "createTokenRequestBody" - ); - const client = new UsernamePasswordClient(config); + const usernamePasswordRequest: CommonUsernamePasswordRequest = { authority: Constants.DEFAULT_AUTHORITY, scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - username: "mock_name", - password: "mock_password", + username: MOCK_USERNAME, + password: MOCK_PASSWORD, correlationId: RANDOM_TEST_GUID, claims: "{}", }; @@ -352,80 +301,100 @@ describe("Username Password unit tests", () => { ); expect(authResult.state).toBe(""); - expect( - createTokenRequestBodySpy.calledWith(usernamePasswordRequest) - ).toBe(true); + expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( + usernamePasswordRequest + ); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0]}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.CLIENT_ID}=${encodeURIComponent( - TEST_CONFIG.MSAL_CLIENT_ID - )}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.GRANT_TYPE}=${encodeURIComponent( - GrantType.RESOURCE_OWNER_PASSWORD_GRANT - )}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${PasswordGrantConstants.username}=mock_name` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${PasswordGrantConstants.password}=mock_password` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.CLAIMS}=${encodeURIComponent( - TEST_CONFIG.CLAIMS - )}` - ) - ).toBe(false); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_CLIENT_SKU}=${Constants.SKU}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_CLIENT_VER}=${TEST_CONFIG.TEST_VERSION}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_CLIENT_OS}=${TEST_CONFIG.TEST_OS}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_CLIENT_CPU}=${TEST_CONFIG.TEST_CPU}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_APP_NAME}=${TEST_CONFIG.applicationName}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_APP_VER}=${TEST_CONFIG.applicationVersion}` - ) - ).toBe(true); - expect( - createTokenRequestBodySpy.returnValues[0].includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` - ) - ).toBe(true); + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; + const checks = { + graphScope: true, + clientId: true, + grantType: GrantType.RESOURCE_OWNER_PASSWORD_GRANT, + clientSecret: true, + clientSku: true, + clientVersion: true, + clientOs: true, + clientCpu: true, + appName: true, + appVersion: true, + msLibraryCapability: true, + claims: false, + responseType: true, + username: MOCK_USERNAME, + password: MOCK_PASSWORD, + }; + checkMockedNetworkRequest(returnVal, checks); }); + + it.each([ + TEST_CONFIG.TEST_CONFIG_ASSERTION, + getClientAssertionCallback(TEST_CONFIG.TEST_CONFIG_ASSERTION), + ])( + "Uses clientAssertion from ClientConfiguration when no client assertion is added to request", + async (clientAssertion) => { + config.clientCredentials = { + ...config.clientCredentials, + clientAssertion: { + assertion: clientAssertion, + assertionType: TEST_CONFIG.TEST_ASSERTION_TYPE, + }, + }; + const client: UsernamePasswordClient = new UsernamePasswordClient( + config + ); + + const usernamePasswordRequest: CommonUsernamePasswordRequest = { + authority: Constants.DEFAULT_AUTHORITY, + scopes: TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + username: MOCK_USERNAME, + password: MOCK_PASSWORD, + correlationId: RANDOM_TEST_GUID, + }; + + const authResult = (await client.acquireToken( + usernamePasswordRequest + )) as AuthenticationResult; + const expectedScopes = [ + Constants.OPENID_SCOPE, + Constants.PROFILE_SCOPE, + Constants.OFFLINE_ACCESS_SCOPE, + TEST_CONFIG.DEFAULT_GRAPH_SCOPE[0], + ]; + expect(authResult.scopes).toEqual(expectedScopes); + expect(authResult.idToken).toEqual( + AUTHENTICATION_RESULT_DEFAULT_SCOPES.body.id_token + ); + expect(authResult.accessToken).toEqual( + AUTHENTICATION_RESULT_DEFAULT_SCOPES.body.access_token + ); + expect(authResult.state).toBe(""); + + expect(createTokenRequestBodySpy.mock.lastCall[0]).toEqual( + usernamePasswordRequest + ); + + const returnVal: string = await createTokenRequestBodySpy.mock + .results[0].value; + const checks = { + graphScope: true, + clientId: true, + grantType: GrantType.RESOURCE_OWNER_PASSWORD_GRANT, + clientSecret: true, + clientSku: true, + clientVersion: true, + clientOs: true, + clientCpu: true, + appName: true, + appVersion: true, + msLibraryCapability: true, + responseType: true, + username: MOCK_USERNAME, + password: MOCK_PASSWORD, + testConfigAssertion: true, + testAssertionType: true, + }; + checkMockedNetworkRequest(returnVal, checks); + } + ); }); diff --git a/lib/msal-node/test/test_kit/StringConstants.ts b/lib/msal-node/test/test_kit/StringConstants.ts index e63153e68e..965909e140 100644 --- a/lib/msal-node/test/test_kit/StringConstants.ts +++ b/lib/msal-node/test/test_kit/StringConstants.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. */ -import { AuthenticationResult } from "@azure/msal-common"; +import { + AuthenticationResult, + PasswordGrantConstants, +} from "@azure/msal-common"; import { AuthenticationScheme, Constants, @@ -514,3 +517,6 @@ export const CORS_SIMPLE_REQUEST_HEADERS = [ ]; export const THREE_SECONDS_IN_MILLI = 3000; + +export const MOCK_USERNAME = `mock_${PasswordGrantConstants.username}`; +export const MOCK_PASSWORD = `mock_${PasswordGrantConstants.password}`;