Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New release #66

Merged
merged 18 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading