Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(oidc): dpop inside serviceworker #1306

Merged
merged 6 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/react-oidc-demo/public/OidcTrustedDomains.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions examples/react-oidc-demo/public/staticwebapp.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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=()"
}
}
8 changes: 4 additions & 4 deletions examples/react-oidc-demo/src/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<body>Foo<script>alert("youhou");</script></body>';
iframe.srcdoc = html;
document.body.appendChild(iframe);
}*/
}

export const Home = () => {
const { login, logout, renewTokens, isAuthenticated } = useOidc();
Expand All @@ -18,9 +18,9 @@ export const Home = () => {
navigate("/profile");
};

/*useEffect(() => {
useEffect(() => {
createIframeHack();
}, []);*/
}, []);


return (
Expand Down
2 changes: 1 addition & 1 deletion examples/react-oidc-demo/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;",
},
},
});
65 changes: 44 additions & 21 deletions packages/oidc-client-service-worker/src/OidcServiceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -176,16 +192,18 @@ const handleFetch = async (event: FetchEvent) => {
if (numberDatabase > 0) {
const maPromesse = new Promise<Response>((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)) {
Expand All @@ -194,6 +212,7 @@ const handleFetch = async (event: FetchEvent) => {
encodeURIComponent(currentDb.tokens.refresh_token as string),
);
currentDatabase = currentDb;

break;
}
const keyAccessToken =
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -340,18 +362,23 @@ const handleMessage = (event: ExtendableMessageEvent) => {
convertAllRequestsToCorsExceptNavigate ?? false,
demonstratingProofOfPossessionNonce: null,
demonstratingProofOfPossessionJwkJson: null,
demonstratingProofOfPossessionConfiguration: null,
};
currentDatabase = database[configurationName];

if (!trustedDomains[configurationName]) {
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;
Expand All @@ -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,
Expand Down Expand Up @@ -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 });
Expand Down
20 changes: 20 additions & 0 deletions packages/oidc-client-service-worker/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
return new Promise((resolve, reject) => {
crypto.subtle.digest('SHA-256', textEncodeLite(code)).then(buffer => {
return resolve(uint8ToUrlBase64(new Uint8Array(buffer)));
}, error => reject(error));
});
}
22 changes: 22 additions & 0 deletions packages/oidc-client-service-worker/src/dpop.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading