Skip to content

Access Token Proof-of-Possession for msal-browser #2289

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

Merged
merged 70 commits into from
Oct 2, 2020
Merged
Show file tree
Hide file tree
Changes from 65 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
dd1507d
Adding generateCnf step for pop flow.
Aug 14, 2020
0d9f9b4
Generate kid in browser
Aug 14, 2020
03472db
Update BrowserCrypto.ts
Aug 14, 2020
5a9fb0b
Update AuthorizationUrlRequest.ts
Aug 14, 2020
41553fe
Update AuthorizationCodeRequest.ts
Aug 14, 2020
fc055de
Update AuthorizationCodeClient.ts
Aug 14, 2020
c5608c8
Returns token as Proof-of-possession token
Aug 15, 2020
efe1dea
Revert "Returns token as Proof-of-possession token"
Aug 15, 2020
03e051d
msal-common changes only
Aug 15, 2020
589a22a
Adds browser implementation of token signing and key storage
Aug 15, 2020
0864567
Update refresh token flow to include req_cnf
Aug 15, 2020
559b4e4
Update PopTokenGenerator.ts
Aug 15, 2020
d0f5010
Adding feedback from PR
Aug 18, 2020
b883959
Merge branch 'pop-req-thumbprint' into pop-handle-response
Aug 18, 2020
4d8104c
Update authConfig.js
Aug 18, 2020
9ee748b
change to authenticationScheme
Aug 18, 2020
224f9a7
Update CryptoOps.ts
Aug 18, 2020
536987e
removing id token references from JwtToken class
Aug 18, 2020
a12d371
Merge branch 'pop-handle-response' into pop-browser-sign-token
Aug 18, 2020
028d676
Moving constants
Aug 18, 2020
c6bdfee
Merge branch 'pop-browser-sign-token' into pop-refresh-token
Aug 18, 2020
3ec1a9e
Update type name
Aug 18, 2020
5710d0e
update browser crypto
Aug 18, 2020
ab4d418
Updating token exports in browser
Aug 18, 2020
fbff7dc
Merge branch 'pop-handle-response' into pop-browser-sign-token
Aug 18, 2020
30785a5
Merge branch 'pop-browser-sign-token' into pop-refresh-token
Aug 18, 2020
def300e
Update BrowserCrypto.ts
Aug 18, 2020
ee43a7d
Fixing and adding tests
Aug 20, 2020
67df5d4
adding browser tests
Aug 20, 2020
aaa3567
Adding timeout to relevant test
Aug 20, 2020
6a0cbf9
Update CryptoOps.spec.ts
Aug 20, 2020
134b0e6
Merge pull request #2151 from AzureAD/pop-req-thumbprint
Aug 20, 2020
a28a61a
Merge branch 'pop-req-thumbprint' into pop-handle-response
Aug 20, 2020
7719ee4
rename JwtToken to AuthToken
Aug 27, 2020
19f9780
Adding changes from PR feedback
Aug 27, 2020
56070a9
Fixing msal-common tests
Aug 27, 2020
21add17
update browser tests
Aug 27, 2020
ef47cd8
All tests fixed and passing
Aug 28, 2020
372a3eb
Update CryptoProvider.ts
Aug 28, 2020
572bbda
Update CryptoProvider.ts
Aug 28, 2020
d9a92c7
Update CryptoProvider.ts
Aug 28, 2020
f6aa724
Update CryptoOps.spec.ts
Aug 28, 2020
9b1144f
Merge pull request #2153 from AzureAD/pop-handle-response
Aug 28, 2020
7046ebf
Merge branch 'pop-handle-response' into pop-browser-sign-token
Aug 28, 2020
f6ff68e
Merge branch 'pop-browser-sign-token' into pop-refresh-token
Aug 28, 2020
c33aa5a
Fixing tests
Sep 1, 2020
fb00edf
Adding feedback from PR - no underscore in private var, generic types…
Sep 2, 2020
bc32581
Merge pull request #2154 from AzureAD/pop-browser-sign-token
Sep 2, 2020
361b00b
fixing msal-common tests
Sep 8, 2020
3e13ba8
Merge branch 'pop-browser-sign-token' into pop-refresh-token
Sep 8, 2020
9ac59d6
Merge pull request #2209 from AzureAD/pop-refresh-token
Sep 9, 2020
1b22589
Merge branch 'dev' into pop
Sep 14, 2020
cef47f3
common build passing
Sep 14, 2020
20307e3
common tests passing
Sep 14, 2020
aeabefb
browser tests passing
Sep 14, 2020
9ce1357
Merge branch 'dev' into pop
Sep 14, 2020
89a0bfb
Merge branch 'dev' into pop
Sep 16, 2020
c1a07a2
updating samples for pop
Sep 17, 2020
57887a0
Merge branch 'dev' into pop
Sep 29, 2020
c58a6e4
Merge branch 'dev' into pop
Sep 29, 2020
9caeebe
Fix tests
Sep 29, 2020
03e705d
Update CryptoOps.spec.ts
Sep 29, 2020
8862eb6
Merge branch 'dev' into pop
Sep 30, 2020
9f5c303
Fix issue with token validation
Oct 1, 2020
3dc2e3b
Merge branch 'dev' into pop
Oct 1, 2020
f2568e6
made node obo samples private packages
Oct 1, 2020
3cffbc9
Change files
Oct 1, 2020
541531c
Update CryptoOps.ts
Oct 1, 2020
8916635
Update graph.js
Oct 1, 2020
65db2f0
Adding signature validation test for pop tokens
Oct 2, 2020
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
9 changes: 0 additions & 9 deletions lib/msal-angular/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/msal-browser/src/app/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class PublicClientApplication extends ClientApplication implements IPubli
// Telemetry manager only used to increment cacheHits here
const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenSilent_silentFlow, silentRequest.correlationId);
const silentAuthClient = await this.createSilentFlowClient(serverTelemetryManager, silentRequest.authority);
return silentAuthClient.acquireCachedToken(silentRequest);
return await silentAuthClient.acquireCachedToken(silentRequest);
} catch (e) {
return this.acquireTokenByRefreshToken(request);
}
Expand Down
95 changes: 95 additions & 0 deletions lib/msal-browser/src/cache/DatabaseStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

interface IDBOpenDBRequestEvent extends Event {
target: IDBOpenDBRequest & EventTarget;
}

interface IDBOpenOnUpgradeNeededEvent extends IDBVersionChangeEvent {
target: IDBOpenDBRequest & EventTarget;
}

interface IDBRequestEvent extends Event {
target: IDBRequest & EventTarget;
}

/**
* Storage wrapper for IndexedDB storage in browsers: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
*/
export class DatabaseStorage<T>{
private db : IDBDatabase;
private dbName: string;
private tableName: string;
private version: number;
private dbOpen: boolean;

constructor(dbName: string, tableName: string, version: number) {
this.dbName = dbName;
this.tableName = tableName;
this.version = version;
this.dbOpen = false;
}

/**
* Opens IndexedDB instance.
*/
async open(): Promise<void> {
return new Promise((resolve, reject) => {
// TODO: Add timeouts?
const openDB = window.indexedDB.open(this.dbName, this.version);
openDB.addEventListener("upgradeneeded", (e: IDBOpenOnUpgradeNeededEvent) => {
e.target.result.createObjectStore(this.tableName);
});
openDB.addEventListener("success", (e: IDBOpenDBRequestEvent) => {
this.db = e.target.result;
this.dbOpen = true;
resolve();
});

openDB.addEventListener("error", error => reject(error));
});
}

/**
* Retrieves item from IndexedDB instance.
* @param key
*/
async get(key: string): Promise<T> {
if (!this.dbOpen) {
await this.open();
}

return new Promise<T>((resolve, reject) => {
// TODO: Add timeouts?
const transaction = this.db.transaction([this.tableName], "readonly");

const objectStore = transaction.objectStore(this.tableName);
const dbGet = objectStore.get(key);
dbGet.addEventListener("success", (e: IDBRequestEvent) => resolve(e.target.result));
dbGet.addEventListener("error", e => reject(e));
});
}

/**
* Adds item to IndexedDB under given key
* @param key
* @param payload
*/
async put(key: string, payload: T): Promise<T> {
if (!this.dbOpen) {
await this.open();
}

return new Promise<T>((resolve: any, reject: any) => {
// TODO: Add timeouts?
const transaction = this.db.transaction([this.tableName], "readwrite");
const objectStore = transaction.objectStore(this.tableName);

const dbPut = objectStore.put(payload, key);
dbPut.addEventListener("success", (e: IDBRequestEvent) => resolve(e.target.result));
dbPut.addEventListener("error", e => reject(e));
});
}
}
184 changes: 174 additions & 10 deletions lib/msal-browser/src/crypto/BrowserCrypto.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
import { BrowserStringUtils } from "../utils/BrowserStringUtils";
import { BrowserAuthError } from "../error/BrowserAuthError";

import { KEY_FORMAT_JWK } from "../utils/BrowserConstants";
/**
* See here for more info on RsaHashedKeyGenParams: https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams
*/
// RSA KeyGen Algorithm
const PKCS1_V15_KEYGEN_ALG = "RSASSA-PKCS1-v1_5";
// SHA-256 hashing algorithm
const HASH_ALG = "SHA-256";
const S256_HASH_ALG = "SHA-256";
// MOD length for PoP tokens
const MODULUS_LENGTH = 2048;
// Public Exponent
const PUBLIC_EXPONENT: Uint8Array = new Uint8Array([0x01, 0x00, 0x01]);

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

private _keygenAlgorithmOptions: RsaHashedKeyGenParams;

constructor() {
if (!(this.hasCryptoAPI())) {
throw BrowserAuthError.createCryptoNotAvailableError("Browser crypto or msCrypto object not available.");
}

this._keygenAlgorithmOptions = {
name: PKCS1_V15_KEYGEN_ALG,
hash: S256_HASH_ALG,
modulusLength: MODULUS_LENGTH,
publicExponent: PUBLIC_EXPONENT
};
}

/**
Expand All @@ -27,7 +46,7 @@ export class BrowserCrypto {
async sha256Digest(dataString: string): Promise<ArrayBuffer> {
const data = BrowserStringUtils.stringToUtf8Arr(dataString);

return this.hasIECrypto() ? this.getMSCryptoDigest(HASH_ALG, data) : this.getSubtleCryptoDigest(HASH_ALG, data);
return this.hasIECrypto() ? this.getMSCryptoDigest(S256_HASH_ALG, data) : this.getSubtleCryptoDigest(S256_HASH_ALG, data);
}

/**
Expand All @@ -43,17 +62,52 @@ export class BrowserCrypto {
}

/**
* Checks whether IE crypto (AKA msCrypto) is available.
* Generates a keypair based on current keygen algorithm config.
* @param extractable
* @param usages
*/
private hasIECrypto(): boolean {
return !!window["msCrypto"];
async generateKeyPair(extractable: boolean, usages: Array<KeyUsage>): Promise<CryptoKeyPair> {
return (
this.hasIECrypto() ?
this.msCryptoGenerateKey(extractable, usages)
: window.crypto.subtle.generateKey(this._keygenAlgorithmOptions, extractable, usages)
) as Promise<CryptoKeyPair>;
}

/**
* Check whether browser crypto is available.
* Export key as Json Web Key (JWK)
* @param key
* @param format
*/
private hasBrowserCrypto(): boolean {
return !!window.crypto;
async exportJwk(key: CryptoKey): Promise<JsonWebKey> {
return this.hasIECrypto() ? this.msCryptoExportJwk(key) : window.crypto.subtle.exportKey(KEY_FORMAT_JWK, key);
}

/**
* Imports key as Json Web Key (JWK), can set extractable and usages.
* @param key
* @param format
* @param extractable
* @param usages
*/
async importJwk(key: JsonWebKey, extractable: boolean, usages: Array<KeyUsage>): Promise<CryptoKey> {
const keyString = BrowserCrypto.getJwkString(key);
const keyBuffer = BrowserStringUtils.stringToArrayBuffer(keyString);

return this.hasIECrypto() ?
this.msCryptoImportKey(keyBuffer, extractable, usages)
: window.crypto.subtle.importKey(KEY_FORMAT_JWK, key, this._keygenAlgorithmOptions, extractable, usages);
}

/**
* Signs given data with given key
* @param key
* @param data
*/
async sign(key: CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer> {
return this.hasIECrypto() ?
this.msCryptoSign(key, data)
: window.crypto.subtle.sign(this._keygenAlgorithmOptions, key, data);
}

/**
Expand All @@ -63,17 +117,32 @@ export class BrowserCrypto {
return this.hasIECrypto() || this.hasBrowserCrypto();
}

/**
* Checks whether IE crypto (AKA msCrypto) is available.
*/
private hasIECrypto(): boolean {
return "msCrypto" in window;
}

/**
* Check whether browser crypto is available.
*/
private hasBrowserCrypto(): boolean {
return "crypto" in window;
}

/**
* Helper function for SHA digest.
* @param algorithm
* @param data
*/
private async getSubtleCryptoDigest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer> {
console.log(algorithm);
return window.crypto.subtle.digest(algorithm, data);
}

/**
* Helper function for SHA digest.
* IE Helper function for SHA digest.
* @param algorithm
* @param data
*/
Expand All @@ -88,4 +157,99 @@ export class BrowserCrypto {
});
});
}

/**
* IE Helper function for generating a keypair
* @param extractable
* @param usages
*/
private async msCryptoGenerateKey(extractable: boolean, usages: Array<KeyUsage>): Promise<CryptoKeyPair> {
return new Promise((resolve: any, reject: any) => {
const msGenerateKey = window["msCrypto"].subtle.generateKey(this._keygenAlgorithmOptions, extractable, usages);
msGenerateKey.addEventListener("complete", (e: { target: { result: CryptoKeyPair | PromiseLike<CryptoKeyPair>; }; }) => {
resolve(e.target.result);
});

msGenerateKey.addEventListener("error", (error: any) => {
reject(error);
});
});
}

/**
* IE Helper function for exportKey
* @param key
* @param format
*/
private async msCryptoExportJwk(key: CryptoKey): Promise<JsonWebKey> {
return new Promise((resolve: any, reject: any) => {
const msExportKey = window["msCrypto"].subtle.exportKey(KEY_FORMAT_JWK, key);
msExportKey.addEventListener("complete", (e: { target: { result: ArrayBuffer; }; }) => {
const resultBuffer: ArrayBuffer = e.target.result;

const resultString = BrowserStringUtils.utf8ArrToString(new Uint8Array(resultBuffer))
.replace(/\r/g, "")
.replace(/\n/g, "")
.replace(/\t/g, "")
.split(" ").join("")
.replace("\u0000", "");

try {
resolve(JSON.parse(resultString));
} catch (e) {
reject(e);
}
});

msExportKey.addEventListener("error", (error: any) => {
reject(error);
});
});
}

/**
* IE Helper function for importKey
* @param key
* @param format
* @param extractable
* @param usages
*/
private async msCryptoImportKey(keyBuffer: ArrayBuffer, extractable: boolean, usages: Array<KeyUsage>): Promise<CryptoKey> {
return new Promise((resolve: any, reject: any) => {
const msImportKey = window["msCrypto"].subtle.importKey(KEY_FORMAT_JWK, keyBuffer, this._keygenAlgorithmOptions, extractable, usages);
msImportKey.addEventListener("complete", (e: { target: { result: CryptoKey | PromiseLike<CryptoKey>; }; }) => {
resolve(e.target.result);
});

msImportKey.addEventListener("error", (error: any) => {
reject(error);
});
});
}

/**
* IE Helper function for sign JWT
* @param key
* @param data
*/
private async msCryptoSign(key: CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer> {
return new Promise((resolve: any, reject: any) => {
const msSign = window["msCrypto"].subtle.sign(this._keygenAlgorithmOptions, key, data);
msSign.addEventListener("complete", (e: { target: { result: ArrayBuffer | PromiseLike<ArrayBuffer>; }; }) => {
resolve(e.target.result);
});

msSign.addEventListener("error", (error: any) => {
reject(error);
});
});
}

/**
* Returns stringified jwk.
* @param jwk
*/
static getJwkString(jwk: JsonWebKey): string {
return JSON.stringify(jwk, Object.keys(jwk).sort());
}
}
Loading