Skip to content

Commit

Permalink
Merge pull request #3838 from AzureAD/ropc-cca-support
Browse files Browse the repository at this point in the history
ROPC added for confidential clients
  • Loading branch information
sameerag committed Jul 13, 2021
2 parents a0fee03 + f37884e commit 15b7652
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "ROPC added for Confidential Clients (#3838)",
"packageName": "@azure/msal-node",
"email": "sameera.gajjarapu@microsoft.com",
"dependentChangeType": "patch"
}
12 changes: 11 additions & 1 deletion lib/msal-common/src/client/UsernamePasswordClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,24 @@ export class UsernamePasswordClient extends BaseClient {
parameterBuilder.addLibraryInfo(this.config.libraryInfo);

parameterBuilder.addThrottling();

if (this.serverTelemetryManager) {
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
}

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);
}
Expand Down
39 changes: 37 additions & 2 deletions lib/msal-node/src/client/ClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
CommonRefreshTokenRequest,
CommonAuthorizationCodeRequest,
CommonAuthorizationUrlRequest,
CommonUsernamePasswordRequest,
UsernamePasswordClient,
AuthenticationScheme,
ResponseMode,
AuthorityOptions,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<AuthenticationResult | null> {
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.
*/
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/msal-node/src/client/IConfidentialClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -36,6 +37,9 @@ export interface IConfidentialClientApplication {
/** Acquires tokens from the authority for the application */
acquireTokenOnBehalfOf(request: OnBehalfOfRequest): Promise<AuthenticationResult | null>;

/** Acquires tokens with password grant by exchanging client applications username and password for credentials */
acquireTokenByUsernamePassword(request: UsernamePasswordRequest): Promise<AuthenticationResult | null>;

/** Gets the token cache for the application */
getTokenCache(): TokenCache;

Expand Down
37 changes: 1 addition & 36 deletions lib/msal-node/src/client/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<AuthenticationResult | null> {
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;
}
}
}
28 changes: 23 additions & 5 deletions lib/msal-node/test/client/ConfidentialClientApplication.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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";
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');

Expand Down Expand Up @@ -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)
);
});
Expand All @@ -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)
);
});
Expand Down
19 changes: 19 additions & 0 deletions lib/msal-node/test/client/PublicClientApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
AuthorizationCodeClient,
DeviceCodeClient,
RefreshTokenClient,
UsernamePasswordClient,
ClientConfiguration,
ProtocolMode,
Logger,
Expand All @@ -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";

Expand Down Expand Up @@ -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 = {
Expand Down
4 changes: 3 additions & 1 deletion lib/msal-node/test/utils/TestConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions samples/msal-node-samples/username-password-cca/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
31 changes: 31 additions & 0 deletions samples/msal-node-samples/username-password-cca/index.js
Original file line number Diff line number Diff line change
@@ -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);
});



18 changes: 18 additions & 0 deletions samples/msal-node-samples/username-password-cca/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}

0 comments on commit 15b7652

Please sign in to comment.