Skip to content

Commit

Permalink
feat: Add support to get a client id from an offer, and from state JW…
Browse files Browse the repository at this point in the history
…Ts. EBSI for instance is using this
  • Loading branch information
nklomp committed Jan 24, 2024
1 parent 7888933 commit f089116
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 4 deletions.
3 changes: 3 additions & 0 deletions packages/client/lib/CredentialOfferClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CredentialOfferRequestWithBaseUrl,
CredentialOfferV1_0_11,
determineSpecVersionFromURI,
getClientIdFromCredentialOfferPayload,
OpenId4VCIVersion,
toUniformCredentialOfferRequest,
} from '@sphereon/oid4vci-common';
Expand Down Expand Up @@ -43,6 +44,7 @@ export class CredentialOfferClient {
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
}
}
const clientId = getClientIdFromCredentialOfferPayload(credentialOffer?.credential_offer);
const request = await toUniformCredentialOfferRequest(credentialOffer, {
...opts,
version,
Expand All @@ -53,6 +55,7 @@ export class CredentialOfferClient {
return {
scheme,
baseUrl,
clientId,
...request,
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
...(grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code'] && {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class OpenID4VCIClient {
credentialOffer: credentialOfferClient,
kid,
alg,
clientId: clientId ?? authorizationRequest?.clientId,
clientId: clientId ?? authorizationRequest?.clientId ?? credentialOfferClient.clientId,
pkce,
authorizationRequest,
});
Expand Down
53 changes: 51 additions & 2 deletions packages/common/lib/__tests__/CredentialOfferUtil.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { determineSpecVersionFromURI } from '../functions';
import { OpenId4VCIVersion } from '../types';
import { determineSpecVersionFromURI, getClientIdFromCredentialOfferPayload } from '../functions';
import { CredentialOfferPayload, OpenId4VCIVersion } from '../types';

export const UNIT_TEST_TIMEOUT = 30000;

Expand Down Expand Up @@ -52,4 +52,53 @@ describe('CredentialOfferUtil should', () => {
},
UNIT_TEST_TIMEOUT,
);

it('get client_id from JWT pre-auth code offer', () => {
const offer = {
credential_issuer: 'https://conformance-test.ebsi.eu/conformance/v3/issuer-mock',
credentials: [
{
format: 'jwt_vc',
types: ['VerifiableCredential', 'VerifiableAttestation', 'CTWalletCrossPreAuthorisedInTime'],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
trust_framework: { name: 'ebsi', type: 'Accreditation', uri: 'TIR link towards accreditation' },
},
],
grants: {
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
'pre-authorized_code':
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkJFTmRqRGZhdGxLai11UW92WUpsT184U2pPY1ZIdmk2SHJYS0xLRUI3UG8ifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnoyZG16RDgxY2dQeDhWa2k3SmJ1dU1tRllyV1BnWW95dHlrVVozZXlxaHQxajlLYnFTWlpGakc0dFZnS2hFd0twcm9qcUxCM0MyWXBqNEg3M1N0Z2pNa1NYZzJtUXh1V0xmenVSMTJRc052Z1FXenJ6S1NmN1lSQk5yUlhLNzF2ZnExMkJieXhUTEZFWkJXZm5IcWV6QlZHUWlOTGZxZXV5d1pIZ3N0TUNjUzQ0VFhmYjIiLCJhdXRob3JpemF0aW9uX2RldGFpbHMiOlt7InR5cGUiOiJvcGVuaWRfY3JlZGVudGlhbCIsImZvcm1hdCI6Imp3dF92YyIsImxvY2F0aW9ucyI6WyJodHRwczovL2NvbmZvcm1hbmNlLXRlc3QuZWJzaS5ldS9jb25mb3JtYW5jZS92My9pc3N1ZXItbW9jayJdLCJ0eXBlcyI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlZlcmlmaWFibGVBdHRlc3RhdGlvbiIsIkNUV2FsbGV0Q3Jvc3NQcmVBdXRob3Jpc2VkSW5UaW1lIl19XSwiaWF0IjoxNzA2MTI2NDI5LCJleHAiOjE3MDYxMjY3MjksImlzcyI6Imh0dHBzOi8vY29uZm9ybWFuY2UtdGVzdC5lYnNpLmV1L2NvbmZvcm1hbmNlL3YzL2lzc3Vlci1tb2NrIiwiYXVkIjoiaHR0cHM6Ly9jb25mb3JtYW5jZS10ZXN0LmVic2kuZXUvY29uZm9ybWFuY2UvdjMvYXV0aC1tb2NrIiwic3ViIjoiZGlkOmtleTp6MmRtekQ4MWNnUHg4VmtpN0pidXVNbUZZcldQZ1lveXR5a1VaM2V5cWh0MWo5S2JxU1paRmpHNHRWZ0toRXdLcHJvanFMQjNDMllwajRINzNTdGdqTWtTWGcybVF4dVdMZnp1UjEyUXNOdmdRV3pyektTZjdZUkJOclJYSzcxdmZxMTJCYnl4VExGRVpCV2ZuSHFlekJWR1FpTkxmcWV1eXdaSGdzdE1DY1M0NFRYZmIyIn0.Zq2o33CU4wlRNtWOIITI5qbJcuNc2c9hLwIio7OlsHBa5ZAyQR8UUU_r5EufSChe4ji15Ihrr20m5-oUiZW80A',
user_pin_required: true,
},
},
} as CredentialOfferPayload;
expect(getClientIdFromCredentialOfferPayload(offer)).toEqual(
'did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbqSZZFjG4tVgKhEwKprojqLB3C2Ypj4H73StgjMkSXg2mQxuWLfzuR12QsNvgQWzrzKSf7YRBNrRXK71vfq12BbyxTLFEZBWfnHqezBVGQiNLfqeuywZHgstMCcS44TXfb2',
);
});

it('get client_id from JWT authorization code offer', () => {
const offer = {
credential_issuer: 'https://conformance-test.ebsi.eu/conformance/v3/issuer-mock',
credentials: [
{
format: 'jwt_vc',
types: ['VerifiableCredential', 'VerifiableAttestation', 'CTWalletCrossAuthorisedInTime'],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
trust_framework: { name: 'ebsi', type: 'Accreditation', uri: 'TIR link towards accreditation' },
},
],
grants: {
authorization_code: {
issuer_state:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkJFTmRqRGZhdGxLai11UW92WUpsT184U2pPY1ZIdmk2SHJYS0xLRUI3UG8ifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnoyZG16RDgxY2dQeDhWa2k3SmJ1dU1tRllyV1BnWW95dHlrVVozZXlxaHQxajlLYnFTWlpGakc0dFZnS2hFd0twcm9qcUxCM0MyWXBqNEg3M1N0Z2pNa1NYZzJtUXh1V0xmenVSMTJRc052Z1FXenJ6S1NmN1lSQk5yUlhLNzF2ZnExMkJieXhUTEZFWkJXZm5IcWV6QlZHUWlOTGZxZXV5d1pIZ3N0TUNjUzQ0VFhmYjIiLCJjcmVkZW50aWFsX3R5cGVzIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiQ1RXYWxsZXRDcm9zc0F1dGhvcmlzZWRJblRpbWUiXSwiaWF0IjoxNzA2MTI1ODUwLCJleHAiOjE3MDYxMjYxNTAsImlzcyI6Imh0dHBzOi8vY29uZm9ybWFuY2UtdGVzdC5lYnNpLmV1L2NvbmZvcm1hbmNlL3YzL2lzc3Vlci1tb2NrIiwiYXVkIjoiaHR0cHM6Ly9jb25mb3JtYW5jZS10ZXN0LmVic2kuZXUvY29uZm9ybWFuY2UvdjMvYXV0aC1tb2NrIiwic3ViIjoiZGlkOmtleTp6MmRtekQ4MWNnUHg4VmtpN0pidXVNbUZZcldQZ1lveXR5a1VaM2V5cWh0MWo5S2JxU1paRmpHNHRWZ0toRXdLcHJvanFMQjNDMllwajRINzNTdGdqTWtTWGcybVF4dVdMZnp1UjEyUXNOdmdRV3pyektTZjdZUkJOclJYSzcxdmZxMTJCYnl4VExGRVpCV2ZuSHFlekJWR1FpTkxmcWV1eXdaSGdzdE1DY1M0NFRYZmIyIn0.jxzbE6OdqnJfLzSfwYcgRZQURI5UcAtuYU9gPOZYyUwjWMDtVo1k4PCYH4mnjok7pfj47ik8FnaHWE7d99u-_w',
},
},
} as CredentialOfferPayload;
expect(getClientIdFromCredentialOfferPayload(offer)).toEqual(
'did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9KbqSZZFjG4tVgKhEwKprojqLB3C2Ypj4H73StgjMkSXg2mQxuWLfzuR12QsNvgQWzrzKSf7YRBNrRXK71vfq12BbyxTLFEZBWfnHqezBVGQiNLfqeuywZHgstMCcS44TXfb2',
);
});
});
44 changes: 44 additions & 0 deletions packages/common/lib/functions/CredentialOfferUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Debug from 'debug';
import jwtDecode, { JwtPayload } from 'jwt-decode';

import {
AssertedUniformCredentialOffer,
Expand Down Expand Up @@ -68,6 +69,49 @@ export function getIssuerFromCredentialOfferPayload(request: CredentialOfferPayl
return 'issuer' in request ? request.issuer : request['credential_issuer'];
}

export const getClientIdFromCredentialOfferPayload = (credentialOffer?: CredentialOfferPayload): string | undefined => {
if (!credentialOffer) {
return;
}
if ('client_id' in credentialOffer) {
return credentialOffer.client_id;
}

const state: string | undefined = getStateFromCredentialOfferPayload(credentialOffer);
if (state && isJWT(state)) {
const decoded = jwtDecode<JwtPayload>(state, { header: false });
if ('client_id' in decoded && typeof decoded.client_id === 'string') {
return decoded.client_id;
}
}
return;
};

const isJWT = (input?: string) => {
if (!input) {
return false;
}
const noParts = input?.split('.').length;
return input?.startsWith('ey') && noParts === 3;
};
export const getStateFromCredentialOfferPayload = (credentialOffer: CredentialOfferPayload): string | undefined => {
if ('grants' in credentialOffer) {
if (credentialOffer.grants?.authorization_code) {
return credentialOffer.grants.authorization_code.issuer_state;
} else if (credentialOffer.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']) {
return credentialOffer.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code'];
}
}
if ('op_state' in credentialOffer) {
// older spec versions
return credentialOffer.op_state;
} else if ('pre-authorized_code' in credentialOffer) {
return credentialOffer['pre-authorized_code'];
}

return;
};

export function determineSpecVersionFromOffer(offer: CredentialOfferPayload | CredentialOffer): OpenId4VCIVersion {
if (isCredentialOfferV1_0_11(offer)) {
return OpenId4VCIVersion.VER_1_0_11;
Expand Down
8 changes: 7 additions & 1 deletion packages/common/lib/types/CredentialIssuance.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface CredentialResponse {

export interface CredentialOfferRequestWithBaseUrl extends UniformCredentialOfferRequest {
scheme: string;
clientId?: string;
baseUrl: string;
userPinRequired: boolean;
issuerState?: string;
Expand All @@ -26,7 +27,9 @@ export interface CredentialOfferRequestWithBaseUrl extends UniformCredentialOffe

export type CredentialOffer = CredentialOfferV1_0_09 | CredentialOfferV1_0_11;

export type CredentialOfferPayload = CredentialOfferPayloadV1_0_08 | CredentialOfferPayloadV1_0_09 | CredentialOfferPayloadV1_0_11;
export type CredentialOfferPayload = (CredentialOfferPayloadV1_0_08 | CredentialOfferPayloadV1_0_09 | CredentialOfferPayloadV1_0_11) & {
[x: string]: any;
};

export interface AssertedUniformCredentialOffer extends UniformCredentialOffer {
credential_offer: UniformCredentialOfferPayload;
Expand Down Expand Up @@ -107,6 +110,7 @@ export interface JWK extends BaseJWK {
x5t?: string;
'x5t#S256'?: string;
x5u?: string;

[propName: string]: unknown;
}

Expand Down Expand Up @@ -147,6 +151,7 @@ export interface JWSHeaderParameters extends JoseHeaderParameters {
alg?: Alg | string; // REQUIRED by the JWT signer
b64?: boolean;
crit?: string[];

[propName: string]: unknown;
}

Expand All @@ -172,6 +177,7 @@ export interface JWTPayload {

export type JWTSignerCallback = (jwt: Jwt, kid?: string) => Promise<string>;
export type JWTVerifyCallback<DIDDoc> = (args: { jwt: string; kid?: string }) => Promise<JwtVerifyResult<DIDDoc>>;

export interface JwtVerifyResult<DIDDoc> {
jwt: Jwt;
kid?: string;
Expand Down
5 changes: 5 additions & 0 deletions packages/common/lib/types/v1_0_11.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface CredentialOfferPayloadV1_0_11 {
* using the respective metadata. When multiple grants are present, it's at the Wallet's discretion which one to use.
*/
grants?: Grant;

/**
* Some implementations might include a client_id in the offer. For instance EBSI in a same-device flow. (Cross-device tucks it in the state JWT)
*/
client_id?: string;
}

export type CredentialRequestV1_0_11 = CommonCredentialRequest &
Expand Down

0 comments on commit f089116

Please sign in to comment.