diff --git a/change/@azure-msal-common-dc2c24bd-f38c-4580-ba04-b44ee8ac5b9f.json b/change/@azure-msal-common-dc2c24bd-f38c-4580-ba04-b44ee8ac5b9f.json new file mode 100644 index 0000000000..ad91dc7ee7 --- /dev/null +++ b/change/@azure-msal-common-dc2c24bd-f38c-4580-ba04-b44ee8ac5b9f.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Adding ROPC for confidential client apps (#3838)", + "packageName": "@azure/msal-common", + "email": "sameera.gajjarapu@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-node-c1284212-4af4-45ff-829d-e7c277947e88.json b/change/@azure-msal-node-c1284212-4af4-45ff-829d-e7c277947e88.json new file mode 100644 index 0000000000..aca5cae53a --- /dev/null +++ b/change/@azure-msal-node-c1284212-4af4-45ff-829d-e7c277947e88.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "ROPC added for Confidential Clients (#3838)", + "packageName": "@azure/msal-node", + "email": "sameera.gajjarapu@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-common/src/client/UsernamePasswordClient.ts b/lib/msal-common/src/client/UsernamePasswordClient.ts index 4818f4297f..e94a6d8d3c 100644 --- a/lib/msal-common/src/client/UsernamePasswordClient.ts +++ b/lib/msal-common/src/client/UsernamePasswordClient.ts @@ -94,7 +94,7 @@ export class UsernamePasswordClient extends BaseClient { parameterBuilder.addLibraryInfo(this.config.libraryInfo); parameterBuilder.addThrottling(); - + if (this.serverTelemetryManager) { parameterBuilder.addServerTelemetry(this.serverTelemetryManager); } @@ -102,6 +102,16 @@ export class UsernamePasswordClient extends BaseClient { const correlationId = request.correlationId || this.config.cryptoInterface.createNewGuid(); parameterBuilder.addCorrelationId(correlationId); + 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); + } + if (!StringUtils.isEmptyObj(request.claims) || this.config.authOptions.clientCapabilities && this.config.authOptions.clientCapabilities.length > 0) { parameterBuilder.addClaims(request.claims, this.config.authOptions.clientCapabilities); } diff --git a/lib/msal-node/src/client/ClientApplication.ts b/lib/msal-node/src/client/ClientApplication.ts index f8eb045015..b443442c1a 100644 --- a/lib/msal-node/src/client/ClientApplication.ts +++ b/lib/msal-node/src/client/ClientApplication.ts @@ -19,6 +19,8 @@ import { CommonRefreshTokenRequest, CommonAuthorizationCodeRequest, CommonAuthorizationUrlRequest, + CommonUsernamePasswordRequest, + UsernamePasswordClient, AuthenticationScheme, ResponseMode, AuthorityOptions, @@ -36,6 +38,7 @@ import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest"; import { RefreshTokenRequest } from "../request/RefreshTokenRequest"; import { SilentFlowRequest } from "../request/SilentFlowRequest"; import { version, name } from "../packageMetadata"; +import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest"; /** * Base abstract class for all ClientApplications - public and confidential @@ -99,7 +102,7 @@ export abstract class ClientApplication { responseMode: request.responseMode || ResponseMode.QUERY, authenticationScheme: AuthenticationScheme.BEARER }; - + const authClientConfig = await this.buildOauthClientConfiguration( validRequest.authority, validRequest.correlationId @@ -210,6 +213,38 @@ export abstract class ClientApplication { } } + /** + * Acquires tokens with password grant by exchanging client applications username and password for credentials + * + * The latest OAuth 2.0 Security Best Current Practice disallows the password grant entirely. + * More details on this recommendation at https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-3.4 + * Microsoft's documentation and recommendations are at: + * https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#usernamepassword + * + * @param request - UsenamePasswordRequest + */ + async acquireTokenByUsernamePassword(request: UsernamePasswordRequest): Promise { + this.logger.info("acquireTokenByUsernamePassword called", request.correlationId); + const validRequest: CommonUsernamePasswordRequest = { + ...request, + ...this.initializeBaseRequest(request) + }; + const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenByUsernamePassword, validRequest.correlationId!); + try { + const usernamePasswordClientConfig = await this.buildOauthClientConfiguration( + validRequest.authority, + validRequest.correlationId, + serverTelemetryManager + ); + const usernamePasswordClient = new UsernamePasswordClient(usernamePasswordClientConfig); + this.logger.verbose("Username password client created", validRequest.correlationId); + return usernamePasswordClient.acquireToken(validRequest); + } catch (e) { + serverTelemetryManager.cacheFailedRequest(e); + throw e; + } + } + /** * Gets the token cache for the application. */ @@ -337,7 +372,7 @@ export abstract class ClientApplication { knownAuthorities: this.config.auth.knownAuthorities!, cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata!, authorityMetadata: this.config.auth.authorityMetadata!, - azureRegionConfiguration + azureRegionConfiguration }; return await AuthorityFactory.createDiscoveredInstance(authorityString, this.config.system!.networkClient!, this.storage, authorityOptions); } diff --git a/lib/msal-node/src/client/IConfidentialClientApplication.ts b/lib/msal-node/src/client/IConfidentialClientApplication.ts index a99810656b..db684282fb 100644 --- a/lib/msal-node/src/client/IConfidentialClientApplication.ts +++ b/lib/msal-node/src/client/IConfidentialClientApplication.ts @@ -10,6 +10,7 @@ import { ClientCredentialRequest } from "../request/ClientCredentialRequest"; import { OnBehalfOfRequest } from "../request/OnBehalfOfRequest"; import { RefreshTokenRequest } from "../request/RefreshTokenRequest"; import { SilentFlowRequest } from "../request/SilentFlowRequest"; +import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest"; import { TokenCache } from "../cache/TokenCache"; /** @@ -36,6 +37,9 @@ export interface IConfidentialClientApplication { /** Acquires tokens from the authority for the application */ acquireTokenOnBehalfOf(request: OnBehalfOfRequest): Promise; + /** Acquires tokens with password grant by exchanging client applications username and password for credentials */ + acquireTokenByUsernamePassword(request: UsernamePasswordRequest): Promise; + /** Gets the token cache for the application */ getTokenCache(): TokenCache; diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index 12aa7c6db2..78b28f93e2 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -7,15 +7,12 @@ import { ApiId } from "../utils/Constants"; import { DeviceCodeClient, AuthenticationResult, - CommonDeviceCodeRequest, - CommonUsernamePasswordRequest, - UsernamePasswordClient + CommonDeviceCodeRequest } from "@azure/msal-common"; import { Configuration } from "../config/Configuration"; import { ClientApplication } from "./ClientApplication"; import { IPublicClientApplication } from "./IPublicClientApplication"; import { DeviceCodeRequest } from "../request/DeviceCodeRequest"; -import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest"; /** * This class is to be used to acquire tokens for public client applications (desktop, mobile). Public client applications @@ -71,36 +68,4 @@ export class PublicClientApplication extends ClientApplication implements IPubli throw e; } } - - /** - * Acquires tokens with password grant by exchanging client applications username and password for credentials - * - * The latest OAuth 2.0 Security Best Current Practice disallows the password grant entirely. - * More details on this recommendation at https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-3.4 - * Microsoft's documentation and recommendations are at: - * https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#usernamepassword - * - * @param request - UsenamePasswordRequest - */ - async acquireTokenByUsernamePassword(request: UsernamePasswordRequest): Promise { - this.logger.info("acquireTokenByUsernamePassword called", request.correlationId); - const validRequest: CommonUsernamePasswordRequest = { - ...request, - ...this.initializeBaseRequest(request) - }; - const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenByUsernamePassword, validRequest.correlationId!); - try { - const usernamePasswordClientConfig = await this.buildOauthClientConfiguration( - validRequest.authority, - validRequest.correlationId, - serverTelemetryManager - ); - const usernamePasswordClient = new UsernamePasswordClient(usernamePasswordClientConfig); - this.logger.verbose("Username password client created", validRequest.correlationId); - return usernamePasswordClient.acquireToken(validRequest); - } catch (e) { - serverTelemetryManager.cacheFailedRequest(e); - throw e; - } - } } diff --git a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts index 7ae47c7873..d082770716 100644 --- a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts +++ b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts @@ -1,5 +1,5 @@ import { ConfidentialClientApplication } from './../../src/client/ConfidentialClientApplication'; -import { Authority, ClientConfiguration, AuthorityFactory, AuthorizationCodeClient, RefreshTokenClient, StringUtils } from '@azure/msal-common'; +import { Authority, ClientConfiguration, AuthorityFactory, AuthorizationCodeClient, RefreshTokenClient, StringUtils, ClientCredentialClient, OnBehalfOfClient, UsernamePasswordClient } from '@azure/msal-common'; import { TEST_CONSTANTS } from '../utils/TestConstants'; import { Configuration } from "../../src/config/Configuration"; import { AuthorizationCodeRequest } from "../../src/request/AuthorizationCodeRequest"; @@ -7,6 +7,7 @@ import { mocked } from 'ts-jest/utils'; import { RefreshTokenRequest } from "../../src/request/RefreshTokenRequest"; import { ClientCredentialRequest } from "../../src/request/ClientCredentialRequest"; import { OnBehalfOfRequest } from "../../src/request/OnBehalfOfRequest"; +import { UsernamePasswordRequest } from '../../src/request/UsernamePasswordRequest'; jest.mock('@azure/msal-common'); @@ -94,8 +95,8 @@ describe('ConfidentialClientApplication', () => { const authApp = new ConfidentialClientApplication(appConfig); await authApp.acquireTokenByClientCredential(request); - expect(AuthorizationCodeClient).toHaveBeenCalledTimes(1); - expect(AuthorizationCodeClient).toHaveBeenCalledWith( + expect(ClientCredentialClient).toHaveBeenCalledTimes(1); + expect(ClientCredentialClient).toHaveBeenCalledWith( expect.objectContaining(expectedConfig) ); }); @@ -110,8 +111,25 @@ describe('ConfidentialClientApplication', () => { const authApp = new ConfidentialClientApplication(appConfig); await authApp.acquireTokenOnBehalfOf(request); - expect(AuthorizationCodeClient).toHaveBeenCalledTimes(1); - expect(AuthorizationCodeClient).toHaveBeenCalledWith( + expect(OnBehalfOfClient).toHaveBeenCalledTimes(1); + expect(OnBehalfOfClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); + }); + + test('acquireTokenByUsernamePassword', async () => { + const request: UsernamePasswordRequest = { + scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, + username: TEST_CONSTANTS.USERNAME, + password: TEST_CONSTANTS.PASSWORD + }; + + mocked(AuthorityFactory.createDiscoveredInstance).mockReturnValue(Promise.resolve(authority)); + + const authApp = new ConfidentialClientApplication(appConfig); + await authApp.acquireTokenByUsernamePassword(request); + expect(UsernamePasswordClient).toHaveBeenCalledTimes(1); + expect(UsernamePasswordClient).toHaveBeenCalledWith( expect.objectContaining(expectedConfig) ); }); diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index 296dd8c0a3..cdf587eb16 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -9,6 +9,7 @@ import { AuthorizationCodeClient, DeviceCodeClient, RefreshTokenClient, + UsernamePasswordClient, ClientConfiguration, ProtocolMode, Logger, @@ -18,6 +19,7 @@ import { import { AuthorizationUrlRequest } from "../../src/request/AuthorizationUrlRequest"; import { DeviceCodeRequest } from "../../src/request/DeviceCodeRequest"; import { RefreshTokenRequest } from "../../src/request/RefreshTokenRequest"; +import { UsernamePasswordRequest } from '../../src/request/UsernamePasswordRequest'; import { NodeStorage } from "../../src/cache/NodeStorage"; import { HttpClient } from "../../src/network/HttpClient"; @@ -131,6 +133,23 @@ describe('PublicClientApplication', () => { ); }); + test('acquireTokenByUsernamePassword', async () => { + const request: UsernamePasswordRequest = { + scopes: TEST_CONSTANTS.DEFAULT_GRAPH_SCOPE, + username: TEST_CONSTANTS.USERNAME, + password: TEST_CONSTANTS.PASSWORD + }; + + mocked(AuthorityFactory.createDiscoveredInstance).mockReturnValue(Promise.resolve(authority)); + + const authApp = new PublicClientApplication(appConfig); + await authApp.acquireTokenByUsernamePassword(request); + expect(UsernamePasswordClient).toHaveBeenCalledTimes(1); + expect(UsernamePasswordClient).toHaveBeenCalledWith( + expect.objectContaining(expectedConfig) + ); + }); + test('acquireToken default authority', async () => { // No authority set in app configuration or request, should default to common authority const config: Configuration = { diff --git a/lib/msal-node/test/utils/TestConstants.ts b/lib/msal-node/test/utils/TestConstants.ts index a09f1ffe88..d7f205df0c 100644 --- a/lib/msal-node/test/utils/TestConstants.ts +++ b/lib/msal-node/test/utils/TestConstants.ts @@ -14,6 +14,8 @@ export const TEST_CONSTANTS = { REDIRECT_URI: "http://localhost:8080", CLIENT_SECRET: "MOCK_CLIENT_SECRET", DEFAULT_GRAPH_SCOPE: ["user.read"], + USERNAME: "mockuser", + PASSWORD: "mockpassword", AUTHORIZATION_CODE: "0.ASgAqPq4kJXMDkamGO53C-4XWVm3ypmrKgtCkdhePY1PBjsoAJg.AQABAAIAAAAm-06blBE1TpVMil8KPQ41DOje1jDj1oK3KxTXGKg89VjLYJi71gx_npOoxVfC7X49MqOX7IltTJOilUId-IAHndHXlfWzoSGq3GUmwAOLMisftceBRtq3YBsvHX7giiuSZXJgpgu03uf3V2h5Z3GJNpnSXT1f7iVFuRvGh1-jqjWxKs2un8AS5rhti1ym1zxkeicKT43va5jQeHVUlTQo69llnwQJ3iKmKLDVq_Q25Au4EQjYaeEx6TP5IZSqPPm7x0bynmjE8cqR5r4ySP4wH8fjnxlLySrUEZObk2VgREB1AdH6-xKIa04EnJEj9dUgTwiFvQumkuHHetFOgH7ep_9diFOdAOQLUK8C9N4Prlj0JiOcgn6l0xYd5Q9691Ylw8UfifLwq_B7f30mMLN64_XgoBY9K9CR1L4EC1kPPwIhVv3m6xmbhXZ3efx-A-bbV2SYcO4D4ZlnQztHzie_GUlredtsdEMAOE3-jaMJs7i2yYMuIEEtRcHIjV_WscVooCDdKmVncHOObWhNUSdULAejBr3pFs0v3QO_xZ269eLu5Z0qHzCZ_EPg2aL-ERz-rpgdclQ_H_KnEtMsC4F1RgAnDjVmSRKJZZdnNLfKSX_Wd40t_nuo4kjN2cSt8QzzeL533zIZ4CxthOsC4HH2RcUZDIgHdLDLT2ukg-Osc6J9URpZP-IUpdjXg_uwbkHEjrXDMBMo2pmCqaWbMJKo5Lr7CrystifnDITXzZmmOah8HV83Xyb6EP8Gno6JRuaG80j8BKDWyb1Yof4rnLI1kZ59n_t2d0LnRBXz50PdWCWX6vtkg-kAV-bGJQr45XDSKBSv0Q_fVsdLMk24NacUZcF5ujUtqv__Bv-wATzCHWlbUDGHC8nHEi84PcYAjSsgAA", ACCESS_TOKEN: "ThisIsAnAccessT0ken", @@ -24,7 +26,7 @@ export const TEST_CONSTANTS = { CLIENT_ASSERTION: "MOCK_CLIENT_ASSERTION", THUMBPRINT: "6182de7d4b84517655fe0bfa97076890d66bf37a", PRIVATE_KEY: "PRIVATE_KEY", - PUBLIC_CERTIFICATE: + PUBLIC_CERTIFICATE: `-----BEGIN CERTIFICATE----- line1 line2 diff --git a/samples/msal-node-samples/username-password-cca/.npmrc b/samples/msal-node-samples/username-password-cca/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/samples/msal-node-samples/username-password-cca/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/samples/msal-node-samples/username-password-cca/index.js b/samples/msal-node-samples/username-password-cca/index.js new file mode 100644 index 0000000000..aa5a933ade --- /dev/null +++ b/samples/msal-node-samples/username-password-cca/index.js @@ -0,0 +1,31 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +var msal = require("@azure/msal-node"); + +const msalConfig = { + auth: { + clientId: "", // Enter your client_id here + authority: "", // Enter your authority here + clientSecret: "" // Enter your client_secret here + } +}; + +const cca = new msal.ConfidentialClientApplication(msalConfig); + +const usernamePasswordRequest = { + scopes: ["user.read"], + username: "", // Add your username here + password: "", // Add your password here +}; + +cca.acquireTokenByUsernamePassword(usernamePasswordRequest).then((response) => { + console.log("acquired token by password grant in confidential clients"); +}).catch((error) => { + console.log(error); +}); + + + diff --git a/samples/msal-node-samples/username-password-cca/package.json b/samples/msal-node-samples/username-password-cca/package.json new file mode 100644 index 0000000000..47d26349db --- /dev/null +++ b/samples/msal-node-samples/username-password-cca/package.json @@ -0,0 +1,18 @@ +{ + "name": "msal-node-username-password-cca", + "version": "1.0.0", + "description": "Command line app that uses Oauth password grant flow to get a token from Azure AD", + "main": "index.js", + "private": true, + "scripts": { + "start": "node index.js", + "build:package": "cd ../../lib/msal-common && npm run build && cd ../msal-node && npm run build", + "start:build": "npm run build:package && npm start", + "install:local": "npm install ../../../lib/msal-node" + }, + "author": "Microsoft", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "file:../../../lib/msal-node" + } +}