Skip to content

Commit 25aefea

Browse files
sameeragtnorlinglalimashardahectormmg
authored
Support pop as optional for full framed apps (#7119)
- This PR signs the POP tokens only if the reqCnf is not passed in as a request parameter. This is to enable any clients that choose to sign their tokens. However, please consider this an advanced feature only. - This PR also addresses the native flow bug where cnf is to be sent a string instead of a hash! - Removes reqCnfHash in the ReqCnfData since we do not use it. It is only internal, so this should not be a breaking change. --------- Co-authored-by: Thomas Norling <[email protected]> Co-authored-by: Lalima Sharda <[email protected]> Co-authored-by: Hector Morales <[email protected]>
1 parent 7563498 commit 25aefea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+633
-66
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add support for apps to set their own `reqCnf` and correct native flows cnf format #6357",
4+
"packageName": "@azure/msal-browser",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Add support for apps to set their own `reqCnf` and correct native flows cnf format #6357",
4+
"packageName": "@azure/msal-common",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "Fixed msal-node unit tests for PoP token support #\u0016\u00167119",
4+
"packageName": "@azure/msal-node",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-browser/apiReview/msal-browser.api.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,8 @@ declare namespace BrowserAuthErrorCodes {
234234
nativeConnectionNotEstablished,
235235
uninitializedPublicClientApplication,
236236
nativePromptNotSupported,
237-
invalidBase64String
237+
invalidBase64String,
238+
invalidPopTokenRequest
238239
}
239240
}
240241
export { BrowserAuthErrorCodes }
@@ -423,6 +424,10 @@ export const BrowserAuthErrorMessage: {
423424
code: string;
424425
desc: string;
425426
};
427+
invalidPopTokenRequest: {
428+
code: string;
429+
desc: string;
430+
};
426431
};
427432

428433
// Warning: (ae-missing-release-tag) "BrowserAuthOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -1022,6 +1027,11 @@ const invalidBase64String = "invalid_base64_string";
10221027
// @public (undocumented)
10231028
const invalidCacheType = "invalid_cache_type";
10241029

1030+
// Warning: (ae-missing-release-tag) "invalidPopTokenRequest" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
1031+
//
1032+
// @public (undocumented)
1033+
const invalidPopTokenRequest = "invalid_pop_token_request";
1034+
10251035
export { IPerformanceClient }
10261036

10271037
// Warning: (ae-missing-release-tag) "IPublicClientApplication" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)

lib/msal-browser/docs/access-token-proof-of-possession.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ The Proof-of-Possession authentication scheme relies on an asymmetric cryptograp
151151

152152
In the event of refreshing a bound access token, MSAL will delete the cryptographic keypair that was generated when requesting the expired bound access token, generate a new cryptographic keypair for the new access token, and store the new keypair in the keystore.
153153

154+
## Advanced feature: Application managed cryptographic keypair
155+
156+
> :warning: We do not recommend using this feature unless you are familiar with the [Proof of Possession protocol](https://oauth.net/2/dpop/) and have a specific requirement to generate your own cryptographic keypair. For most cases, we recommend the PoP usage as described in the rest of this document.
157+
158+
If you choose to generate your own cryptographic keypair, then this feature enables the application to provide the `popKid` as a request parameter. MSAL JS ensures the token issuer embeds the `cnf` in the token but returns the issued token _unsigned_. The onus of signing the access token before it is forwarded to the intended resource will be on the application.
159+
160+
Please also note to make sure the remaining [pop parameters](#at-pop-request-parameters) except the `AuthenticationScheme` are not set if you choose to leverage this behavior.
161+
154162
### Why access tokens are saved asynchronously
155163

156164
Most MSAL credentials and cache items, like `ID Tokens` for example, can be stored and removed synchronously. This is because these cache items are stored in either `localStorage` or `sessionStorage` (which can be manipulated synchronously), and they have no dependencies on other stored items that have asynchronous access restrictions.

lib/msal-browser/src/broker/nativeBroker/NativeRequest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type NativeTokenRequest = {
3131
extendedExpiryToken?: boolean;
3232
extraParameters?: StringDict;
3333
storeInCache?: StoreInCache; // Object of booleans indicating whether to store tokens in the cache or not (default is true)
34+
signPopToken?: boolean; // Set to true only if token request deos not contain a PoP keyId
3435
};
3536

3637
/**

lib/msal-browser/src/crypto/CryptoOps.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ export class CryptoOps implements ICrypto {
7878
return base64Decode(input);
7979
}
8080

81+
/**
82+
* Encodes input string to base64 URL safe string.
83+
* @param input
84+
*/
85+
base64UrlEncode(input: string): string {
86+
return urlEncode(input);
87+
}
88+
89+
/**
90+
* Stringifies and base64Url encodes input public key
91+
* @param inputKid
92+
* @returns Base64Url encoded public key
93+
*/
94+
encodeKid(inputKid: string): string {
95+
return this.base64UrlEncode(JSON.stringify({ kid: inputKid }));
96+
}
97+
8198
/**
8299
* Generates a keypair, stores it and returns a thumbprint
83100
* @param request

lib/msal-browser/src/error/BrowserAuthError.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export const BrowserAuthErrorMessages = {
9090
"The provided prompt is not supported by the native platform. This request should be routed to the web based flow.",
9191
[BrowserAuthErrorCodes.invalidBase64String]:
9292
"Invalid base64 encoded string.",
93+
[BrowserAuthErrorCodes.invalidPopTokenRequest]:
94+
"Invalid PoP token request. The request should not have both a popKid value and signPopToken set to true.",
9395
};
9496

9597
/**
@@ -333,6 +335,12 @@ export const BrowserAuthErrorMessage = {
333335
BrowserAuthErrorCodes.invalidBase64String
334336
],
335337
},
338+
invalidPopTokenRequest: {
339+
code: BrowserAuthErrorCodes.invalidPopTokenRequest,
340+
desc: BrowserAuthErrorMessages[
341+
BrowserAuthErrorCodes.invalidPopTokenRequest
342+
],
343+
},
336344
};
337345

338346
/**

lib/msal-browser/src/error/BrowserAuthErrorCodes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ export const uninitializedPublicClientApplication =
5555
"uninitialized_public_client_application";
5656
export const nativePromptNotSupported = "native_prompt_not_supported";
5757
export const invalidBase64String = "invalid_base64_string";
58+
export const invalidPopTokenRequest = "invalid_pop_token_request";

lib/msal-browser/src/interaction_client/NativeInteractionClient.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,12 @@ export class NativeInteractionClient extends BaseInteractionClient {
170170
);
171171
}
172172

173+
const { ...nativeTokenRequest } = nativeRequest;
174+
173175
// fall back to native calls
174176
const messageBody: NativeExtensionRequestBody = {
175177
method: NativeExtensionMethod.GetToken,
176-
request: nativeRequest,
178+
request: nativeTokenRequest,
177179
};
178180

179181
const response: object = await this.nativeMessageHandler.sendMessage(
@@ -289,9 +291,11 @@ export class NativeInteractionClient extends BaseInteractionClient {
289291
);
290292
const nativeRequest = await this.initializeNativeRequest(request);
291293

294+
const { ...nativeTokenRequest } = nativeRequest;
295+
292296
const messageBody: NativeExtensionRequestBody = {
293297
method: NativeExtensionMethod.GetToken,
294-
request: nativeRequest,
298+
request: nativeTokenRequest,
295299
};
296300

297301
try {
@@ -481,7 +485,7 @@ export class NativeInteractionClient extends BaseInteractionClient {
481485
request,
482486
homeAccountIdentifier,
483487
idTokenClaims,
484-
result.accessToken,
488+
response.access_token,
485489
result.tenantId,
486490
reqTimestamp
487491
);
@@ -535,7 +539,10 @@ export class NativeInteractionClient extends BaseInteractionClient {
535539
response: NativeResponse,
536540
request: NativeTokenRequest
537541
): Promise<string> {
538-
if (request.tokenType === AuthenticationScheme.POP) {
542+
if (
543+
request.tokenType === AuthenticationScheme.POP &&
544+
request.signPopToken
545+
) {
539546
/**
540547
* This code prioritizes SHR returned from the native layer. In case of error/SHR not calculated from WAM and the AT
541548
* is still received, SHR is calculated locally
@@ -725,7 +732,11 @@ export class NativeInteractionClient extends BaseInteractionClient {
725732
responseScopes.printScopes(),
726733
tokenExpirationSeconds,
727734
0,
728-
base64Decode
735+
base64Decode,
736+
undefined,
737+
request.tokenType as AuthenticationScheme,
738+
undefined,
739+
request.keyId
729740
);
730741

731742
const nativeCacheRecord = new CacheRecord(
@@ -917,8 +928,16 @@ export class NativeInteractionClient extends BaseInteractionClient {
917928
...request.tokenQueryParameters,
918929
},
919930
extendedExpiryToken: false, // Make this configurable?
931+
keyId: request.popKid,
920932
};
921933

934+
// Check for PoP token requests: signPopToken should only be set to true if popKid is not set
935+
if (validatedRequest.signPopToken && !!request.popKid) {
936+
throw createBrowserAuthError(
937+
BrowserAuthErrorCodes.invalidPopTokenRequest
938+
);
939+
}
940+
922941
this.handleExtraBrokerParams(validatedRequest);
923942
validatedRequest.extraParameters =
924943
validatedRequest.extraParameters || {};
@@ -935,17 +954,29 @@ export class NativeInteractionClient extends BaseInteractionClient {
935954
};
936955

937956
const popTokenGenerator = new PopTokenGenerator(this.browserCrypto);
938-
const reqCnfData = await invokeAsync(
939-
popTokenGenerator.generateCnf.bind(popTokenGenerator),
940-
PerformanceEvents.PopTokenGenerateCnf,
941-
this.logger,
942-
this.performanceClient,
943-
this.correlationId
944-
)(shrParameters, this.logger);
945-
946-
// to reduce the URL length, it is recommended to send the hash of the req_cnf instead of the whole string
947-
validatedRequest.reqCnf = reqCnfData.reqCnfHash;
948-
validatedRequest.keyId = reqCnfData.kid;
957+
958+
// generate reqCnf if not provided in the request
959+
let reqCnfData;
960+
if (!validatedRequest.keyId) {
961+
const generatedReqCnfData = await invokeAsync(
962+
popTokenGenerator.generateCnf.bind(popTokenGenerator),
963+
PerformanceEvents.PopTokenGenerateCnf,
964+
this.logger,
965+
this.performanceClient,
966+
request.correlationId
967+
)(shrParameters, this.logger);
968+
reqCnfData = generatedReqCnfData.reqCnfString;
969+
validatedRequest.keyId = generatedReqCnfData.kid;
970+
validatedRequest.signPopToken = true;
971+
} else {
972+
reqCnfData = this.browserCrypto.base64UrlEncode(
973+
JSON.stringify({ kid: validatedRequest.keyId })
974+
);
975+
validatedRequest.signPopToken = false;
976+
}
977+
978+
// SPAs require whole string to be passed to broker
979+
validatedRequest.reqCnf = reqCnfData;
949980
}
950981

951982
return validatedRequest;

lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
TEST_URIS,
2323
DEFAULT_TENANT_DISCOVERY_RESPONSE,
2424
DEFAULT_OPENID_CONFIG_RESPONSE,
25+
TEST_REQ_CNF_DATA,
2526
} from "../utils/StringConstants";
2627
import { AuthorizationUrlRequest } from "../../src/request/AuthorizationUrlRequest";
2728
import { RedirectRequest } from "../../src/request/RedirectRequest";
@@ -141,6 +142,56 @@ describe("StandardInteractionClient", () => {
141142
await testClient.initializeAuthorizationCodeRequest(request);
142143
expect(request.codeChallenge).toBe(TEST_CONFIG.TEST_CHALLENGE);
143144
expect(authCodeRequest.codeVerifier).toBe(TEST_CONFIG.TEST_VERIFIER);
145+
expect(authCodeRequest.popKid).toBeUndefined;
146+
});
147+
148+
it("initializeAuthorizationCodeRequest validates the request and does not influence undefined popKid param", async () => {
149+
const request: AuthorizationUrlRequest = {
150+
redirectUri: TEST_URIS.TEST_REDIR_URI,
151+
scopes: ["scope"],
152+
loginHint: "[email protected]",
153+
state: TEST_STATE_VALUES.USER_STATE,
154+
authority: TEST_CONFIG.validAuthority,
155+
correlationId: TEST_CONFIG.CORRELATION_ID,
156+
responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode,
157+
nonce: "",
158+
authenticationScheme:
159+
TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme,
160+
};
161+
162+
jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({
163+
challenge: TEST_CONFIG.TEST_CHALLENGE,
164+
verifier: TEST_CONFIG.TEST_VERIFIER,
165+
});
166+
167+
const authCodeRequest =
168+
await testClient.initializeAuthorizationCodeRequest(request);
169+
expect(authCodeRequest.popKid).toBeUndefined;
170+
});
171+
172+
it("initializeAuthorizationCodeRequest validates the request and adds reqCnf param when user defined", async () => {
173+
const request: AuthorizationUrlRequest = {
174+
redirectUri: TEST_URIS.TEST_REDIR_URI,
175+
scopes: ["scope"],
176+
loginHint: "[email protected]",
177+
state: TEST_STATE_VALUES.USER_STATE,
178+
authority: TEST_CONFIG.validAuthority,
179+
correlationId: TEST_CONFIG.CORRELATION_ID,
180+
responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode,
181+
nonce: "",
182+
authenticationScheme:
183+
TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme,
184+
popKid: TEST_REQ_CNF_DATA.kid,
185+
};
186+
187+
jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({
188+
challenge: TEST_CONFIG.TEST_CHALLENGE,
189+
verifier: TEST_CONFIG.TEST_VERIFIER,
190+
});
191+
192+
const authCodeRequest =
193+
await testClient.initializeAuthorizationCodeRequest(request);
194+
expect(authCodeRequest.popKid).toEqual(TEST_REQ_CNF_DATA.kid);
144195
});
145196

146197
it("getDiscoveredAuthority - request authority only", async () => {

lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ const cryptoInterface = {
129129
base64Encode: (input: string): string => {
130130
return "testEncodedString";
131131
},
132+
base64UrlEncode(input: string): string {
133+
return Buffer.from(input, "utf-8").toString("base64url");
134+
},
135+
encodeKid(input: string): string {
136+
return Buffer.from(JSON.stringify({ kid: input }), "utf-8").toString(
137+
"base64url"
138+
);
139+
},
132140
generatePkceCodes: async (): Promise<PkceCodes> => {
133141
return testPkceCodes;
134142
},

lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,15 @@ describe("RedirectHandler.ts Unit Tests", () => {
146146
base64Encode: (input: string): string => {
147147
return "testEncodedString";
148148
},
149+
base64UrlEncode(input: string): string {
150+
return Buffer.from(input, "utf-8").toString("base64url");
151+
},
152+
encodeKid(input: string): string {
153+
return Buffer.from(
154+
JSON.stringify({ kid: input }),
155+
"utf-8"
156+
).toString("base64url");
157+
},
149158
getPublicKeyThumbprint: async (): Promise<string> => {
150159
return TEST_POP_VALUES.ENCODED_REQ_CNF;
151160
},

lib/msal-browser/test/utils/BrowserUtils.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { TEST_CONFIG, TEST_URIS } from "./StringConstants";
77
import {
88
BrowserUtils,
99
BrowserAuthError,
10-
BrowserAuthErrorMessage,
11-
InteractionType,
10+
BrowserAuthErrorCodes,
1211
} from "../../src";
1312

1413
describe("BrowserUtils.ts Function Unit Tests", () => {
@@ -101,7 +100,7 @@ describe("BrowserUtils.ts Function Unit Tests", () => {
101100
} catch (e) {
102101
const browserAuthError = e as BrowserAuthError;
103102
expect(browserAuthError.errorCode).toBe(
104-
BrowserAuthErrorMessage.redirectInIframeError.code
103+
BrowserAuthErrorCodes.redirectInIframe
105104
);
106105
done();
107106
}

lib/msal-browser/test/utils/StringConstants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ export const TEST_POP_VALUES = {
170170
'{"kid":"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs","xms_ksl":"sw"}',
171171
};
172172

173+
export const TEST_REQ_CNF_DATA = {
174+
kid: TEST_POP_VALUES.KID,
175+
reqCnfString: TEST_POP_VALUES.DECODED_REQ_CNF,
176+
};
177+
173178
export const TEST_SSH_VALUES = {
174179
SSH_JWK:
175180
'{"kty":"RSA","n":"wDJwv083ZhGGkpMPVcBMwtSBNLu7qhT2VmKv7AyPEz_dWb8GQzNEnWT1niNjFI0isDMFWQ7X2O-dhTL9J1QguQ==","e":"AQAB"}',

0 commit comments

Comments
 (0)