Skip to content

Commit

Permalink
wip: pre-authorized_code flow
Browse files Browse the repository at this point in the history
  • Loading branch information
jessevanmuijden committed Oct 22, 2024
1 parent 692df88 commit e54a5ee
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 9 deletions.
4 changes: 2 additions & 2 deletions src/components/Credentials/CredentialImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ const CredentialImage = ({ credential, className, onClick, showRibbon = true })
<img src={svgImage} alt={"Credential"} className={className} onClick={onClick} />
)}
</>
) : parsedCredential && parsedCredential.credentialImage.credentialImageURL && (
<img src={parsedCredential.credentialImage.credentialImageURL} alt={"Credential"} className={className} onClick={onClick} />
) : (
<img src={parsedCredential?.credentialImage?.credentialImageURL || 'https://picsum.photos/300/200' } alt={"Credential"} className={className} onClick={onClick} />
)}

{parsedCredential && showRibbon &&
Expand Down
5 changes: 5 additions & 0 deletions src/components/Credentials/CredentialInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ const CredentialInfo = ({ credential, mainClassName = "text-sm lg:text-base w-fu
{renderRow('grade', 'Grade', parsedCredential?.grade, screenType)}
{renderRow('id', 'Social Security Number', parsedCredential?.ssn, screenType)}
{renderRow('id', 'Document Number', parsedCredential?.document_number, screenType)}

{renderRow('id', 'ID', parsedCredential?.vc?.credentialSubject?.sub)}
{renderRow('familyName', 'Family Name', parsedCredential?.vc?.credentialSubject?.family_name)}
{renderRow('firstName', 'Given Name', parsedCredential?.vc?.credentialSubject?.given_name)}
{renderRow('diplomaTitle', 'Affiliation', parsedCredential?.vc?.credentialSubject?.eduperson_affiliation)}
</>
)}
</tbody>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Credentials/CredentialJson.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const CredentialJson = ({ credential, textAreaRows='10' }) => {

useEffect(() => {
if (container) {
container.credentialParserRegistry.parse(credential).then((c) => {
container.credentialParserRegistry.parse(credential.vc || credential).then((c) => {
if ('error' in c) {
return;
}
Expand Down
14 changes: 14 additions & 0 deletions src/functions/parseSdJwtCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ const hasherAndAlgorithm: HasherAndAlgorithm = {
algorithm: HasherAlgorithm.Sha256
}

const parseJwt = (token: string) => {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));

return JSON.parse(jsonPayload);
}

export const parseSdJwtCredential = async (credential: string | object): Promise<{ beautifiedForm: any; } | { error: string }> => {
try {
if (typeof credential == 'string') { // is JWT
Expand All @@ -32,6 +42,10 @@ export const parseSdJwtCredential = async (credential: string | object): Promise
}

}

return {
beautifiedForm: parseJwt(credential),
}
}
return { error: "Could not parse SDJWT credential" };
}
Expand Down
86 changes: 85 additions & 1 deletion src/hooks/useCheckURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import SessionContext from '../context/SessionContext';
import { BackgroundTasksContext } from '../context/BackgroundTasksContext';
import { useContainer } from './useContainer';
import { HandleAuthorizationRequestError } from '../lib/interfaces/IOpenID4VPRelyingParty';
import { generateRandomIdentifier } from '../lib/utils/generateRandomIdentifier';
import { StorableCredential } from '../lib/types/StorableCredential';
import { VerifiableCredentialFormat } from '../lib/schemas/vc';


function useCheckURL(urlToCheck: string): {
Expand Down Expand Up @@ -41,7 +44,88 @@ function useCheckURL(urlToCheck: string): {
throw new Error("User handle could not be extracted from keystore");
}
const u = new URL(urlToCheck);
if (u.protocol === 'openid-credential-offer' || u.searchParams.get('credential_offer') || u.searchParams.get('credential_offer_uri') ) {
if (u.searchParams.get('credential_offer_uri') ) {
const credentailOfferResponse = await fetch(u.searchParams.get('credential_offer_uri'));
const credentialOffer = await credentailOfferResponse.json();

if (!credentialOffer.grants['urn:ietf:params:oauth:grant-type:pre-authorized_code'] || !credentialOffer.credential_issuer) {
return;
}

const body = {
grant_type: 'urn:ietf:params:oauth:grant-type:pre-authorized_code',
'pre-authorized_code': credentialOffer.grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']['pre-authorized_code'],
};

const tokenResponse = await fetch(`${credentialOffer.credential_issuer}/token`, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
}
});
const { access_token: accesToken, c_nonce: cNonce } = await tokenResponse.json();

const openIdCredentialIssuerResponse = await fetch(`${credentialOffer.credential_issuer}/.well-known/openid-credential-issuer`, {
headers: {
'Cache-Control': 'no-cache',
},
});
const openIdCredentialIssuer = await openIdCredentialIssuerResponse.json();
// const metadata = OpenidCredentialIssuerMetadataSchema.parse(openIdCredentialIssuer);
const metadata = openIdCredentialIssuer; // @todo: investigate schema errors

const generateNonceProof = async (cNonce: string, audience: string, clientId: string): Promise<{ jws: string }> => {
const [{ proof_jwt }] = await keystore.generateOpenid4vciProof(cNonce, audience, clientId);
return { jws: proof_jwt };
};

const storeCredential = async (c: StorableCredential) => {
await api.post('/storage/vc', {
...c
});
};

const credentialEndpoint = metadata.credential_endpoint;
const credentialRequestHeaders = {
'Authorization': `Bearer ${accesToken}`,
'Content-Type': 'application/json',
};

const { jws } = await generateNonceProof(cNonce, metadata.credential_issuer, 'wwwallet');

const credentialEndpointBody = {
'proof': {
'proof_type': 'jwt',
'jwt': jws,
},
format: VerifiableCredentialFormat.JWT_VC_JSON, // @todo: retrieve from credential_configurations_supported
...(metadata.credential_configurations_supported.AcademicBaseCredential), // @todo: retrieve from credential_configurations_supported
};

const didResponse = await fetch(`${credentialOffer.credential_issuer}/.well-known/did.json`, {
headers: {
'Cache-Control': 'no-cache',
},
});
const did = await didResponse.json();

const credentialResponse = await fetch(credentialEndpoint, {
method: 'POST',
body: JSON.stringify(credentialEndpointBody),
headers: credentialRequestHeaders
});
const credential = await credentialResponse.json();

await storeCredential({
credentialConfigurationId: did.id,
credentialIssuerIdentifier: credentialOffer.credential_issuer,
credentialIdentifier: generateRandomIdentifier(32),
credential: credential.credential,
format: VerifiableCredentialFormat.JWT_VC_JSON, // @todo: retrieve from credential_configurations_supported
});
}
else if (u.protocol === 'openid-credential-offer' || u.searchParams.get('credential_offer') || u.searchParams.get('credential_offer_uri') ) {
for (const credentialIssuerIdentifier of Object.keys(container.openID4VCIClients)) {
await container.openID4VCIClients[credentialIssuerIdentifier].handleCredentialOffer(u.toString())
.then(({ credentialIssuer, selectedCredentialConfigurationSupported, issuer_state }) => {
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ export function useContainer() {
else {
let credentialImageURL = credentialHeader?.vctm?.display && credentialHeader.vctm.display[0] && credentialHeader.vctm.display[0][defaultLocale] ?
credentialHeader.vctm.display[0][defaultLocale]?.rendering?.simple?.logo?.uri
: null;
: 'https://picsum.photos/300/200';

if (!credentialImageURL) { // prrovide fallback method through the OpenID credential issuer metadata
if (!credentialImageURL) { // provide fallback method through the OpenID credential issuer metadata
const { metadata } = await cont.resolve<IOpenID4VCIHelper>('OpenID4VCIHelper').getCredentialIssuerMetadata(result.beautifiedForm.iss);
const credentialConfigurationSupportedObj: CredentialConfigurationSupported = Object.values(metadata.credential_configurations_supported)
.filter((x: any) => x?.vct && result.beautifiedForm?.vct && x.vct === result.beautifiedForm?.vct)
Expand Down
1 change: 1 addition & 0 deletions src/lib/schemas/vc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum VerifiableCredentialFormat {
SD_JWT_VC = "vc+sd-jwt",
VC_JWT = "vc_jwt",
JWT_VC_JSON = "jwt_vc_json",
MSO_MDOC = "mso_mdoc"
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/services/OpenID4VCIHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class OpenID4VCIHelper implements IOpenID4VCIHelper {
}
catch(err) {
console.error(err);
throw new Error("Couldn't get Credential Issuer Metadata");
// throw new Error("Couldn't get Credential Issuer Metadata");
}

}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/types/StorableCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export type StorableCredential = {
credentialIdentifier: string;
format: VerifiableCredentialFormat;
credential: string;
credentialIssuerIdentifier?: string;
credentialConfigurationId?: string;
};
8 changes: 6 additions & 2 deletions src/services/keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,7 @@ async function addNewCredentialKeypair(
privateKey: CryptoKey,
keypair: CredentialKeyPair,
newPrivateData: OpenedContainer,
did: string,
}> {
const { publicKey, privateKey } = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
Expand All @@ -1069,6 +1070,7 @@ async function addNewCredentialKeypair(
};

return {
did,
privateKey,
keypair,
newPrivateData: await updatePrivateData(
Expand Down Expand Up @@ -1139,17 +1141,19 @@ export async function generateOpenid4vciProof(
const jwkThumbprint = await jose.calculateJwkThumbprint(pubKey as JWK, "sha256");
return jwkThumbprint;
};
const { privateKey, keypair, newPrivateData } = await addNewCredentialKeypair(container, didKeyVersion, deriveKid);
const { privateKey, keypair, newPrivateData, did } = await addNewCredentialKeypair(container, didKeyVersion, deriveKid);

const jws = await new SignJWT({
nonce: nonce,
aud: audience,
iss: issuer,
client_id: issuer,
})
.setProtectedHeader({
alg: keypair.alg,
typ: "openid4vci-proof+jwt",
jwk: { ...keypair.publicKey, key_ops: ['verify'] } as JWK,
// jwk: { ...keypair.publicKey, key_ops: ['verify'] } as JWK,
kid: did,
})
.setIssuedAt()
.sign(privateKey);
Expand Down

0 comments on commit e54a5ee

Please sign in to comment.