From 34c39709d1eedf3e051d80a978e94fe8f75b44cf Mon Sep 17 00:00:00 2001 From: Thomas Norling Date: Mon, 7 Oct 2024 17:04:14 -0700 Subject: [PATCH] PoP Support for Node when brokered (#7360) Fixes PoP support for Node when using the native broker --- ...-d62fddd6-6196-4003-a391-7c4d7ee1abc5.json | 7 + extensions/msal-node-extensions/package.json | 2 +- .../src/broker/NativeBrokerPlugin.ts | 24 ++- .../test/broker/NativeBrokerPlugin.spec.ts | 142 ++++++++++++++++++ lib/msal-node/docs/brokering.md | 42 ++++++ package-lock.json | 6 +- 6 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 change/@azure-msal-node-extensions-d62fddd6-6196-4003-a391-7c4d7ee1abc5.json diff --git a/change/@azure-msal-node-extensions-d62fddd6-6196-4003-a391-7c4d7ee1abc5.json b/change/@azure-msal-node-extensions-d62fddd6-6196-4003-a391-7c4d7ee1abc5.json new file mode 100644 index 0000000000..8880e2b6bf --- /dev/null +++ b/change/@azure-msal-node-extensions-d62fddd6-6196-4003-a391-7c4d7ee1abc5.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Fix POP token acquisition via MsalRuntime", + "packageName": "@azure/msal-node-extensions", + "email": "thomas.norling@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/extensions/msal-node-extensions/package.json b/extensions/msal-node-extensions/package.json index d7ead39a52..85a87b5908 100644 --- a/extensions/msal-node-extensions/package.json +++ b/extensions/msal-node-extensions/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@azure/msal-common": "14.15.0", - "@azure/msal-node-runtime": "^0.13.6-alpha.0", + "@azure/msal-node-runtime": "^0.17.1", "keytar": "^7.8.0" }, "devDependencies": { diff --git a/extensions/msal-node-extensions/src/broker/NativeBrokerPlugin.ts b/extensions/msal-node-extensions/src/broker/NativeBrokerPlugin.ts index 19332accd3..5527a47c2c 100644 --- a/extensions/msal-node-extensions/src/broker/NativeBrokerPlugin.ts +++ b/extensions/msal-node-extensions/src/broker/NativeBrokerPlugin.ts @@ -466,11 +466,10 @@ export class NativeBrokerPlugin implements INativeBrokerPlugin { if (request.authenticationScheme === AuthenticationScheme.POP) { if ( !request.resourceRequestMethod || - !request.resourceRequestUri || - !request.shrNonce + !request.resourceRequestUri ) { throw new Error( - "Authentication Scheme set to POP but one or more of the following parameters are missing: resourceRequestMethod, resourceRequestUri, shrNonce" + "Authentication Scheme set to POP but one or more of the following parameters are missing: resourceRequestMethod, resourceRequestUri" ); } const resourceUrl = new URL(request.resourceRequestUri); @@ -478,7 +477,7 @@ export class NativeBrokerPlugin implements INativeBrokerPlugin { request.resourceRequestMethod, resourceUrl.host, resourceUrl.pathname, - request.shrNonce + request.shrNonce || "" ); } @@ -548,6 +547,17 @@ export class NativeBrokerPlugin implements INativeBrokerPlugin { idTokenClaims ); + let accessToken; + let tokenType; + if (authResult.isPopAuthorization) { + // Header includes 'pop ' prefix + accessToken = authResult.authorizationHeader.split(" ")[1]; + tokenType = AuthenticationScheme.POP; + } else { + accessToken = authResult.accessToken; + tokenType = AuthenticationScheme.BEARER; + } + const result: AuthenticationResult = { authority: request.authority, uniqueId: idTokenClaims.oid || idTokenClaims.sub || "", @@ -556,12 +566,10 @@ export class NativeBrokerPlugin implements INativeBrokerPlugin { account: accountInfo, idToken: authResult.rawIdToken, idTokenClaims: idTokenClaims, - accessToken: authResult.accessToken, + accessToken: accessToken, fromCache: fromCache, expiresOn: new Date(authResult.expiresOn), - tokenType: authResult.isPopAuthorization - ? AuthenticationScheme.POP - : AuthenticationScheme.BEARER, + tokenType: tokenType, correlationId: request.correlationId, fromNativeBroker: true, }; diff --git a/extensions/msal-node-extensions/test/broker/NativeBrokerPlugin.spec.ts b/extensions/msal-node-extensions/test/broker/NativeBrokerPlugin.spec.ts index 29ae271543..9cdb57a23e 100644 --- a/extensions/msal-node-extensions/test/broker/NativeBrokerPlugin.spec.ts +++ b/extensions/msal-node-extensions/test/broker/NativeBrokerPlugin.spec.ts @@ -23,6 +23,7 @@ import { NativeSignOutRequest, PromptValue, ServerError, + AuthenticationScheme, } from "@azure/msal-common"; import { randomUUID } from "crypto"; import { NativeAuthError } from "../../src/error/NativeAuthError"; @@ -292,6 +293,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -325,6 +327,65 @@ if (process.platform === "win32") { ); }); + it("Signs user in and returns PoP token", async () => { + const testCorrelationId = generateCorrelationId(); + const testAuthenticationResult = + getTestAuthenticationResult(testCorrelationId); + const popAT = "shr.access.token"; + jest.spyOn( + msalNodeRuntime, + "SignInSilentlyAsync" + ).mockImplementation( + ( + authParams: AuthParameters, + correlationId: string, + callback: (result: AuthResult) => void + ) => { + const result: AuthResult = { + idToken: JSON.stringify( + testAuthenticationResult.idTokenClaims + ), + accessToken: testAuthenticationResult.accessToken, + authorizationHeader: `pop ${popAT}`, + rawIdToken: testAuthenticationResult.idToken, + grantedScopes: + testAuthenticationResult.scopes.join(" "), + expiresOn: + testAuthenticationResult.expiresOn!.getTime(), + isPopAuthorization: true, + account: testMsalRuntimeAccount, + CheckError: () => {}, + telemetryData: "", + }; + expect(correlationId).toEqual(testCorrelationId); + callback(result); + + return asyncHandle; + } + ); + + const nativeBrokerPlugin = new NativeBrokerPlugin(); + const request: NativeRequest = { + clientId: TEST_CLIENT_ID, + scopes: testAuthenticationResult.scopes, + correlationId: testCorrelationId, + authority: testAuthenticationResult.authority, + redirectUri: TEST_REDIRECTURI, + authenticationScheme: AuthenticationScheme.POP, + resourceRequestMethod: "POST", + resourceRequestUri: "https://contoso.com/resource", + shrNonce: "some-random-nonce", + }; + const response = await nativeBrokerPlugin.acquireTokenSilent( + request + ); + expect(response).toStrictEqual({ + ...testAuthenticationResult, + accessToken: popAT, + tokenType: AuthenticationScheme.POP, + }); + }); + it("Returns successful response if user is already signed in", async () => { const testCorrelationId = generateCorrelationId(); const testAuthenticationResult = @@ -365,6 +426,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -494,6 +556,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -549,6 +612,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -581,6 +645,64 @@ if (process.platform === "win32") { ); }); + it("Calls SignInAsync and returns successful response if user is not already signed in", async () => { + const testCorrelationId = generateCorrelationId(); + const testAuthenticationResult = + getTestAuthenticationResult(testCorrelationId); + const popAT = "shr.access.token"; + + jest.spyOn(msalNodeRuntime, "SignInAsync").mockImplementation( + ( + windowHandle: Buffer, + authParams: AuthParameters, + correlationId: string, + accountHint: string, + callback: (result: AuthResult) => void + ) => { + const result: AuthResult = { + idToken: JSON.stringify( + testAuthenticationResult.idTokenClaims + ), + accessToken: testAuthenticationResult.accessToken, + authorizationHeader: `pop ${popAT}`, + rawIdToken: testAuthenticationResult.idToken, + grantedScopes: + testAuthenticationResult.scopes.join(" "), + expiresOn: + testAuthenticationResult.expiresOn!.getTime(), + isPopAuthorization: true, + account: testMsalRuntimeAccount, + CheckError: () => {}, + telemetryData: "", + }; + expect(correlationId).toEqual(testCorrelationId); + callback(result); + + return asyncHandle; + } + ); + + const nativeBrokerPlugin = new NativeBrokerPlugin(); + const request: NativeRequest = { + clientId: TEST_CLIENT_ID, + scopes: testAuthenticationResult.scopes, + correlationId: testCorrelationId, + authority: testAuthenticationResult.authority, + redirectUri: TEST_REDIRECTURI, + authenticationScheme: AuthenticationScheme.POP, + resourceRequestMethod: "POST", + resourceRequestUri: "https://contoso.com/resource", + shrNonce: "some-random-nonce", + }; + const response = + await nativeBrokerPlugin.acquireTokenInteractive(request); + expect(response).toStrictEqual({ + ...testAuthenticationResult, + accessToken: popAT, + tokenType: AuthenticationScheme.POP, + }); + }); + it("Calls AcquireTokenInteractivelyAsync and returns successful response if user is already signed in", async () => { const testCorrelationId = generateCorrelationId(); const testAuthenticationResult = @@ -622,6 +744,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -696,6 +819,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -750,6 +874,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -804,6 +929,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -858,6 +984,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -912,6 +1039,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -962,6 +1090,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1030,6 +1159,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1076,6 +1206,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1142,6 +1273,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1189,6 +1321,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1450,6 +1583,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1507,6 +1641,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1563,6 +1698,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1621,6 +1757,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1680,6 +1817,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1735,6 +1873,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1793,6 +1932,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, @@ -1855,6 +1995,7 @@ if (process.platform === "win32") { testAuthenticationResult.idTokenClaims ), accessToken: testAuthenticationResult.accessToken, + authorizationHeader: "", rawIdToken: testAuthenticationResult.idToken, grantedScopes: testAuthenticationResult.scopes.join(" "), @@ -1913,6 +2054,7 @@ if (process.platform === "win32") { const result: AuthResult = { idToken: "", accessToken: "", + authorizationHeader: "", rawIdToken: "", grantedScopes: "", expiresOn: 0, diff --git a/lib/msal-node/docs/brokering.md b/lib/msal-node/docs/brokering.md index 9534c0cb15..b1b509b50f 100644 --- a/lib/msal-node/docs/brokering.md +++ b/lib/msal-node/docs/brokering.md @@ -55,6 +55,48 @@ pca.acquireTokenInteractive({ }); ``` +## Proof of Possession + +Access token proof of possession is supported when acquiring tokens through the native broker. To request a PoP token you will need to add a couple additional properties to the request object provided to `acquireTokenInteractive` or `acquireTokenSilent` + +### AT PoP Request Parameters + +| Name | Description | Required | +|-------------------------| ----------------------------------------------------------- | -------- | +| `authenticationScheme` | Indicates whether MSAL should acquire a `Bearer` or `PoP` token. Default is `Bearer`. | **Required** | +| `resourceRequestMethod` | The all-caps name of the HTTP method of the request that will use the signed token (`GET`, `POST`, `PUT`, etc.) | **Required** | +| `resourceRequestUri` | The URL of the protected resource for which the access token is being issued | **Required** | +| `shrNonce` | A server-generated, signed timestamp that is Base64URL encoded as a string. This nonce is used to mitigate clock-skew and time-travel attacks meant to enable PoP token pre-generation. | *Optional* | + +### Usage Example + +```javascript +import { PublicClientApplication, Configuration } from "@azure/msal-node"; +import { NativeBrokerPlugin } from "@azure/msal-node-extensions"; + +const msalConfig: Configuration = { + auth: { + clientId: "your-client-id", + }, + broker: { + nativeBrokerPlugin: new NativeBrokerPlugin(), + }, +}; + +const pca = new PublicClientApplication(msalConfig); + +const popTokenRequest = { + scopes: ["User.Read"], + authenticationScheme: msal.AuthenticationScheme.POP, + resourceRequestMethod: "POST", + resourceRequestUri: "YOUR_RESOURCE_ENDPOINT", + shrNonce: "NONCE_ACQUIRED_FROM_RESOURCE_SERVER" +} + +pca.acquireTokenInteractive(popTokenRequest); +pca.acquireTokenSilent(popTokenRequest); +``` + ## Differences when using the broker to acquire tokens There are a few things that may behave a little differently when acquiring tokens through the native broker. diff --git a/package-lock.json b/package-lock.json index a8f51b6337..2f431e4318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "license": "MIT", "dependencies": { "@azure/msal-common": "14.15.0", - "@azure/msal-node-runtime": "^0.13.6-alpha.0", + "@azure/msal-node-runtime": "^0.17.1", "keytar": "^7.8.0" }, "devDependencies": { @@ -2715,7 +2715,9 @@ "link": true }, "node_modules/@azure/msal-node-runtime": { - "version": "0.13.6-alpha.0", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.17.1.tgz", + "integrity": "sha512-qAfTg+iGJsg+XvD9nmknI63+XuoX32oT+SX4wJdFz7CS6ETVpSHoroHVaUmsTU1H7H0+q1/ZkP988gzPRMYRsg==", "hasInstallScript": true, "license": "MIT" },