Skip to content

Commit

Permalink
fix: Many v11 fixes on server and client side
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed May 26, 2023
1 parent e2d2876 commit 08be1ed
Show file tree
Hide file tree
Showing 20 changed files with 681 additions and 281 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ xdescribe('issuerCallback', () => {
//FIXME Here a CredentialFormatEnum is passed in, but later it is matched against a CredentialFormat
.withFormat('jwt_vc_json')
.withId('UniversityDegree_JWT')
.withCredentialDisplay({
.withCredentialSupportedDisplay({
name: 'University Credential',
locale: 'en-US',
logo: {
Expand All @@ -83,7 +83,7 @@ xdescribe('issuerCallback', () => {
background_color: '#12107c',
text_color: '#FFFFFF',
})
.withIssuerCredentialSubjectDisplay('given_name', {
.addCredentialSubjectPropertyDisplay('given_name', {
name: 'given name',
locale: 'en-US',
} as IssuerCredentialSubjectDisplay)
Expand All @@ -93,7 +93,7 @@ xdescribe('issuerCallback', () => {
issuerState: 'existing-state',
clientId,
createdAt: +new Date(),
userPin: 123456,
userPin: '123456',
credentialOffer: {
credential_offer: {
credential_issuer: 'did:key:test',
Expand Down
10 changes: 7 additions & 3 deletions packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ export class AccessTokenClient {

const credentialOffer = await assertedUniformCredentialOffer(opts.credentialOffer);
const isPinRequired = this.isPinRequiredValue(credentialOffer.credential_offer);
const issuer = getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) ?? (metadata?.issuer as string);
if (!issuer) {
throw Error('Issuer required at this point');
}
const issuerOpts = {
issuer: getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) ?? (metadata?.issuer as string),
issuer,
};

return await this.acquireAccessTokenUsingRequest({
Expand Down Expand Up @@ -96,7 +100,7 @@ export class AccessTokenClient {
request[PRE_AUTH_CODE_LITERAL] =
credentialOfferRequest?.credential_offer.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.[PRE_AUTH_CODE_LITERAL];
}
if (credentialOfferRequest.credential_offer.grants?.authorization_code?.issuer_state) {
if (!isPreAuth && credentialOfferRequest.credential_offer.grants?.authorization_code?.issuer_state) {
this.throwNotSupportedFlow(); // not supported yet
request.grant_type = GrantTypes.AUTHORIZATION_CODE;
}
Expand Down Expand Up @@ -215,7 +219,7 @@ export class AccessTokenClient {
} else if (metadata?.token_endpoint) {
url = metadata.token_endpoint;
} else {
if (!issuerOpts) {
if (!issuerOpts?.issuer) {
throw Error('Either authorization server options, a token endpoint or issuer options are required at this point');
}
url = this.creatTokenURLFromURL(issuerOpts.issuer, asOpts?.allowInsecureEndpoints, issuerOpts.tokenEndpoint);
Expand Down
12 changes: 4 additions & 8 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export class OpenID4VCIClient {
}
return authorizationDetails;
}

private handleLocations(authorizationDetails: AuthDetails) {
if (authorizationDetails && (this.endpointMetadata.issuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)) {
if (authorizationDetails.locations) {
Expand All @@ -212,19 +213,14 @@ export class OpenID4VCIClient {
return authorizationDetails;
}

public async acquireAccessToken({
pin,
clientId,
codeVerifier,
code,
redirectUri,
}: {
public async acquireAccessToken(opts?: {
pin?: string;
clientId?: string;
codeVerifier?: string;
code?: string;
redirectUri?: string;
}): Promise<AccessTokenResponse> {
const { pin, clientId, codeVerifier, code, redirectUri } = opts ?? {};
this.assertIssuerData();
if (clientId) {
this._clientId = clientId;
Expand All @@ -239,7 +235,7 @@ export class OpenID4VCIClient {
codeVerifier,
code,
redirectUri,
asOpts: { clientId: this.clientId },
asOpts: { clientId },
});

if (response.errorBody) {
Expand Down
65 changes: 61 additions & 4 deletions packages/client/lib/__tests__/JsonURIConversions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { convertJsonToURI, convertURIToJsonObject, OpenId4VCIVersion } from '@sphereon/oid4vci-common';

describe('JSON To URI', () => {
describe('JSON To URI v8', () => {
it('should parse an object into open-id-URI with a single credential_type', () => {
expect(
convertJsonToURI(
Expand All @@ -11,7 +11,7 @@ describe('JSON To URI', () => {
},
{
uriTypeProperties: ['issuer', 'credential_type'],
version: OpenId4VCIVersion.VER_1_0_09,
version: OpenId4VCIVersion.VER_1_0_08,
}
)
).toEqual(
Expand All @@ -29,7 +29,7 @@ describe('JSON To URI', () => {
{
arrayTypeProperties: ['credential_type'],
uriTypeProperties: ['issuer', 'credential_type'],
version: OpenId4VCIVersion.VER_1_0_09,
version: OpenId4VCIVersion.VER_1_0_08,
}
)
).toEqual(
Expand All @@ -47,14 +47,71 @@ describe('JSON To URI', () => {
{
arrayTypeProperties: ['credential_type'],
uriTypeProperties: ['issuer', 'credential_type'],
version: OpenId4VCIVersion.VER_1_0_09,
version: OpenId4VCIVersion.VER_1_0_08,
}
)
).toEqual(
'issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FdriverLicense&op_state=eyJhbGciOiJSU0Et...FYUaBy'
);
});
});

describe('JSON To URI v9', () => {
it('should parse an object into open-id-URI with a single credential_type', () => {
expect(
convertJsonToURI(
{
issuer: 'https://server.example.com',
credential_type: 'https://did.example.org/healthCard',
op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
},
{
uriTypeProperties: ['issuer', 'credential_type'],
version: OpenId4VCIVersion.VER_1_0_09,
}
)
).toEqual(
'%7B%22issuer%22%3A%22https%3A%2F%2Fserver.example.com%22%2C%22credential_type%22%3A%22https%3A%2F%2Fdid.example.org%2FhealthCard%22%2C%22op_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D'
);
});
it('should parse an object into open-id-URI with an array of credential_type', () => {
expect(
convertJsonToURI(
{
issuer: 'https://server.example.com',
credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'],
op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
},
{
arrayTypeProperties: ['credential_type'],
uriTypeProperties: ['issuer', 'credential_type'],
version: OpenId4VCIVersion.VER_1_0_09,
}
)
).toEqual(
'%7B%22issuer%22%3A%22https%3A%2F%2Fserver.example.com%22%2C%22credential_type%22%3A%5B%22https%3A%2F%2Fdid.example.org%2FhealthCard%22%2C%22https%3A%2F%2Fdid.example.org%2FdriverLicense%22%5D%2C%22op_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D'
);
});
it('should parse an object into open-id-URI with an array of credential_type and json string', () => {
expect(
convertJsonToURI(
JSON.stringify({
issuer: 'https://server.example.com',
credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'],
op_state: 'eyJhbGciOiJSU0Et...FYUaBy',
}),
{
arrayTypeProperties: ['credential_type'],
uriTypeProperties: ['issuer', 'credential_type'],
version: OpenId4VCIVersion.VER_1_0_09,
}
)
).toEqual(
'%7B%22issuer%22%3A%22https%3A%2F%2Fserver.example.com%22%2C%22credential_type%22%3A%5B%22https%3A%2F%2Fdid.example.org%2FhealthCard%22%2C%22https%3A%2F%2Fdid.example.org%2FdriverLicense%22%5D%2C%22op_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D'
);
});
});

describe('URI To Json Object', () => {
it('should parse open-id-URI as json object with a single credential_type', () => {
expect(
Expand Down
17 changes: 11 additions & 6 deletions packages/common/lib/functions/CredentialOfferUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ export function isCredentialOfferVersion(offer: CredentialOfferPayload | Credent
}
const version = determineSpecVersionFromOffer(offer);
if (version.valueOf() < min.valueOf()) {
throw Error(`Credential offer version (${version.valueOf()}) is lower than minimum required version (${min.valueOf()})`);
console.log(`Credential offer version (${version.valueOf()}) is lower than minimum required version (${min.valueOf()})`);
return false;
} else if (max && version.valueOf() > max.valueOf()) {
throw Error(`Credential offer version (${version.valueOf()}) is higher than maximum required version (${max.valueOf()})`);
console.log(`Credential offer version (${version.valueOf()}) is higher than maximum required version (${max.valueOf()})`);
return false;
}
return true;
}
Expand Down Expand Up @@ -182,7 +184,7 @@ export async function assertedUniformCredentialOffer(
if (!credentialOffer.credential_offer) {
throw Error(`No credential_offer present`);
}
credentialOffer.credential_offer = await toUniformCredentialOfferPayload(credentialOffer.credential_offer);
credentialOffer.credential_offer = await toUniformCredentialOfferPayload(credentialOffer.credential_offer, { version: credentialOffer.version });
return credentialOffer as AssertedUniformCredentialOffer;
}

Expand All @@ -203,18 +205,20 @@ export function toUniformCredentialOfferPayload(
version?: OpenId4VCIVersion;
}
): UniformCredentialOfferPayload {
// todo: create test to check idempotence once a payload is already been made uniform.
const version = opts?.version ?? determineSpecVersionFromOffer(offer);
if (version >= OpenId4VCIVersion.VER_1_0_11) {
const orig = offer as UniformCredentialOfferPayload;
return {
...orig,
};
}
const grants: Grant = {};
const grants: Grant = 'grants' in offer ? (offer.grants as Grant) : {};
let offerPayloadAsV8V9 = offer as CredentialOfferPayloadV1_0_08 | CredentialOfferPayloadV1_0_09;
if (isCredentialOfferVersion(offer, OpenId4VCIVersion.VER_1_0_08, OpenId4VCIVersion.VER_1_0_09)) {
if (offerPayloadAsV8V9.op_state) {
grants.authorization_code = {
...grants.authorization_code,
issuer_state: offerPayloadAsV8V9.op_state,
};
}
Expand All @@ -231,19 +235,20 @@ export function toUniformCredentialOfferPayload(
};
}
}
const issuer = getIssuerFromCredentialOfferPayload(offer);
if (version === OpenId4VCIVersion.VER_1_0_09) {
offerPayloadAsV8V9 = offer as CredentialOfferPayloadV1_0_09;
return {
// credential_definition: getCredentialsSupported(never, offerPayloadAsV8V9.credentials).map(sup => {credentialSubject: sup.credentialSubject})[0],
credential_issuer: offerPayloadAsV8V9.issuer,
credential_issuer: issuer ?? offerPayloadAsV8V9.issuer,
credentials: offerPayloadAsV8V9.credentials,
grants,
};
}
if (version === OpenId4VCIVersion.VER_1_0_08) {
offerPayloadAsV8V9 = offer as CredentialOfferPayloadV1_0_08;
return {
credential_issuer: offerPayloadAsV8V9.issuer,
credential_issuer: issuer ?? offerPayloadAsV8V9.issuer,
credentials: Array.isArray(offerPayloadAsV8V9.credential_type) ? offerPayloadAsV8V9.credential_type : [offerPayloadAsV8V9.credential_type],
grants,
} as UniformCredentialOfferPayload;
Expand Down
8 changes: 4 additions & 4 deletions packages/common/lib/functions/Encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export function convertJsonToURI(
}

let components: string;
if (opts?.version && opts.version < OpenId4VCIVersion.VER_1_0_11) {
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;
Expand Down Expand Up @@ -60,9 +63,6 @@ export function convertJsonToURI(
results.push(encoded);
}
components = results.join('&');
} else {
// v11 changed from encoding every param to a encoded json object with a credential_offer param key
components = encodeAndStripWhitespace(JSON.stringify(json));
}
if (opts?.baseUrl) {
if (opts.baseUrl.endsWith('=')) {
Expand Down
2 changes: 1 addition & 1 deletion packages/common/lib/functions/HttpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const openIdFetch = async <T>(
const isJSONResponse = accept === 'application/json' || origResponse.headers.get('Content-Type') === 'application/json';
const success = origResponse && origResponse.status >= 200 && origResponse.status < 400;
const responseText = await origResponse.text();
const responseBody = isJSONResponse ? JSON.parse(responseText) : responseText;
const responseBody = isJSONResponse && responseText.includes('{') ? JSON.parse(responseText) : responseText;

debug(`${success ? 'success' : 'error'} status: ${origResponse.status}, body:\r\n${JSON.stringify(responseBody)}`);
if (!success && opts?.exceptionOnHttpErrorStatus) {
Expand Down
2 changes: 1 addition & 1 deletion packages/common/lib/types/StateManager.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface StateType {
export interface CredentialOfferSession extends StateType {
clientId?: string;
credentialOffer: CredentialOfferV1_0_11;
userPin?: number;
userPin?: string;
issuerState?: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value
preAuthorizedCode?: string; //todo: Probably good to hash it here, since it would come in from the client and we could match the hash and thus use the client value
}
Expand Down
10 changes: 1 addition & 9 deletions packages/issuer-rest/lib/IssuerTokenEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import {
Alg,
CredentialOfferSession,
EXPIRED_PRE_AUTHORIZED_CODE,
getNumberOrUndefined,
GrantType,
INVALID_PRE_AUTHORIZED_CODE,
Jwt,
JWTSignerCallback,
PIN_NOT_MATCH_ERROR,
PIN_NOT_MATCHING_ERROR,
PIN_VALIDATION_ERROR,
PRE_AUTH_CODE_LITERAL,
PRE_AUTHORIZED_CODE_REQUIRED_ERROR,
Expand Down Expand Up @@ -183,13 +181,7 @@ export const verifyTokenRequest = ({
error_message: PIN_VALIDATION_ERROR,
})
}
if (credentialOfferSession.userPin != getNumberOrUndefined(request.body.user_pin)) {
return sendErrorResponse(response, 400, {
error: TokenErrorResponse.invalid_grant,
error_message: PIN_NOT_MATCHING_ERROR,
})
}
if (getNumberOrUndefined(request.body.user_pin) !== credentialOfferSession.userPin) {
if (request.body.user_pin !== credentialOfferSession.userPin) {
return sendErrorResponse(response, 400, {
error: TokenErrorResponse.invalid_grant,
error_message: PIN_NOT_MATCH_ERROR,
Expand Down
Loading

0 comments on commit 08be1ed

Please sign in to comment.