@axa-fr/oidc-client-service-worker
Version:
OpenID Connect & OAuth authentication service worker
280 lines (253 loc) • 9.16 kB
text/typescript
// 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;
};