Skip to content

Commit 09cbd0d

Browse files
committed
feat: Pass in issuer_state to regular state in auth code flow, so we get a better integration with any external OIDC solution
1 parent e6222ff commit 09cbd0d

9 files changed

Lines changed: 78 additions & 21 deletions

packages/client/lib/CredentialRequestClient.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
UniformCredentialRequest,
1919
URL_NOT_VALID,
2020
} from '@sphereon/oid4vci-common';
21-
import { CredentialFormat, DIDDocument } from '@sphereon/ssi-types';
21+
import { CredentialFormat } from '@sphereon/ssi-types';
2222
import Debug from 'debug';
2323

2424
import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11';
@@ -41,9 +41,10 @@ export interface CredentialRequestOpts {
4141
token: string;
4242
version: OpenId4VCIVersion;
4343
subjectIssuance?: ExperimentalSubjectIssuance;
44+
issuerState?: string;
4445
}
4546

46-
export type CreateCredentialRequestOpts<DIDDoc = DIDDocument> = {
47+
export type CreateCredentialRequestOpts = {
4748
credentialIdentifier?: string;
4849
credentialTypes?: string | string[];
4950
context?: string[];
@@ -52,7 +53,7 @@ export type CreateCredentialRequestOpts<DIDDoc = DIDDocument> = {
5253
version: OpenId4VCIVersion;
5354
};
5455

55-
export async function buildProof<DIDDoc = DIDDocument>(
56+
export async function buildProof(
5657
proofInput: ProofOfPossessionBuilder | ProofOfPossession,
5758
opts: {
5859
version: OpenId4VCIVersion;
@@ -101,7 +102,7 @@ export class CredentialRequestClient {
101102
* like using DPoP together with an authorization code flow. These are however rare, so you should be using the acquireCredentialsUsingProof normally
102103
* @param opts
103104
*/
104-
public async acquireCredentialsWithoutProof<DIDDoc = DIDDocument>(opts: {
105+
public async acquireCredentialsWithoutProof(opts: {
105106
credentialIdentifier?: string;
106107
credentialTypes?: string | string[];
107108
context?: string[];
@@ -122,7 +123,7 @@ export class CredentialRequestClient {
122123
return await this.acquireCredentialsUsingRequestWithoutProof(request, opts.createDPoPOpts);
123124
}
124125

125-
public async acquireCredentialsUsingProof<DIDDoc = DIDDocument>(opts: {
126+
public async acquireCredentialsUsingProof(opts: {
126127
proofInput: ProofOfPossessionBuilder | ProofOfPossession;
127128
credentialIdentifier?: string;
128129
credentialTypes?: string | string[];
@@ -245,21 +246,19 @@ export class CredentialRequestClient {
245246
});
246247
}
247248

248-
public async createCredentialRequestWithoutProof<DIDDoc = DIDDocument>(
249-
opts: CreateCredentialRequestOpts,
250-
): Promise<CredentialRequestWithoutProofV1_0_13> {
249+
public async createCredentialRequestWithoutProof(opts: CreateCredentialRequestOpts): Promise<CredentialRequestWithoutProofV1_0_13> {
251250
return await this.createCredentialRequestImpl(opts);
252251
}
253252

254-
public async createCredentialRequest<DIDDoc = DIDDocument>(
253+
public async createCredentialRequest(
255254
opts: CreateCredentialRequestOpts & {
256255
proofInput: ProofOfPossessionBuilder | ProofOfPossession;
257256
},
258257
): Promise<CredentialRequestV1_0_13> {
259258
return await this.createCredentialRequestImpl(opts);
260259
}
261260

262-
private async createCredentialRequestImpl<DIDDoc = DIDDocument>(
261+
private async createCredentialRequestImpl(
263262
opts: CreateCredentialRequestOpts & {
264263
proofInput?: ProofOfPossessionBuilder | ProofOfPossession;
265264
},
@@ -295,6 +294,7 @@ export class CredentialRequestClient {
295294
if (types.length === 0) {
296295
throw Error(`Credential type(s) need to be provided`);
297296
}
297+
const issuer_state = this.credentialRequestOpts.issuerState;
298298

299299
// TODO: we should move format specific logic
300300
if (format === 'jwt_vc_json' || format === 'jwt_vc') {
@@ -303,6 +303,7 @@ export class CredentialRequestClient {
303303
type: types,
304304
},
305305
format,
306+
...(issuer_state && { issuer_state }),
306307
...(proof && { proof }),
307308
...opts.subjectIssuance,
308309
};
@@ -313,6 +314,7 @@ export class CredentialRequestClient {
313314

314315
return {
315316
format,
317+
...(issuer_state && { issuer_state }),
316318
...(proof && { proof }),
317319
...opts.subjectIssuance,
318320

@@ -327,6 +329,7 @@ export class CredentialRequestClient {
327329
}
328330
return {
329331
format,
332+
...(issuer_state && { issuer_state }),
330333
...(proof && { proof }),
331334
vct: types[0],
332335
...opts.subjectIssuance,
@@ -337,6 +340,7 @@ export class CredentialRequestClient {
337340
}
338341
return {
339342
format,
343+
...(issuer_state && { issuer_state }),
340344
...(proof && { proof }),
341345
doctype: types[0],
342346
...opts.subjectIssuance,

packages/client/lib/CredentialRequestClientBuilder.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ export class CredentialRequestClientBuilder {
168168
return this;
169169
}
170170

171+
public withIssuerState(issuerState?: string): this {
172+
this._builder.withIssuerState(issuerState);
173+
return this;
174+
}
171175
public withCredentialType(credentialTypes: string | string[]): this {
172176
this._builder.withCredentialType(credentialTypes);
173177
return this;

packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class CredentialRequestClientBuilderV1_0_11 {
2828
token?: string;
2929
version?: OpenId4VCIVersion;
3030
subjectIssuance?: ExperimentalSubjectIssuance;
31+
issuerState?: string;
3132

3233
public static fromCredentialIssuer({
3334
credentialIssuer,
@@ -98,6 +99,11 @@ export class CredentialRequestClientBuilderV1_0_11 {
9899
});
99100
}
100101

102+
public withIssuerState(issuerState?: string): this {
103+
this.issuerState = issuerState;
104+
return this;
105+
}
106+
101107
public withCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadata): this {
102108
this.credentialEndpoint = metadata.credential_endpoint;
103109
return this;

packages/client/lib/CredentialRequestClientBuilderV1_0_13.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class CredentialRequestClientBuilderV1_0_13 {
2727
token?: string;
2828
version?: OpenId4VCIVersion;
2929
subjectIssuance?: ExperimentalSubjectIssuance;
30+
issuerState?: string;
3031

3132
public static fromCredentialIssuer({
3233
credentialIssuer,
@@ -86,6 +87,7 @@ export class CredentialRequestClientBuilderV1_0_13 {
8687
if (ids.length && ids.length === 1) {
8788
builder.withCredentialIdentifier(ids[0]);
8889
}
90+
8991
return builder;
9092
}
9193

@@ -96,11 +98,13 @@ export class CredentialRequestClientBuilderV1_0_13 {
9698
credentialOffer: CredentialOfferRequestWithBaseUrl;
9799
metadata?: EndpointMetadata;
98100
}): CredentialRequestClientBuilderV1_0_13 {
99-
return CredentialRequestClientBuilderV1_0_13.fromCredentialOfferRequest({
101+
const builder = CredentialRequestClientBuilderV1_0_13.fromCredentialOfferRequest({
100102
request: credentialOffer,
101103
metadata,
102104
version: credentialOffer.version,
103105
});
106+
107+
return builder;
104108
}
105109

106110
public withCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadataV1_0_13): this {
@@ -113,6 +117,11 @@ export class CredentialRequestClientBuilderV1_0_13 {
113117
return this;
114118
}
115119

120+
public withIssuerState(issuerState?: string): this {
121+
this.issuerState = issuerState;
122+
return this;
123+
}
124+
116125
public withDeferredCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadataV1_0_13): this {
117126
this.deferredCredentialEndpoint = metadata.deferred_credential_endpoint;
118127
return this;

packages/client/lib/OpenID4VCIClient.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,15 @@ export class OpenID4VCIClient {
455455
version: this.version(),
456456
});
457457
}
458+
// If we are in an auth code flow, without a c nonce, we return the issuerState back to the issuer in case it is present
459+
const issuerState =
460+
this.issuerSupportedFlowTypes().includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) &&
461+
this._state.authorizationCodeResponse &&
462+
!this.accessTokenResponse?.c_nonce &&
463+
this._state.credentialOffer?.issuerState
464+
? this._state.credentialOffer.issuerState
465+
: undefined;
466+
requestBuilder.withIssuerState(issuerState);
458467

459468
requestBuilder.withTokenFromResponse(this.accessTokenResponse);
460469
requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS);

packages/client/lib/OpenID4VCIClientV1_0_13.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export interface OpenID4VCIClientStateV1_0_13 {
6161
pkce: PKCEOpts;
6262
accessToken?: string;
6363
authorizationURL?: string;
64+
sendIssuerStateIfNoNonce?: boolean;
6465
}
6566

6667
export class OpenID4VCIClientV1_0_13 {
@@ -459,7 +460,15 @@ export class OpenID4VCIClientV1_0_13 {
459460
metadata: this.endpointMetadata,
460461
version: this.version(),
461462
});
462-
463+
// If we are in an auth code flow, without a c nonce, we return the issuerState back to the issuer in case it is present
464+
const issuerState =
465+
this.issuerSupportedFlowTypes().includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) &&
466+
this._state.authorizationCodeResponse &&
467+
!this.accessTokenResponse?.c_nonce &&
468+
this._state.credentialOffer?.issuerState
469+
? this._state.credentialOffer.issuerState
470+
: undefined;
471+
requestBuilder.withIssuerState(issuerState);
463472
requestBuilder.withTokenFromResponse(this.accessTokenResponse);
464473
requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS);
465474
let subjectIssuance: ExperimentalSubjectIssuance | undefined;

packages/issuer/lib/VcIssuer.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ export class VcIssuer {
405405
const credentialDataSupplierInput = opts.credentialDataSupplierInput ?? session.credentialDataSupplierInput
406406

407407
const result = await credentialDataSupplier({
408-
...cNonceState,
408+
...(cNonceState ? { ...cNonceState } : { ...authSession }),
409409
credentialRequest: opts.credentialRequest,
410410
credentialSupplierConfig: this._issuerMetadata.credential_supplier_config,
411411
credentialOffer /*todo: clientId: */,
@@ -477,8 +477,10 @@ export class VcIssuer {
477477
// credential: OPTIONAL. Contains issued Credential. MUST be present when acceptance_token is not returned. MAY be a JSON string or a JSON object, depending on the Credential format. See Appendix E for the Credential format specific encoding requirements
478478
throw new Error(CREDENTIAL_MISSING_ERROR)
479479
}
480-
// remove the previous nonce
481-
await this.cNonces.delete(cNonceState.cNonce)
480+
if (cNonceState) {
481+
// remove the previous nonce
482+
await this.cNonces.delete(cNonceState.cNonce)
483+
}
482484

483485
let notification_id: string | undefined
484486

@@ -624,14 +626,24 @@ export class VcIssuer {
624626

625627
const { didDocument, did, jwt } = jwtVerifyResult
626628
const { header, payload } = jwt
627-
const { iss, aud, iat, nonce } = payload
628-
if (!nonce) {
629+
const { iss, aud, iat, nonce, issuer_state } = payload
630+
if (!nonce && !issuer_state) {
629631
throw Error('No nonce was found in the Proof of Possession')
630632
}
631-
const cNonceState = await this.cNonces.getAsserted(nonce)
632-
preAuthorizedCode = cNonceState.preAuthorizedCode
633-
issuerState = cNonceState.issuerState
634-
const createdAt = cNonceState.createdAt
633+
let createdAt: number
634+
let cNonceState: CNonceState | undefined
635+
if (nonce) {
636+
cNonceState = await this.cNonces.getAsserted(nonce)
637+
preAuthorizedCode = cNonceState.preAuthorizedCode
638+
issuerState = cNonceState.issuerState
639+
createdAt = cNonceState.createdAt
640+
} else if (issuer_state) {
641+
const session = await this._credentialOfferSessions.getAsserted(issuer_state as string)
642+
issuerState = issuer_state as string | undefined
643+
createdAt = session.createdAt
644+
} else {
645+
throw Error('No nonce or issuer_state was found in the Proof of Possession')
646+
}
635647
// The verify callback should set the correct values, but let's look at the JWT ourselves to to be sure
636648
const alg = jwtVerifyResult.alg ?? header.alg
637649
const kid = jwtVerifyResult.kid ?? header.kid

packages/oid4vci-common/lib/types/Authorization.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export interface AuthorizationChallengeCodeResponse {
177177
* The authorization code issued by the authorization server.
178178
*/
179179
authorization_code: string;
180+
state?: string;
180181
}
181182

182183
// https://www.ietf.org/archive/id/draft-parecki-oauth-first-party-apps-02.html#name-error-response

packages/oid4vci-common/lib/types/v1_0_13.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ export type CredentialRequestV1_0_13ResponseEncryption = {
108108
export interface CredentialRequestV1_0_13Common extends ExperimentalSubjectIssuance {
109109
credential_response_encryption?: CredentialRequestV1_0_13ResponseEncryption;
110110
proof?: ProofOfPossession;
111+
112+
// We allow sending a issuer state back to the credential offer in case an auth code flow is used with an external AS and no nonces are used (not recommended), but does allow to integrate any OIDC server
113+
state?: string;
111114
}
112115

113116
export type CredentialRequestV1_0_13 = CredentialRequestV1_0_13Common &

0 commit comments

Comments
 (0)