diff --git a/packages/client/lib/__tests__/SdJwt.spec.ts b/packages/client/lib/__tests__/SdJwt.spec.ts index 3d58e94a..2e1a8e03 100644 --- a/packages/client/lib/__tests__/SdJwt.spec.ts +++ b/packages/client/lib/__tests__/SdJwt.spec.ts @@ -85,7 +85,7 @@ describe('sd-jwt vc', () => { nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/oauth-authorization-server').reply(404); expect(offerUri.uri).toEqual( - 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22123%22%2C%22tx_code%22%3A%7B%22input_mode%22%3A%22text%22%2C%22length%22%3A3%7D%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22SdJwtCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%22%7D', + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%22%2C%22credential_configuration_ids%22%3A%5B%22SdJwtCredential%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22tx_code%22%3A%7B%22input_mode%22%3A%22text%22%2C%22length%22%3A3%7D%2C%22pre-authorized_code%22%3A%22123%22%7D%7D%7D', ); const client = await OpenID4VCIClientV1_0_13.fromURI({ @@ -188,7 +188,7 @@ describe('sd-jwt vc', () => { nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/oauth-authorization-server').reply(404); expect(offerUri.uri).toEqual( - 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22123%22%2C%22tx_code%22%3A%7B%22input_mode%22%3A%22text%22%2C%22length%22%3A3%7D%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22SdJwtCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%22%7D', + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%22%2C%22credential_configuration_ids%22%3A%5B%22SdJwtCredential%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22tx_code%22%3A%7B%22input_mode%22%3A%22text%22%2C%22length%22%3A3%7D%2C%22pre-authorized_code%22%3A%22123%22%7D%7D%7D', ); const client = await OpenID4VCIClientV1_0_13.fromURI({ diff --git a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts index 22756dba..307d5cf8 100644 --- a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts +++ b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts @@ -205,7 +205,7 @@ describe('VcIssuer', () => { }) .then((response) => response.uri) expect(uri).toEqual( - 'http://localhost:3456/test?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%22tx_code%22%3A%7B%22input_mode%22%3A%22text%22%2C%22length%22%3A4%7D%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22UniversityDegree_JWT%22%5D%2C%22credential_issuer%22%3A%22http%3A%2F%2Flocalhost%3A3456%2Ftest%22%7D', + 'http://localhost:3456/test?credential_offer=%7B%22credential_issuer%22%3A%22http%3A%2F%2Flocalhost%3A3456%2Ftest%22%2C%22credential_configuration_ids%22%3A%5B%22UniversityDegree_JWT%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22test_code%22%2C%22tx_code%22%3A%7B%22input_mode%22%3A%22text%22%2C%22length%22%3A4%7D%7D%2C%22authorization_code%22%3A%7B%22issuer_state%22%3A%22previously-created-state%22%7D%7D%7D', ) }) diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index e9d6eb5b..6cf9ec3f 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -8,14 +8,12 @@ import { CredentialConfigurationSupportedV1_0_13, CredentialDataSupplierInput, CredentialIssuerMetadata, - CredentialOfferPayloadV1_0_13, CredentialOfferSession, CredentialOfferV1_0_13, CredentialRequest, CredentialRequestV1_0_13, CredentialResponse, DID_NO_DIDDOC_ERROR, - Grant, IAT_ERROR, ISSUER_CONFIG_ERROR, IssueStatus, @@ -29,7 +27,6 @@ import { NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT, OID4VCICredentialFormat, OpenId4VCIVersion, - PRE_AUTH_CODE_LITERAL, PRE_AUTH_GRANT_LITERAL, QRCodeOpts, TokenErrorResponse, @@ -42,7 +39,7 @@ import { CredentialEventNames, CredentialOfferEventNames, EVENTS } from '@sphere import { CredentialIssuerMetadataOptsV1_0_13 } from '@sphereon/oid4vci-common' import { CompactSdJwtVc, CredentialMapper, InitiatorType, SubSystem, System, W3CVerifiableCredential } from '@sphereon/ssi-types' -import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject } from './functions' +import { assertValidPinNumber, createCredentialOfferObject, createCredentialOfferURIFromObject, CredentialOfferGrantInput } from './functions' import { LookupStateManager } from './state-manager' import { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialIssuanceInput, CredentialSignerCallback } from './types' @@ -92,7 +89,7 @@ export class VcIssuer { } public async createCredentialOfferURI(opts: { - grants?: Grant + grants?: CredentialOfferGrantInput credential_configuration_ids?: Array credentialDefinition?: JsonLdIssuerCredentialDefinition credentialOfferUri?: string @@ -102,58 +99,39 @@ export class VcIssuer { pinLength?: number qrCodeOpts?: QRCodeOpts }): Promise { - let preAuthorizedCode: string | undefined = undefined - let issuerState: string | undefined = undefined - const { grants, credential_configuration_ids } = opts - - if (!grants?.authorization_code && !grants?.[PRE_AUTH_GRANT_LITERAL]) { - throw Error(`No grant issuer state or pre-authorized code could be deduced`) - } - const credentialOfferPayload: CredentialOfferPayloadV1_0_13 = { - ...(grants && { grants }), - ...(credential_configuration_ids && { credential_configuration_ids: credential_configuration_ids ?? [] }), - credential_issuer: this.issuerMetadata.credential_issuer, - } as CredentialOfferPayloadV1_0_13 - if (grants?.authorization_code) { - issuerState = grants?.authorization_code.issuer_state - if (!issuerState) { - issuerState = uuidv4() - grants.authorization_code.issuer_state = issuerState - } - } - - let txCode: TxCode | undefined - if (grants?.[PRE_AUTH_GRANT_LITERAL]) { - preAuthorizedCode = grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL] - txCode = grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code - - if (txCode !== undefined) { - if (!txCode?.length) { - txCode.length = opts.pinLength ?? 4 - } - grants[PRE_AUTH_GRANT_LITERAL].tx_code = txCode - } - if (!preAuthorizedCode) { - preAuthorizedCode = uuidv4() - grants[PRE_AUTH_GRANT_LITERAL][PRE_AUTH_CODE_LITERAL] = preAuthorizedCode + const { credential_configuration_ids } = opts + + const grants = opts.grants ? { ...opts.grants } : {} + // for backwards compat, would be better if user sets the prop on the grants directly + if (opts.pinLength !== undefined) { + const preAuth = grants[PRE_AUTH_GRANT_LITERAL] + if (preAuth && preAuth.tx_code) { + preAuth.tx_code.length = opts.pinLength } } const baseUri = opts?.baseUri ?? this.defaultCredentialOfferBaseUri - const credentialOfferObject = createCredentialOfferObject(this._issuerMetadata, { ...opts, - txCode, - credentialOffer: credentialOfferPayload, - baseUri, - preAuthorizedCode, - issuerState, + grants, + credentialOffer: credential_configuration_ids + ? { + credential_issuer: this._issuerMetadata.credential_issuer, + credential_configuration_ids, + } + : undefined, }) + const preAuthGrant = credentialOfferObject.credential_offer.grants?.[PRE_AUTH_GRANT_LITERAL] + const authGrant = credentialOfferObject.credential_offer.grants?.authorization_code + + const preAuthorizedCode = preAuthGrant?.['pre-authorized_code'] + const issuerState = authGrant?.issuer_state + const txCode = preAuthGrant?.tx_code + let userPin: string | undefined - // todo: Double check this can only happen in pre-auth flow and if so make sure to not do the below when in a state is present (authorized flow) - if (txCode) { - const pinLength = txCode.length ?? opts.pinLength ?? 4 + if (preAuthGrant?.tx_code) { + const pinLength = preAuthGrant.tx_code.length ?? 4 userPin = ('' + Math.round((Math.pow(10, pinLength) - 1) * Math.random())).padStart(pinLength, '0') assertValidPinNumber(userPin, pinLength) diff --git a/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts b/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts index 76a584f1..fd03459b 100644 --- a/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts +++ b/packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts @@ -1,6 +1,6 @@ -import { CredentialOfferPayloadV1_0_11, CredentialOfferPayloadV1_0_13 } from '@sphereon/oid4vci-common' +import { CredentialOfferPayloadV1_0_11, CredentialOfferPayloadV1_0_13, PRE_AUTH_CODE_LITERAL, PRE_AUTH_GRANT_LITERAL } from '@sphereon/oid4vci-common' -import { createCredentialOfferURI, createCredentialOfferURIv1_0_11 } from '../index' +import { createCredentialOfferObject, createCredentialOfferURI, createCredentialOfferURIv1_0_11 } from '../index' describe('CredentialOfferUtils should', () => { it('create a deeplink from credentialOffer object', () => { @@ -14,12 +14,57 @@ describe('CredentialOfferUtils should', () => { issuer_state: 'eyJhbGciOiJSU0Et...FYUaBy', }, }, - } as CredentialOfferPayloadV1_0_13 - expect(createCredentialOfferURI(undefined, { credentialOffer, state: 'eyJhbGciOiJSU0Et...FYUaBy' })).toEqual( + } satisfies CredentialOfferPayloadV1_0_13 + expect(createCredentialOfferURI(undefined, { credentialOffer })).toEqual( 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%22%2C%22credential_configuration_ids%22%3A%5B%22UniversityDegreeCredential%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D', ) }) + it('create a v13 credential offer with grants', () => { + const credentialOffer = { + credential_issuer: 'https://credential-issuer.example.com', + credential_configuration_ids: ['UniversityDegreeCredential'], + grants: { + authorization_code: { + issuer_state: 'eyJhbGciOiJSU0Et...FYUaBy', + }, + }, + } satisfies CredentialOfferPayloadV1_0_13 + expect( + createCredentialOfferObject(undefined, { + credentialOfferUri: 'https://test.com', + credentialOffer: { + credential_configuration_ids: ['one'], + credential_issuer: credentialOffer.credential_issuer, + }, + grants: { + authorization_code: { + authorization_server: 'https://test.com', + }, + [PRE_AUTH_GRANT_LITERAL]: { + authorization_server: 'https://test.com', + }, + }, + }), + ).toEqual({ + credential_offer: { + credential_configuration_ids: ['one'], + credential_issuer: 'https://credential-issuer.example.com', + grants: { + authorization_code: { + authorization_server: 'https://test.com', + issuer_state: expect.any(String), + }, + [PRE_AUTH_GRANT_LITERAL]: { + [PRE_AUTH_CODE_LITERAL]: expect.any(String), + authorization_server: 'https://test.com', + }, + }, + }, + credential_offer_uri: 'https://test.com', + }) + }) + it('create an https link from credentialOffer object', () => { // below is the example from spec (https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-11.html#name-sending-credential-offer-by) and is wrong, the issuer_state should be in the grants and not a top-level property // https://credential-issuer.example.com?credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D @@ -47,7 +92,7 @@ describe('CredentialOfferUtils should', () => { issuer: 'test_issuer', credentials_supported: [], }, - { credentialOffer, state: 'eyJhbGciOiJSU0Et...FYUaBy', scheme: 'https' }, + { credentialOffer, scheme: 'https' }, ), ).toEqual( `${credentialOffer.credential_issuer}?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fcredential-issuer.example.com%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D`, @@ -80,7 +125,7 @@ describe('CredentialOfferUtils should', () => { issuer: 'test_issuer', credentials_supported: [], }, - { credentialOffer, state: 'eyJhbGciOiJSU0Et...FYUaBy', scheme: 'http' }, + { credentialOffer, scheme: 'http' }, ), ).toEqual( `${credentialOffer.credential_issuer}?credential_offer=%7B%22credential_issuer%22%3A%22http%3A%2F%2Fcredential-issuer.example.com%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D`, diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index cfd07f2e..1e8ff5d5 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -285,7 +285,7 @@ describe('VcIssuer', () => { credentialOfferUri: 'https://somehost.com/offer-id', }) .then((response) => response.uri), - ).resolves.toEqual('http://issuer-example.com?credential_offer_uri=https://somehost.com/offer-id') + ).resolves.toEqual('http://issuer-example.com?credential_offer_uri=https%3A%2F%2Fsomehost.com%2Foffer-id') }) // Of course this doesn't work. The state is part of the proof to begin with diff --git a/packages/issuer/lib/functions/CredentialOfferUtils.ts b/packages/issuer/lib/functions/CredentialOfferUtils.ts index eb106eb9..869cdebb 100644 --- a/packages/issuer/lib/functions/CredentialOfferUtils.ts +++ b/packages/issuer/lib/functions/CredentialOfferUtils.ts @@ -1,59 +1,98 @@ import { uuidv4 } from '@sphereon/oid4vc-common' import { + AssertedUniformCredentialOffer, CredentialIssuerMetadataOpts, CredentialIssuerMetadataOptsV1_0_13, CredentialIssuerMetadataV1_0_11, CredentialOfferPayloadV1_0_11, CredentialOfferPayloadV1_0_13, CredentialOfferSession, - CredentialOfferV1_0_11, CredentialOfferV1_0_13, Grant, + GrantAuthorizationCode, GrantUrnIetf, IssuerMetadataV1_0_13, PIN_NOT_MATCH_ERROR, PRE_AUTH_GRANT_LITERAL, - TxCode, UniformCredentialOffer, } from '@sphereon/oid4vci-common' +export interface CredentialOfferGrantInput { + authorization_code?: Partial + [PRE_AUTH_GRANT_LITERAL]?: Partial +} + +function createCredentialOfferGrants(inputGrants?: CredentialOfferGrantInput) { + // Grants is optional + if (!inputGrants || Object.keys(inputGrants).length === 0) { + return undefined + } + + const grants: Grant = {} + if (inputGrants?.[PRE_AUTH_GRANT_LITERAL]) { + const grant = { + ...inputGrants[PRE_AUTH_GRANT_LITERAL], + 'pre-authorized_code': inputGrants[PRE_AUTH_GRANT_LITERAL]['pre-authorized_code'] ?? uuidv4(), + } + + if (grant.tx_code && !grant.tx_code.length) { + grant.tx_code.length = 4 + } + + grants[PRE_AUTH_GRANT_LITERAL] = grant + } + + if (inputGrants?.authorization_code) { + grants.authorization_code = { + ...inputGrants.authorization_code, + + // TODO: it should be possible to create offer without issuer_state + // this is added to avoid breaking changes. + issuer_state: inputGrants.authorization_code.issuer_state ?? uuidv4(), + } + } + + return grants +} + +function parseCredentialOfferSchemeAndBaseUri(scheme?: string, baseUri?: string, credentialIssuer?: string): { scheme: string; baseUri: string } { + const newScheme = scheme?.replace('://', '') ?? (baseUri?.includes('://') ? baseUri.split('://')[0] : 'openid-credential-offer') + let newBaseUri: string + + if (baseUri) { + newBaseUri = baseUri + } else if (newScheme.startsWith('http')) { + if (credentialIssuer) { + newBaseUri = credentialIssuer + if (!newBaseUri.startsWith(`${newScheme}://`)) { + throw Error(`scheme ${newScheme} is different from base uri ${newBaseUri}`) + } + } else { + throw Error(`A '${newScheme}' scheme requires a URI to be present as baseUri`) + } + } else { + newBaseUri = '' + } + newBaseUri = newBaseUri?.replace(`${newScheme}://`, '') + + return { scheme: newScheme, baseUri: newBaseUri } +} + export function createCredentialOfferObject( issuerMetadata?: CredentialIssuerMetadataOptsV1_0_13, // todo: probably it's wise to create another builder for CredentialOfferPayload that will generate different kinds of CredentialOfferPayload opts?: { credentialOffer?: CredentialOfferPayloadV1_0_13 credentialOfferUri?: string - scheme?: string - baseUri?: string - issuerState?: string - grants?: Grant - txCode?: TxCode - preAuthorizedCode?: string + grants?: CredentialOfferGrantInput }, -): CredentialOfferV1_0_13 & { scheme: string; grants: Grant; baseUri: string } { +): AssertedUniformCredentialOffer { if (!issuerMetadata && !opts?.credentialOffer && !opts?.credentialOfferUri) { throw new Error('You have to provide issuerMetadata or credentialOffer object for creating a deeplink') } - const scheme = opts?.scheme?.replace('://', '') ?? (opts?.baseUri?.includes('://') ? opts.baseUri.split('://')[0] : 'openid-credential-offer') - let baseUri: string - if (opts?.baseUri) { - baseUri = opts.baseUri - } else if (scheme.startsWith('http')) { - if (issuerMetadata?.credential_issuer) { - baseUri = issuerMetadata?.credential_issuer - if (!baseUri.startsWith(`${scheme}://`)) { - throw Error(`scheme ${scheme} is different from base uri ${baseUri}`) - } - } else { - throw Error(`A '${scheme}' scheme requires a URI to be present as baseUri`) - } - } else { - baseUri = '' - } - baseUri = baseUri.replace(`${scheme}://`, '') + const grants = createCredentialOfferGrants(opts?.grants) - const credential_offer_uri = opts?.credentialOfferUri ? `${scheme}://${baseUri}?credential_offer_uri=${opts?.credentialOfferUri}` : undefined let credential_offer: CredentialOfferPayloadV1_0_13 if (opts?.credentialOffer) { credential_offer = { @@ -64,43 +103,17 @@ export function createCredentialOfferObject( throw new Error('credential_configurations_supported is mandatory in the metadata') } credential_offer = { - credential_issuer: issuerMetadata?.credential_issuer, - credential_configuration_ids: Object.keys(issuerMetadata?.credential_configurations_supported), - } - } - if (!credential_offer.grants) { - credential_offer.grants = {} - } - if (opts?.preAuthorizedCode) { - credential_offer.grants[PRE_AUTH_GRANT_LITERAL] = { - 'pre-authorized_code': opts.preAuthorizedCode, - tx_code: ((opts.grants as Grant)?.[PRE_AUTH_GRANT_LITERAL] as GrantUrnIetf).tx_code ?? undefined, - } - } else if (!credential_offer.grants?.authorization_code?.issuer_state) { - credential_offer.grants = { - authorization_code: { - issuer_state: opts?.issuerState ?? uuidv4(), - }, + credential_issuer: issuerMetadata.credential_issuer, + credential_configuration_ids: Object.keys(issuerMetadata.credential_configurations_supported), } } - // todo: check payload against issuer metadata. Especially strings in the credentials array: When processing, the Wallet MUST resolve this string value to the respective object. - if (!credential_offer.grants) { - credential_offer.grants = {} - } - if (opts?.preAuthorizedCode) { - credential_offer.grants[PRE_AUTH_GRANT_LITERAL] = { - 'pre-authorized_code': opts.preAuthorizedCode, - tx_code: opts.txCode, - } - } else if (!credential_offer.grants?.authorization_code?.issuer_state) { - credential_offer.grants = { - authorization_code: { - issuer_state: opts?.issuerState ?? uuidv4(), - }, - } + if (grants) { + credential_offer.grants = grants } - return { credential_offer, credential_offer_uri, scheme, baseUri, grants: credential_offer.grants } + + // todo: check payload against issuer metadata. Especially strings in the credentials array: When processing, the Wallet MUST resolve this string value to the respective object. + return { credential_offer, credential_offer_uri: opts?.credentialOfferUri } } export function createCredentialOfferObjectv1_0_11( @@ -111,83 +124,54 @@ export function createCredentialOfferObjectv1_0_11( credentialOfferUri?: string scheme?: string baseUri?: string - issuerState?: string - preAuthorizedCode?: string - userPinRequired?: boolean + grants?: CredentialOfferGrantInput }, -): CredentialOfferV1_0_11 & { scheme: string; grants: Grant; baseUri: string } { +): AssertedUniformCredentialOffer { if (!issuerMetadata && !opts?.credentialOffer && !opts?.credentialOfferUri) { throw new Error('You have to provide issuerMetadata or credentialOffer object for creating a deeplink') } - const scheme = opts?.scheme?.replace('://', '') ?? (opts?.baseUri?.includes('://') ? opts.baseUri.split('://')[0] : 'openid-credential-offer') - let baseUri: string - if (opts?.baseUri) { - baseUri = opts.baseUri - } else if (scheme.startsWith('http')) { - if (issuerMetadata?.credential_issuer) { - baseUri = issuerMetadata?.credential_issuer - if (!baseUri.startsWith(`${scheme}://`)) { - throw Error(`scheme ${scheme} is different from base uri ${baseUri}`) - } - } else { - throw Error(`A '${scheme}' scheme requires a URI to be present as baseUri`) + // v13 to v11 grant + const grants = createCredentialOfferGrants(opts?.grants) + if (grants?.[PRE_AUTH_GRANT_LITERAL]?.tx_code) { + const { tx_code, ...rest } = grants[PRE_AUTH_GRANT_LITERAL] + grants[PRE_AUTH_GRANT_LITERAL] = { + user_pin_required: true, + ...rest, } - } else { - baseUri = '' } - baseUri = baseUri.replace(`${scheme}://`, '') - const credential_offer_uri = opts?.credentialOfferUri ? `${scheme}://${baseUri}?credential_offer_uri=${opts?.credentialOfferUri}` : undefined let credential_offer: CredentialOfferPayloadV1_0_11 if (opts?.credentialOffer) { credential_offer = { ...opts.credentialOffer, - credentials: opts.credentialOffer?.credentials ?? issuerMetadata?.credentials_supported, + credentials: + opts.credentialOffer?.credentials ?? issuerMetadata?.credentials_supported.map((s) => s.id).filter((i): i is string => i !== undefined), } } else { - credential_offer = { - credential_issuer: issuerMetadata?.credential_issuer, - credentials: issuerMetadata?.credentials_supported, - } as CredentialOfferPayloadV1_0_11 - } - // todo: check payload against issuer metadata. Especially strings in the credentials array: When processing, the Wallet MUST resolve this string value to the respective object. - - if (!credential_offer.grants) { - credential_offer.grants = {} - } - if (opts?.preAuthorizedCode) { - credential_offer.grants[PRE_AUTH_GRANT_LITERAL] = { - 'pre-authorized_code': opts.preAuthorizedCode, - user_pin_required: opts.userPinRequired ?? false, + if (!issuerMetadata) { + throw new Error('Issuer metadata is required when no credential offer is provided') } - } else if (!credential_offer.grants?.authorization_code?.issuer_state) { - credential_offer.grants = { - authorization_code: { - issuer_state: opts?.issuerState ?? uuidv4(), - }, + credential_offer = { + credential_issuer: issuerMetadata.credential_issuer, + credentials: issuerMetadata?.credentials_supported.map((s) => s.id).filter((i): i is string => i !== undefined), } } - return { credential_offer, credential_offer_uri, scheme, baseUri, grants: credential_offer.grants } + + return { credential_offer, credential_offer_uri: opts?.credentialOfferUri } } export function createCredentialOfferURIFromObject( - credentialOffer: (CredentialOfferV1_0_13 | UniformCredentialOffer) & { scheme?: string; baseUri?: string; grant?: Grant }, + credentialOffer: CredentialOfferV1_0_13 | UniformCredentialOffer, opts?: { scheme?: string; baseUri?: string }, ) { - const scheme = opts?.scheme?.replace('://', '') ?? credentialOffer?.scheme?.replace('://', '') ?? 'openid-credential-offer' - let baseUri = opts?.baseUri ?? credentialOffer?.baseUri ?? '' - if (baseUri.includes('://')) { - baseUri = baseUri.split('://')[1] - } - if (scheme.startsWith('http') && baseUri === '') { - throw Error(`Cannot use scheme '${scheme}' without providing a baseUri value`) - } + const { scheme, baseUri } = parseCredentialOfferSchemeAndBaseUri(opts?.scheme, opts?.baseUri, credentialOffer.credential_offer?.credential_issuer) + if (credentialOffer.credential_offer_uri) { if (credentialOffer.credential_offer_uri.includes('credential_offer_uri=')) { // discard the scheme. Apparently a URI is set and it already contains the actual uri, so assume that takes priority return credentialOffer.credential_offer_uri } - return `${scheme}://${baseUri}?credential_offer_uri=${credentialOffer.credential_offer_uri}` + return `${scheme}://${baseUri}?credential_offer_uri=${encodeURIComponent(credentialOffer.credential_offer_uri)}` } return `${scheme}://${baseUri}?credential_offer=${encodeURIComponent(JSON.stringify(credentialOffer.credential_offer))}` } @@ -196,12 +180,11 @@ export function createCredentialOfferURI( issuerMetadata?: IssuerMetadataV1_0_13, // todo: probably it's wise to create another builder for CredentialOfferPayload that will generate different kinds of CredentialOfferPayload opts?: { - state?: string credentialOffer?: CredentialOfferPayloadV1_0_13 credentialOfferUri?: string scheme?: string - preAuthorizedCode?: string - userPinRequired?: boolean + baseUri?: string + grants?: CredentialOfferGrantInput }, ): string { const credentialOffer = createCredentialOfferObject(issuerMetadata, opts) @@ -212,12 +195,11 @@ export function createCredentialOfferURIv1_0_11( issuerMetadata?: CredentialIssuerMetadataV1_0_11, // todo: probably it's wise to create another builder for CredentialOfferPayload that will generate different kinds of CredentialOfferPayload opts?: { - state?: string credentialOffer?: CredentialOfferPayloadV1_0_11 credentialOfferUri?: string scheme?: string - preAuthorizedCode?: string - userPinRequired?: boolean + baseUri?: string + grants?: CredentialOfferGrantInput }, ): string { const credentialOffer = createCredentialOfferObjectv1_0_11(issuerMetadata, opts) diff --git a/packages/oid4vci-common/lib/types/Generic.types.ts b/packages/oid4vci-common/lib/types/Generic.types.ts index a9680f0d..f6b8187d 100644 --- a/packages/oid4vci-common/lib/types/Generic.types.ts +++ b/packages/oid4vci-common/lib/types/Generic.types.ts @@ -314,7 +314,7 @@ export interface IssuerCredentialSubject { export interface Grant { authorization_code?: GrantAuthorizationCode; - 'urn:ietf:params:oauth:grant-type:pre-authorized_code'?: GrantUrnIetf; + [PRE_AUTH_GRANT_LITERAL]?: GrantUrnIetf; } export interface GrantAuthorizationCode {