-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add demonstrating proof of possession (#1461)
* setup test env to handle TextEncoder + IndexedDb * add dev deps for testing * Add IndexedDb store to handle private crypto keys * Add DPoPService and associated cypto + jwt helper functions * Add DPoP settings to UserManagerSettings * Wire up userManager to DPoP functionality for code exchange and refresh token * Add tests for DPoPService exception handling --------- Co-authored-by: Chris Keogh <[email protected]>
- Loading branch information
Showing
23 changed files
with
1,102 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,3 +22,6 @@ node_modules/ | |
.vscode/ | ||
temp/ | ||
.history/ | ||
|
||
# Jetbrains IDEs | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
'use strict'; | ||
|
||
const { TextEncoder, TextDecoder } = require('util'); | ||
const { default: $JSDOMEnvironment, TestEnvironment } = require('jest-environment-jsdom'); | ||
const crypto = require("crypto"); | ||
|
||
Object.defineProperty(exports, '__esModule', { | ||
value: true | ||
}); | ||
|
||
class JSDOMEnvironment extends $JSDOMEnvironment { | ||
constructor(...args) { | ||
const { global } = super(...args); | ||
// see https://github.com/jsdom/jsdom/issues/2524 | ||
global.TextEncoder = TextEncoder; | ||
global.TextDecoder = TextDecoder; | ||
// see https://github.com/jestjs/jest/issues/9983 | ||
global.Uint8Array = Uint8Array; | ||
global.crypto.subtle = crypto.subtle; | ||
global.crypto.randomUUID = crypto.randomUUID; | ||
// see https://github.com/dumbmatter/fakeIndexedDB#jsdom-often-used-with-jest | ||
global.structuredClone = structuredClone; | ||
} | ||
} | ||
|
||
exports.default = JSDOMEnvironment; | ||
exports.TestEnvironment = TestEnvironment === $JSDOMEnvironment ? | ||
JSDOMEnvironment : TestEnvironment; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* @public | ||
*/ | ||
export interface DPoPStore { | ||
set(key: string, value: CryptoKeyPair): Promise<void>; | ||
get(key: string): Promise<CryptoKeyPair>; | ||
remove(key: string): Promise<CryptoKeyPair>; | ||
getAllKeys(): Promise<string[]>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { IndexedDbDPoPStore } from "./IndexedDbDPoPStore"; | ||
|
||
describe("DPoPStore", () => { | ||
const subject = new IndexedDbDPoPStore(); | ||
|
||
let data: CryptoKeyPair; | ||
|
||
const createCryptoKeyPair = async () => { | ||
return await window.crypto.subtle.generateKey( | ||
{ | ||
name: "ECDSA", | ||
namedCurve: "P-256", | ||
}, | ||
false, | ||
["sign", "verify"], | ||
); | ||
}; | ||
|
||
beforeEach(async () => { | ||
data = await createCryptoKeyPair(); | ||
}); | ||
|
||
describe("set", () => { | ||
it("should return a promise", async () => { | ||
// act | ||
const p = subject.set("key", data); | ||
|
||
// assert | ||
expect(p).toBeInstanceOf(Promise); | ||
// eslint-disable-next-line no-empty | ||
try { await p; } catch {} | ||
}); | ||
|
||
it("should store a key in IndexedDB", async () => { | ||
await subject.set("foo", data); | ||
const result = await subject.get("foo"); | ||
|
||
expect(result).toEqual(data); | ||
}); | ||
}); | ||
|
||
describe("remove", () => { | ||
it("should return a promise", async () => { | ||
// act | ||
const p = subject.remove("key"); | ||
|
||
// assert | ||
expect(p).toBeInstanceOf(Promise); | ||
// eslint-disable-next-line no-empty | ||
try { await p; } catch {} | ||
}); | ||
|
||
it("should remove a key from IndexedDB", async () => { | ||
await subject.set("foo", data); | ||
let result = await subject.get("foo"); | ||
|
||
expect(result).toEqual(data); | ||
|
||
await subject.remove("foo"); | ||
result = await subject.get("foo"); | ||
expect(result).toBeUndefined(); | ||
}); | ||
|
||
it("should return a value if key exists", async () => { | ||
await subject.set("foo", data); | ||
const result = await subject.remove("foo"); | ||
|
||
expect(result).toEqual(data); | ||
}); | ||
}); | ||
|
||
describe("getAllKeys", () => { | ||
it("should return a promise", async () => { | ||
// act | ||
const p = subject.getAllKeys(); | ||
|
||
// assert | ||
expect(p).toBeInstanceOf(Promise); | ||
// eslint-disable-next-line no-empty | ||
try { await p; } catch {} | ||
}); | ||
|
||
it("should get all keys in IndexedDB", async () => { | ||
await subject.set("foo", data); | ||
const dataTwo = await createCryptoKeyPair(); | ||
await subject.set("boo", dataTwo); | ||
|
||
const result = await subject.getAllKeys(); | ||
expect(result.length).toEqual(2); | ||
expect(result).toContain("foo"); | ||
expect(result).toContain("boo"); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import type { DPoPStore } from "./DPoPStore"; | ||
|
||
/** | ||
* Provides a default implementation of the DPoP store using IndexedDB. | ||
* | ||
* @public | ||
*/ | ||
export class IndexedDbDPoPStore implements DPoPStore { | ||
readonly _dbName: string = "oidc"; | ||
readonly _storeName: string = "dpop"; | ||
|
||
public async set(key: string, value: CryptoKeyPair): Promise<void> { | ||
const store = await this.createStore(this._dbName, this._storeName); | ||
await store("readwrite", (str: IDBObjectStore) => { | ||
str.put(value, key); | ||
return this.promisifyRequest(str.transaction); | ||
}); | ||
} | ||
|
||
public async get(key: string): Promise<CryptoKeyPair> { | ||
const store = await this.createStore(this._dbName, this._storeName); | ||
return await store("readonly", (str) => { | ||
return this.promisifyRequest(str.get(key)); | ||
}) as CryptoKeyPair; | ||
} | ||
|
||
public async remove(key: string): Promise<CryptoKeyPair> { | ||
const item = await this.get(key); | ||
const store = await this.createStore(this._dbName, this._storeName); | ||
await store("readwrite", (str) => { | ||
return this.promisifyRequest(str.delete(key)); | ||
}); | ||
return item; | ||
} | ||
|
||
public async getAllKeys(): Promise<string[]> { | ||
const store = await this.createStore(this._dbName, this._storeName); | ||
return await store("readonly", (str) => { | ||
return this.promisifyRequest(str.getAllKeys()); | ||
}) as string[]; | ||
} | ||
|
||
promisifyRequest<T = undefined>( | ||
request: IDBRequest<T> | IDBTransaction): Promise<T> { | ||
return new Promise<T>((resolve, reject) => { | ||
(request as IDBTransaction).oncomplete = (request as IDBRequest<T>).onsuccess = () => resolve((request as IDBRequest<T>).result); | ||
(request as IDBTransaction).onabort = (request as IDBRequest<T>).onerror = () => reject((request as IDBRequest<T>).error); | ||
}); | ||
} | ||
|
||
async createStore<T>( | ||
dbName: string, | ||
storeName: string, | ||
): Promise<(txMode: IDBTransactionMode, callback: (store: IDBObjectStore) => T | PromiseLike<T>) => Promise<T>> { | ||
const request = indexedDB.open(dbName); | ||
request.onupgradeneeded = () => request.result.createObjectStore(storeName); | ||
const db = await this.promisifyRequest<IDBDatabase>(request); | ||
|
||
return async ( | ||
txMode: IDBTransactionMode, | ||
callback: (store: IDBObjectStore) => T | PromiseLike<T>, | ||
) => { | ||
const tx = db.transaction(storeName, txMode); | ||
const store = tx.objectStore(storeName); | ||
return await callback(store); | ||
}; | ||
} | ||
} |
Oops, something went wrong.