UNPKG

@axa-fr/oidc-client-service-worker

Version:

OpenID Connect & OAuth authentication service worker

280 lines (253 loc) 9.16 kB
// code base on https://coolaj86.com/articles/sign-jwt-webcrypto-vanilla-js/ // String (UCS-2) to Uint8Array // // because... JavaScript, Strings, and Buffers // @ts-ignore import { DemonstratingProofOfPossessionConfiguration } from './types'; function strToUint8(str: string) { return new TextEncoder().encode(str); } // Binary String to URL-Safe Base64 // // btoa (Binary-to-Ascii) means "binary string" to base64 // @ts-ignore function binToUrlBase64(bin) { return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+/g, ''); } // UTF-8 to Binary String // // Because JavaScript has a strange relationship with strings // https://coolaj86.com/articles/base64-unicode-utf-8-javascript-and-you/ // @ts-ignore function utf8ToBinaryString(str) { const escstr = encodeURIComponent(str); // replaces any uri escape sequence, such as %0A, // with binary escape, such as 0x0A // @ts-ignore return escstr.replace(/%([0-9A-F]{2})/g, function (match: string, p1) { return String.fromCharCode(parseInt(p1, 16)); }); } // Uint8Array to URL Safe Base64 // // the shortest distant between two encodings... binary string // @ts-ignore export const uint8ToUrlBase64 = (uint8: Uint8Array) => { let bin = ''; // @ts-ignore uint8.forEach(function (code) { bin += String.fromCharCode(code); }); return binToUrlBase64(bin); }; // UCS-2 String to URL-Safe Base64 // // btoa doesn't work on UTF-8 strings // @ts-ignore function strToUrlBase64(str) { return binToUrlBase64(utf8ToBinaryString(str)); } export const defaultDemonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration = { importKeyAlgorithm: { name: 'ECDSA', namedCurve: 'P-256', hash: { name: 'ES256' }, }, signAlgorithm: { name: 'ECDSA', hash: { name: 'SHA-256' } }, generateKeyAlgorithm: { name: 'ECDSA', namedCurve: 'P-256', }, digestAlgorithm: { name: 'SHA-256' }, jwtHeaderAlgorithm: 'ES256', }; // @ts-ignore const sign = (w: any) => async ( jwk: any, headers: any, claims: any, demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration, jwtHeaderType = 'dpop+jwt', ) => { // Make a shallow copy of the key // (to set ext if it wasn't already set) jwk = Object.assign({}, jwk); // The headers should probably be empty headers.typ = jwtHeaderType; headers.alg = demonstratingProofOfPossessionConfiguration.jwtHeaderAlgorithm; switch (headers.alg) { case 'ES256': //if (!headers.kid) { // alternate: see thumbprint function below headers.jwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y }; //} break; case 'RS256': headers.jwk = { kty: jwk.kty, n: jwk.n, e: jwk.e, kid: headers.kid }; break; default: throw new Error('Unknown or not implemented JWS algorithm'); } const jws = { // @ts-ignore // JWT "headers" really means JWS "protected headers" protected: strToUrlBase64(JSON.stringify(headers)), // @ts-ignore // JWT "claims" are really a JSON-defined JWS "payload" payload: strToUrlBase64(JSON.stringify(claims)), }; // To import as EC (ECDSA, P-256, SHA-256, ES256) const keyType = demonstratingProofOfPossessionConfiguration.importKeyAlgorithm; // To make re-exportable as JSON (or DER/PEM) const exportable = true; // Import as a private key that isn't black-listed from signing const privileges = ['sign']; // Actually do the import, which comes out as an abstract key type // @ts-ignore const privateKey = await w.crypto.subtle.importKey('jwk', jwk, keyType, exportable, privileges); // Convert UTF-8 to Uint8Array ArrayBuffer // @ts-ignore const data = strToUint8(`${jws.protected}.${jws.payload}`); // The signature and hash should match the bit-entropy of the key // https://tools.ietf.org/html/rfc7518#section-3 const signatureType = demonstratingProofOfPossessionConfiguration.signAlgorithm; const signature = await w.crypto.subtle.sign(signatureType, privateKey, data); // returns an ArrayBuffer containing a JOSE (not X509) signature, // which must be converted to Uint8 to be useful // @ts-ignore jws.signature = uint8ToUrlBase64(new Uint8Array(signature)); // JWT is just a "compressed", "protected" JWS // @ts-ignore return `${jws.protected}.${jws.payload}.${jws.signature}`; }; export const JWT = { sign }; // @ts-ignore const generate = (w: any) => async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => { const keyType = generateKeyAlgorithm; const exportable = true; const privileges = ['sign', 'verify']; // @ts-ignore const key = await w.crypto.subtle.generateKey(keyType, exportable, privileges); // returns an abstract and opaque WebCrypto object, // which in most cases you'll want to export as JSON to be able to save return await w.crypto.subtle.exportKey('jwk', key.privateKey); }; // Create a Public Key from a Private Key // // chops off the private parts // @ts-ignore const neuter = jwk => { const copy = Object.assign({}, jwk); delete copy.d; copy.key_ops = ['verify']; return copy; }; const EC = { generate, neuter, }; // @ts-ignore const thumbprint = (w: any) => async (jwk, digestAlgorithm: AlgorithmIdentifier) => { let sortedPub; // lexigraphically sorted, no spaces switch (jwk.kty) { case 'EC': sortedPub = '{"crv":"CRV","kty":"EC","x":"X","y":"Y"}' .replace('CRV', jwk.crv) .replace('X', jwk.x) .replace('Y', jwk.y); break; case 'RSA': sortedPub = '{"e":"E","kty":"RSA","n":"N"}'.replace('E', jwk.e).replace('N', jwk.n); break; default: throw new Error('Unknown or not implemented JWK type'); } // The hash should match the size of the key, // but we're only dealing with P-256 const hash = await w.crypto.subtle.digest(digestAlgorithm, strToUint8(sortedPub)); return uint8ToUrlBase64(new Uint8Array(hash)); }; export const JWK = { thumbprint }; export const generateJwkAsync = (w: any) => async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => { // @ts-ignore const jwk = await EC.generate(w)(generateKeyAlgorithm); // console.info('Private Key:', JSON.stringify(jwk)); // @ts-ignore // console.info('Public Key:', JSON.stringify(EC.neuter(jwk))); return jwk; }; export const generateJwtDemonstratingProofOfPossessionAsync = (w: any) => (demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration) => async (jwk: any, method = 'POST', url: string, extrasClaims = {}) => { const claims = { // https://www.rfc-editor.org/rfc/rfc9449.html#name-concept jti: btoa(guid()), htm: method, htu: url, iat: Math.round(Date.now() / 1000), ...extrasClaims, }; // @ts-ignore const kid = await JWK.thumbprint(w)( jwk, demonstratingProofOfPossessionConfiguration.digestAlgorithm, ); // @ts-ignore const jwt = await JWT.sign(w)( jwk, { kid: kid }, claims, demonstratingProofOfPossessionConfiguration, ); // console.info('JWT:', jwt); return jwt; }; const guid = () => { // RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or // pseudo-random numbers. // The algorithm is as follows: // Set the two most significant bits (bits 6 and 7) of the // clock_seq_hi_and_reserved to zero and one, respectively. // Set the four most significant bits (bits 12 through 15) of the // time_hi_and_version field to the 4-bit version number from // Section 4.1.3. Version4 // Set all the other bits to randomly (or pseudo-randomly) chosen // values. // UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node // time-low = 4hexOctet // time-mid = 2hexOctet // time-high-and-version = 2hexOctet // clock-seq-and-reserved = hexOctet: // clock-seq-low = hexOctet // node = 6hexOctet // Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx // y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10 // y values are 8, 9, A, B const guidHolder = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; const hex = '0123456789abcdef'; let r = 0; let guidResponse = ''; for (let i = 0; i < 36; i++) { if (guidHolder[i] !== '-' && guidHolder[i] !== '4') { // each x and y needs to be random r = (Math.random() * 16) | 0; } if (guidHolder[i] === 'x') { guidResponse += hex[r]; } else if (guidHolder[i] === 'y') { // clock-seq-and-reserved first hex is filtered and remaining hex values are random r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0?? r |= 0x8; // set pos 3 to 1 as 1??? guidResponse += hex[r]; } else { guidResponse += guidHolder[i]; } } return guidResponse; };