Skip to content

Commit

Permalink
Merge pull request #66 from Sphereon-Opensource/develop
Browse files Browse the repository at this point in the history
New release
  • Loading branch information
nklomp authored Sep 28, 2023
2 parents bb9fcd4 + 214e3c6 commit 33a982e
Show file tree
Hide file tree
Showing 23 changed files with 2,182 additions and 2,508 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-test-on-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
node-version: '16.x'
- uses: pnpm/action-setup@v2
with:
version: 8.1.0
version: 8
- run: pnpm install
- run: pnpm build
- name: run CI tests
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/build-test-publish-on-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
branches:
- 'main'
- 'develop'
- 'fix/**'
- 'fixes/**'
- 'feature/**'
- 'feat/**'

Expand Down Expand Up @@ -37,7 +39,7 @@ jobs:
node-version: '16.x'
- uses: pnpm/action-setup@v2
with:
version: 8.1.0
version: 8
# - name: Get yarn cache directory path
# id: yarn-cache-dir-path
# run: echo "::set-output name=dir::$(yarn cache dir)"
Expand Down Expand Up @@ -87,6 +89,10 @@ jobs:
if: github.ref == 'refs/heads/develop'
run: pnpm publish:next

- name: publish @next when on fix
if: startsWith(github.ref, 'refs/heads/fix')
run: pnpm publish:next

- name: publish @unstable when on unstable branch
if: startsWith(github.ref, 'refs/heads/feat')
run: pnpm publish:unstable
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"engines": {
"node": ">=16"
},
"resolutions": {
"node-fetch": "2.6.12"
},
"prettier": {
"endOfLine": "auto",
"semi": false,
Expand Down
7 changes: 1 addition & 6 deletions packages/callback-example/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

# [0.7.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.6.0...v0.7.0) (2023-08-19)


### Features

* Integrate ssi-express-support to allow for future authn/authz. Also moved endpoints to functions, so solutions can include their own set of endpoints ([c749aba](https://github.com/Sphereon-Opensource/OID4VCI/commit/c749ababd4bec567d6aeeda49b76f195ec792201))




- Integrate ssi-express-support to allow for future authn/authz. Also moved endpoints to functions, so solutions can include their own set of endpoints ([c749aba](https://github.com/Sphereon-Opensource/OID4VCI/commit/c749ababd4bec567d6aeeda49b76f195ec792201))

# [0.6.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.4.0...v0.6.0) (2023-06-24)

Expand Down
10 changes: 2 additions & 8 deletions packages/client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,13 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

# [0.7.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.6.0...v0.7.0) (2023-08-19)


### Bug Fixes

* Revise well-known metadata retrieval for OID4VCI, OAuth 2.0 and OIDC. fixes [#62](https://github.com/Sphereon-Opensource/OID4VCI/issues/62) ([a750cc7](https://github.com/Sphereon-Opensource/OID4VCI/commit/a750cc76e084f12aeb58f2b1ac44b1bb5e69b5ae))

- Revise well-known metadata retrieval for OID4VCI, OAuth 2.0 and OIDC. fixes [#62](https://github.com/Sphereon-Opensource/OID4VCI/issues/62) ([a750cc7](https://github.com/Sphereon-Opensource/OID4VCI/commit/a750cc76e084f12aeb58f2b1ac44b1bb5e69b5ae))

### Features

* Integrate ssi-express-support to allow for future authn/authz. Also moved endpoints to functions, so solutions can include their own set of endpoints ([c749aba](https://github.com/Sphereon-Opensource/OID4VCI/commit/c749ababd4bec567d6aeeda49b76f195ec792201))




- Integrate ssi-express-support to allow for future authn/authz. Also moved endpoints to functions, so solutions can include their own set of endpoints ([c749aba](https://github.com/Sphereon-Opensource/OID4VCI/commit/c749ababd4bec567d6aeeda49b76f195ec792201))

# [0.6.0](https://github.com/Sphereon-Opensource/OID4VCI/compare/v0.4.0...v0.6.0) (2023-06-24)

Expand Down
4 changes: 3 additions & 1 deletion packages/client/lib/MetadataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ export class MetadataClient {
}
debug(`Issuer ${issuer} has ${authorizationServerType} Server metadata in well-known location`);
if (!authMetadata.authorization_endpoint) {
throw Error(`Authorization Sever ${authorization_server} did not provide an authorization_endpoint`);
console.warn(
`Issuer ${issuer} of type ${authorizationServerType} has no authorization_endpoint! Will use ${authorization_endpoint}. This only works for pre-authorized flows`,
);
} else if (authorization_endpoint && authMetadata.authorization_endpoint !== authorization_endpoint) {
throw Error(
`Credential issuer has a different authorization_endpoint (${authorization_endpoint}) from the Authorization Server (${authMetadata.authorization_endpoint})`,
Expand Down
83 changes: 16 additions & 67 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
CredentialOfferRequestWithBaseUrl,
CredentialResponse,
CredentialSupported,
EndpointMetadata,
EndpointMetadataResult,
OID4VCICredentialFormat,
OpenId4VCIVersion,
Expand Down Expand Up @@ -99,7 +98,7 @@ export class OpenID4VCIClient {
return client;
}

public async retrieveServerMetadata(): Promise<EndpointMetadata> {
public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
this.assertIssuerData();
if (!this._endpointMetadata) {
this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
Expand Down Expand Up @@ -366,79 +365,29 @@ export class OpenID4VCIClient {
return response.successBody;
}

getCredentialsSupported(restrictToInitiationTypes: boolean, supportedType?: string): CredentialSupported[] {
getCredentialsSupported(
restrictToInitiationTypes: boolean,
format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[],
): CredentialSupported[] {
return getSupportedCredentials({
issuerMetadata: this.endpointMetadata.credentialIssuerMetadata,
version: this.version(),
supportedType,
credentialTypes: restrictToInitiationTypes ? this.getCredentialTypes() : undefined,
format: format,
types: restrictToInitiationTypes ? this.getCredentialTypes() : undefined,
});
/*//FIXME: delegate to getCredentialsSupported from IssuerMetadataUtils
let credentialsSupported = this.endpointMetadata?.issuerMetadata?.credentials_supported
if (this.version() === OpenId4VCIVersion.VER_1_0_08 || typeof credentialsSupported === 'object') {
const issuerMetadata = this.endpointMetadata.issuerMetadata as IssuerMetadataV1_0_08
const v8CredentialsSupported = issuerMetadata.credentials_supported
credentialsSupported = []
credentialsSupported = Object.entries(v8CredentialsSupported).map((key, value) => )
}
if (!credentialsSupported) {
return []
} else if (!restrictToInitiationTypes) {
return credentialsSupported
}
/!**
* the following (not array part is a legacy code from version 1_0-08 which jff implementors used)
*!/
if (!Array.isArray(credentialsSupported)) {
const credentialsSupportedV8: CredentialSupportedV1_0_08 = credentialsSupported as CredentialSupportedV1_0_08;
const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes();
const supported: IssuerCredentialSubject = {};
for (const [key, value] of Object.entries(credentialsSupportedV8)) {
if (initiationTypes.includes(key)) {
supported[key] = value;
}
}
// todo: fix this later. we're returning CredentialSupportedV1_0_08 as a list of CredentialSupported (for v09 onward)
return supported as unknown as CredentialSupported[];
}
const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes()
const credentialSupportedOverlap: CredentialSupported[] = []
for (const supported of credentialsSupported) {
const supportedTypeOverlap: string[] = []
for (const type of supported.types) {
initiationTypes.includes(type)
supportedTypeOverlap.push(type)
}
if (supportedTypeOverlap.length > 0) {
credentialSupportedOverlap.push({
...supported,
types: supportedTypeOverlap
})
}
}
return credentialSupportedOverlap as CredentialSupported[]*/
}

getCredentialMetadata(type: string): CredentialSupported[] {
return this.getCredentialsSupported(false, type);
}

// todo https://sphereon.atlassian.net/browse/VDX-184
getCredentialTypes(): string[] {
getCredentialTypes(): string[][] {
if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
return typeof (this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type === 'string'
? [(this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type as string]
: ((this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type as string[]);
const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08;
const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type;
const result: string[][] = [];
result[0] = types;
return result;
} else {
// FIXME: this for sure isn't correct. It would also include VerifiableCredential. The whole call to this getCredentialsTypes should be changed to begin with
return this.credentialOffer.credential_offer.credentials.flatMap((c) => (typeof c === 'string' ? c : c.types));
return this.credentialOffer.credential_offer.credentials.map((c, index) => {
return typeof c === 'string' ? [c] : c.types;
});
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/__tests__/AccessTokenClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
AccessTokenResponse,
GrantTypes,
OpenIDResponse,
WellKnownEndpoints
} from '@sphereon/oid4vci-common'
WellKnownEndpoints,
} from '@sphereon/oid4vci-common';
import nock from 'nock';

import { AccessTokenClient } from '../AccessTokenClient';
Expand Down
5 changes: 3 additions & 2 deletions packages/client/lib/__tests__/IT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
CredentialOfferRequestWithBaseUrl,
Jwt,
OpenId4VCIVersion,
ProofOfPossession, WellKnownEndpoints
} from '@sphereon/oid4vci-common'
ProofOfPossession,
WellKnownEndpoints,
} from '@sphereon/oid4vci-common';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import nock from 'nock';
Expand Down
13 changes: 13 additions & 0 deletions packages/client/lib/__tests__/IssuanceInitiation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { OpenId4VCIVersion } from '@sphereon/oid4vci-common';

import { CredentialOfferClient } from '../CredentialOfferClient';

import { INITIATION_TEST, INITIATION_TEST_HTTPS_URI, INITIATION_TEST_URI } from './MetadataMocks';
Expand Down Expand Up @@ -45,4 +47,15 @@ describe('Issuance Initiation', () => {
const issuanceInitiationURI = INITIATION_TEST_HTTPS_URI.replace('?', '');
await expect(async () => CredentialOfferClient.fromURI(issuanceInitiationURI)).rejects.toThrowError('Invalid Credential Offer Request');
});

it('Should return Credential Offer', async () => {
const client = await CredentialOfferClient.fromURI(
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
);
expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11);
expect(client.baseUrl).toEqual('openid-credential-offer://');
expect(client.scheme).toEqual('openid-credential-offer');
expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io');
expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X');
});
});
106 changes: 106 additions & 0 deletions packages/client/lib/__tests__/MattrE2E.spec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Alg, AuthzFlowType, Jwt } from '@sphereon/oid4vci-common';
import { CredentialMapper } from '@sphereon/ssi-types';
import { fetch } from 'cross-fetch';
import { importJWK, JWK, SignJWT } from 'jose';

import { OpenID4VCIClient } from '..';

export const UNIT_TEST_TIMEOUT = 30000;

const ISSUER_URL = 'https://launchpad.vii.electron.mattrlabs.io';

const jwk: JWK = {
crv: 'Ed25519',
d: 'kTRm0aONHYwNPA-w_DtjMHUIWjE3K70qgCIhWojZ0eU',
x: 'NeA0d8sp86xRh3DczU4m5wPNIbl0HCSwOBcMN3sNmdk',
kty: 'OKP',
};

// pub hex: 35e03477cb29f3ac518770dccd4e26e703cd21b9741c24b038170c377b0d99d9
// priv hex: 913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5
const did = `did:key:z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
const kid = `${did}#z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
describe('OID4VCI-Client using Mattr issuer should', () => {
async function test(format: 'ldp_vc' | 'jwt_vc_json') {
const offer = await getCredentialOffer(format);
const client = await OpenID4VCIClient.fromURI({
uri: offer.offerUrl,
flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW,
kid,
alg: Alg.EdDSA,
});
expect(client.flowType).toEqual(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW);
expect(client.credentialOffer).toBeDefined();
expect(client.endpointMetadata).toBeDefined();
expect(client.getCredentialEndpoint()).toEqual(`${ISSUER_URL}/oidc/v1/auth/credential`);
expect(client.getAccessTokenEndpoint()).toEqual('https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token');

const accessToken = await client.acquireAccessToken();
// console.log(accessToken);
expect(accessToken).toMatchObject({
expires_in: 3600,
scope: 'OpenBadgeCredential',
token_type: 'Bearer',
});

const credentialResponse = await client.acquireCredentials({
credentialTypes: 'OpenBadgeCredential',
format,
proofCallbacks: {
signCallback: proofOfPossessionCallbackFunction,
},
});
expect(credentialResponse.credential).toBeDefined();
const wrappedVC = CredentialMapper.toWrappedVerifiableCredential(credentialResponse.credential!);
expect(format.startsWith(wrappedVC.format)).toEqual(true);
}

it(
'succeed in a full flow with the client using OpenID4VCI version 11 and ldp_vc',
async () => {
await test('ldp_vc');
},
UNIT_TEST_TIMEOUT,
);
it(
'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
async () => {
await test('jwt_vc_json');
},
UNIT_TEST_TIMEOUT,
);
});

interface CreateCredentialOfferResponse {
id: string;
offerUrl: string;
}

async function getCredentialOffer(format: 'ldp_vc' | 'jwt_vc_json'): Promise<CreateCredentialOfferResponse> {
const credentialOffer = await fetch('https://launchpad.mattrlabs.com/api/credential-offer', {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},

//make sure to serialize your JSON body
body: JSON.stringify({
format,
type: 'OpenBadgeCredential',
userId: '622a9f65-21c0-4c0b-9a6a-f7574c2a1549',
userAuthenticationRequired: false,
}),
});

return (await credentialOffer.json()) as CreateCredentialOfferResponse;
}

async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
const importedJwk = await importJWK(jwk, 'EdDSA');
return await new SignJWT({ ...args.payload })
.setProtectedHeader({ ...args.header })
.setIssuedAt()
.setExpirationTime('2h')
.sign(importedJwk);
}
Loading

0 comments on commit 33a982e

Please sign in to comment.