Skip to content

Commit

Permalink
fix: (WIP) refactored and fixed parts of the logic for v1_0_13.
Browse files Browse the repository at this point in the history
  • Loading branch information
sksadjad committed May 13, 2024
1 parent d8c2c4f commit 06117c0
Show file tree
Hide file tree
Showing 20 changed files with 171 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossess
import {
Alg,
CNonceState,
CredentialConfigurationSupported, CredentialIssuerMetadata,
CredentialConfigurationSupported,
CredentialIssuerMetadata,
IssuerCredentialSubjectDisplay,
IssueStatus,
Jwt,
JwtVerifyResult,
OpenId4VCIVersion,
ProofOfPossession
ProofOfPossession,
} from '@sphereon/oid4vci-common'
import { CredentialOfferSession } from '@sphereon/oid4vci-common/dist'
import { CredentialSupportedBuilderV1_13, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer'
Expand Down Expand Up @@ -215,9 +216,9 @@ describe('issuerCallback', () => {
const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI }))
.withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential')
.withCredentialEndpointFromMetadata({
credential_configurations_supported: {"VeriCred":{format: 'jwt_vc_json' } as CredentialConfigurationSupported}
credential_configurations_supported: { VeriCred: { format: 'jwt_vc_json' } as CredentialConfigurationSupported },
} as unknown as CredentialIssuerMetadata)
.withFormat('jwt_vc_json')
.withFormat('jwt_vc_json')
.withCredentialType('credentialType')
.withToken('token')

Expand Down
58 changes: 36 additions & 22 deletions packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
PRE_AUTH_CODE_LITERAL,
TokenErrorResponse,
toUniformCredentialOfferRequest,
TxCode,
TxCodeAndPinRequired,
UniformCredentialOfferPayload,
} from '@sphereon/oid4vci-common';
import { ObjectUtils } from '@sphereon/ssi-types';
Expand All @@ -34,8 +34,7 @@ export class AccessTokenClient {
const { asOpts, pin, codeVerifier, code, redirectUri, metadata } = opts;

const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : undefined;
const txCode: TxCode | undefined =
credentialOffer?.credential_offer.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code ?? undefined;
const pinMetadata: TxCodeAndPinRequired | undefined = credentialOffer && this.getPinMetadata(credentialOffer.credential_offer);
const issuer =
opts.credentialIssuer ??
(credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : (metadata?.issuer as string));
Expand All @@ -54,9 +53,9 @@ export class AccessTokenClient {
code,
redirectUri,
pin,
txCode,
pinMetadata,
}),
txCode,
pinMetadata,
metadata,
asOpts,
issuerOpts,
Expand All @@ -65,18 +64,18 @@ export class AccessTokenClient {

public async acquireAccessTokenUsingRequest({
accessTokenRequest,
txCode,
pinMetadata,
metadata,
asOpts,
issuerOpts,
}: {
accessTokenRequest: AccessTokenRequest;
txCode?: TxCode;
pinMetadata?: TxCodeAndPinRequired;
metadata?: EndpointMetadata;
asOpts?: AuthorizationServerOpts;
issuerOpts?: IssuerOpts;
}): Promise<OpenIDResponse<AccessTokenResponse>> {
this.validate(accessTokenRequest, txCode);
this.validate(accessTokenRequest, pinMetadata);

const requestTokenURL = AccessTokenClient.determineTokenURL({
asOpts,
Expand Down Expand Up @@ -105,7 +104,7 @@ export class AccessTokenClient {
}

if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin);
this.assertAlphanumericPin(opts.pinMetadata, pin);
request.user_pin = pin;

request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE;
Expand Down Expand Up @@ -143,24 +142,39 @@ export class AccessTokenClient {
}
}

private isPinRequiredValue(requestPayload: UniformCredentialOfferPayload): boolean {
let isPinRequired = false;
private getPinMetadata(requestPayload: UniformCredentialOfferPayload): TxCodeAndPinRequired {
if (!requestPayload) {
throw new Error(TokenErrorResponse.invalid_request);
}
const issuer = getIssuerFromCredentialOfferPayload(requestPayload);
if (requestPayload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code) {
isPinRequired = true;
}

const grantDetails = requestPayload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code'];
const isPinRequired = !!grantDetails?.tx_code || !!grantDetails?.['pre-authorized_code'];

debug(`Pin required for issuer ${issuer}: ${isPinRequired}`);
return isPinRequired;
return {
txCode: grantDetails?.tx_code,
isPinRequired,
};
}

private assertNumericPin(isPinRequired: boolean, pin?: string): void {
if (isPinRequired) {
if (!pin || !/^\d{1,8}$/.test(pin)) {
debug(`Pin is not 1 to 8 digits long`);
throw new Error('A valid pin consisting of maximal 8 numeric characters must be present.');
private assertAlphanumericPin(pinMeta?: TxCodeAndPinRequired, pin?: string): void {
if (pinMeta && pinMeta.isPinRequired) {
let regex;
if (pinMeta.txCode && pinMeta.txCode.input_mode === 'numeric') {
regex = new RegExp(`^\\d{1,${pinMeta.txCode.length || 8}}$`);
} else if (pinMeta.txCode && pinMeta.txCode.input_mode === 'text') {
regex = new RegExp(`^[a-zA-Z0-9]{1,${pinMeta.txCode.length || 8}}$`);
} else {
// default regex that limits the length to 8
regex = /^[a-zA-Z0-9]{1,8}$/;
}

if (!pin || !regex.test(pin)) {
debug(
`Pin is not valid. Expected format: ${pinMeta?.txCode?.input_mode || 'alphanumeric'}, Length: up to ${pinMeta?.txCode?.length || 8} characters`,
);
throw new Error('A valid pin must be present according to the specified transaction code requirements.');
}
} else if (pin) {
debug(`Pin set, whilst not required`);
Expand Down Expand Up @@ -188,11 +202,11 @@ export class AccessTokenClient {
throw new Error('Authorization flow requires the code to be present');
}
}
private validate(accessTokenRequest: AccessTokenRequest, txCode?: TxCode): void {
private validate(accessTokenRequest: AccessTokenRequest, pinMeta?: TxCodeAndPinRequired): void {
if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {
this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type);
this.assertNonEmptyPreAuthorizedCode(accessTokenRequest);
this.assertNumericPin(!!txCode, accessTokenRequest.user_pin);
this.assertAlphanumericPin(pinMeta, accessTokenRequest['pre-authorized_code']);
} else if (accessTokenRequest.grant_type === GrantTypes.AUTHORIZATION_CODE) {
this.assertAuthorizationGrantType(accessTokenRequest.grant_type);
this.assertNonEmptyCodeVerifier(accessTokenRequest);
Expand Down
19 changes: 10 additions & 9 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
acquireDeferredCredential, CredentialRequestV1_0_13,
acquireDeferredCredential,
CredentialRequestV1_0_13,
CredentialResponse,
getCredentialRequestForVersion,
getUniformFormat,
Expand All @@ -9,14 +10,14 @@ import {
OpenIDResponse,
ProofOfPossession,
UniformCredentialRequest,
URL_NOT_VALID
} from '@sphereon/oid4vci-common'
import { CredentialFormat } from '@sphereon/ssi-types'
import Debug from 'debug'
URL_NOT_VALID,
} from '@sphereon/oid4vci-common';
import { CredentialFormat } from '@sphereon/ssi-types';
import Debug from 'debug';

import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder'
import { isValidURL, post } from './functions'
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'
import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
import { isValidURL, post } from './functions';
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';

const debug = Debug('sphereon:oid4vci:credential');

Expand Down Expand Up @@ -89,7 +90,7 @@ export class CredentialRequestClient {

public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise<OpenIDResponse<CredentialResponse>> {
if (this.version() < OpenId4VCIVersion.VER_1_0_13) {
throw new Error('Versions below v1.0.13 (draft 13) are not supported.')
throw new Error('Versions below v1.0.13 (draft 13) are not supported.');
}
const request: CredentialRequestV1_0_13 = getCredentialRequestForVersion(uniformRequest, this.version()) as CredentialRequestV1_0_13;
const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;
Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
} from '@sphereon/oid4vci-common';
import { CredentialFormat } from '@sphereon/ssi-types';

import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11'
import { CredentialRequestClientV1_0_11 } from './CredentialRequestClientV1_0_11'
import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11';
import { CredentialRequestClientV1_0_11 } from './CredentialRequestClientV1_0_11';

export class CredentialRequestClientBuilderV1_0_11 {
credentialEndpoint?: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/CredentialRequestClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
import { CredentialFormat } from '@sphereon/ssi-types';
import Debug from 'debug';

import { buildProof } from './CredentialRequestClient'
import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'
import { buildProof } from './CredentialRequestClient';
import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11';
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
import { isValidURL, post } from './functions';

Expand Down
1 change: 0 additions & 1 deletion packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,6 @@ export class OpenID4VCIClient {
if (this._state.authorizationRequestOpts?.redirectUri && !redirectUri) {
redirectUri = this._state.authorizationRequestOpts.redirectUri;
}

const response = await accessTokenClient.acquireAccessToken({
credentialOffer: this.credentialOffer,
metadata: this.endpointMetadata,
Expand Down
4 changes: 2 additions & 2 deletions packages/client/lib/OpenID4VCIClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import {
import { CredentialFormat } from '@sphereon/ssi-types';
import Debug from 'debug';

import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11'
import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11';
import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11';
import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11';
import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'
import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11';
import { MetadataClient } from './MetadataClient';
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
import { generateMissingPKCEOpts } from './functions/AuthorizationUtil';
Expand Down
44 changes: 31 additions & 13 deletions packages/client/lib/__tests__/AccessTokenClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ describe('AccessTokenClient should', () => {

const accessTokenResponse: OpenIDResponse<AccessTokenResponse> = await accessTokenClient.acquireAccessTokenUsingRequest({
accessTokenRequest,
pinMetadata: {
isPinRequired: true,
txCode: {
length: accessTokenRequest['pre-authorized_code'].length,
input_mode: 'numeric',
},
},
asOpts: { as: MOCK_URL },
});

Expand Down Expand Up @@ -121,13 +128,16 @@ describe('AccessTokenClient should', () => {
await expect(
accessTokenClient.acquireAccessTokenUsingRequest({
accessTokenRequest,
txCode: {
length: 8,
input_mode: 'text',
pinMetadata: {
isPinRequired: true,
txCode: {
length: 6,
input_mode: 'text',
},
},
asOpts: { as: MOCK_URL },
}),
).rejects.toThrow('A valid pin consisting of maximal 8 numeric characters must be present.');
).rejects.toThrow('A valid pin must be present according to the specified transaction code requirements.');
},
UNIT_TEST_TIMEOUT,
);
Expand All @@ -149,13 +159,16 @@ describe('AccessTokenClient should', () => {
await expect(
accessTokenClient.acquireAccessTokenUsingRequest({
accessTokenRequest,
txCode: {
length: 9,
input_mode: 'text',
pinMetadata: {
isPinRequired: true,
txCode: {
length: 6,
input_mode: 'text',
},
},
asOpts: { as: MOCK_URL },
}),
).rejects.toThrow(Error('A valid pin consisting of maximal 8 numeric characters must be present.'));
).rejects.toThrow(Error('A valid pin must be present according to the specified transaction code requirements.'));
},
UNIT_TEST_TIMEOUT,
);
Expand Down Expand Up @@ -184,9 +197,12 @@ describe('AccessTokenClient should', () => {

const response = await accessTokenClient.acquireAccessTokenUsingRequest({
accessTokenRequest,
txCode: {
length: 8,
input_mode: 'text',
pinMetadata: {
isPinRequired: true,
txCode: {
length: 8,
input_mode: 'text',
},
},
asOpts: { as: MOCK_URL },
});
Expand All @@ -199,14 +215,16 @@ describe('AccessTokenClient should', () => {
const accessTokenClient: AccessTokenClient = new AccessTokenClient();

nock(MOCK_URL).post(/.*/).reply(200, {});
nock(INITIATION_TEST.credential_offer.credential_issuer+'token').post(/.*/).reply(200, {});
nock(INITIATION_TEST.credential_offer.credential_issuer + 'token')
.post(/.*/)
.reply(200, {});

await expect(() =>
accessTokenClient.acquireAccessToken({
credentialOffer: INITIATION_TEST,
pin: '1234',
}),
).rejects.toThrow(Error('Cannot set a pin, when the pin is not required.'));
).rejects.toThrow(Error('A valid pin must be present according to the specified transaction code requirements.'));
});

it('get error if no as, issuer and metadata values are present', async () => {
Expand Down
10 changes: 4 additions & 6 deletions packages/client/lib/__tests__/CredentialRequestClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import {
CredentialRequestClientBuilder,
CredentialRequestClientBuilderV1_0_11,
MetadataClient,
ProofOfPossessionBuilder
} from '..'
import { CredentialOfferClient } from '../CredentialOfferClient';
ProofOfPossessionBuilder,
} from '..';

import { IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA, INITIATION_TEST, WALT_OID4VCI_METADATA } from './MetadataMocks';
import { getMockData } from './data/VciDataFixtures';
Expand Down Expand Up @@ -73,7 +72,6 @@ afterEach(async () => {
nock.cleanAll();
});
describe('Credential Request Client ', () => {

it('should get success credential response', async function () {
const mockedVC =
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA';
Expand Down Expand Up @@ -170,7 +168,7 @@ describe('Credential Request Client with different issuers ', () => {
});
it('should create correct CredentialRequest for Spruce', async () => {
const IRR_URI =
'openid-initiate-issuance://?issuer=https%3A%2F%2Fngi%2Doidc4vci%2Dtest%2Espruceid%2Exyz&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJFUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOlsiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJleHAiOiIyMDIzLTA0LTIwVDA5OjA0OjM2WiIsIm5vbmNlIjoibWFibmVpT0VSZVB3V3BuRFFweEt3UnRsVVRFRlhGUEwifQ.qOZRPN8sTv_knhp7WaWte2-aDULaPZX--2i9unF6QDQNUllqDhvxgIHMDCYHCV8O2_Gj-T2x1J84fDMajE3asg&user_pin_required=false';
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credential_configuration_ids%22:%5B%22OpenBadgeCredential%22%5D,%22grants%22:%7B%22urn:ietf:params:oauth:grant-type:pre-authorized_code%22:%7B%22pre-authorized_code%22:%22oaKazRN8I0IbtZ0C7JuMn5%22,%22tx_code%22:%7B%22input_mode%22:%22text%22,%22description%22:%22Please%20enter%20the%20serial%20number%20of%20your%20physical%20drivers%20license%22%7D%7D%7D%7D';
const credentialRequest = await (
await CredentialRequestClientBuilder.fromURI({
uri: IRR_URI,
Expand All @@ -185,7 +183,7 @@ describe('Credential Request Client with different issuers ', () => {
},
credentialTypes: ['OpenBadgeCredential'],
format: 'jwt_vc',
version: OpenId4VCIVersion.VER_1_0_08,
version: OpenId4VCIVersion.VER_1_0_13,
});
const draft8CredentialRequest = getCredentialRequestForVersion(credentialRequest, OpenId4VCIVersion.VER_1_0_08);
expect(draft8CredentialRequest).toEqual(getMockData('spruce')?.credential.request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ import * as jose from 'jose';
// @ts-ignore
import nock from 'nock';

import { CredentialOfferClientV1_0_11, CredentialRequestClientBuilderV1_0_11, MetadataClient, ProofOfPossessionBuilder } from '..'
import { CredentialOfferClientV1_0_11, CredentialRequestClientBuilderV1_0_11, MetadataClient, ProofOfPossessionBuilder } from '..';

import {
IDENTIPROOF_ISSUER_URL,
IDENTIPROOF_OID4VCI_METADATA,
INITIATION_TEST,
INITIATION_TEST_V1_0_08,
WALT_OID4VCI_METADATA
} from './MetadataMocks'
WALT_OID4VCI_METADATA,
} from './MetadataMocks';
import { getMockData } from './data/VciDataFixtures';

const partialJWT = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmN';
Expand Down
Loading

0 comments on commit 06117c0

Please sign in to comment.