0
0
// DPoP key management (IndexedDB) and proof creation
import { randomString, sha256Base64Url, signJwt } from './crypto.js'
const DB_VERSION = 1
const KEY_STORE = 'dpop-keys'
const KEY_ID = 'dpop-key'
const dbPromises = new Map()
function openDatabase(namespace) {
const existing = dbPromises.get(namespace)
if (existing) return existing
const promise = new Promise((resolve, reject) => {
const req = indexedDB.open(`appview-oauth-${namespace}`, DB_VERSION)
req.onerror = () => reject(req.error)
req.onsuccess = () => resolve(req.result)
req.onupgradeneeded = (e) => {
const db = e.target.result
if (!db.objectStoreNames.contains(KEY_STORE)) {
db.createObjectStore(KEY_STORE, { keyPath: 'id' })
}
}
})
dbPromises.set(namespace, promise)
return promise
}
async function getStoredKey(namespace) {
const db = await openDatabase(namespace)
return new Promise((resolve, reject) => {
const tx = db.transaction(KEY_STORE, 'readonly')
const req = tx.objectStore(KEY_STORE).get(KEY_ID)
req.onsuccess = () => resolve(req.result || null)
req.onerror = () => reject(req.error)
})
}
async function storeKey(namespace, privateKey, publicJwk) {
const db = await openDatabase(namespace)
return new Promise((resolve, reject) => {
const tx = db.transaction(KEY_STORE, 'readwrite')
const req = tx.objectStore(KEY_STORE).put({
id: KEY_ID,
privateKey,
publicJwk,
createdAt: Date.now(),
})
req.onsuccess = () => resolve()
req.onerror = () => reject(req.error)
})
}
export async function getOrCreateDPoPKey(namespace) {
const existing = await getStoredKey(namespace)
if (existing) return existing
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
false, // non-extractable private key
['sign'],
)
const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
await storeKey(namespace, keyPair.privateKey, publicJwk)
return { id: KEY_ID, privateKey: keyPair.privateKey, publicJwk, createdAt: Date.now() }
}
export async function clearDPoPKey(namespace) {
const db = await openDatabase(namespace)
return new Promise((resolve, reject) => {
const tx = db.transaction(KEY_STORE, 'readwrite')
const req = tx.objectStore(KEY_STORE).delete(KEY_ID)
req.onsuccess = () => resolve()
req.onerror = () => reject(req.error)
})
}
export async function createDPoPProof(namespace, method, url, accessToken) {
const keyData = await getOrCreateDPoPKey(namespace)
const { kty, crv, x, y } = keyData.publicJwk
const header = { alg: 'ES256', typ: 'dpop+jwt', jwk: { kty, crv, x, y } }
const payload = {
jti: randomString(16),
htm: method,
htu: url.split('?')[0],
iat: Math.floor(Date.now() / 1000),
}
if (accessToken) {
payload.ath = await sha256Base64Url(accessToken)
}
return signJwt(header, payload, keyData.privateKey)
}