Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ROPC added for confidential clients #3838

Merged
merged 11 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!);
sameerag marked this conversation as resolved.
Show resolved Hide resolved
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"
}
}