Skip to content

Commit

Permalink
exposed OpenID4VCI on context + isolated the token request logic
Browse files Browse the repository at this point in the history
  • Loading branch information
kkmanos committed Jan 9, 2025
1 parent 778c119 commit 7bfd6a2
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 66 deletions.
7 changes: 5 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import CredentialDetails from './pages/Home/CredentialDetails';
import { withUriHandler } from './UriHandler';
import { withCredentialParserContext } from './context/CredentialParserContext';
import { withOpenID4VPContext } from './context/OpenID4VPContext';
import { withOpenID4VCIContext } from './context/OpenID4VCIContext';

const reactLazyWithNonDefaultExports = (load, ...names) => {
const nonDefaults = (names ?? []).map(name => {
Expand Down Expand Up @@ -161,8 +162,10 @@ export default withSessionContext(
withCredentialsContext(
withCredentialParserContext(
withOpenID4VPContext(
withUriHandler(
App
withOpenID4VCIContext(
withUriHandler(
App
)
)
)
)
Expand Down
34 changes: 34 additions & 0 deletions src/context/OpenID4VCIContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { createContext } from "react";
import { IOpenID4VCI } from "../lib/interfaces/IOpenID4VCI";
import { OpenID4VCI } from "../lib/services/OpenID4VCI/OpenID4VCI";


export type OpenID4VPContextValue = {
openID4VCI: IOpenID4VCI;
}

const OpenID4VCIContext: React.Context<OpenID4VPContextValue> = createContext({
openID4VCI: null
});

export const OpenID4VCIContextProvider = ({ children }) => {

const errorCallback = (title: string, msg: string) => {
throw new Error("Not implemented");
}

const openID4VCI = OpenID4VCI({ errorCallback });
return (
<OpenID4VCIContext.Provider value={{ openID4VCI }}>
{children}
</OpenID4VCIContext.Provider>
);
}

export const withOpenID4VCIContext: <P>(component: React.ComponentType<P>) => React.ComponentType<P> = (Component) =>
(props) => (
<OpenID4VCIContextProvider>
<Component {...props} />
</OpenID4VCIContextProvider>
);
export default OpenID4VCIContext;
6 changes: 3 additions & 3 deletions src/context/OpenID4VPContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useContext, createContext } from "react";
import React, { useState, useContext, createContext } from "react";
import SelectCredentialsPopup from "../components/Popups/SelectCredentialsPopup";
import CredentialsContext from '../context/CredentialsContext';
import { useOpenID4VP } from "../lib/services/OpenID4VP/OpenID4VP";
import { OpenID4VP } from "../lib/services/OpenID4VP/OpenID4VP";
import { IOpenID4VP } from "../lib/interfaces/IOpenID4VP";

export type OpenID4VPContextValue = {
Expand Down Expand Up @@ -45,7 +45,7 @@ export const OpenID4VPContextProvider = ({ children }) => {
return showPopup({ conformantCredentialsMap, verifierDomainName });
}

const openID4VP = useOpenID4VP({ showCredentialSelectionPopup });
const openID4VP = OpenID4VP({ showCredentialSelectionPopup });

return (
<OpenID4VPContext.Provider value={{ openID4VP }}>
Expand Down
82 changes: 28 additions & 54 deletions src/lib/services/OpenID4VCI/OpenID4VCI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ import SessionContext from '../../../context/SessionContext';
import { useOpenID4VCIPushedAuthorizationRequest } from './OpenID4VCIAuthorizationRequest/OpenID4VCIPushedAuthorizationRequest';
import { useOpenID4VCIAuthorizationRequestForFirstPartyApplications } from './OpenID4VCIAuthorizationRequest/OpenID4VCIAuthorizationRequestForFirstPartyApplications';
import { useOpenID4VCIHelper } from '../OpenID4VCIHelper';
import { GrantType, useTokenRequest } from './TokenRequest';
import OpenID4VCIContext from '../../../context/OpenID4VCIContext';

const redirectUri = config.OPENID4VCI_REDIRECT_URI as string;

export function useOpenID4VCI(): IOpenID4VCI {

export function useOpenID4VCI() {
const openID4VCI = useContext(OpenID4VCIContext);
if (!openID4VCI.openID4VCI) {
throw new Error("OpenID4VCIContext is not defined in the context");
}
return openID4VCI.openID4VCI;
}

export function OpenID4VCI({ errorCallback }: { errorCallback: (title: string, message: string) => void }): IOpenID4VCI {

const httpProxy = useHttpProxy();
const openID4VCIClientStateRepository = useOpenID4VCIClientStateRepository();
Expand All @@ -29,6 +40,7 @@ export function useOpenID4VCI(): IOpenID4VCI {
const openID4VCIPushedAuthorizationRequest = useOpenID4VCIPushedAuthorizationRequest();
const openID4VCIAuthorizationRequestForFirstPartyApplications = useOpenID4VCIAuthorizationRequestForFirstPartyApplications();

const tokenRequestBuilder = useTokenRequest();

async function handleAuthorizationResponse(url: string, dpopNonceHeader?: string) {

Expand Down Expand Up @@ -315,74 +327,36 @@ export function useOpenID4VCI(): IOpenID4VCI {
}
const jti = generateRandomIdentifier(8);

let tokenRequestHeaders = {
'Content-Type': 'application/x-www-form-urlencoded',
};
tokenRequestBuilder.setTokenEndpoint(tokenEndpoint);

if (authzServerMetadata.authzServeMetadata.dpop_signing_alg_values_supported) {
const dpop = await generateDPoP(
dpopPrivateKey as jose.KeyLike,
dpopPublicKeyJwk,
jti,
"POST",
tokenEndpoint,
requestCredentialsParams.dpopNonceHeader
);
await tokenRequestBuilder.setDpopHeader(dpopPrivateKey as jose.KeyLike, dpopPublicKeyJwk, jti);
flowState.dpop = {
dpopAlg: 'ES256',
dpopJti: jti,
dpopPrivateKeyJwk: dpopPrivateKeyJwk,
dpopPublicKeyJwk: dpopPublicKeyJwk,
}
tokenRequestHeaders['DPoP'] = dpop;
}

tokenRequestBuilder.setClientId(clientId.client_id);
tokenRequestBuilder.setGrantType(requestCredentialsParams.authorizationCodeGrant ? GrantType.AUTHORIZATION_CODE : GrantType.REFRESH);
tokenRequestBuilder.setAuthorizationCode(requestCredentialsParams?.authorizationCodeGrant?.code);
tokenRequestBuilder.setCodeVerifier(flowState?.code_verifier);

const formData = new URLSearchParams();
formData.append('client_id', clientId.client_id);
if (requestCredentialsParams.authorizationCodeGrant) {
formData.append('grant_type', 'authorization_code');
formData.append('code', requestCredentialsParams.authorizationCodeGrant.code);
formData.append('code_verifier', flowState.code_verifier);
}
else if (requestCredentialsParams.refreshTokenGrant) {
if (!flowState?.tokenResponse?.data.refresh_token) {
console.info("Found no refresh_token to execute refesh_token grant")
throw new Error("Found no refresh_token to execute refesh_token grant");
}
formData.append('grant_type', 'refresh_token');
formData.append('refresh_token', flowState.tokenResponse.data.refresh_token);
}
else {
throw new Error("No grant type selected in requestCredentials()");
}
formData.append('redirect_uri', redirectUri);
tokenRequestBuilder.setRefreshToken(flowState?.tokenResponse?.data?.refresh_token);

const response = await httpProxy.post(tokenEndpoint, formData.toString(), tokenRequestHeaders);
tokenRequestBuilder.setRedirectUri(redirectUri);

if (response.err) {
const { err } = response;
console.log("failed token request")
console.log(JSON.stringify(err));
console.log("Dpop nonce found = ", err.headers['dpop-nonce'])
if (err.headers['dpop-nonce']) {
requestCredentialsParams.dpopNonceHeader = err.headers['dpop-nonce'];
if (requestCredentialsParams.dpopNonceHeader) {
await requestCredentials(credentialIssuerIdentifier, requestCredentialsParams);
return;
}
}
else if (err.data.error) {
console.error("OID4VCI Token Response Error: ", JSON.stringify(err.data))
}
return;

const result = await tokenRequestBuilder.execute();

if ('error' in result) {
throw new Error("Token request failed");
}

console.log("== response = ", response)
try { // try to extract the response and update the OpenID4VCIClientStateRepository
const {
data: { access_token, c_nonce, expires_in, c_nonce_expires_in, refresh_token },
} = response;
const { access_token, c_nonce, expires_in, c_nonce_expires_in, refresh_token } = result.response;

if (!access_token) {
console.log("Missing access_token from response");
Expand All @@ -393,7 +367,7 @@ export function useOpenID4VCI(): IOpenID4VCI {
data: {
access_token, c_nonce, expiration_timestamp: Math.floor(Date.now() / 1000) + expires_in, c_nonce_expiration_timestamp: Math.floor(Date.now() / 1000) + c_nonce_expires_in, refresh_token
},
headers: { ...response.headers }
headers: { ...result.response.httpResponseHeaders }
}

await openID4VCIClientStateRepository.updateState(flowState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { OpenID4VCIClientState } from "../../../types/OpenID4VCIClientState";
import { useOpenID4VCIClientStateRepository } from "../../OpenID4VCIClientStateRepository";
import { useHttpProxy } from "../../HttpProxy/HttpProxy";
import { useContext } from "react";
import OpenID4VPContext from "../../../../context/OpenID4VPContext";
import SessionContext from "../../../../context/SessionContext";
import { useOpenID4VP } from "../../OpenID4VP/OpenID4VP";

export function useOpenID4VCIAuthorizationRequestForFirstPartyApplications(): IOpenID4VCIAuthorizationRequest {
const httpProxy = useHttpProxy();
const openID4VCIClientStateRepository = useOpenID4VCIClientStateRepository();

const { openID4VP } = useContext(OpenID4VPContext);
const openID4VP = useOpenID4VP();

const { keystore } = useContext(SessionContext);

return {
Expand Down
159 changes: 159 additions & 0 deletions src/lib/services/OpenID4VCI/TokenRequest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { JWK, KeyLike } from 'jose';
import { useHttpProxy } from '../HttpProxy/HttpProxy';
import { generateDPoP } from '../../utils/dpop';

export type AccessToken = {
access_token: string;
c_nonce: string;
expires_in: number;
c_nonce_expires_in: number;
refresh_token?: string;

httpResponseHeaders: {
"dpop-nonce"?: string
}
}

export enum GrantType {
AUTHORIZATION_CODE = "code",
REFRESH = "refresh_token",
}


export enum TokenRequestError {
FAILED,
}

export function useTokenRequest() {

const httpProxy = useHttpProxy();

let tokenEndpointURL = null;

let grant_type: GrantType = GrantType.AUTHORIZATION_CODE;
let refresh_token = null;
let code = null;
let code_verifier = null;
let redirect_uri = null;
let client_id = null;

const httpHeaders = {
'Content-Type': 'application/x-www-form-urlencoded',
};

function setClientId(clientId: string) {
client_id = clientId;
}

function setGrantType(grant: GrantType) {
grant_type = grant;
}

function setAuthorizationCode(authzCode: string) {
code = authzCode;
}

function setCodeVerifier(codeVerifier: string) {
code_verifier = codeVerifier;
}

function setRefreshToken(tok: string) {
refresh_token = tok;
}

function setRedirectUri(redirectUri: string) {
redirect_uri = redirectUri;
}

function setTokenEndpoint(tokenEndpoint: string) {
tokenEndpointURL = tokenEndpoint;
}

async function setDpopHeader(dpopPrivateKey: KeyLike, dpopPublicKeyJwk: JWK, jti: string) {
if (!tokenEndpointURL) {
throw new Error("tokenEndpointURL was not defined");
}
const dpop = await generateDPoP(
dpopPrivateKey as KeyLike,
dpopPublicKeyJwk,
jti,
"POST",
tokenEndpointURL,
httpHeaders['dpop-nonce']
);

httpHeaders['DPoP'] = dpop;
}


async function execute(): Promise<{ response: AccessToken} | { error: TokenRequestError }> {
const formData = new URLSearchParams();

formData.append('client_id', client_id);
if (grant_type == GrantType.AUTHORIZATION_CODE) {
console.log("Executing authorization code grant...");

formData.append('grant_type', 'authorization_code');
formData.append('code', code);
formData.append('code_verifier', code_verifier);
}
else if (grant_type == GrantType.REFRESH) {
console.log("Executing refresh token grant...");
if (!refresh_token) {
console.info("Found no refresh_token to execute refesh_token grant")
throw new Error("Found no refresh_token to execute refesh_token grant");
}
formData.append('grant_type', 'refresh_token');
formData.append('refresh_token', refresh_token);
}
else {
throw new Error("No grant type selected in requestCredentials()");
}
formData.append('redirect_uri', redirect_uri);

const response = await httpProxy.post(tokenEndpointURL, formData.toString(), httpHeaders);

if (response.err) {
const { err } = response;
console.log("failed token request")
console.log(JSON.stringify(err));
console.log("Dpop nonce found = ", err.headers['dpop-nonce'])
if (err.headers['dpop-nonce']) {
httpHeaders['dpop-nonce'] = err.headers['dpop-nonce'];
if (httpHeaders['dpop-nonce']) {
return execute();
}
}
else if (err.data.error) {
console.error("OID4VCI Token Response Error: ", JSON.stringify(err.data))
}
return { error: TokenRequestError.FAILED };
}

return {
response: {
access_token: response.data.access_token,
c_nonce: response.data.c_nonce,
c_nonce_expires_in: response.data.c_nonce_expires_in,
expires_in: response.data.expires_in,
refresh_token: response.data?.refresh_token,
httpResponseHeaders: {
...response.headers
}
}
}
}

return {
setClientId,
setGrantType,
setAuthorizationCode,
setCodeVerifier,
setRefreshToken,
setRedirectUri,
setTokenEndpoint,
setDpopHeader,

execute,
}
}
Loading

0 comments on commit 7bfd6a2

Please sign in to comment.