Skip to content

Commit

Permalink
Merge pull request #159 from TimoGlastra/offer-creation-improvements
Browse files Browse the repository at this point in the history
fix: offer creation improvements
  • Loading branch information
nklomp authored Oct 24, 2024
2 parents 8a3c5f7 + fa2b31b commit f1a3b75
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 176 deletions.
4 changes: 2 additions & 2 deletions packages/client/lib/__tests__/SdJwt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
})

Expand Down
74 changes: 26 additions & 48 deletions packages/issuer/lib/VcIssuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,7 +27,6 @@ import {
NO_ISS_IN_AUTHORIZATION_CODE_CONTEXT,
OID4VCICredentialFormat,
OpenId4VCIVersion,
PRE_AUTH_CODE_LITERAL,
PRE_AUTH_GRANT_LITERAL,
QRCodeOpts,
TokenErrorResponse,
Expand All @@ -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'

Expand Down Expand Up @@ -92,7 +89,7 @@ export class VcIssuer<DIDDoc extends object> {
}

public async createCredentialOfferURI(opts: {
grants?: Grant
grants?: CredentialOfferGrantInput
credential_configuration_ids?: Array<string>
credentialDefinition?: JsonLdIssuerCredentialDefinition
credentialOfferUri?: string
Expand All @@ -102,58 +99,39 @@ export class VcIssuer<DIDDoc extends object> {
pinLength?: number
qrCodeOpts?: QRCodeOpts
}): Promise<CreateCredentialOfferURIResult> {
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)
Expand Down
57 changes: 51 additions & 6 deletions packages/issuer/lib/__tests__/CredentialOfferUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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`,
Expand Down
2 changes: 1 addition & 1 deletion packages/issuer/lib/__tests__/VcIssuer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit f1a3b75

Please sign in to comment.