Skip to content

Commit

Permalink
Remove flowType configuration, use clientId that was passed during cl…
Browse files Browse the repository at this point in the history
…ient initialization, update tests
  • Loading branch information
Linas Išganaitis committed Oct 6, 2023
1 parent 0e3c7e9 commit 52f715f
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 133 deletions.
1 change: 0 additions & 1 deletion packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ import { OpenID4VCIClient } from '@sphereon/oid4vci-client';
// The client is initiated from a URI. This URI is provided by the Issuer, typically as a URL or QR code.
const client = await OpenID4VCIClient.fromURI({
uri: 'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true',
flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW, // The flow to use
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21#key-1', // Our DID. You can defer this also to when the acquireCredential method is called
alg: Alg.ES256, // The signing Algorithm we will use. You can defer this also to when the acquireCredential method is called
clientId: 'test-clientId', // The clientId if the Authrozation Service requires it. If a clientId is needed you can defer this also to when the acquireAccessToken method is called
Expand Down
70 changes: 26 additions & 44 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
AccessTokenResponse,
Alg,
AuthorizationRequestV1_0_09,
AuthzFlowType,
CodeChallengeMethod,
CredentialOfferPayloadV1_0_08,
Expand Down Expand Up @@ -38,7 +37,6 @@ interface AuthDetails {
}

interface AuthRequestOpts {
clientId: string;
codeChallenge: string;
codeChallengeMethod: CodeChallengeMethod;
authorizationDetails?: AuthDetails | AuthDetails[];
Expand All @@ -47,22 +45,14 @@ interface AuthRequestOpts {
}

export class OpenID4VCIClient {
private readonly _flowType: AuthzFlowType;
private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl;
private _clientId?: string;
private _kid: string | undefined;
private _alg: Alg | string | undefined;
private _endpointMetadata: EndpointMetadataResult | undefined;
private _accessTokenResponse: AccessTokenResponse | undefined;

private constructor(
credentialOffer: CredentialOfferRequestWithBaseUrl,
flowType: AuthzFlowType,
kid?: string,
alg?: Alg | string,
clientId?: string,
) {
this._flowType = flowType;
private constructor(credentialOffer: CredentialOfferRequestWithBaseUrl, kid?: string, alg?: Alg | string, clientId?: string) {
this._credentialOffer = credentialOffer;
this._kid = kid;
this._alg = alg;
Expand All @@ -71,22 +61,20 @@ export class OpenID4VCIClient {

public static async fromURI({
uri,
flowType,
kid,
alg,
retrieveServerMetadata,
clientId,
resolveOfferUri,
}: {
uri: string;
flowType: AuthzFlowType;
kid?: string;
alg?: Alg | string;
retrieveServerMetadata?: boolean;
resolveOfferUri?: boolean;
clientId?: string;
}): Promise<OpenID4VCIClient> {
const client = new OpenID4VCIClient(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), flowType, kid, alg, clientId);
const client = new OpenID4VCIClient(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), kid, alg, clientId);

if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
await client.retrieveServerMetadata();
Expand All @@ -102,14 +90,7 @@ export class OpenID4VCIClient {
return this.endpointMetadata;
}

public createAuthorizationRequestUrl({
clientId,
codeChallengeMethod,
codeChallenge,
authorizationDetails,
redirectUri,
scope,
}: AuthRequestOpts): string {
public createAuthorizationRequestUrl({ codeChallengeMethod, codeChallenge, authorizationDetails, redirectUri, scope }: AuthRequestOpts): string {
// Scope and authorization_details can be used in the same authorization request
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
if (!scope && !authorizationDetails) {
Expand All @@ -129,31 +110,31 @@ export class OpenID4VCIClient {
}

// add 'openid' scope if not present
if (scope && !scope.includes('openid')) {
scope = `openid ${scope}`;
if (!scope?.includes('openid')) {
scope = ['openid', scope].filter((s) => !!s).join(' ');
}

//fixme: handle this for v11
const queryObj = {
const queryObj: { [key: string]: string } = {
response_type: ResponseType.AUTH_CODE,
client_id: clientId,
client_id: this.clientId || '',
code_challenge_method: codeChallengeMethod,
code_challenge: codeChallenge,
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
redirect_uri: redirectUri,
scope: scope,
issuer_state: this.credentialOffer.issuerState,
} as AuthorizationRequestV1_0_09;
};

if (this.credentialOffer.issuerState) {
queryObj['issuer_state'] = this.credentialOffer.issuerState;
}

return convertJsonToURI(queryObj, {
baseUrl: this._endpointMetadata.authorization_endpoint,
uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
version: this.version(),
});
}

public async acquirePushedAuthorizationRequestURI({
clientId,
codeChallengeMethod,
codeChallenge,
authorizationDetails,
Expand All @@ -180,29 +161,31 @@ export class OpenID4VCIClient {
const parEndpoint: string = this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint;

// add 'openid' scope if not present
if (scope && !scope.includes('openid')) {
scope = `openid ${scope}`;
if (!scope?.includes('openid')) {
scope = ['openid', scope].filter((s) => !!s).join(' ');
}

const queryObj = {
const queryObj: { [key: string]: string } = {
response_type: ResponseType.AUTH_CODE,
client_id: clientId,
client_id: this.clientId || '',
code_challenge_method: codeChallengeMethod,
code_challenge: codeChallenge,
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
redirect_uri: redirectUri,
scope: scope || 'openid',
issuer_state: this.credentialOffer.issuerState || '',
scope: scope,
};

if (this.credentialOffer.issuerState) {
queryObj['issuer_state'] = this.credentialOffer.issuerState;
}

const response = await formPost<PushedAuthorizationResponse>(parEndpoint, new URLSearchParams(queryObj));

return convertJsonToURI(
{ request_uri: response.successBody?.request_uri },
{
baseUrl: this._endpointMetadata.authorization_endpoint,
baseUrl: this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint,
uriTypeProperties: ['request_uri'],
version: this.version(),
},
);
}
Expand Down Expand Up @@ -322,7 +305,10 @@ export class OpenID4VCIClient {
if (!supportedCredential.types || supportedCredential.types.length === 0) {
throw Error('types is required in the credentials supported');
}
if (supportedCredential.types.sort().every((t, i) => types[i] === t) || (types.length === 1 && types[0] === supportedCredential.id)) {
if (
supportedCredential.types.sort().every((t, i) => types[i] === t) ||
(types.length === 1 && (types[0] === supportedCredential.id || supportedCredential.types.includes(types[0])))
) {
typeSupported = true;
}
});
Expand Down Expand Up @@ -406,10 +392,6 @@ export class OpenID4VCIClient {
}
}

get flowType(): AuthzFlowType {
return this._flowType;
}

issuerSupportedFlowTypes(): AuthzFlowType[] {
return this.credentialOffer.supportedFlows;
}
Expand Down
27 changes: 1 addition & 26 deletions packages/client/lib/__tests__/AccessTokenClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
AccessTokenRequest,
AccessTokenRequestOpts,
AccessTokenResponse,
GrantTypes,
OpenIDResponse,
WellKnownEndpoints,
} from '@sphereon/oid4vci-common';
import { AccessTokenRequest, AccessTokenResponse, GrantTypes, OpenIDResponse, WellKnownEndpoints } from '@sphereon/oid4vci-common';
import nock from 'nock';

import { AccessTokenClient } from '../AccessTokenClient';
Expand Down Expand Up @@ -204,24 +197,6 @@ describe('AccessTokenClient should', () => {
).rejects.toThrow(Error('Cannot set a pin, when the pin is not required.'));
});

it('get error if code_verifier is present when flow type is pre-authorized', async () => {
const accessTokenClient: AccessTokenClient = new AccessTokenClient();

nock(MOCK_URL).post(/.*/).reply(200, {});

const requestOpts: AccessTokenRequestOpts = {
credentialOffer: INITIATION_TEST,
pin: undefined,
codeVerifier: 'RylyWGQ-dzpObnEcoMBDIH9cTAwZXk1wYzktKxsOFgA',
code: 'LWCt225yj7gzT2cWeMP4hXj4B4oIYkEiGs4T6pfez91',
redirectUri: 'http://example.com/cb',
};

await expect(() => accessTokenClient.acquireAccessToken(requestOpts)).rejects.toThrow(
Error('Cannot pass a code_verifier when flow type is pre-authorized'),
);
});

it('get error if no as, issuer and metadata values are present', async () => {
await expect(() =>
AccessTokenClient.determineTokenURL({
Expand Down
4 changes: 0 additions & 4 deletions packages/client/lib/__tests__/IT.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
AccessTokenResponse,
Alg,
AuthzFlowType,
CredentialOfferRequestWithBaseUrl,
Jwt,
OpenId4VCIVersion,
Expand Down Expand Up @@ -72,7 +71,6 @@ describe('OID4VCI-Client should', () => {
succeedWithAFullFlowWithClientSetup();
const client = await OpenID4VCIClient.fromURI({
uri: INITIATE_QR,
flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW,
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
alg: Alg.ES256,
clientId: 'test-clientId',
Expand All @@ -84,7 +82,6 @@ describe('OID4VCI-Client should', () => {
succeedWithAFullFlowWithClientSetup();
const client = await OpenID4VCIClient.fromURI({
uri: OFFER_QR,
flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW,
kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1',
alg: Alg.ES256,
clientId: 'test-clientId',
Expand All @@ -93,7 +90,6 @@ describe('OID4VCI-Client should', () => {
});

async function assertionOfsucceedWithAFullFlowWithClient(client: OpenID4VCIClient) {
expect(client.flowType).toEqual(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW);
expect(client.credentialOffer).toBeDefined();
expect(client.endpointMetadata).toBeDefined();
expect(client.getIssuer()).toEqual('https://issuer.research.identiproof.io');
Expand Down
4 changes: 1 addition & 3 deletions packages/client/lib/__tests__/MattrE2E.spec.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alg, AuthzFlowType, Jwt } from '@sphereon/oid4vci-common';
import { Alg, Jwt } from '@sphereon/oid4vci-common';
import { CredentialMapper } from '@sphereon/ssi-types';
import { fetch } from 'cross-fetch';
import { importJWK, JWK, SignJWT } from 'jose';
Expand All @@ -25,11 +25,9 @@ describe('OID4VCI-Client using Mattr issuer should', () => {
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`);
Expand Down
15 changes: 4 additions & 11 deletions packages/client/lib/__tests__/OpenID4VCIClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthzFlowType, CodeChallengeMethod, WellKnownEndpoints } from '@sphereon/oid4vci-common';
import { CodeChallengeMethod, WellKnownEndpoints } from '@sphereon/oid4vci-common';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import nock from 'nock';
Expand All @@ -15,8 +15,8 @@ describe('OpenID4VCIClient should', () => {
nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(404, {});
nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {});
client = await OpenID4VCIClient.fromURI({
clientId: 'test-client',
uri: 'openid-initiate-issuance://?issuer=https://server.example.com&credential_type=TestCredential',
flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW,
});
});

Expand All @@ -29,7 +29,6 @@ describe('OpenID4VCIClient should', () => {
// @ts-ignore
client._endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;
const url = client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
scope: 'openid TestCredential',
Expand All @@ -44,7 +43,6 @@ describe('OpenID4VCIClient should', () => {
it('throw an error if authorization endpoint is not set in server metadata', async () => {
expect(() => {
client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
scope: 'openid TestCredential',
Expand All @@ -58,7 +56,6 @@ describe('OpenID4VCIClient should', () => {
client._endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;

const url = client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
scope: 'TestCredential',
Expand All @@ -77,7 +74,6 @@ describe('OpenID4VCIClient should', () => {

expect(() => {
client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
redirectUri: 'http://localhost:8881/cb',
Expand All @@ -91,7 +87,6 @@ describe('OpenID4VCIClient should', () => {

expect(
client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
authorizationDetails: [
Expand All @@ -112,7 +107,7 @@ describe('OpenID4VCIClient should', () => {
redirectUri: 'http://localhost:8881/cb',
}),
).toEqual(
'https://server.example.com/v1/auth/authorize?response_type=code&client_id=test-client&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb',
'https://server.example.com/v1/auth/authorize?response_type=code&client_id=test-client&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid',
);
});
it('create an authorization request url with authorization_details object property', async () => {
Expand All @@ -122,7 +117,6 @@ describe('OpenID4VCIClient should', () => {

expect(
client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
authorizationDetails: {
Expand All @@ -136,7 +130,7 @@ describe('OpenID4VCIClient should', () => {
redirectUri: 'http://localhost:8881/cb',
}),
).toEqual(
'https://server.example.com/v1/auth/authorize?response_type=code&client_id=test-client&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb',
'https://server.example.com/v1/auth/authorize?response_type=code&client_id=test-client&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid',
);
});
it('create an authorization request url with authorization_details and scope', async () => {
Expand All @@ -146,7 +140,6 @@ describe('OpenID4VCIClient should', () => {

expect(
client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
authorizationDetails: {
Expand Down
Loading

0 comments on commit 52f715f

Please sign in to comment.