Skip to content

Commit

Permalink
feat(oidc): control dpop injection (release) (#1342)
Browse files Browse the repository at this point in the history
* feat(oidc): control dpop injection

* fix (alpha)
  • Loading branch information
guillaume-chervet committed Apr 10, 2024
1 parent ef1597e commit 032a00b
Show file tree
Hide file tree
Showing 14 changed files with 55 additions and 24 deletions.
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.

12 changes: 10 additions & 2 deletions packages/oidc-client-service-worker/src/OidcServiceWorker.ts
Expand Up @@ -18,7 +18,7 @@ import {extractConfigurationNameFromCodeVerifier, replaceCodeVerifier} from './u
import { normalizeUrl } from './utils/normalizeUrl';
import version from './version';
import {generateJwkAsync, generateJwtDemonstratingProofOfPossessionAsync} from "./jwt";
import {getDpopConfiguration} from "./dpop";
import {getDpopConfiguration, getDpopOnlyWhenDpopHeaderPresent} from "./dpop";
import {base64urlOfHashOfASCIIEncodingAsync} from "./crypto";

// @ts-ignore
Expand Down Expand Up @@ -97,7 +97,11 @@ const keepAliveAsync = async (event: FetchEvent) => {

async function generateDpopAsync(originalRequest: Request, currentDatabase:OidcConfig|null, url: string, extrasClaims={} ) {
const headersExtras = serializeHeaders(originalRequest.headers);
if (currentDatabase && currentDatabase.demonstratingProofOfPossessionConfiguration && currentDatabase.demonstratingProofOfPossessionJwkJson) {
if (currentDatabase &&
currentDatabase.demonstratingProofOfPossessionConfiguration &&
currentDatabase.demonstratingProofOfPossessionJwkJson &&
(!currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent || currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent && headersExtras['dpop'])
) {
const dpopConfiguration = currentDatabase.demonstratingProofOfPossessionConfiguration;
const jwk = currentDatabase.demonstratingProofOfPossessionJwkJson;
headersExtras['dpop'] = await generateJwtDemonstratingProofOfPossessionAsync(self)(dpopConfiguration)(jwk, 'POST', url, extrasClaims);
Expand Down Expand Up @@ -363,6 +367,7 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
demonstratingProofOfPossessionNonce: null,
demonstratingProofOfPossessionJwkJson: null,
demonstratingProofOfPossessionConfiguration: null,
demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: false,
};
currentDatabase = database[configurationName];

Expand All @@ -379,6 +384,7 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
currentDatabase.demonstratingProofOfPossessionNonce = null;
currentDatabase.demonstratingProofOfPossessionJwkJson = null;
currentDatabase.demonstratingProofOfPossessionConfiguration = null;
currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent = false;
currentDatabase.status = data.data.status;
port.postMessage({ configurationName });
return;
Expand All @@ -398,6 +404,7 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
}
currentDatabase.oidcServerConfiguration = oidcServerConfiguration;
currentDatabase.oidcConfiguration = data.data.oidcConfiguration;


if(currentDatabase.demonstratingProofOfPossessionConfiguration == null ){
const demonstratingProofOfPossessionConfiguration = getDpopConfiguration(trustedDomains[configurationName]);
Expand All @@ -407,6 +414,7 @@ const handleMessage = async (event: ExtendableMessageEvent) => {
}
currentDatabase.demonstratingProofOfPossessionConfiguration = demonstratingProofOfPossessionConfiguration;
currentDatabase.demonstratingProofOfPossessionJwkJson = await generateJwkAsync(self)(demonstratingProofOfPossessionConfiguration.generateKeyAlgorithm);
currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent = getDpopOnlyWhenDpopHeaderPresent(trustedDomains[configurationName]) ?? false;
}
}

Expand Down
13 changes: 13 additions & 0 deletions packages/oidc-client-service-worker/src/dpop.ts
Expand Up @@ -19,4 +19,17 @@ export const getDpopConfiguration = (trustedDomain: Domain[] | DomainDetails) =>
}

return trustedDomain.demonstratingProofOfPossessionConfiguration ?? defaultDemonstratingProofOfPossessionConfiguration;
}

export const getDpopOnlyWhenDpopHeaderPresent = (trustedDomain: Domain[] | DomainDetails) => {

if(!isDpop(trustedDomain)) {
return null;
}

if (Array.isArray(trustedDomain)) {
return null;
}

return trustedDomain.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent ?? true;
}
2 changes: 2 additions & 0 deletions packages/oidc-client-service-worker/src/types.ts
Expand Up @@ -6,6 +6,7 @@ export type DomainDetails = {
convertAllRequestsToCorsExceptNavigate?: boolean,
setAccessTokenToNavigateRequests?: boolean,
demonstratingProofOfPossession?:boolean;
demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent?:boolean;
demonstratingProofOfPossessionConfiguration?: DemonstratingProofOfPossessionConfiguration;
}

Expand Down Expand Up @@ -84,6 +85,7 @@ export type OidcConfig = {
setAccessTokenToNavigateRequests: boolean,
demonstratingProofOfPossessionNonce: string | null;
demonstratingProofOfPossessionJwkJson: string | null;
demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: boolean;
}

export type IdTokenPayload = {
Expand Down
Expand Up @@ -54,6 +54,7 @@ describe('domains', () => {
demonstratingProofOfPossessionNonce: null,
demonstratingProofOfPossessionJwkJson: null,
demonstratingProofOfPossessionConfiguration: null,
demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: false,
},
};
});
Expand Down
Expand Up @@ -129,6 +129,7 @@ class OidcConfigBuilder {
demonstratingProofOfPossessionNonce: null,
demonstratingProofOfPossessionJwkJson: null,
demonstratingProofOfPossessionConfiguration: null,
demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: false,
};

public withTestingDefault(): OidcConfigBuilder {
Expand Down
9 changes: 6 additions & 3 deletions packages/oidc-client/README.md
Expand Up @@ -102,7 +102,8 @@ trustedDomains.config_show_access_token = {
// DPoP (Demonstrating Proof of Possession) will be activated for the following domains
trustedDomains.config_with_dpop = {
domains: ["https://demo.duendesoftware.com"],
demonstratingProofOfPossession: true
demonstratingProofOfPossession: true,
demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: true, // default value is false, inject DPOP token only when DPOP header is present
// Optional, more details bellow
/*demonstratingProofOfPossessionConfiguration: {
importKeyAlgorithm: {
Expand Down Expand Up @@ -386,16 +387,18 @@ export class OidcClient {
/**
* Retrieves a new fetch function that inject bearer tokens (also DPOP tokens).
* @param fetch The current fetch function to use
* @param demonstrating_proof_of_possession Indicates whether the demonstration of proof of possession should be used.
* @returns Fetch A new fectch function that inject bearer tokens (also DPOP tokens).
*/
fetchWithTokens(fetch: Fetch): Fetch;
fetchWithTokens(fetch: Fetch, demonstrating_proof_of_possession=false): Fetch;

/**
* Retrieves OIDC user information.
* @param noCache Indicates whether user information should be retrieved bypassing the cache.
* @param demonstrating_proof_of_possession Indicates whether the demonstration of proof of possession should be used.
* @returns A promise resolved with the user information, or rejected with an error.
*/
async userInfoAsync<T extends OidcUserInfo = OidcUserInfo>(noCache = false): Promise<T>;
async userInfoAsync<T extends OidcUserInfo = OidcUserInfo>(noCache = false, demonstrating_proof_of_possession=false): Promise<T>;

/**
* Generate Demonstration of proof of possession.
Expand Down
4 changes: 2 additions & 2 deletions packages/oidc-client/src/fetch.ts
Expand Up @@ -3,7 +3,7 @@ import {OidcClient} from "./oidcClient";
import {getValidTokenAsync} from "./parseTokens";

// @ts-ignore
export const fetchWithTokens = (fetch: Fetch, oidcClient: Oidc | null) : Fetch => async (...params: Parameters<Fetch>) :Promise<Response> => {
export const fetchWithTokens = (fetch: Fetch, oidcClient: Oidc | null, demonstrating_proof_of_possession:boolean=false) : Fetch => async (...params: Parameters<Fetch>) :Promise<Response> => {
const [url, options, ...rest] = params;
const optionTmp = options ? { ...options } : { method: 'GET' };
let headers = new Headers();
Expand All @@ -21,7 +21,7 @@ export const fetchWithTokens = (fetch: Fetch, oidcClient: Oidc | null) : Fetch =
headers.set('Accept', 'application/json');
}
if (accessToken) {
if(oidc.configuration.demonstrating_proof_of_possession) {
if(oidc.configuration.demonstrating_proof_of_possession && demonstrating_proof_of_possession) {
const demonstrationOdProofOfPossession = await oidc.generateDemonstrationOfProofOfPossessionAsync(accessToken, url.toString(), optionTmp.method);
headers.set('Authorization', `PoP ${accessToken}`);
headers.set('DPoP', demonstrationOdProofOfPossession);
Expand Down
4 changes: 2 additions & 2 deletions packages/oidc-client/src/oidc.ts
Expand Up @@ -332,11 +332,11 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
}

userInfoPromise:Promise<any> = null;
userInfoAsync(noCache = false) {
userInfoAsync(noCache = false, demonstrating_proof_of_possession=false) {
if (this.userInfoPromise !== null) {
return this.userInfoPromise;
}
this.userInfoPromise = userInfoAsync(this)(noCache);
this.userInfoPromise = userInfoAsync(this)(noCache, demonstrating_proof_of_possession);
return this.userInfoPromise.then(result => {
this.userInfoPromise = null;
return result;
Expand Down
6 changes: 3 additions & 3 deletions packages/oidc-client/src/oidcClient.ts
Expand Up @@ -75,11 +75,11 @@ export class OidcClient {
return getValidTokenAsync(this._oidc, waitMs, numberWait);
}

fetchWithTokens(fetch: Fetch): Fetch {
return fetchWithTokens(fetch, this);
fetchWithTokens(fetch: Fetch, demonstrating_proof_of_possession:false): Fetch {
return fetchWithTokens(fetch, this, demonstrating_proof_of_possession);
}

async userInfoAsync<T extends OidcUserInfo = OidcUserInfo>(noCache = false):Promise<T> {
async userInfoAsync<T extends OidcUserInfo = OidcUserInfo>(noCache = false, demonstrating_proof_of_possession:boolean=false):Promise<T> {
return this._oidc.userInfoAsync(noCache);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/oidc-client/src/user.ts
@@ -1,15 +1,15 @@
import Oidc from "./oidc";
import {fetchWithTokens} from "./fetch";

export const userInfoAsync = (oidc:Oidc) => async (noCache = false) => {
export const userInfoAsync = (oidc:Oidc) => async (noCache = false, demonstrating_proof_of_possession=false) => {
if (oidc.userInfo != null && !noCache) {
return oidc.userInfo;
}
const configuration = oidc.configuration;
const oidcServerConfiguration = await oidc.initAsync(configuration.authority, configuration.authority_configuration);
const url = oidcServerConfiguration.userInfoEndpoint;
const fetchUserInfo = async () => {
const oidcFetch = fetchWithTokens(fetch, oidc);
const oidcFetch = fetchWithTokens(fetch, oidc, demonstrating_proof_of_possession);
const response = await oidcFetch(url);
if (response.status !== 200) {
return null;
Expand Down
3 changes: 2 additions & 1 deletion packages/react-oidc/README.md
Expand Up @@ -101,7 +101,8 @@ trustedDomains.config_show_access_token = {
// DPoP (Demonstrating Proof of Possession) will be activated for the following domains
trustedDomains.config_with_dpop = {
domains: ["https://demo.duendesoftware.com"],
demonstratingProofOfPossession: true
demonstratingProofOfPossession: true,
demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: true, // default value is false, inject DPOP token only when DPOP header is present
// Optional, more details bellow
/*demonstratingProofOfPossessionConfiguration: {
importKeyAlgorithm: {
Expand Down
12 changes: 6 additions & 6 deletions packages/react-oidc/src/FetchToken.tsx
Expand Up @@ -7,27 +7,27 @@ export interface ComponentWithOidcFetchProps {

const defaultConfigurationName = 'default';

const fetchWithToken = (fetch: Fetch, getOidcWithConfigurationName: () => OidcClient | null) => async (...params: Parameters<Fetch>) => {
const fetchWithToken = (fetch: Fetch, getOidcWithConfigurationName: () => OidcClient | null, demonstrating_proof_of_possession=false) => async (...params: Parameters<Fetch>) => {
const oidc = getOidcWithConfigurationName();
const newFetch = oidc.fetchWithTokens(fetch);
const newFetch = oidc.fetchWithTokens(fetch, demonstrating_proof_of_possession);
return await newFetch(...params);
};

export const withOidcFetch = (fetch: Fetch = null, configurationName = defaultConfigurationName) => (
export const withOidcFetch = (fetch: Fetch = null, configurationName = defaultConfigurationName, demonstrating_proof_of_possession:boolean=false) => (
WrappedComponent,
) => (props: ComponentWithOidcFetchProps) => {
const { fetch: newFetch } = useOidcFetch(fetch || props.fetch, configurationName);
const { fetch: newFetch } = useOidcFetch(fetch || props.fetch, configurationName, demonstrating_proof_of_possession);
return <WrappedComponent {...props} fetch={newFetch} />;
};

export const useOidcFetch = (fetch: Fetch = null, configurationName = defaultConfigurationName) => {
export const useOidcFetch = (fetch: Fetch = null, configurationName = defaultConfigurationName, demonstrating_proof_of_possession:boolean=false) => {
const previousFetch = fetch || window.fetch;
const getOidc = OidcClient.get;

const memoizedFetchCallback = useCallback(
(input: RequestInfo | URL, init?: RequestInit) => {
const getOidcWithConfigurationName = () => getOidc(configurationName);
const newFetch = fetchWithToken(previousFetch, getOidcWithConfigurationName);
const newFetch = fetchWithToken(previousFetch, getOidcWithConfigurationName, demonstrating_proof_of_possession);
return newFetch(input, init);
},
[previousFetch, configurationName],
Expand Down
4 changes: 2 additions & 2 deletions packages/react-oidc/src/User.ts
Expand Up @@ -13,7 +13,7 @@ export type OidcUser<T extends OidcUserInfo = OidcUserInfo> = {
status: OidcUserStatus;
}

export const useOidcUser = <T extends OidcUserInfo = OidcUserInfo>(configurationName = 'default') => {
export const useOidcUser = <T extends OidcUserInfo = OidcUserInfo>(configurationName = 'default', demonstrating_proof_of_possession=false) => {
const [oidcUser, setOidcUser] = useState<OidcUser<T>>({ user: null, status: OidcUserStatus.Unauthenticated });
const [oidcUserId, setOidcUserId] = useState<string>('');

Expand All @@ -23,7 +23,7 @@ export const useOidcUser = <T extends OidcUserInfo = OidcUserInfo>(configuration
if (oidc && oidc.tokens) {
setOidcUser({ ...oidcUser, status: OidcUserStatus.Loading });
const isNoCache = oidcUserId !== '';
oidc.userInfoAsync(isNoCache)
oidc.userInfoAsync(isNoCache, demonstrating_proof_of_possession)
.then((info) => {
if (isMounted) {
// @ts-ignore
Expand Down

0 comments on commit 032a00b

Please sign in to comment.