diff --git a/packages/client/README.md b/packages/client/README.md index ff28f488..b1da2a32 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -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 diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 32d85172..fe90709a 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -1,7 +1,6 @@ import { AccessTokenResponse, Alg, - AuthorizationRequestV1_0_09, AuthzFlowType, CodeChallengeMethod, CredentialOfferPayloadV1_0_08, @@ -38,7 +37,6 @@ interface AuthDetails { } interface AuthRequestOpts { - clientId: string; codeChallenge: string; codeChallengeMethod: CodeChallengeMethod; authorizationDetails?: AuthDetails | AuthDetails[]; @@ -47,7 +45,6 @@ interface AuthRequestOpts { } export class OpenID4VCIClient { - private readonly _flowType: AuthzFlowType; private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl; private _clientId?: string; private _kid: string | undefined; @@ -55,14 +52,7 @@ export class OpenID4VCIClient { 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; @@ -71,7 +61,6 @@ export class OpenID4VCIClient { public static async fromURI({ uri, - flowType, kid, alg, retrieveServerMetadata, @@ -79,14 +68,13 @@ export class OpenID4VCIClient { resolveOfferUri, }: { uri: string; - flowType: AuthzFlowType; kid?: string; alg?: Alg | string; retrieveServerMetadata?: boolean; resolveOfferUri?: boolean; clientId?: string; }): Promise { - 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(); @@ -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) { @@ -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, @@ -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(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(), }, ); } @@ -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; } }); @@ -406,10 +392,6 @@ export class OpenID4VCIClient { } } - get flowType(): AuthzFlowType { - return this._flowType; - } - issuerSupportedFlowTypes(): AuthzFlowType[] { return this.credentialOffer.supportedFlows; } diff --git a/packages/client/lib/__tests__/AccessTokenClient.spec.ts b/packages/client/lib/__tests__/AccessTokenClient.spec.ts index 52b7345a..045b2148 100644 --- a/packages/client/lib/__tests__/AccessTokenClient.spec.ts +++ b/packages/client/lib/__tests__/AccessTokenClient.spec.ts @@ -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'; @@ -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({ diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index 74c3a741..00ca84ea 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -1,7 +1,6 @@ import { AccessTokenResponse, Alg, - AuthzFlowType, CredentialOfferRequestWithBaseUrl, Jwt, OpenId4VCIVersion, @@ -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', @@ -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', @@ -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'); diff --git a/packages/client/lib/__tests__/MattrE2E.spec.test.ts b/packages/client/lib/__tests__/MattrE2E.spec.test.ts index 70f5f5f0..9000831c 100644 --- a/packages/client/lib/__tests__/MattrE2E.spec.test.ts +++ b/packages/client/lib/__tests__/MattrE2E.spec.test.ts @@ -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'; @@ -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`); diff --git a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts index c3d9da59..4524f30e 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts @@ -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'; @@ -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, }); }); @@ -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', @@ -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', @@ -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', @@ -77,7 +74,6 @@ describe('OpenID4VCIClient should', () => { expect(() => { client.createAuthorizationRequestUrl({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', redirectUri: 'http://localhost:8881/cb', @@ -91,7 +87,6 @@ describe('OpenID4VCIClient should', () => { expect( client.createAuthorizationRequestUrl({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', authorizationDetails: [ @@ -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 () => { @@ -122,7 +117,6 @@ describe('OpenID4VCIClient should', () => { expect( client.createAuthorizationRequestUrl({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', authorizationDetails: { @@ -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 () => { @@ -146,7 +140,6 @@ describe('OpenID4VCIClient should', () => { expect( client.createAuthorizationRequestUrl({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', authorizationDetails: { diff --git a/packages/client/lib/__tests__/OpenID4VCIClientPAR.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientPAR.spec.ts index 8a7e8ee6..bad3da3c 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClientPAR.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClientPAR.spec.ts @@ -1,4 +1,4 @@ -import { AuthzFlowType, CodeChallengeMethod, WellKnownEndpoints } from '@sphereon/oid4vci-common'; +import { CodeChallengeMethod, WellKnownEndpoints } from '@sphereon/oid4vci-common'; import nock from 'nock'; import { OpenID4VCIClient } from '../OpenID4VCIClient'; @@ -13,8 +13,8 @@ describe('OpenID4VCIClient', () => { nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {}); nock(`${MOCK_URL}`).post('/v1/auth/par').reply(201, { request_uri: 'test_uri', expires_in: 90 }); client = await OpenID4VCIClient.fromURI({ + clientId: 'test-client', uri: 'openid-initiate-issuance://?issuer=https://server.example.com&credential_type=TestCredential', - flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW, }); }); @@ -24,20 +24,19 @@ describe('OpenID4VCIClient', () => { it('should successfully retrieve the authorization code using PAR', async () => { client.endpointMetadata.credentialIssuerMetadata!.pushed_authorization_request_endpoint = `${MOCK_URL}v1/auth/par`; + client.endpointMetadata.credentialIssuerMetadata!.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; const actual = await client.acquirePushedAuthorizationRequestURI({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', scope: 'openid TestCredential', redirectUri: 'http://localhost:8881/cb', }); - expect(actual.successBody).toEqual({ request_uri: 'test_uri', expires_in: 90 }); + expect(actual).toEqual('https://server.example.com/v1/auth/authorize?request_uri=test_uri'); }); it('should fail when pushed_authorization_request_endpoint is not present', async () => { await expect(() => client.acquirePushedAuthorizationRequestURI({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', scope: 'openid TestCredential', @@ -49,7 +48,6 @@ describe('OpenID4VCIClient', () => { it('should fail when authorization_details and scope are not present', async () => { await expect(() => client.acquirePushedAuthorizationRequestURI({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', redirectUri: 'http://localhost:8881/cb', @@ -59,8 +57,8 @@ describe('OpenID4VCIClient', () => { it('should not fail when only authorization_details is present', async () => { client.endpointMetadata.credentialIssuerMetadata!.pushed_authorization_request_endpoint = `${MOCK_URL}v1/auth/par`; + client.endpointMetadata.credentialIssuerMetadata!.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; const actual = await client.acquirePushedAuthorizationRequestURI({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', authorizationDetails: [ @@ -75,25 +73,25 @@ describe('OpenID4VCIClient', () => { ], redirectUri: 'http://localhost:8881/cb', }); - expect(actual.successBody).toEqual({ request_uri: 'test_uri', expires_in: 90 }); + expect(actual).toEqual('https://server.example.com/v1/auth/authorize?request_uri=test_uri'); }); it('should not fail when only scope is present', async () => { client.endpointMetadata.credentialIssuerMetadata!.pushed_authorization_request_endpoint = `${MOCK_URL}v1/auth/par`; + client.endpointMetadata.credentialIssuerMetadata!.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; const actual = await client.acquirePushedAuthorizationRequestURI({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', scope: 'openid TestCredential', redirectUri: 'http://localhost:8881/cb', }); - expect(actual.successBody).toEqual({ request_uri: 'test_uri', expires_in: 90 }); + expect(actual).toEqual('https://server.example.com/v1/auth/authorize?request_uri=test_uri'); }); it('should not fail when both authorization_details and scope are present', async () => { client.endpointMetadata.credentialIssuerMetadata!.pushed_authorization_request_endpoint = `${MOCK_URL}v1/auth/par`; + client.endpointMetadata.credentialIssuerMetadata!.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; const actual = await client.acquirePushedAuthorizationRequestURI({ - clientId: 'test-client', codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', authorizationDetails: [ @@ -109,6 +107,6 @@ describe('OpenID4VCIClient', () => { scope: 'openid TestCredential', redirectUri: 'http://localhost:8881/cb', }); - expect(actual.successBody).toEqual({ request_uri: 'test_uri', expires_in: 90 }); + expect(actual).toEqual('https://server.example.com/v1/auth/authorize?request_uri=test_uri'); }); }); diff --git a/packages/common/lib/functions/Encoding.ts b/packages/common/lib/functions/Encoding.ts index 3e789765..b84a918a 100644 --- a/packages/common/lib/functions/Encoding.ts +++ b/packages/common/lib/functions/Encoding.ts @@ -1,4 +1,4 @@ -import { BAD_PARAMS, DecodeURIAsJsonOpts, EncodeJsonAsURIOpts, SearchValue } from '../types'; +import { BAD_PARAMS, DecodeURIAsJsonOpts, EncodeJsonAsURIOpts, OpenId4VCIVersion, SearchValue } from '../types'; /** * @function encodeJsonAsURI encodes a Json object into a URI @@ -29,35 +29,39 @@ export function convertJsonToURI( return encodeURIComponent(key.replace(' ', '')); } - for (const [key, value] of Object.entries(json)) { - if (!value) { - continue; - } - //Skip properties that are not of URL type - if (!opts?.uriTypeProperties?.includes(key)) { - results.push(`${key}=${value}`); - continue; - } - if (opts?.arrayTypeProperties?.includes(key) && Array.isArray(value)) { - results.push(value.map((v) => `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(v, /\./g)}`).join('&')); - continue; - } - const isBool = typeof value == 'boolean'; - const isNumber = typeof value == 'number'; - const isString = typeof value == 'string'; - let encoded; - if (isBool || isNumber) { - encoded = `${encodeAndStripWhitespace(key)}=${value}`; - } else if (isString) { - encoded = `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(value, /\./g)}`; - } else { - encoded = `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(JSON.stringify(value), /\./g)}`; + let components: string; + if (opts?.version && opts.version > OpenId4VCIVersion.VER_1_0_08) { + // v11 changed from encoding every param to a encoded json object with a credential_offer param key + components = encodeAndStripWhitespace(JSON.stringify(json)); + } else { + for (const [key, value] of Object.entries(json)) { + if (!value) { + continue; + } + //Skip properties that are not of URL type + if (!opts?.uriTypeProperties?.includes(key)) { + results.push(`${key}=${value}`); + continue; + } + if (opts?.arrayTypeProperties?.includes(key) && Array.isArray(value)) { + results.push(value.map((v) => `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(v, /\./g)}`).join('&')); + continue; + } + const isBool = typeof value == 'boolean'; + const isNumber = typeof value == 'number'; + const isString = typeof value == 'string'; + let encoded; + if (isBool || isNumber) { + encoded = `${encodeAndStripWhitespace(key)}=${value}`; + } else if (isString) { + encoded = `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(value, /\./g)}`; + } else { + encoded = `${encodeAndStripWhitespace(key)}=${customEncodeURIComponent(JSON.stringify(value), /\./g)}`; + } + results.push(encoded); } - results.push(encoded); + components = results.join('&'); } - - const components = results.join('&'); - if (opts?.baseUrl) { if (opts.baseUrl.endsWith('=')) { if (opts.param) { diff --git a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts index 6364a7b6..c19316be 100644 --- a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts +++ b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts @@ -5,7 +5,6 @@ import { OpenID4VCIClient } from '@sphereon/oid4vci-client' import { AccessTokenResponse, Alg, - AuthzFlowType, CredentialOfferSession, CredentialSupported, IssuerCredentialSubjectDisplay, @@ -207,7 +206,6 @@ describe('VcIssuer', () => { it('should create client from credential offer URI', async () => { client = await OpenID4VCIClient.fromURI({ uri, - flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW, kid: subjectDIDKey.didDocument.authentication[0], alg: 'ES256', }) diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index 4ff68ce6..dd5d9e97 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -1,7 +1,6 @@ import { OpenID4VCIClient } from '@sphereon/oid4vci-client' import { Alg, - AuthzFlowType, CredentialOfferLdpVcV1_0_11, CredentialOfferSession, CredentialSupported, @@ -153,7 +152,7 @@ describe('VcIssuer', () => { 'http://issuer-example.com?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22previously-created-state%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22test_code%22%2C%22user_pin_required%22%3Atrue%7D%7D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%5D%2C%22credentialSubject%22%3A%7B%22given_name%22%3A%7B%22name%22%3A%22given%20name%22%2C%22locale%22%3A%22en-US%22%7D%7D%2C%22cryptographic_suites_supported%22%3A%5B%22ES256K%22%5D%2C%22cryptographic_binding_methods_supported%22%3A%5B%22did%22%5D%2C%22id%22%3A%22UniversityDegree_JWT%22%2C%22display%22%3A%5B%7B%22name%22%3A%22University%20Credential%22%2C%22locale%22%3A%22en-US%22%2C%22logo%22%3A%7B%22url%22%3A%22https%3A%2F%2Fexampleuniversity.com%2Fpublic%2Flogo.png%22%2C%22alt_text%22%3A%22a%20square%20logo%20of%20a%20university%22%7D%2C%22background_color%22%3A%22%2312107c%22%2C%22text_color%22%3A%22%23FFFFFF%22%7D%5D%7D%5D%7D', ) - const client = await OpenID4VCIClient.fromURI({ uri, flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW }) + const client = await OpenID4VCIClient.fromURI({ uri }) expect(client.credentialOffer).toEqual({ baseUrl: 'http://issuer-example.com', credential_offer: {