diff --git a/examples/react-oidc-demo/public/OidcTrustedDomains.js b/examples/react-oidc-demo/public/OidcTrustedDomains.js
index 416c4626a..e7b4ee1c1 100644
--- a/examples/react-oidc-demo/public/OidcTrustedDomains.js
+++ b/examples/react-oidc-demo/public/OidcTrustedDomains.js
@@ -22,5 +22,7 @@ trustedDomains.config_separate_oidc_access_token_domains = {
accessTokenDomains: ["https://myapi"]
};
-trustedDomains.config_with_dpop = { domains: ["https://demo.duendesoftware.com"], showAccessToken: true };
+trustedDomains.config_with_dpop = {
+ domains: ["https://demo.duendesoftware.com"],
+ demonstratingProofOfPossession: true };
//# sourceMappingURL=OidcTrustedDomains.js.map
\ No newline at end of file
diff --git a/examples/react-oidc-demo/public/staticwebapp.config.json b/examples/react-oidc-demo/public/staticwebapp.config.json
index ba338eae6..5877917f4 100644
--- a/examples/react-oidc-demo/public/staticwebapp.config.json
+++ b/examples/react-oidc-demo/public/staticwebapp.config.json
@@ -2,5 +2,14 @@
"navigationFallback": {
"rewrite": "index.html",
"exclude": ["*.{svg,png,jpg,gif}","*.{css,scss}","*.js"]
+ },
+ "globalHeaders": {
+ "content-security-policy": "script-src 'self'",
+ "Access-Control-Allow-Origin": "*",
+ "X-Frame-Options": "SAMEORIGIN",
+ "X-Permitted-Cross-Domain-Policies": "none",
+ "Referrer-Policy":"no-referrer",
+ "X-Content-Type-Options": "nosniff",
+ "Permissions-Policy": "autoplay=()"
}
}
\ No newline at end of file
diff --git a/examples/react-oidc-demo/src/Home.tsx b/examples/react-oidc-demo/src/Home.tsx
index 118e9b7ee..b8c035158 100644
--- a/examples/react-oidc-demo/src/Home.tsx
+++ b/examples/react-oidc-demo/src/Home.tsx
@@ -3,12 +3,12 @@ import React, {useEffect} from 'react';
import {useNavigate} from "react-router-dom";
-/*const createIframeHack =() => {
+ const createIframeHack =() => {
const iframe = document.createElement('iframe');
const html = '
Foo';
iframe.srcdoc = html;
document.body.appendChild(iframe);
-}*/
+}
export const Home = () => {
const { login, logout, renewTokens, isAuthenticated } = useOidc();
@@ -18,9 +18,9 @@ export const Home = () => {
navigate("/profile");
};
- /*useEffect(() => {
+ useEffect(() => {
createIframeHack();
- }, []);*/
+ }, []);
return (
diff --git a/examples/react-oidc-demo/vite.config.js b/examples/react-oidc-demo/vite.config.js
index 9fd73d00c..197494e22 100644
--- a/examples/react-oidc-demo/vite.config.js
+++ b/examples/react-oidc-demo/vite.config.js
@@ -12,7 +12,7 @@ export default defineConfig({
},
server: {
headers: {
- // "Content-Security-Policy": "script-src 'self' 'unsafe-inline';",
+ //"Content-Security-Policy": "script-src 'unsafe-inline' https://www.google-analitics.com;",
},
},
});
diff --git a/packages/oidc-client-service-worker/src/OidcServiceWorker.ts b/packages/oidc-client-service-worker/src/OidcServiceWorker.ts
index 26eef6755..22271eb52 100644
--- a/packages/oidc-client-service-worker/src/OidcServiceWorker.ts
+++ b/packages/oidc-client-service-worker/src/OidcServiceWorker.ts
@@ -17,6 +17,9 @@ import {
import {extractConfigurationNameFromCodeVerifier, replaceCodeVerifier} from './utils/codeVerifier';
import { normalizeUrl } from './utils/normalizeUrl';
import version from './version';
+import {generateJwkAsync, generateJwtDemonstratingProofOfPossessionAsync} from "./jwt";
+import {getDpopConfiguration} from "./dpop";
+import {base64urlOfHashOfASCIIEncodingAsync} from "./crypto";
// @ts-ignore
if (typeof trustedTypes !== 'undefined' && typeof trustedTypes.createPolicy == 'function') {
@@ -92,6 +95,19 @@ const keepAliveAsync = async (event: FetchEvent) => {
return response;
};
+async function generateDpopAsync(originalRequest: Request, currentDatabase:OidcConfig|null, url: string, extrasClaims={} ) {
+ const headersExtras = serializeHeaders(originalRequest.headers);
+ if (currentDatabase && currentDatabase.demonstratingProofOfPossessionConfiguration && currentDatabase.demonstratingProofOfPossessionJwkJson) {
+ const dpopConfiguration = currentDatabase.demonstratingProofOfPossessionConfiguration;
+ const jwk = currentDatabase.demonstratingProofOfPossessionJwkJson;
+ headersExtras['dpop'] = await generateJwtDemonstratingProofOfPossessionAsync(self)(dpopConfiguration)(jwk, 'POST', url, extrasClaims);
+ if(currentDatabase.demonstratingProofOfPossessionNonce != null) {
+ headersExtras['nonce'] = currentDatabase.demonstratingProofOfPossessionNonce;
+ }
+ }
+ return headersExtras;
+}
+
const handleFetch = async (event: FetchEvent) => {
const originalRequest = event.request;
const url = normalizeUrl(originalRequest.url);
@@ -176,16 +192,18 @@ const handleFetch = async (event: FetchEvent) => {
if (numberDatabase > 0) {
const maPromesse = new Promise((resolve, reject) => {
const clonedRequest = originalRequest.clone();
- const response = clonedRequest.text().then((actualBody) => {
+ const response = clonedRequest.text().then(async (actualBody) => {
if (
actualBody.includes(TOKEN.REFRESH_TOKEN) ||
actualBody.includes(TOKEN.ACCESS_TOKEN)
) {
+ let headers = serializeHeaders(originalRequest.headers);
let newBody = actualBody;
for (let i = 0; i < numberDatabase; i++) {
const currentDb = currentDatabases[i];
-
if (currentDb && currentDb.tokens != null) {
+ const claimsExtras = {ath: await base64urlOfHashOfASCIIEncodingAsync(currentDb.tokens.access_token),};
+ headers = await generateDpopAsync(originalRequest, currentDb, url, claimsExtras);
const keyRefreshToken =
TOKEN.REFRESH_TOKEN + '_' + currentDb.configurationName;
if (actualBody.includes(keyRefreshToken)) {
@@ -194,6 +212,7 @@ const handleFetch = async (event: FetchEvent) => {
encodeURIComponent(currentDb.tokens.refresh_token as string),
);
currentDatabase = currentDb;
+
break;
}
const keyAccessToken =
@@ -208,11 +227,12 @@ const handleFetch = async (event: FetchEvent) => {
}
}
}
+
const fetchPromise = fetch(originalRequest, {
body: newBody,
method: clonedRequest.method,
headers: {
- ...serializeHeaders(originalRequest.headers),
+ ...headers,
},
mode: clonedRequest.mode,
cache: clonedRequest.cache,
@@ -254,12 +274,14 @@ const handleFetch = async (event: FetchEvent) => {
currentDatabase.codeVerifier,
);
}
-
+
+ const headersExtras = await generateDpopAsync(originalRequest, currentDatabase, url);
+
return fetch(originalRequest, {
body: newBody,
method: clonedRequest.method,
headers: {
- ...serializeHeaders(originalRequest.headers),
+ ...headersExtras,
},
mode: clonedRequest.mode,
cache: clonedRequest.cache,
@@ -301,7 +323,7 @@ const handleFetch = async (event: FetchEvent) => {
}
};
-const handleMessage = (event: ExtendableMessageEvent) => {
+const handleMessage = async (event: ExtendableMessageEvent) => {
const port = event.ports[0];
const data = event.data as MessageEventData;
if (event.data.type === 'claim') {
@@ -340,6 +362,7 @@ const handleMessage = (event: ExtendableMessageEvent) => {
convertAllRequestsToCorsExceptNavigate ?? false,
demonstratingProofOfPossessionNonce: null,
demonstratingProofOfPossessionJwkJson: null,
+ demonstratingProofOfPossessionConfiguration: null,
};
currentDatabase = database[configurationName];
@@ -347,11 +370,15 @@ const handleMessage = (event: ExtendableMessageEvent) => {
trustedDomains[configurationName] = [];
}
}
+
switch (data.type) {
case 'clear':
currentDatabase.tokens = null;
currentDatabase.state = null;
currentDatabase.codeVerifier = null;
+ currentDatabase.demonstratingProofOfPossessionNonce = null;
+ currentDatabase.demonstratingProofOfPossessionJwkJson = null;
+ currentDatabase.demonstratingProofOfPossessionConfiguration = null;
currentDatabase.status = data.data.status;
port.postMessage({ configurationName });
return;
@@ -372,6 +399,17 @@ const handleMessage = (event: ExtendableMessageEvent) => {
currentDatabase.oidcServerConfiguration = oidcServerConfiguration;
currentDatabase.oidcConfiguration = data.data.oidcConfiguration;
+ if(currentDatabase.demonstratingProofOfPossessionConfiguration == null ){
+ const demonstratingProofOfPossessionConfiguration = getDpopConfiguration(trustedDomains[configurationName]);
+ if(demonstratingProofOfPossessionConfiguration != null){
+ if(currentDatabase.oidcConfiguration.demonstrating_proof_of_possession){
+ console.warn("In service worker, demonstrating_proof_of_possession must be configured from trustedDomains file")
+ }
+ currentDatabase.demonstratingProofOfPossessionConfiguration = demonstratingProofOfPossessionConfiguration;
+ currentDatabase.demonstratingProofOfPossessionJwkJson = await generateJwkAsync(self)(demonstratingProofOfPossessionConfiguration.generateKeyAlgorithm);
+ }
+ }
+
if (!currentDatabase.tokens) {
port.postMessage({
tokens: null,
@@ -421,21 +459,6 @@ const handleMessage = (event: ExtendableMessageEvent) => {
});
return;
}
- case 'setDemonstratingProofOfPossessionJwk': {
- currentDatabase.demonstratingProofOfPossessionJwkJson =
- data.data.demonstratingProofOfPossessionJwkJson;
- port.postMessage({ configurationName });
- return;
- }
- case 'getDemonstratingProofOfPossessionJwk': {
- const demonstratingProofOfPossessionJwkJson =
- currentDatabase.demonstratingProofOfPossessionJwkJson;
- port.postMessage({
- configurationName,
- demonstratingProofOfPossessionJwkJson,
- });
- return;
- }
case 'setState': {
currentDatabase.state = data.data.state;
port.postMessage({ configurationName });
diff --git a/packages/oidc-client-service-worker/src/crypto.ts b/packages/oidc-client-service-worker/src/crypto.ts
new file mode 100644
index 000000000..e68d89c9d
--- /dev/null
+++ b/packages/oidc-client-service-worker/src/crypto.ts
@@ -0,0 +1,20 @@
+import {uint8ToUrlBase64} from "./jwt";
+
+
+export function textEncodeLite(str: string) {
+ const buf = new ArrayBuffer(str.length);
+ const bufView = new Uint8Array(buf);
+
+ for (let i = 0; i < str.length; i++) {
+ bufView[i] = str.charCodeAt(i);
+ }
+ return bufView;
+}
+
+export function base64urlOfHashOfASCIIEncodingAsync(code: string):Promise {
+ return new Promise((resolve, reject) => {
+ crypto.subtle.digest('SHA-256', textEncodeLite(code)).then(buffer => {
+ return resolve(uint8ToUrlBase64(new Uint8Array(buffer)));
+ }, error => reject(error));
+ });
+}
diff --git a/packages/oidc-client-service-worker/src/dpop.ts b/packages/oidc-client-service-worker/src/dpop.ts
new file mode 100644
index 000000000..dfa516d1e
--- /dev/null
+++ b/packages/oidc-client-service-worker/src/dpop.ts
@@ -0,0 +1,22 @@
+import {Domain, DomainDetails} from "./types.js";
+import {defaultDemonstratingProofOfPossessionConfiguration} from "./jwt";
+
+const isDpop= (trustedDomain: Domain[] | DomainDetails) : boolean => {
+ if (Array.isArray(trustedDomain)) {
+ return false;
+ }
+ return trustedDomain.demonstratingProofOfPossession ?? false;
+}
+
+export const getDpopConfiguration = (trustedDomain: Domain[] | DomainDetails) => {
+
+ if(!isDpop(trustedDomain)) {
+ return null;
+ }
+
+ if (Array.isArray(trustedDomain)) {
+ return null;
+ }
+
+ return trustedDomain.demonstratingProofOfPossessionConfiguration ?? defaultDemonstratingProofOfPossessionConfiguration;
+}
\ No newline at end of file
diff --git a/packages/oidc-client-service-worker/src/jwt.ts b/packages/oidc-client-service-worker/src/jwt.ts
new file mode 100644
index 000000000..ef7665f19
--- /dev/null
+++ b/packages/oidc-client-service-worker/src/jwt.ts
@@ -0,0 +1,267 @@
+// 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, headers, claims, 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 var 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 var 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;
+};
+
+
diff --git a/packages/oidc-client-service-worker/src/types.ts b/packages/oidc-client-service-worker/src/types.ts
index d669b0b96..ef3244671 100644
--- a/packages/oidc-client-service-worker/src/types.ts
+++ b/packages/oidc-client-service-worker/src/types.ts
@@ -5,6 +5,16 @@ export type DomainDetails = {
showAccessToken: boolean;
convertAllRequestsToCorsExceptNavigate?: boolean,
setAccessTokenToNavigateRequests?: boolean,
+ demonstratingProofOfPossession?:boolean;
+ demonstratingProofOfPossessionConfiguration?: DemonstratingProofOfPossessionConfiguration;
+}
+
+export interface DemonstratingProofOfPossessionConfiguration {
+ generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams,
+ digestAlgorithm: AlgorithmIdentifier,
+ importKeyAlgorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm,
+ signAlgorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams,
+ jwtHeaderAlgorithm: string
}
export type Domain = string | RegExp;
@@ -23,6 +33,7 @@ export type OidcServerConfiguration = {
export type OidcConfiguration = {
token_renew_mode: string;
+ demonstrating_proof_of_possession: boolean;
}
// Uncertain why the Headers interface in lib.webworker.d.ts does not have a keys() function, so extending
@@ -57,6 +68,7 @@ export type Nonce = {
} | null;
export type OidcConfig = {
+ demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration | null;
configurationName: string;
tokens: Tokens | null;
status: Status;
diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/domains.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/domains.spec.ts
index 3ec9cc4bd..f2ab1e8fe 100644
--- a/packages/oidc-client-service-worker/src/utils/__tests__/domains.spec.ts
+++ b/packages/oidc-client-service-worker/src/utils/__tests__/domains.spec.ts
@@ -53,6 +53,7 @@ describe('domains', () => {
setAccessTokenToNavigateRequests: true,
demonstratingProofOfPossessionNonce: null,
demonstratingProofOfPossessionJwkJson: null,
+ demonstratingProofOfPossessionConfiguration: null,
},
};
});
diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/testHelper.ts b/packages/oidc-client-service-worker/src/utils/__tests__/testHelper.ts
index 39ce16af3..6e30e61e2 100644
--- a/packages/oidc-client-service-worker/src/utils/__tests__/testHelper.ts
+++ b/packages/oidc-client-service-worker/src/utils/__tests__/testHelper.ts
@@ -96,6 +96,7 @@ class TokenBuilder {
class OidcConfigurationBuilder {
private oidcConfiguration: OidcConfiguration = {
token_renew_mode: 'offline',
+ demonstrating_proof_of_possession: false,
};
public withTokenRenewMode(
@@ -127,6 +128,7 @@ class OidcConfigBuilder {
setAccessTokenToNavigateRequests: true,
demonstratingProofOfPossessionNonce: null,
demonstratingProofOfPossessionJwkJson: null,
+ demonstratingProofOfPossessionConfiguration: null,
};
public withTestingDefault(): OidcConfigBuilder {
diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/tokens.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/tokens.spec.ts
index b35b6ba6d..72bf460a9 100644
--- a/packages/oidc-client-service-worker/src/utils/__tests__/tokens.spec.ts
+++ b/packages/oidc-client-service-worker/src/utils/__tests__/tokens.spec.ts
@@ -125,7 +125,7 @@ describe('tokens', () => {
// @ts-ignore
delete token.idTokenPayload;
const oidcConfiguration = new OidcConfigBuilder()
- .withOidcConfiguration({token_renew_mode: "access_token_invalid"})
+ .withOidcConfiguration({token_renew_mode: "access_token_invalid", demonstrating_proof_of_possession: false})
.withOidcServerConfiguration({issuer: "",
authorizationEndpoint:"",
revocationEndpoint:"",
diff --git a/packages/oidc-client-service-worker/src/utils/domains.ts b/packages/oidc-client-service-worker/src/utils/domains.ts
index d811b2ea3..80533dc8c 100644
--- a/packages/oidc-client-service-worker/src/utils/domains.ts
+++ b/packages/oidc-client-service-worker/src/utils/domains.ts
@@ -36,6 +36,8 @@ export const getDomains = (
return trustedDomain[`${type}Domains`] ?? trustedDomain.domains ?? [];
};
+
+
export const getCurrentDatabaseDomain = (
database: Database,
url: string,
diff --git a/packages/oidc-client-service-worker/src/utils/tokens.ts b/packages/oidc-client-service-worker/src/utils/tokens.ts
index e21933e55..67c7296c5 100644
--- a/packages/oidc-client-service-worker/src/utils/tokens.ts
+++ b/packages/oidc-client-service-worker/src/utils/tokens.ts
@@ -1,5 +1,5 @@
/* eslint-disable simple-import-sort/exports */
-import { TOKEN, TokenRenewMode } from '../constants';
+import {TOKEN, TokenRenewMode} from '../constants';
import {
AccessTokenPayload,
IdTokenPayload,
@@ -8,7 +8,7 @@ import {
OidcServerConfiguration,
Tokens
} from '../types';
-import { countLetter } from './strings';
+import {countLetter} from './strings';
export const parseJwt = (payload: string) => {
return JSON.parse(
@@ -221,16 +221,27 @@ function _hideTokens(tokens: Tokens, currentDatabaseElement: OidcConfig, configu
return secureTokens;
}
+const demonstratingProofOfPossessionNonceResponseHeader = "DPoP-Nonce";
function hideTokens(currentDatabaseElement: OidcConfig) {
const configurationName = currentDatabaseElement.configurationName;
return (response: Response) => {
if (response.status !== 200) {
return response;
}
+ const newHeaders = new Headers(response.headers);
+ if( response.headers.has(demonstratingProofOfPossessionNonceResponseHeader)){
+ currentDatabaseElement.demonstratingProofOfPossessionNonce = response.headers.get(demonstratingProofOfPossessionNonceResponseHeader);
+ newHeaders.delete(demonstratingProofOfPossessionNonceResponseHeader);
+ }
+
return response.json().then((tokens: Tokens) => {
const secureTokens = _hideTokens(tokens, currentDatabaseElement, configurationName);
const body = JSON.stringify(secureTokens);
- return new Response(body, response);
+ return new Response(body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: newHeaders
+ });
});
};
}
diff --git a/packages/oidc-client/README.md b/packages/oidc-client/README.md
index 2cc14490b..be012d1a7 100644
--- a/packages/oidc-client/README.md
+++ b/packages/oidc-client/README.md
@@ -94,10 +94,32 @@ const trustedDomains = {
trustedDomains.config_show_access_token = {
oidcDomains :["https://demo.duendesoftware.com"],
accessTokenDomains : ["https://www.myapi.com/users"],
- showAccessToken: true,
+ showAccessToken: false,
// convertAllRequestsToCorsExceptNavigate: false, // default value is false
// setAccessTokenToNavigateRequests: true, // default value is true
};
+
+// DPoP (Demonstrating Proof of Possession) will be activated for the following domains
+trustedDomains.config_with_dpop = {
+ domains: ["https://demo.duendesoftware.com"],
+ demonstratingProofOfPossession: true
+ // Optional, more details bellow
+ /*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'
+ }*/
+};
+
```
The code of the demo :
@@ -113,7 +135,7 @@ export const configuration = {
authority: 'https://demo.duendesoftware.com',
service_worker_relative_url: '/OidcServiceWorker.js', // just comment that line to disable service worker mode
service_worker_only: false,
- demonstrating_proof_of_possession: false, // demonstrating proof of possession will work only if access_token is accessible from the client (This is because WebCrypto API is not available inside a Service Worker)
+ demonstrating_proof_of_possession: false,
};
const href = window.location.href;
diff --git a/packages/oidc-client/src/jwt.ts b/packages/oidc-client/src/jwt.ts
index f65ec7b7e..4aadb41fe 100644
--- a/packages/oidc-client/src/jwt.ts
+++ b/packages/oidc-client/src/jwt.ts
@@ -73,7 +73,7 @@ export const defaultDemonstratingProofOfPossessionConfiguration: DemonstratingPr
// @ts-ignore
-const sign = async (jwk, headers, claims, demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration, jwtHeaderType= 'dpop+jwt') => {
+const sign = (w:any) => async (jwk, headers, claims, 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);
@@ -114,7 +114,7 @@ const sign = async (jwk, headers, claims, demonstratingProofOfPossessionConfigur
// Actually do the import, which comes out as an abstract key type
// @ts-ignore
- const privateKey = await window.crypto.subtle.importKey('jwk', jwk, keyType, exportable, privileges);
+ 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}`);
@@ -123,7 +123,7 @@ const sign = async (jwk, headers, claims, demonstratingProofOfPossessionConfigur
// https://tools.ietf.org/html/rfc7518#section-3
const signatureType = demonstratingProofOfPossessionConfiguration.signAlgorithm;
- const signature = await window.crypto.subtle.sign(signatureType, privateKey, data);
+ 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
@@ -137,15 +137,15 @@ export var JWT = {sign};
// @ts-ignore
-const generate = async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => {
+const generate = (w:any) => async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => {
const keyType = generateKeyAlgorithm;
const exportable = true;
const privileges = ['sign', 'verify'];
// @ts-ignore
- const key = await window.crypto.subtle.generateKey(keyType, exportable, privileges);
+ 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 window.crypto.subtle.exportKey('jwk', key.privateKey);
+ return await w.crypto.subtle.exportKey('jwk', key.privateKey);
};
// Create a Public Key from a Private Key
@@ -164,7 +164,7 @@ const EC = {
neuter
};
// @ts-ignore
-const thumbprint = async (jwk, digestAlgorithm: AlgorithmIdentifier) => {
+const thumbprint = (w:any) => async (jwk, digestAlgorithm: AlgorithmIdentifier) => {
let sortedPub;
// lexigraphically sorted, no spaces
switch (jwk.kty) {
@@ -184,22 +184,22 @@ const thumbprint = async (jwk, digestAlgorithm: AlgorithmIdentifier) => {
}
// The hash should match the size of the key,
// but we're only dealing with P-256
- const hash = await window.crypto.subtle.digest(digestAlgorithm, strToUint8(sortedPub));
+ const hash = await w.crypto.subtle.digest(digestAlgorithm, strToUint8(sortedPub));
return uint8ToUrlBase64(new Uint8Array(hash));
}
export var JWK = {thumbprint};
-export const generateJwkAsync = async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => {
+export const generateJwkAsync = (w:any) => async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => {
// @ts-ignore
- const jwk = await EC.generate(generateKeyAlgorithm);
+ 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 = (demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration) => async (jwk, method = 'POST', url: string, extrasClaims={}) => {
+export const generateJwtDemonstratingProofOfPossessionAsync = (w:any) => (demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration) => async (jwk, method = 'POST', url: string, extrasClaims={}) => {
const claims = {
// https://www.rfc-editor.org/rfc/rfc9449.html#name-concept
@@ -210,9 +210,9 @@ export const generateJwtDemonstratingProofOfPossessionAsync = (demonstratingProo
...extrasClaims,
};
// @ts-ignore
- const kid = await JWK.thumbprint(jwk, demonstratingProofOfPossessionConfiguration.digestAlgorithm);
+ const kid = await JWK.thumbprint(w)(jwk, demonstratingProofOfPossessionConfiguration.digestAlgorithm);
// @ts-ignore
- const jwt = await JWT.sign(jwk, { kid: kid }, claims, demonstratingProofOfPossessionConfiguration)
+ const jwt = await JWT.sign(w)(jwk, { kid: kid }, claims, demonstratingProofOfPossessionConfiguration)
// console.info('JWT:', jwt);
return jwt;
}
diff --git a/packages/oidc-client/src/login.ts b/packages/oidc-client/src/login.ts
index 473c14055..7f37919bb 100644
--- a/packages/oidc-client/src/login.ts
+++ b/packages/oidc-client/src/login.ts
@@ -149,14 +149,14 @@ export const loginCallbackAsync = (oidc:Oidc) => async (isSilentSignin = false)
const url = oidcServerConfiguration.tokenEndpoint;
const headersExtras = {};
if(configuration.demonstrating_proof_of_possession) {
- const jwk = await generateJwkAsync(configuration.demonstrating_proof_of_possession_configuration.generateKeyAlgorithm);
if (serviceWorker) {
- await serviceWorker.setDemonstratingProofOfPossessionJwkAsync(jwk);
+ headersExtras['DPoP'] = `DPOP_SECURED_BY_OIDC_SERVICE_WORKER_${oidc.configurationName}`;
} else {
+ const jwk = await generateJwkAsync(window)(configuration.demonstrating_proof_of_possession_configuration.generateKeyAlgorithm);
const session = initSession(oidc.configurationName, configuration.storage);
await session.setDemonstratingProofOfPossessionJwkAsync(jwk);
+ headersExtras['DPoP'] = await generateJwtDemonstratingProofOfPossessionAsync(window)(configuration.demonstrating_proof_of_possession_configuration)(jwk, 'POST', url);
}
- headersExtras['DPoP'] = await generateJwtDemonstratingProofOfPossessionAsync(configuration.demonstrating_proof_of_possession_configuration)(jwk, 'POST', url);
}
const tokenResponse = await performFirstTokenRequestAsync(storage)(url,
diff --git a/packages/oidc-client/src/oidc.ts b/packages/oidc-client/src/oidc.ts
index afcc232bf..f2eff47fd 100644
--- a/packages/oidc-client/src/oidc.ts
+++ b/packages/oidc-client/src/oidc.ts
@@ -300,21 +300,21 @@ Please checkout that you are using OIDC hook inside a = null;
diff --git a/packages/react-oidc/README.md b/packages/react-oidc/README.md
index 00c30601b..1fee9a95f 100644
--- a/packages/react-oidc/README.md
+++ b/packages/react-oidc/README.md
@@ -98,6 +98,28 @@ trustedDomains.config_show_access_token = {
// setAccessTokenToNavigateRequests: true, // default value is true
};
+// DPoP (Demonstrating Proof of Possession) will be activated for the following domains
+trustedDomains.config_with_dpop = {
+ domains: ["https://demo.duendesoftware.com"],
+ demonstratingProofOfPossession: true
+ // Optional, more details bellow
+ /*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'
+ }*/
+};
+
+
```
## Run The Demo
@@ -142,7 +164,7 @@ const configuration = {
authority: "https://demo.duendesoftware.com",
service_worker_relative_url: "/OidcServiceWorker.js", // just comment that line to disable service worker mode
service_worker_only: false,
- demonstrating_proof_of_possession: false, // demonstrating proof of possession will work only if access_token is accessible from the client (This is because WebCrypto API is not available inside a Service Worker)
+ demonstrating_proof_of_possession: false,
};
const App = () => (