Skip to content

Commit 61f0eb8

Browse files
nklompgithub-actions[bot]Fendy PutraTimoGlastrarkreutzer
authored
Merge pull request #91 from Sphereon-Opensource/develop
fix: opts passed to getCredentialOfferEndpoint() chore: add Sphereon E2E test as well as code from #63 to test potential issue chore: remove duplicate test feat: add sd-jwt support feat: Add optional QR code generation docs: Update README.md for code discrepancies docs: Update README.md for DIDDocument feat: update sd-jwt profile from oid4vci feat: Add initial support for creating a client without credential offer fix: Add back jwt_vc format support for older versions feat: EBSI compatibility chore: test fix chore: issuer type fix chore: test fixes feat: Add deferred credential support * chore: update lock file * fix(sd-jwt): cnf instead of kid * chore: missed to remove actual es-line line chore: missed to remove actual es-line line * Update VcIssuer.ts * chore: update ssi-types to 0.18 Signed-off-by: Timo Glastra <timo@animo.id> * feat: ldp issuance Signed-off-by: Timo Glastra <timo@animo.id> * feat: Support sd-jwt 0.2.0 library * chore: update lock file * chore: update lock file * fix: add sd-jwt to issuer callback Signed-off-by: Timo Glastra <timo@animo.id> * Update packages/client/lib/CredentialRequestClient.ts * feat: PKCE support improvements. Now you can omit PKCE code verifier/challenge params for authorization code flows. They will be generated automatically. Be aware the API of the createAuthorizationUrl method changed as a result. It now has a PKCE param * feat: PAR improvements The createAuthorizationRequestUrl now automatically handles PAR, instead of having a separate method. There is a new Param to determine whether PAR should be used automatically, never be used, or whether it is required. The latter is also set when the AS has a PAR required metadata value. As a result the createAuthorizationUrl method now is asynchronous * chore: Use calculated codeVerifier when acquiring the access token * feat: Allow to create an authorization request URL when initiating the OID4VCI client Including support for PAR, when initializing the client from an issuer, or when using a credential offer that supports an authorization code flow. Improvements to create authorization request URL in general. Please be aware that the API for this method has changed * chore: fix test * feat: Add support to get a client id from an offer, and from state JWTs. EBSI for instance is using this * chore: fix clientId from offer * chore: fix clientId from offer * chore: fix clientId from offer * chore: fix clientId from offer * chore: disable EBSI tests, because of timeout * chore: investigate req opts not properly filled * chore: investigate req opts not properly filled * chore: investigate req opts not properly filled * feature: Add support to pass in an authorization response to the access token request. The authorization response can either be a JSON or URI * feat: Make sure redirect_uri is the same for authorization and token endpoint when used and made redirect_uri optional. The redirect_uri is automatically passed to the token request in case one was used for authorization * fix: Fix uri to json conversion when no required params are provided * fix: Do not set a default redirect_uri, unless no authorization request options are set at all * fix: the client_id used in the auth request was not taken into account when requesting access token * fix: Do not sort credential types, as issuers might rely on their order * chore: Make sure the body debug log does not confuse people, by not JSON stringifying when the body is already a string. Was only used in logging, not in the actual payload * chore: Add a default clientId * fix: disable awesome-qr in rn Signed-off-by: Timo Glastra <timo@animo.id> * chore: v11 context fix * chore: v11 context fix * chore: allow to set clientId * chore: fix imports * feat: Allow to set the clientId at a later point on the VCI client * chore: Cleanup * fix: Do not set default client_id * feat: added state recovery * chore: move OpenID4VCIClient fields into state object * chore: constructor fixes * chore: test fixes * chore: addressing pr comments * chore: code cleanup * chore: code cleanup * chore: Disable E2E test because of infra issue * chore: PAR fixes --------- Signed-off-by: Timo Glastra <timo@animo.id> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Fendy Putra <fendy.putra@meeco.me> Co-authored-by: Timo Glastra <timo@animo.id> Co-authored-by: Ron Kreutzer <ron@rktechworks.com> Co-authored-by: A.G.J. Cate <brummos@gmail.com>
2 parents 535b3ca + 78abccf commit 61f0eb8

73 files changed

Lines changed: 3898 additions & 761 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-test-on-pr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ jobs:
2020
with:
2121
fetch-depth: 0
2222
- name: Use Node.js
23-
uses: actions/setup-node@v2
23+
uses: actions/setup-node@v4
2424
with:
25-
node-version: '16.x'
25+
node-version: '18.18.0'
2626
- uses: pnpm/action-setup@v2
2727
with:
2828
version: 8

.github/workflows/build-test-publish-on-push.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ jobs:
3434
with:
3535
fetch-depth: 0
3636
- name: Use Node.js
37-
uses: actions/setup-node@v2
37+
uses: actions/setup-node@v4
3838
with:
39-
node-version: '16.x'
39+
node-version: '18.18.0'
4040
- uses: pnpm/action-setup@v2
4141
with:
4242
version: 8

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"publish:unstable": "lerna publish --conventional-prerelease --force-publish --canary --no-git-tag-version --include-merged-tags --preid unstable --pre-dist-tag unstable --yes --registry https://registry.npmjs.org"
2121
},
2222
"engines": {
23-
"node": ">=16"
23+
"node": ">=18"
2424
},
2525
"resolutions": {
2626
"node-fetch": "2.6.12"
@@ -43,7 +43,7 @@
4343
"prettier": "^3.0.1",
4444
"rimraf": "^5.0.1",
4545
"ts-jest": "^29.1.1",
46-
"typescript": "4.9.5"
46+
"typescript": "5.3.3"
4747
},
4848
"keywords": [
4949
"Sphereon",

packages/callback-example/CHANGELOG.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
77

88
**Note:** Version bump only for package @sphereon/oid4vci-callback-example
99

10-
11-
12-
13-
1410
## [0.7.3](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.7.2...v0.7.3) (2023-09-30)
1511

1612
**Note:** Version bump only for package @sphereon/oid4vci-callback-example

packages/callback-example/lib/IssuerCallback.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { Ed25519VerificationKey2020 } from '@digitalcredentials/ed25519-verifica
44
import { securityLoader } from '@digitalcredentials/security-document-loader'
55
import vc from '@digitalcredentials/vc'
66
import { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common'
7-
import { ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types'
7+
import { CredentialIssuanceInput } from '@sphereon/oid4vci-issuer'
8+
import { CompactSdJwtVc, W3CVerifiableCredential } from '@sphereon/ssi-types'
89

910
// Example on how to generate a did:key to issue a verifiable credential
1011
export const generateDid = async () => {
@@ -14,12 +15,15 @@ export const generateDid = async () => {
1415
}
1516

1617
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17-
export const getIssuerCallback = (credential: ICredential, keyPair: any, verificationMethod: string) => {
18+
export const getIssuerCallback = (credential: CredentialIssuanceInput, keyPair: any, verificationMethod: string) => {
1819
if (!credential) {
1920
throw new Error('A credential needs to be provided')
2021
}
2122
// eslint-disable-next-line @typescript-eslint/no-unused-vars
22-
return async (_opts: { credentialRequest?: CredentialRequestV1_0_11; credential?: ICredential }): Promise<W3CVerifiableCredential> => {
23+
return async (_opts: {
24+
credentialRequest?: CredentialRequestV1_0_11
25+
credential?: CredentialIssuanceInput
26+
}): Promise<W3CVerifiableCredential | CompactSdJwtVc> => {
2327
const documentLoader = securityLoader().build()
2428
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2529
const verificationKey: any = Array.from(keyPair.values())[0]

packages/callback-example/lib/__tests__/issuerCallback.spec.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossess
44
import {
55
Alg,
66
CNonceState,
7-
CredentialOfferLdpVcV1_0_11,
87
CredentialSupported,
98
IssuerCredentialSubjectDisplay,
109
IssueStatus,
@@ -118,19 +117,24 @@ describe('issuerCallback', () => {
118117
credentialOffer: {
119118
credential_offer: {
120119
credential_issuer: 'did:key:test',
121-
credential_definition: {
122-
types: ['VerifiableCredential'],
123-
'@context': ['https://www.w3.org/2018/credentials/v1'],
124-
credentialSubject: {},
125-
},
120+
credentials: [
121+
{
122+
format: 'ldp_vc',
123+
credential_definition: {
124+
types: ['VerifiableCredential'],
125+
'@context': ['https://www.w3.org/2018/credentials/v1'],
126+
credentialSubject: {},
127+
},
128+
},
129+
],
126130
grants: {
127131
authorization_code: { issuer_state: 'test_code' },
128132
'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
129133
'pre-authorized_code': 'test_code',
130134
user_pin_required: true,
131135
},
132136
},
133-
} as CredentialOfferLdpVcV1_0_11,
137+
},
134138
},
135139
})
136140

@@ -163,7 +167,7 @@ describe('issuerCallback', () => {
163167
)
164168
.withCredentialSignerCallback((opts) =>
165169
Promise.resolve({
166-
...opts.credential,
170+
...(opts.credential as ICredential),
167171
proof: {
168172
type: IProofType.JwtProof2020,
169173
jwt: 'ye.ye.ye',

packages/callback-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@sphereon/oid4vci-client": "workspace:*",
1919
"@sphereon/oid4vci-common": "workspace:*",
2020
"@sphereon/oid4vci-issuer": "workspace:*",
21-
"@sphereon/ssi-types": "0.17.2",
21+
"@sphereon/ssi-types": "^0.18.1",
2222
"jose": "^4.10.0"
2323
},
2424
"devDependencies": {

packages/client/CHANGELOG.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
77

88
**Note:** Version bump only for package @sphereon/oid4vci-client
99

10-
11-
12-
13-
1410
## [0.7.3](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.7.2...v0.7.3) (2023-09-30)
1511

1612
**Note:** Version bump only for package @sphereon/oid4vci-client

packages/client/README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,10 @@ console.log(client.getAccessTokenEndpoint()); // https://auth.research.identipro
7272

7373
The OID4VCI Server metadata contains information about token endpoints, credential endpoints, as well as additional
7474
information about supported Credentials, and their cryptographic suites and formats.
75-
The code above already retrieved the metadata, so it will not be fetched again. If you however not used
75+
The code above already retrieved the metadata, so it will not be fetched again, and this method places the data in another variable. If you however have not used
7676
the `retrieveServerMetadata` option, you can use this method to fetch it from the Issuer:
7777

7878
```typescript
79-
import { OpenID4VCIClient } from '@sphereon/oid4vci-client';
80-
8179
const metadata = await client.retrieveServerMetadata();
8280
```
8381

@@ -111,6 +109,9 @@ the [Proof of Posession](#proof-of-possession) chapter for more information.
111109
The Proof of Possession using a signature callback function. The example uses the `jose` library.
112110

113111
```typescript
112+
import * as jose from 'jose';
113+
import { DIDDocument } from 'did-resolver';
114+
114115
const { privateKey, publicKey } = await jose.generateKeyPair('ES256');
115116

116117
// Must be JWS
@@ -121,10 +122,10 @@ async function signCallback(args: Jwt, kid: string): Promise<string> {
121122
.setIssuer(kid)
122123
.setAudience(args.payload.aud)
123124
.setExpirationTime('2h')
124-
.sign(keypair.privateKey);
125+
.sign(privateKey);
125126
}
126127

127-
const callbacks: ProofOfPossessionCallbacks = {
128+
const callbacks: ProofOfPossessionCallbacks<DIDDocument> = {
128129
signCallback,
129130
};
130131
```
@@ -133,14 +134,14 @@ Now it is time to get the actual credential
133134

134135
```typescript
135136
const credentialResponse = await client.acquireCredentials({
136-
credentialType: 'OpenBadgeCredential',
137+
credentialTypes: 'OpenBadgeCredential',
137138
proofCallbacks: callbacks,
138-
format: 'jwt_vc',
139+
format: 'jwt_vc_json',
139140
alg: Alg.ES256K,
140141
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21#keys-1',
141142
});
142143
console.log(credentialResponse.credential);
143-
// JWT format. (LDP/JSON-LD is also supported by the client)
144+
// JWT format. (LDP / JSON-LD ('ldp_vc' / 'jwt_vc_json-ld') is also supported by the client)
144145
// eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA
145146
```
146147

packages/client/lib/AccessTokenClient.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getIssuerFromCredentialOfferPayload,
1010
GrantTypes,
1111
IssuerOpts,
12+
JsonURIMode,
1213
OpenIDResponse,
1314
PRE_AUTH_CODE_LITERAL,
1415
TokenErrorResponse,
@@ -27,9 +28,11 @@ export class AccessTokenClient {
2728
public async acquireAccessToken(opts: AccessTokenRequestOpts): Promise<OpenIDResponse<AccessTokenResponse>> {
2829
const { asOpts, pin, codeVerifier, code, redirectUri, metadata } = opts;
2930

30-
const credentialOffer = await assertedUniformCredentialOffer(opts.credentialOffer);
31-
const isPinRequired = this.isPinRequiredValue(credentialOffer.credential_offer);
32-
const issuer = getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) ?? (metadata?.issuer as string);
31+
const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : undefined;
32+
const isPinRequired = credentialOffer && this.isPinRequiredValue(credentialOffer.credential_offer);
33+
const issuer =
34+
opts.credentialIssuer ??
35+
(credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : (metadata?.issuer as string));
3336
if (!issuer) {
3437
throw Error('Issuer required at this point');
3538
}
@@ -83,14 +86,14 @@ export class AccessTokenClient {
8386

8487
public async createAccessTokenRequest(opts: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
8588
const { asOpts, pin, codeVerifier, code, redirectUri } = opts;
86-
const credentialOfferRequest = await toUniformCredentialOfferRequest(opts.credentialOffer);
89+
const credentialOfferRequest = opts.credentialOffer ? await toUniformCredentialOfferRequest(opts.credentialOffer) : undefined;
8790
const request: Partial<AccessTokenRequest> = {};
8891

8992
if (asOpts?.clientId) {
9093
request.client_id = asOpts.clientId;
9194
}
9295

93-
if (credentialOfferRequest.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
96+
if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
9497
this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin);
9598
request.user_pin = pin;
9699

@@ -102,7 +105,7 @@ export class AccessTokenClient {
102105
return request as AccessTokenRequest;
103106
}
104107

105-
if (credentialOfferRequest.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW)) {
108+
if (!credentialOfferRequest || credentialOfferRequest.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW)) {
106109
request.grant_type = GrantTypes.AUTHORIZATION_CODE;
107110
request.code = code;
108111
request.redirect_uri = redirectUri;
@@ -174,14 +177,6 @@ export class AccessTokenClient {
174177
throw new Error('Authorization flow requires the code to be present');
175178
}
176179
}
177-
178-
private assertNonEmptyRedirectUri(accessTokenRequest: AccessTokenRequest): void {
179-
if (!accessTokenRequest.redirect_uri) {
180-
debug('No redirect_uri present, whilst it is required');
181-
throw new Error('Authorization flow requires the redirect_uri to be present');
182-
}
183-
}
184-
185180
private validate(accessTokenRequest: AccessTokenRequest, isPinRequired?: boolean): void {
186181
if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {
187182
this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type);
@@ -191,14 +186,13 @@ export class AccessTokenClient {
191186
this.assertAuthorizationGrantType(accessTokenRequest.grant_type);
192187
this.assertNonEmptyCodeVerifier(accessTokenRequest);
193188
this.assertNonEmptyCode(accessTokenRequest);
194-
this.assertNonEmptyRedirectUri(accessTokenRequest);
195189
} else {
196-
this.throwNotSupportedFlow;
190+
this.throwNotSupportedFlow();
197191
}
198192
}
199193

200194
private async sendAuthCode(requestTokenURL: string, accessTokenRequest: AccessTokenRequest): Promise<OpenIDResponse<AccessTokenResponse>> {
201-
return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest));
195+
return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }));
202196
}
203197

204198
public static determineTokenURL({
@@ -234,7 +228,9 @@ export class AccessTokenClient {
234228

235229
private static creatTokenURLFromURL(url: string, allowInsecureEndpoints?: boolean, tokenEndpoint?: string): string {
236230
if (allowInsecureEndpoints !== true && url.startsWith('http:')) {
237-
throw Error(`Unprotected token endpoints are not allowed ${url}. Adjust settings if you really need this (dev/test settings only!!)`);
231+
throw Error(
232+
`Unprotected token endpoints are not allowed ${url}. Use the 'allowInsecureEndpoints' param if you really need this for dev/testing!`,
233+
);
238234
}
239235
const hostname = url.replace(/https?:\/\//, '').replace(/\/$/, '');
240236
const endpoint = tokenEndpoint ? (tokenEndpoint.startsWith('/') ? tokenEndpoint : tokenEndpoint.substring(1)) : '/token';
@@ -243,7 +239,7 @@ export class AccessTokenClient {
243239
}
244240

245241
private throwNotSupportedFlow(): void {
246-
debug(`Only pre-authorized flow supported.`);
247-
throw new Error('Only pre-authorized-code flow is supported');
242+
debug(`Only pre-authorized or authorization code flows supported.`);
243+
throw new Error('Only pre-authorized-code or authorization code flows are supported');
248244
}
249245
}

0 commit comments

Comments
 (0)