Skip to content

Commit 036ec45

Browse files
committed
chore: Many fixes to get V13 and lower to work
1 parent 9dd2ecf commit 036ec45

8 files changed

Lines changed: 158 additions & 40 deletions

File tree

packages/client/lib/AuthorizationCodeClient.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,64 @@ import {
33
AuthorizationRequestOpts,
44
CodeChallengeMethod,
55
convertJsonToURI,
6+
CreateRequestObjectMode,
67
CredentialConfigurationSupportedV1_0_13,
78
CredentialOfferPayloadV1_0_13,
89
CredentialOfferRequestWithBaseUrl,
910
determineSpecVersionFromOffer,
1011
EndpointMetadataResultV1_0_13,
1112
formPost,
1213
JsonURIMode,
14+
Jwt,
1315
OID4VCICredentialFormat,
1416
OpenId4VCIVersion,
1517
PARMode,
1618
PKCEOpts,
1719
PushedAuthorizationResponse,
20+
RequestObjectOpts,
1821
ResponseType,
1922
} from '@sphereon/oid4vci-common';
2023
import Debug from 'debug';
2124

25+
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
26+
2227
const debug = Debug('sphereon:oid4vci');
2328

29+
export async function createSignedAuthRequestWhenNeeded(requestObject: Record<string, any>, opts: RequestObjectOpts & { aud?: string }) {
30+
if (opts.requestObjectMode === CreateRequestObjectMode.REQUEST_URI) {
31+
throw Error(`Request Object Mode ${opts.requestObjectMode} is not supported yet`);
32+
} else if (opts.requestObjectMode === CreateRequestObjectMode.REQUEST_OBJECT) {
33+
if (typeof opts.signCallbacks?.signCallback !== 'function') {
34+
throw Error(`No request object sign callback found, whilst request object mode was set to ${opts.requestObjectMode}`);
35+
} else if (!opts.kid) {
36+
throw Error(`No kid found, whilst request object mode was set to ${opts.requestObjectMode}`);
37+
}
38+
let client_metadata: any
39+
if (opts.clientMetadata || opts.jwksUri) {
40+
client_metadata = opts.clientMetadata ?? {};
41+
if (opts.jwksUri) {
42+
client_metadata['jwks_uri'] = opts.jwksUri;
43+
}
44+
}
45+
let authorization_details = requestObject['authorization_details']
46+
if (typeof authorization_details === 'string') {
47+
authorization_details = JSON.parse(requestObject.authorization_details);
48+
}
49+
if (!requestObject.aud && opts.aud) {
50+
requestObject.aud = opts.aud;
51+
}
52+
const iss = requestObject.iss ?? opts.iss ?? requestObject.client_id
53+
54+
const jwt: Jwt = { header: { alg: 'ES256', kid: opts.kid, typ: 'jwt' }, payload: {...requestObject, iss, authorization_details, ...(client_metadata && {client_metadata})} };
55+
const pop = await ProofOfPossessionBuilder.fromJwt({
56+
jwt,
57+
callbacks: opts.signCallbacks,
58+
version: OpenId4VCIVersion.VER_1_0_11,
59+
mode: 'jwt',
60+
}).build();
61+
requestObject['request'] = pop.jwt;
62+
}
63+
}
2464
function filterSupportedCredentials(
2565
credentialOffer: CredentialOfferPayloadV1_0_13,
2666
credentialsSupported?: Record<string, CredentialConfigurationSupportedV1_0_13>,
@@ -62,15 +102,13 @@ export const createAuthorizationRequestUrl = async ({
62102
}
63103
}
64104

65-
const { redirectUri } = authorizationRequest;
105+
const { redirectUri, requestObjectOpts = { requestObjectMode: CreateRequestObjectMode.NONE } } = authorizationRequest;
66106
const client_id = clientId ?? authorizationRequest.clientId;
67-
if (!client_id) {
68-
throw Error(`Cannot use PAR without a client_id value set`);
69-
}
107+
70108
let { scope, authorizationDetails } = authorizationRequest;
71109
const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
72110
? PARMode.REQUIRE
73-
: authorizationRequest.parMode ?? PARMode.AUTO;
111+
: authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER);
74112
// Scope and authorization_details can be used in the same authorization request
75113
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
76114
if (!scope && !authorizationDetails) {
@@ -138,15 +176,15 @@ export const createAuthorizationRequestUrl = async ({
138176
scope = ['openid', scope].filter((s) => !!s).join(' ');
139177
}
140178

141-
let queryObj: { [key: string]: string } | PushedAuthorizationResponse = {
179+
let queryObj: Record<string, any> | PushedAuthorizationResponse = {
142180
response_type: ResponseType.AUTH_CODE,
143181
...(!pkce.disabled && {
144182
code_challenge_method: pkce.codeChallengeMethod ?? CodeChallengeMethod.S256,
145183
code_challenge: pkce.codeChallenge,
146184
}),
147185
authorization_details: JSON.stringify(handleAuthorizationDetails(endpointMetadata, authorizationDetails)),
148186
...(redirectUri && { redirect_uri: redirectUri }),
149-
client_id,
187+
...(client_id && { client_id }),
150188
...(credentialOffer?.issuerState && { issuer_state: credentialOffer.issuerState }),
151189
scope,
152190
};
@@ -170,10 +208,11 @@ export const createAuthorizationRequestUrl = async ({
170208
throw Error(`PAR error: ${parResponse.origResponse.statusText}`);
171209
}
172210
} else {
173-
debug(`PAR response: ${(parResponse.successBody, null, 2)}`);
211+
debug(`PAR response: ${JSON.stringify(parResponse.successBody, null, 2)}`);
174212
queryObj = { /*response_type: ResponseType.AUTH_CODE,*/ client_id, request_uri: parResponse.successBody.request_uri };
175213
}
176214
}
215+
await createSignedAuthRequestWhenNeeded(queryObj, { ...requestObjectOpts, aud: endpointMetadata.authorization_server });
177216

178217
debug(`Object that will become query params: ` + JSON.stringify(queryObj, null, 2));
179218
const url = convertJsonToURI(queryObj, {

packages/client/lib/AuthorizationCodeClientV1_0_11.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
AuthorizationRequestOpts,
44
CodeChallengeMethod,
55
convertJsonToURI,
6-
CredentialConfigurationSupported,
6+
CreateRequestObjectMode,
77
CredentialOfferFormat,
88
CredentialOfferPayloadV1_0_11,
99
CredentialOfferRequestWithBaseUrl,
@@ -18,6 +18,8 @@ import {
1818
} from '@sphereon/oid4vci-common';
1919
import Debug from 'debug';
2020

21+
import { createSignedAuthRequestWhenNeeded } from './AuthorizationCodeClient';
22+
2123
const debug = Debug('sphereon:oid4vci');
2224

2325
export const createAuthorizationRequestUrlV1_0_11 = async ({
@@ -33,8 +35,9 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({
3335
credentialOffer?: CredentialOfferRequestWithBaseUrl;
3436
credentialsSupported?: CredentialsSupportedLegacy[];
3537
}): Promise<string> => {
36-
const { redirectUri, clientId } = authorizationRequest;
38+
const { redirectUri, clientId, requestObjectOpts = { requestObjectMode: CreateRequestObjectMode.NONE } } = authorizationRequest;
3739
let { scope, authorizationDetails } = authorizationRequest;
40+
3841
const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
3942
? PARMode.REQUIRE
4043
: authorizationRequest.parMode ?? PARMode.AUTO;
@@ -50,9 +53,7 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({
5053
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5154
// @ts-ignore
5255
authorizationDetails = creds
53-
.flatMap((cred) =>
54-
typeof cred === 'string' && credentialsSupported ? Object.values(credentialsSupported) : (cred as CredentialConfigurationSupported),
55-
)
56+
.flatMap((cred) => (typeof cred === 'string' ? credentialsSupported : (cred as CredentialsSupportedLegacy)))
5657
.filter((cred) => !!cred)
5758
.map((cred) => {
5859
return {
@@ -111,10 +112,11 @@ export const createAuthorizationRequestUrlV1_0_11 = async ({
111112
throw Error(`PAR error: ${parResponse.origResponse.statusText}`);
112113
}
113114
} else {
114-
debug(`PAR response: ${(parResponse.successBody, null, 2)}`);
115+
debug(`PAR response: ${JSON.stringify(parResponse.successBody, null, 2)}`);
115116
queryObj = { request_uri: parResponse.successBody.request_uri };
116117
}
117118
}
119+
await createSignedAuthRequestWhenNeeded(queryObj, { ...requestObjectOpts, aud: endpointMetadata.authorization_server });
118120

119121
debug(`Object that will become query params: ` + JSON.stringify(queryObj, null, 2));
120122
const url = convertJsonToURI(queryObj, {

packages/client/lib/OpenID4VCIClient.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,9 @@ export class OpenID4VCIClient {
524524
issuerSupportedFlowTypes(): AuthzFlowType[] {
525525
return (
526526
this.credentialOffer?.supportedFlows ??
527-
(this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ? [AuthzFlowType.AUTHORIZATION_CODE_FLOW] : [])
527+
(this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ?? this._state.endpointMetadata?.authorization_server
528+
? [AuthzFlowType.AUTHORIZATION_CODE_FLOW]
529+
: [])
528530
);
529531
}
530532

@@ -632,7 +634,8 @@ export class OpenID4VCIClient {
632634
*/
633635
public isEBSI() {
634636
if (
635-
(this.credentialOffer?.credential_offer as CredentialOfferPayloadV1_0_11).credentials?.find(
637+
this.credentialOffer &&
638+
(this.credentialOffer?.credential_offer as CredentialOfferPayloadV1_0_11)?.credentials?.find(
636639
(cred) =>
637640
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
638641
// @ts-ignore
@@ -641,8 +644,11 @@ export class OpenID4VCIClient {
641644
) {
642645
return true;
643646
}
644-
this.assertIssuerData();
645-
return this.endpointMetadata.credentialIssuerMetadata?.authorization_endpoint?.includes('ebsi.eu');
647+
// this.assertIssuerData();
648+
return (
649+
this.endpointMetadata.credentialIssuerMetadata?.authorization_endpoint?.includes('ebsi.eu') ||
650+
this.endpointMetadata.credentialIssuerMetadata?.authorization_server?.includes('ebsi.eu')
651+
);
646652
}
647653

648654
private assertIssuerData(): void {
@@ -666,7 +672,12 @@ export class OpenID4VCIClient {
666672
}
667673

668674
private syncAuthorizationRequestOpts(opts?: AuthorizationRequestOpts): AuthorizationRequestOpts {
669-
let authorizationRequestOpts = { ...this._state?.authorizationRequestOpts, ...opts } as AuthorizationRequestOpts;
675+
const requestObjectOpts = { ...this._state?.authorizationRequestOpts?.requestObjectOpts, ...opts?.requestObjectOpts };
676+
let authorizationRequestOpts = {
677+
...this._state?.authorizationRequestOpts,
678+
...opts,
679+
...(requestObjectOpts && { requestObjectOpts }),
680+
} as AuthorizationRequestOpts;
670681
if (!authorizationRequestOpts) {
671682
// We only set a redirectUri if no options are provided.
672683
// Note that this only works for mobile apps, that can handle a code query param on the default openid-credential-offer deeplink.

packages/client/lib/ProofOfPossessionBuilder.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Jwt,
88
NO_JWT_PROVIDED,
99
OpenId4VCIVersion,
10+
PoPMode,
1011
PROOF_CANT_BE_CONSTRUCTED,
1112
ProofOfPossession,
1213
ProofOfPossessionCallbacks,
@@ -17,9 +18,11 @@ export class ProofOfPossessionBuilder<DIDDoc> {
1718
private readonly proof?: ProofOfPossession;
1819
private readonly callbacks?: ProofOfPossessionCallbacks<DIDDoc>;
1920
private readonly version: OpenId4VCIVersion;
21+
private readonly mode: PoPMode = 'pop';
2022

2123
private kid?: string;
2224
private jwk?: JWK;
25+
private aud?: string | string[];
2326
private clientId?: string;
2427
private issuer?: string;
2528
private jwt?: Jwt;
@@ -34,54 +37,80 @@ export class ProofOfPossessionBuilder<DIDDoc> {
3437
jwt,
3538
accessTokenResponse,
3639
version,
40+
mode = 'pop',
3741
}: {
3842
proof?: ProofOfPossession;
3943
callbacks?: ProofOfPossessionCallbacks<DIDDoc>;
4044
accessTokenResponse?: AccessTokenResponse;
4145
jwt?: Jwt;
4246
version: OpenId4VCIVersion;
47+
mode?: PoPMode;
4348
}) {
49+
this.mode = mode;
4450
this.proof = proof;
4551
this.callbacks = callbacks;
4652
this.version = version;
4753
if (jwt) {
4854
this.withJwt(jwt);
4955
} else {
50-
this.withTyp(version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt');
56+
this.withTyp(version < OpenId4VCIVersion.VER_1_0_11 || mode === 'jwt' ? 'jwt' : 'openid4vci-proof+jwt');
5157
}
5258
if (accessTokenResponse) {
5359
this.withAccessTokenResponse(accessTokenResponse);
5460
}
5561
}
5662

63+
static manual<DIDDoc>({
64+
jwt,
65+
callbacks,
66+
version,
67+
mode = 'jwt',
68+
}: {
69+
jwt?: Jwt;
70+
callbacks: ProofOfPossessionCallbacks<DIDDoc>;
71+
version: OpenId4VCIVersion;
72+
mode?: PoPMode;
73+
}): ProofOfPossessionBuilder<DIDDoc> {
74+
return new ProofOfPossessionBuilder({ callbacks, jwt, version, mode });
75+
}
76+
5777
static fromJwt<DIDDoc>({
5878
jwt,
5979
callbacks,
6080
version,
81+
mode = 'pop',
6182
}: {
6283
jwt: Jwt;
6384
callbacks: ProofOfPossessionCallbacks<DIDDoc>;
6485
version: OpenId4VCIVersion;
86+
mode?: PoPMode;
6587
}): ProofOfPossessionBuilder<DIDDoc> {
66-
return new ProofOfPossessionBuilder({ callbacks, jwt, version });
88+
return new ProofOfPossessionBuilder({ callbacks, jwt, version, mode });
6789
}
6890

6991
static fromAccessTokenResponse<DIDDoc>({
7092
accessTokenResponse,
7193
callbacks,
7294
version,
95+
mode = 'pop',
7396
}: {
7497
accessTokenResponse: AccessTokenResponse;
7598
callbacks: ProofOfPossessionCallbacks<DIDDoc>;
7699
version: OpenId4VCIVersion;
100+
mode?: PoPMode;
77101
}): ProofOfPossessionBuilder<DIDDoc> {
78-
return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version });
102+
return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version, mode });
79103
}
80104

81105
static fromProof<DIDDoc>(proof: ProofOfPossession, version: OpenId4VCIVersion): ProofOfPossessionBuilder<DIDDoc> {
82106
return new ProofOfPossessionBuilder({ proof, version });
83107
}
84108

109+
withAud(aud: string | string[]): this {
110+
this.aud = aud;
111+
return this;
112+
}
113+
85114
withClientId(clientId: string): this {
86115
this.clientId = clientId;
87116
return this;
@@ -113,7 +142,7 @@ export class ProofOfPossessionBuilder<DIDDoc> {
113142
}
114143

115144
withTyp(typ: Typ): this {
116-
if (this.version >= OpenId4VCIVersion.VER_1_0_11) {
145+
if (this.mode === 'pop' && this.version >= OpenId4VCIVersion.VER_1_0_11) {
117146
if (!!typ && typ !== 'openid4vci-proof+jwt') {
118147
throw Error('typ must be openid4vci-proof+jwt for version 1.0.11 and up');
119148
}
@@ -160,7 +189,7 @@ export class ProofOfPossessionBuilder<DIDDoc> {
160189
if (jwt.header.typ) {
161190
this.withTyp(jwt.header.typ as Typ);
162191
}
163-
if (this.version >= OpenId4VCIVersion.VER_1_0_11) {
192+
if (!this.typ && this.version >= OpenId4VCIVersion.VER_1_0_11) {
164193
this.withTyp('openid4vci-proof+jwt');
165194
}
166195
this.withAlg(jwt.header.alg);
@@ -171,8 +200,8 @@ export class ProofOfPossessionBuilder<DIDDoc> {
171200
}
172201

173202
if (jwt.payload) {
174-
if (jwt.payload.iss) this.withClientId(jwt.payload.iss);
175-
if (jwt.payload.aud) this.withIssuer(jwt.payload.aud);
203+
if (jwt.payload.iss) this.mode === 'pop' ? this.withClientId(jwt.payload.iss) : this.withIssuer(jwt.payload.iss);
204+
if (jwt.payload.aud) this.mode === 'pop' ? this.withIssuer(jwt.payload.aud) : this.withAud(jwt.payload.aud);
176205
if (jwt.payload.jti) this.withJti(jwt.payload.jti);
177206
if (jwt.payload.nonce) this.withAccessTokenNonce(jwt.payload.nonce);
178207
}
@@ -184,16 +213,18 @@ export class ProofOfPossessionBuilder<DIDDoc> {
184213
return Promise.resolve(this.proof);
185214
} else if (this.callbacks) {
186215
return await createProofOfPossession(
216+
this.mode,
187217
this.callbacks,
188218
{
189-
typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'),
219+
typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 || this.mode === 'jwt' ? 'jwt' : 'openid4vci-proof+jwt'),
190220
kid: this.kid,
191221
jwk: this.jwk,
192222
jti: this.jti,
193223
alg: this.alg,
224+
aud: this.aud,
194225
issuer: this.issuer,
195226
clientId: this.clientId,
196-
nonce: this.cNonce,
227+
...(this.cNonce && { nonce: this.cNonce }),
197228
},
198229
this.jwt,
199230
);

0 commit comments

Comments
 (0)