Skip to content

Commit 8bbed70

Browse files
authored
EAR protocol response handling (#7656)
Finishes the initial implementation of the EAR protocol by adding response decryption and parsing.
1 parent 3f96e7f commit 8bbed70

16 files changed

+677
-309
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": "EAR Protocol response handling",
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": "patch",
3+
"comment": "Additional PerformanceEvents",
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": "Move addition of extraQueryParameters during request generation",
4+
"packageName": "@azure/msal-node",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ declare namespace BrowserAuthErrorCodes {
188188
export {
189189
pkceNotCreated,
190190
earJwkEmpty,
191+
earJweEmpty,
191192
cryptoNonExistent,
192193
emptyNavigateUri,
193194
hashEmptyError,
@@ -233,7 +234,8 @@ declare namespace BrowserAuthErrorCodes {
233234
invalidBase64String,
234235
invalidPopTokenRequest,
235236
failedToBuildHeaders,
236-
failedToParseHeaders
237+
failedToParseHeaders,
238+
failedToDecryptEarResponse
237239
}
238240
}
239241
export { BrowserAuthErrorCodes }
@@ -711,6 +713,11 @@ const databaseUnavailable = "database_unavailable";
711713
// @public (undocumented)
712714
export const DEFAULT_IFRAME_TIMEOUT_MS = 10000;
713715

716+
// Warning: (ae-missing-release-tag) "earJweEmpty" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
717+
//
718+
// @public (undocumented)
719+
const earJweEmpty = "ear_jwe_empty";
720+
714721
// Warning: (ae-missing-release-tag) "earJwkEmpty" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
715722
//
716723
// @public (undocumented)
@@ -839,6 +846,11 @@ export { ExternalTokenResponse }
839846
// @public (undocumented)
840847
const failedToBuildHeaders = "failed_to_build_headers";
841848

849+
// Warning: (ae-missing-release-tag) "failedToDecryptEarResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
850+
//
851+
// @public (undocumented)
852+
const failedToDecryptEarResponse = "failed_to_decrypt_ear_response";
853+
842854
// Warning: (ae-missing-release-tag) "failedToParseHeaders" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
843855
//
844856
// @public (undocumented)

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

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from "@azure/msal-common/browser";
1414
import { KEY_FORMAT_JWK } from "../utils/BrowserConstants.js";
1515
import { base64Encode, urlEncodeArr } from "../encode/Base64Encode.js";
16-
import { base64DecToArr } from "../encode/Base64Decode.js";
16+
import { base64Decode, base64DecToArr } from "../encode/Base64Decode.js";
1717

1818
/**
1919
* This file defines functions used by the browser library to perform cryptography operations such as
@@ -242,6 +242,79 @@ export async function generateEarKey(): Promise<string> {
242242
return base64Encode(JSON.stringify(jwk));
243243
}
244244

245+
/**
246+
* Parses earJwk for encryption key and returns CryptoKey object
247+
* @param earJwk
248+
* @returns
249+
*/
250+
async function importEarKey(earJwk: string): Promise<CryptoKey> {
251+
const b64DecodedJwk = base64Decode(earJwk);
252+
const jwkJson = JSON.parse(b64DecodedJwk);
253+
const rawKey = jwkJson.k;
254+
const keyBuffer = base64DecToArr(rawKey);
255+
256+
return window.crypto.subtle.importKey(RAW, keyBuffer, AES_GCM, false, [
257+
DECRYPT,
258+
]);
259+
}
260+
261+
/**
262+
* Decrypt ear_jwe response returned in the Encrypted Authorize Response (EAR) flow
263+
* @param earJwk
264+
* @param earJwe
265+
* @returns
266+
*/
267+
export async function decryptEarResponse(
268+
earJwk: string,
269+
earJwe: string
270+
): Promise<string> {
271+
const earJweParts = earJwe.split(".");
272+
if (earJweParts.length !== 5) {
273+
throw createBrowserAuthError(
274+
BrowserAuthErrorCodes.failedToDecryptEarResponse,
275+
"jwe_length"
276+
);
277+
}
278+
279+
const key = await importEarKey(earJwk).catch(() => {
280+
throw createBrowserAuthError(
281+
BrowserAuthErrorCodes.failedToDecryptEarResponse,
282+
"import_key"
283+
);
284+
});
285+
286+
try {
287+
const header = new TextEncoder().encode(earJweParts[0]);
288+
const iv = base64DecToArr(earJweParts[2]);
289+
const ciphertext = base64DecToArr(earJweParts[3]);
290+
const tag = base64DecToArr(earJweParts[4]);
291+
const tagLengthBits = tag.byteLength * 8;
292+
293+
// Concat ciphertext and tag
294+
const encryptedData = new Uint8Array(ciphertext.length + tag.length);
295+
encryptedData.set(ciphertext);
296+
encryptedData.set(tag, ciphertext.length);
297+
298+
const decryptedData = await window.crypto.subtle.decrypt(
299+
{
300+
name: AES_GCM,
301+
iv: iv,
302+
tagLength: tagLengthBits,
303+
additionalData: header,
304+
},
305+
key,
306+
encryptedData
307+
);
308+
309+
return new TextDecoder().decode(decryptedData);
310+
} catch (e) {
311+
throw createBrowserAuthError(
312+
BrowserAuthErrorCodes.failedToDecryptEarResponse,
313+
"decrypt"
314+
);
315+
}
316+
}
317+
245318
/**
246319
* Generates symmetric base encryption key. This may be stored as all encryption/decryption keys will be derived from this one.
247320
*/

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export const BrowserAuthErrorMessages = {
1717
"The PKCE code challenge and verifier could not be generated.",
1818
[BrowserAuthErrorCodes.earJwkEmpty]:
1919
"No EAR encryption key provided. This is unexpected.",
20+
[BrowserAuthErrorCodes.earJweEmpty]:
21+
"Server response does not contain ear_jwe property. This is unexpected.",
2022
[BrowserAuthErrorCodes.cryptoNonExistent]:
2123
"The crypto object or function is not available.",
2224
[BrowserAuthErrorCodes.emptyNavigateUri]:
@@ -96,6 +98,8 @@ export const BrowserAuthErrorMessages = {
9698
"Failed to build request headers object.",
9799
[BrowserAuthErrorCodes.failedToParseHeaders]:
98100
"Failed to parse response headers",
101+
[BrowserAuthErrorCodes.failedToDecryptEarResponse]:
102+
"Failed to decrypt ear response",
99103
};
100104

101105
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
export const pkceNotCreated = "pkce_not_created";
77
export const earJwkEmpty = "ear_jwk_empty";
8+
export const earJweEmpty = "ear_jwe_empty";
89
export const cryptoNonExistent = "crypto_nonexistent";
910
export const emptyNavigateUri = "empty_navigate_uri";
1011
export const hashEmptyError = "hash_empty_error";
@@ -58,3 +59,4 @@ export const invalidBase64String = "invalid_base64_string";
5859
export const invalidPopTokenRequest = "invalid_pop_token_request";
5960
export const failedToBuildHeaders = "failed_to_build_headers";
6061
export const failedToParseHeaders = "failed_to_parse_headers";
62+
export const failedToDecryptEarResponse = "failed_to_decrypt_ear_response";

0 commit comments

Comments
 (0)