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/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index 5b669e39..bab1788f 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -4,10 +4,10 @@ import { AccessTokenResponse, assertedUniformCredentialOffer, AuthorizationServerOpts, + AuthzFlowType, EndpointMetadata, getIssuerFromCredentialOfferPayload, GrantTypes, - isPreAuthCode, IssuerOpts, OpenIDResponse, PRE_AUTH_CODE_LITERAL, @@ -67,6 +67,7 @@ export class AccessTokenClient { issuerOpts?: IssuerOpts; }): Promise> { this.validate(accessTokenRequest, isPinRequired); + const requestTokenURL = AccessTokenClient.determineTokenURL({ asOpts, issuerOpts, @@ -76,6 +77,7 @@ export class AccessTokenClient { ? await MetadataClient.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false }) : undefined, }); + return this.sendAuthCode(requestTokenURL, accessTokenRequest); } @@ -83,38 +85,36 @@ export class AccessTokenClient { const { asOpts, pin, codeVerifier, code, redirectUri } = opts; const credentialOfferRequest = await toUniformCredentialOfferRequest(opts.credentialOffer); const request: Partial = {}; + if (asOpts?.clientId) { request.client_id = asOpts.clientId; } - this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin); - request.user_pin = pin; + if (credentialOfferRequest.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) { + this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin); + request.user_pin = pin; - const isPreAuth = isPreAuthCode(credentialOfferRequest); - if (isPreAuth) { - if (codeVerifier) { - throw new Error('Cannot pass a code_verifier when flow type is pre-authorized'); - } request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE; // we actually know it is there because of the isPreAuthCode call request[PRE_AUTH_CODE_LITERAL] = credentialOfferRequest?.credential_offer.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.[PRE_AUTH_CODE_LITERAL]; + + return request as AccessTokenRequest; } - if (!isPreAuth && credentialOfferRequest.credential_offer.grants?.authorization_code?.issuer_state) { - this.throwNotSupportedFlow(); // not supported yet + + if (credentialOfferRequest.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW)) { request.grant_type = GrantTypes.AUTHORIZATION_CODE; - } - if (codeVerifier) { - request.code_verifier = codeVerifier; request.code = code; request.redirect_uri = redirectUri; - request.grant_type = GrantTypes.AUTHORIZATION_CODE; - } - if (request.grant_type === GrantTypes.AUTHORIZATION_CODE && isPreAuth) { - throw Error('A pre_authorized_code flow cannot have an issuer state in the credential offer'); + + if (codeVerifier) { + request.code_verifier = codeVerifier; + } + + return request as AccessTokenRequest; } - return request as AccessTokenRequest; + throw new Error('Credential offer request does not follow neither pre-authorized code nor authorization code flow requirements.'); } private assertPreAuthorizedGrantType(grantType: GrantTypes): void { diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 592d2dfd..53aa2eaa 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, @@ -11,7 +10,6 @@ import { EndpointMetadataResult, OID4VCICredentialFormat, OpenId4VCIVersion, - OpenIDResponse, ProofOfPossessionCallbacks, PushedAuthorizationResponse, ResponseType, @@ -39,7 +37,6 @@ interface AuthDetails { } interface AuthRequestOpts { - clientId: string; codeChallenge: string; codeChallengeMethod: CodeChallengeMethod; authorizationDetails?: AuthDetails | AuthDetails[]; @@ -48,7 +45,6 @@ interface AuthRequestOpts { } export class OpenID4VCIClient { - private readonly _flowType: AuthzFlowType; private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl; private _clientId?: string; private _kid: string | undefined; @@ -56,17 +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, - ) { - if (!credentialOffer.supportedFlows.includes(flowType)) { - throw Error(`Flows ${flowType} is not supported by issuer ${credentialOffer.credential_offer_uri}`); - } - this._flowType = flowType; + private constructor(credentialOffer: CredentialOfferRequestWithBaseUrl, kid?: string, alg?: Alg | string, clientId?: string) { this._credentialOffer = credentialOffer; this._kid = kid; this._alg = alg; @@ -75,7 +61,6 @@ export class OpenID4VCIClient { public static async fromURI({ uri, - flowType, kid, alg, retrieveServerMetadata, @@ -83,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(); @@ -106,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) { @@ -133,36 +110,40 @@ 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, code_challenge_method: codeChallengeMethod, code_challenge: codeChallenge, authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)), redirect_uri: redirectUri, scope: scope, - } as AuthorizationRequestV1_0_09; + }; + + if (this.clientId) { + queryObj['client_id'] = this.clientId; + } + + if (this.credentialOffer.issuerState) { + queryObj['issuer_state'] = this.credentialOffer.issuerState; + } return convertJsonToURI(queryObj, { baseUrl: this._endpointMetadata.authorization_endpoint, - uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details'], - version: this.version(), + uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'], }); } public async acquirePushedAuthorizationRequestURI({ - clientId, codeChallengeMethod, codeChallenge, authorizationDetails, redirectUri, scope, - }: AuthRequestOpts): Promise> { + }: AuthRequestOpts): Promise { // 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) { @@ -183,21 +164,36 @@ 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(' '); } - //fixme: handle this for v11 - const queryObj: AuthorizationRequestV1_0_09 = { + const queryObj: { [key: string]: string } = { response_type: ResponseType.AUTH_CODE, - client_id: clientId, code_challenge_method: codeChallengeMethod, code_challenge: codeChallenge, authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)), redirect_uri: redirectUri, scope: scope, }; - return await formPost(parEndpoint, JSON.stringify(queryObj)); + + if (this.clientId) { + queryObj['client_id'] = this.clientId; + } + + 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.credentialIssuerMetadata.authorization_endpoint, + uriTypeProperties: ['request_uri'], + }, + ); } public handleAuthorizationDetails(authorizationDetails?: AuthDetails | AuthDetails[]): AuthDetails | AuthDetails[] | undefined { @@ -237,7 +233,9 @@ export class OpenID4VCIClient { redirectUri?: string; }): Promise { const { pin, clientId, codeVerifier, code, redirectUri } = opts ?? {}; + this.assertIssuerData(); + if (clientId) { this._clientId = clientId; } @@ -300,24 +298,29 @@ export class OpenID4VCIClient { credentialOffer: this.credentialOffer, metadata: this.endpointMetadata, }); + requestBuilder.withTokenFromResponse(this.accessTokenResponse); if (this.endpointMetadata?.credentialIssuerMetadata) { const metadata = this.endpointMetadata.credentialIssuerMetadata; - const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes]; + const types = Array.isArray(credentialTypes) ? credentialTypes.sort() : [credentialTypes]; + if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) { - for (const type of types) { - let typeSupported = false; - for (const credentialSupported of metadata.credentials_supported) { - if (!credentialSupported.types || credentialSupported.types.length === 0) { - throw Error('types is required in the credentials supported'); - } - if (credentialSupported.types.indexOf(type) != -1) { - typeSupported = true; - } + let typeSupported = false; + + metadata.credentials_supported.forEach((supportedCredential) => { + if (!supportedCredential.types || supportedCredential.types.length === 0) { + throw Error('types is required in the credentials supported'); } - if (!typeSupported) { - throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`); + if ( + supportedCredential.types.sort().every((t, i) => types[i] === t) || + (types.length === 1 && (types[0] === supportedCredential.id || supportedCredential.types.includes(types[0]))) + ) { + typeSupported = true; } + }); + + if (!typeSupported) { + throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`); } } else if (metadata.credentials_supported && !Array.isArray(metadata.credentials_supported)) { const credentialsSupported = metadata.credentials_supported as CredentialSupportedTypeV1_0_08; @@ -389,16 +392,12 @@ export class OpenID4VCIClient { result[0] = types; return result; } else { - return this.credentialOffer.credential_offer.credentials.map((c, index) => { + return this.credentialOffer.credential_offer.credentials.map((c) => { return typeof c === 'string' ? [c] : c.types; }); } } - 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/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: {