Skip to content

Commit 134b0e6

Browse files
author
Prithvi Kanherkar
authored
Merge pull request #2151 from AzureAD/pop-req-thumbprint
AT Proof-Of-Possession #1: Adding req_cnf to the token request
2 parents d8e196d + 6a0cbf9 commit 134b0e6

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

+948
-74
lines changed

lib/msal-browser/src/app/PublicClientApplication.ts

-3
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ export class PublicClientApplication implements IPublicClientApplication {
6464
// Network interface implementation
6565
private readonly networkClient: INetworkModule;
6666

67-
// Response promise
68-
private readonly tokenExchangePromise: Promise<AuthenticationResult>;
69-
7067
// Input configuration by developer/user
7168
private config: Configuration;
7269

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

+106-10
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,44 @@
11
/*
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
* Licensed under the MIT License.
4+
*
45
*/
56
import { BrowserStringUtils } from "../utils/BrowserStringUtils";
67
import { BrowserAuthError } from "../error/BrowserAuthError";
7-
8+
import { StringUtils } from "@azure/msal-common";
9+
/**
10+
* See here for more info on RsaHashedKeyGenParams: https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams
11+
*/
12+
// RSA KeyGen Algorithm
13+
const PKCS1_V15_KEYGEN_ALG = "RSASSA-PKCS1-v1_5";
814
// SHA-256 hashing algorithm
9-
const HASH_ALG = "SHA-256";
15+
const S256_HASH_ALG = "SHA-256";
16+
// MOD length for PoP tokens
17+
const MODULUS_LENGTH = 2048;
18+
// Public Exponent
19+
const PUBLIC_EXPONENT: Uint8Array = new Uint8Array([0x01, 0x00, 0x01]);
20+
// JWK Key Format string
21+
const KEY_FORMAT_JWK = "jwk";
1022

1123
/**
1224
* This class implements functions used by the browser library to perform cryptography operations such as
1325
* hashing and encoding. It also has helper functions to validate the availability of specific APIs.
1426
*/
1527
export class BrowserCrypto {
1628

29+
private _keygenAlgorithmOptions: RsaHashedKeyGenParams;
30+
1731
constructor() {
1832
if (!(this.hasCryptoAPI())) {
1933
throw BrowserAuthError.createCryptoNotAvailableError("Browser crypto or msCrypto object not available.");
2034
}
35+
36+
this._keygenAlgorithmOptions = {
37+
name: PKCS1_V15_KEYGEN_ALG,
38+
hash: S256_HASH_ALG,
39+
modulusLength: MODULUS_LENGTH,
40+
publicExponent: PUBLIC_EXPONENT
41+
};
2142
}
2243

2344
/**
@@ -27,7 +48,7 @@ export class BrowserCrypto {
2748
async sha256Digest(dataString: string): Promise<ArrayBuffer> {
2849
const data = BrowserStringUtils.stringToUtf8Arr(dataString);
2950

30-
return this.hasIECrypto() ? this.getMSCryptoDigest(HASH_ALG, data) : this.getSubtleCryptoDigest(HASH_ALG, data);
51+
return this.hasIECrypto() ? this.getMSCryptoDigest(S256_HASH_ALG, data) : this.getSubtleCryptoDigest(S256_HASH_ALG, data);
3152
}
3253

3354
/**
@@ -43,17 +64,25 @@ export class BrowserCrypto {
4364
}
4465

4566
/**
46-
* Checks whether IE crypto (AKA msCrypto) is available.
67+
* Generates a keypair based on current keygen algorithm config.
68+
* @param extractable
69+
* @param usages
4770
*/
48-
private hasIECrypto(): boolean {
49-
return !!window["msCrypto"];
71+
async generateKeyPair(extractable: boolean, usages: Array<KeyUsage>): Promise<CryptoKeyPair> {
72+
return (
73+
this.hasIECrypto() ?
74+
this.msCryptoGenerateKey(extractable, usages)
75+
: window.crypto.subtle.generateKey(this._keygenAlgorithmOptions, extractable, usages)
76+
) as Promise<CryptoKeyPair>;
5077
}
5178

5279
/**
53-
* Check whether browser crypto is available.
80+
* Export key as given KeyFormat (see above)
81+
* @param key
82+
* @param format
5483
*/
55-
private hasBrowserCrypto(): boolean {
56-
return !!window.crypto;
84+
async exportJwk(key: CryptoKey): Promise<JsonWebKey> {
85+
return this.hasIECrypto() ? this.msCryptoExportJwk(key) : window.crypto.subtle.exportKey(KEY_FORMAT_JWK, key);
5786
}
5887

5988
/**
@@ -63,17 +92,32 @@ export class BrowserCrypto {
6392
return this.hasIECrypto() || this.hasBrowserCrypto();
6493
}
6594

95+
/**
96+
* Checks whether IE crypto (AKA msCrypto) is available.
97+
*/
98+
private hasIECrypto(): boolean {
99+
return "msCrypto" in window;
100+
}
101+
102+
/**
103+
* Check whether browser crypto is available.
104+
*/
105+
private hasBrowserCrypto(): boolean {
106+
return "crypto" in window;
107+
}
108+
66109
/**
67110
* Helper function for SHA digest.
68111
* @param algorithm
69112
* @param data
70113
*/
71114
private async getSubtleCryptoDigest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer> {
115+
console.log(algorithm);
72116
return window.crypto.subtle.digest(algorithm, data);
73117
}
74118

75119
/**
76-
* Helper function for SHA digest.
120+
* IE Helper function for SHA digest.
77121
* @param algorithm
78122
* @param data
79123
*/
@@ -88,4 +132,56 @@ export class BrowserCrypto {
88132
});
89133
});
90134
}
135+
136+
private async msCryptoGenerateKey(extractable: boolean, usages: Array<KeyUsage>): Promise<CryptoKeyPair> {
137+
return new Promise((resolve: any, reject: any) => {
138+
const msGenerateKey = window["msCrypto"].subtle.generateKey(this._keygenAlgorithmOptions, extractable, usages);
139+
msGenerateKey.addEventListener("complete", (e: { target: { result: CryptoKeyPair | PromiseLike<CryptoKeyPair>; }; }) => {
140+
resolve(e.target.result);
141+
});
142+
143+
msGenerateKey.addEventListener("error", (error: any) => {
144+
reject(error);
145+
});
146+
});
147+
}
148+
149+
/**
150+
* IE Helper function for exporting keys
151+
* @param key
152+
* @param format
153+
*/
154+
private async msCryptoExportJwk(key: CryptoKey): Promise<JsonWebKey> {
155+
return new Promise((resolve: any, reject: any) => {
156+
const msExportKey = window["msCrypto"].subtle.exportKey(KEY_FORMAT_JWK, key);
157+
msExportKey.addEventListener("complete", (e: { target: { result: ArrayBuffer; }; }) => {
158+
const resultBuffer: ArrayBuffer = e.target.result;
159+
160+
const resultString = BrowserStringUtils.utf8ArrToString(new Uint8Array(resultBuffer))
161+
.replace(/\r/g, "")
162+
.replace(/\n/g, "")
163+
.replace(/\t/g, "")
164+
.split(" ").join("")
165+
.replace("\u0000", "");
166+
167+
try {
168+
resolve(JSON.parse(resultString));
169+
} catch (e) {
170+
reject(e);
171+
}
172+
});
173+
174+
msExportKey.addEventListener("error", (error: any) => {
175+
reject(error);
176+
});
177+
});
178+
}
179+
180+
/**
181+
* Returns stringified jwk.
182+
* @param jwk
183+
*/
184+
static getJwkString(jwk: JsonWebKey): string {
185+
return JSON.stringify(jwk, Object.keys(jwk).sort());
186+
}
91187
}

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

+14
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export class CryptoOps implements ICrypto {
2121
private b64Decode: Base64Decode;
2222
private pkceGenerator: PkceGenerator;
2323

24+
private static POP_KEY_USAGES: Array<KeyUsage> = ["sign", "verify"];
25+
private static EXTRACTABLE: boolean = true;
26+
private static POP_HASH_LENGTH = 43; // 256 bit digest / 6 bits per char = 43
27+
2428
constructor() {
2529
// Browser crypto needs to be validated first before any other classes can be set.
2630
this.browserCrypto = new BrowserCrypto();
@@ -30,6 +34,16 @@ export class CryptoOps implements ICrypto {
3034
this.pkceGenerator = new PkceGenerator(this.browserCrypto);
3135
}
3236

37+
async getPublicKeyThumbprint(): Promise<string> {
38+
const keyPair = await this.browserCrypto.generateKeyPair(CryptoOps.EXTRACTABLE, CryptoOps.POP_KEY_USAGES);
39+
// TODO: Store keypair
40+
const publicKeyJwk: JsonWebKey = await this.browserCrypto.exportJwk(keyPair.publicKey);
41+
const publicJwkString: string = BrowserCrypto.getJwkString(publicKeyJwk);
42+
const publicJwkBuffer: ArrayBuffer = await this.browserCrypto.sha256Digest(publicJwkString);
43+
const publicJwkDigest: string = this.b64Encode.urlEncodeArr(new Uint8Array(publicJwkBuffer));
44+
return this.base64Encode(publicJwkDigest).substr(0, CryptoOps.POP_HASH_LENGTH);
45+
}
46+
3347
/**
3448
* Creates a new random GUID - used to populate state and nonce.
3549
* @returns string (GUID)

lib/msal-browser/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { SilentRequest } from "./request/SilentRequest";
1414

1515
// Common Object Formats
1616
export {
17+
AuthenticationScheme,
1718
// Account
1819
AccountInfo,
1920
// Request

lib/msal-browser/test/app/PublicClientApplication.spec.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import * as Mocha from "mocha";
1+
import * as mocha from "mocha";
22
import chai from "chai";
33
import chaiAsPromised from "chai-as-promised";
4-
54
chai.use(chaiAsPromised);
65
const expect = chai.expect;
76
import sinon from "sinon";
@@ -590,7 +589,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
590589
it("Uses adal token from cache if it is present.", async () => {
591590
const idTokenClaims: IdTokenClaims = {
592591
"iss": "https://sts.windows.net/fa15d692-e9c7-4460-a743-29f2956fd429/",
593-
"exp": "1536279024",
592+
"exp": 1536279024,
594593
"name": "abeli",
595594
"nonce": "123523",
596595
"oid": "05833b6b-aa1d-42d4-9ec0-1b2bb9194438",
@@ -637,7 +636,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
637636
it("Does not use adal token from cache if it is present and SSO params have been given.", async () => {
638637
const idTokenClaims: IdTokenClaims = {
639638
"iss": "https://sts.windows.net/fa15d692-e9c7-4460-a743-29f2956fd429/",
640-
"exp": "1536279024",
639+
"exp": 1536279024,
641640
"name": "abeli",
642641
"nonce": "123523",
643642
"oid": "05833b6b-aa1d-42d4-9ec0-1b2bb9194438",
@@ -796,7 +795,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
796795
const testScope = "testscope";
797796
const idTokenClaims: IdTokenClaims = {
798797
"iss": "https://sts.windows.net/fa15d692-e9c7-4460-a743-29f2956fd429/",
799-
"exp": "1536279024",
798+
"exp": 1536279024,
800799
"name": "abeli",
801800
"nonce": "123523",
802801
"oid": "05833b6b-aa1d-42d4-9ec0-1b2bb9194438",
@@ -843,7 +842,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
843842
it("Does not use adal token from cache if it is present and SSO params have been given.", async () => {
844843
const idTokenClaims: IdTokenClaims = {
845844
"iss": "https://sts.windows.net/fa15d692-e9c7-4460-a743-29f2956fd429/",
846-
"exp": "1536279024",
845+
"exp": 1536279024,
847846
"name": "abeli",
848847
"nonce": "123523",
849848
"oid": "05833b6b-aa1d-42d4-9ec0-1b2bb9194438",

lib/msal-browser/test/crypto/CryptoOps.spec.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import crypto from "crypto";
77
import { PkceCodes } from "@azure/msal-common";
88

99
describe("CryptoOps.ts Unit Tests", () => {
10-
1110
let cryptoObj: CryptoOps;
1211
beforeEach(() => {
1312
cryptoObj = new CryptoOps();
@@ -61,7 +60,7 @@ describe("CryptoOps.ts Unit Tests", () => {
6160
expect(cryptoObj.base64Decode("Zm9vYmFy")).to.be.eq("foobar");
6261
});
6362

64-
it("generatePkceCode()", async () => {
63+
it("generatePkceCode() creates a valid Pkce code", async () => {
6564
sinon.stub(BrowserCrypto.prototype, <any>"getSubtleCryptoDigest").callsFake(async (algorithm: string, data: Uint8Array): Promise<ArrayBuffer> => {
6665
expect(algorithm).to.be.eq("SHA-256");
6766
return crypto.createHash("SHA256").update(Buffer.from(data)).digest();
@@ -75,4 +74,21 @@ describe("CryptoOps.ts Unit Tests", () => {
7574
expect(regExp.test(generatedCodes.challenge)).to.be.true;
7675
expect(regExp.test(generatedCodes.verifier)).to.be.true;
7776
});
77+
78+
it("getPublicKeyThumbprint() generates a valid request thumbprint", async () => {
79+
sinon.stub(BrowserCrypto.prototype, <any>"getSubtleCryptoDigest").callsFake(async (algorithm: string, data: Uint8Array): Promise<ArrayBuffer> => {
80+
expect(algorithm).to.be.eq("SHA-256");
81+
return crypto.createHash("SHA256").update(Buffer.from(data)).digest();
82+
});
83+
const generateKeyPairSpy = sinon.spy(BrowserCrypto.prototype, "generateKeyPair");
84+
const exportJwkSpy = sinon.spy(BrowserCrypto.prototype, "exportJwk");
85+
const pkThumbprint = await cryptoObj.getPublicKeyThumbprint();
86+
/**
87+
* Contains alphanumeric, dash '-', underscore '_', plus '+', or slash '/' with length of 43.
88+
*/
89+
const regExp = new RegExp("[A-Za-z0-9-_+/]{43}");
90+
expect(generateKeyPairSpy.calledWith(true, ["sign", "verify"]));
91+
expect(exportJwkSpy.calledWith((await generateKeyPairSpy.returnValues[0]).publicKey));
92+
expect(regExp.test(pkThumbprint)).to.be.true;
93+
}).timeout(2500);
7894
});

lib/msal-browser/test/encode/Base64Decode.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe("Base64Decode.ts Unit Tests", () => {
6767
"ver": "2.0",
6868
"iss": `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`,
6969
"sub": "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ",
70-
"exp": "1536361411",
70+
"exp": 1536361411,
7171
"name": "Abe Lincoln",
7272
"preferred_username": "[email protected]",
7373
"oid": "00000000-0000-0000-66f3-3332eca7ea81",

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from "chai";
22
import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler";
33
import { PkceCodes, NetworkRequestOptions, LogLevel, AccountInfo, AuthorityFactory, AuthorizationCodeRequest, AuthenticationResult, CacheManager, AuthorizationCodeClient } from "@azure/msal-common";
44
import { Configuration, buildConfiguration } from "../../src/config/Configuration";
5-
import { TEST_CONFIG, TEST_URIS, TEST_DATA_CLIENT_INFO, TEST_TOKENS, TEST_TOKEN_LIFETIMES, TEST_HASHES } from "../utils/StringConstants";
5+
import { TEST_CONFIG, TEST_URIS, TEST_DATA_CLIENT_INFO, TEST_TOKENS, TEST_TOKEN_LIFETIMES, TEST_HASHES, TEST_POP_VALUES } from "../utils/StringConstants";
66
import { BrowserStorage } from "../../src/cache/BrowserStorage";
77
import { BrowserAuthErrorMessage, BrowserAuthError } from "../../src/error/BrowserAuthError";
88
import sinon from "sinon";
@@ -108,6 +108,9 @@ describe("InteractionHandler.ts Unit Tests", () => {
108108
},
109109
generatePkceCodes: async (): Promise<PkceCodes> => {
110110
return testPkceCodes;
111+
},
112+
getPublicKeyThumbprint: async (): Promise<string> => {
113+
return TEST_POP_VALUES.ENCODED_REQ_CNF;
111114
}
112115
},
113116
storageInterface: new TestStorageInterface(),

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { PkceCodes, NetworkRequestOptions, LogLevel, AuthorityFactory, Authoriza
66
import { PopupHandler } from "../../src/interaction_handler/PopupHandler";
77
import { BrowserStorage } from "../../src/cache/BrowserStorage";
88
import { Configuration, buildConfiguration } from "../../src/config/Configuration";
9-
import { TEST_CONFIG, TEST_URIS, RANDOM_TEST_GUID } from "../utils/StringConstants";
9+
import { TEST_CONFIG, TEST_URIS, RANDOM_TEST_GUID, TEST_POP_VALUES } from "../utils/StringConstants";
1010
import sinon from "sinon";
1111
import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler";
1212
import { BrowserAuthErrorMessage, BrowserAuthError } from "../../src/error/BrowserAuthError";
@@ -93,6 +93,9 @@ describe("PopupHandler.ts Unit Tests", () => {
9393
generatePkceCodes: async (): Promise<PkceCodes> => {
9494
return testPkceCodes;
9595
},
96+
getPublicKeyThumbprint: async (): Promise<string> => {
97+
return TEST_POP_VALUES.ENCODED_REQ_CNF;
98+
}
9699
},
97100
storageInterface: new TestStorageInterface(),
98101
networkInterface: {

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const expect = chai.expect;
55
import sinon from "sinon";
66
import { Configuration, buildConfiguration } from "../../src/config/Configuration";
77
import { PkceCodes, NetworkRequestOptions, LogLevel, AccountInfo, AuthorityFactory, AuthorizationCodeRequest, Constants, AuthenticationResult, CacheSchemaType, CacheManager, AuthorizationCodeClient } from "@azure/msal-common";
8-
import { TEST_CONFIG, TEST_URIS, TEST_TOKENS, TEST_DATA_CLIENT_INFO, RANDOM_TEST_GUID, TEST_HASHES, TEST_TOKEN_LIFETIMES } from "../utils/StringConstants";
8+
import { TEST_CONFIG, TEST_URIS, TEST_TOKENS, TEST_DATA_CLIENT_INFO, RANDOM_TEST_GUID, TEST_HASHES, TEST_TOKEN_LIFETIMES, TEST_POP_VALUES } from "../utils/StringConstants";
99
import { BrowserStorage } from "../../src/cache/BrowserStorage";
1010
import { RedirectHandler } from "../../src/interaction_handler/RedirectHandler";
1111
import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler";
@@ -73,6 +73,9 @@ describe("RedirectHandler.ts Unit Tests", () => {
7373
generatePkceCodes: async (): Promise<PkceCodes> => {
7474
return testPkceCodes;
7575
},
76+
getPublicKeyThumbprint: async (): Promise<string> => {
77+
return TEST_POP_VALUES.ENCODED_REQ_CNF;
78+
}
7679
},
7780
storageInterface: browserStorage,
7881
networkInterface: {

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import sinon from "sinon";
77
import { SilentHandler } from "../../src/interaction_handler/SilentHandler";
88
import { BrowserStorage } from "../../src/cache/BrowserStorage";
99
import { Configuration, buildConfiguration } from "../../src/config/Configuration";
10-
import { TEST_CONFIG, testNavUrl, TEST_URIS, RANDOM_TEST_GUID } from "../utils/StringConstants";
10+
import { TEST_CONFIG, testNavUrl, TEST_URIS, RANDOM_TEST_GUID, TEST_POP_VALUES } from "../utils/StringConstants";
1111
import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler";
1212
import { BrowserAuthError, BrowserAuthErrorMessage } from "../../src/error/BrowserAuthError";
1313

@@ -94,6 +94,9 @@ describe("SilentHandler.ts Unit Tests", () => {
9494
generatePkceCodes: async (): Promise<PkceCodes> => {
9595
return testPkceCodes;
9696
},
97+
getPublicKeyThumbprint: async (): Promise<string> => {
98+
return TEST_POP_VALUES.ENCODED_REQ_CNF;
99+
}
97100
},
98101
storageInterface: new TestStorageInterface(),
99102
networkInterface: {

0 commit comments

Comments
 (0)