Skip to content

Commit

Permalink
Merge pull request #32 from Sphereon-Opensource/feature/VDX-177/autho…
Browse files Browse the repository at this point in the history
…rization_details

feature/VDX-177/authorization_details
  • Loading branch information
zoemaas authored Mar 24, 2023
2 parents 500c7f0 + 45a0e7f commit f5d1663
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 8 deletions.
45 changes: 45 additions & 0 deletions packages/client/lib/AuthorizationDetailsBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AuthorizationDetails } from '@sphereon/openid4vci-common';
import { CredentialFormat } from '@sphereon/ssi-types';

export class AuthorizationDetailsBuilder {
private authorizationDetails: Partial<AuthorizationDetails>;

constructor() {
this.authorizationDetails = {};
}

withType(type: string): AuthorizationDetailsBuilder {
this.authorizationDetails.type = type;
return this;
}

withFormats(format: CredentialFormat): AuthorizationDetailsBuilder {
this.authorizationDetails.format = format;
return this;
}

withLocations(locations: string[]): AuthorizationDetailsBuilder {
if (this.authorizationDetails.locations) {
this.authorizationDetails.locations.push(...locations);
} else {
this.authorizationDetails.locations = locations;
}
return this;
}

addLocation(location: string): AuthorizationDetailsBuilder {
if (this.authorizationDetails.locations) {
this.authorizationDetails.locations.push(location);
} else {
this.authorizationDetails.locations = [location];
}
return this;
}

build(): AuthorizationDetails {
if (this.authorizationDetails.format && this.authorizationDetails.type) {
return this.authorizationDetails as AuthorizationDetails;
}
throw new Error('Type and format are required properties');
}
}
67 changes: 61 additions & 6 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {
AccessTokenResponse,
Alg,
AuthorizationRequest,
AuthorizationRequestOpts,
AuthzFlowType,
CodeChallengeMethod,
CredentialMetadata,
CredentialResponse,
CredentialsSupported,
Expand All @@ -24,6 +24,23 @@ import { convertJsonToURI } from './functions';

const debug = Debug('sphereon:openid4vci:flow');

interface AuthDetails {
type: 'openid_credential' | string;
locations?: string | string[];
format: CredentialFormat | CredentialFormat[];

[s: string]: unknown;
}

interface AuthRequestOpts {
clientId: string;
codeChallenge: string;
codeChallengeMethod: CodeChallengeMethod;
authorizationDetails?: AuthDetails | AuthDetails[];
redirectUri: string;
scope?: string;
}

export class OpenID4VCIClient {
private readonly _flowType: AuthzFlowType;
private readonly _initiation: IssuanceInitiationWithBaseUrl;
Expand Down Expand Up @@ -71,17 +88,26 @@ export class OpenID4VCIClient {
return this._serverMetadata;
}

public createAuthorizationRequestUrl({ clientId, codeChallengeMethod, codeChallenge, redirectUri, scope }: AuthorizationRequestOpts): string {
if (!scope) {
throw Error('Please provide a scope. authorization_details based requests are not supported at this time');
public createAuthorizationRequestUrl({
clientId,
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) {
throw Error('Please provide a scope or authorization_details');
}

if (!this._serverMetadata?.openid4vci_metadata?.authorization_endpoint) {
throw Error('Server metadata does not contain authorization endpoint');
}

// add 'openid' scope if not present
if (!scope.includes('openid')) {
if (scope && !scope.includes('openid')) {
scope = `openid ${scope}`;
}

Expand All @@ -90,18 +116,47 @@ export class OpenID4VCIClient {
client_id: clientId,
code_challenge_method: codeChallengeMethod,
code_challenge: codeChallenge,
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
redirect_uri: redirectUri,
scope: scope,
};

const authRequestUrl = convertJsonToURI(queryObj, {
baseUrl: this._serverMetadata.openid4vci_metadata.authorization_endpoint,
uriTypeProperties: ['redirect_uri', 'scope'],
uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details'],
});

return authRequestUrl;
}

private handleAuthorizationDetails(authorizationDetails?: AuthDetails | AuthDetails[]): AuthDetails | AuthDetails[] | undefined {
if (authorizationDetails) {
if (Array.isArray(authorizationDetails)) {
return authorizationDetails.map((value) => this.handleLocations({ ...value }));
} else {
return this.handleLocations({ ...authorizationDetails });
}
}
return authorizationDetails;
}
private handleLocations(authorizationDetails: AuthDetails) {
if (
authorizationDetails &&
(this.serverMetadata.openid4vci_metadata?.authorization_server || this.serverMetadata.openid4vci_metadata?.authorization_endpoint)
) {
if (authorizationDetails.locations) {
if (Array.isArray(authorizationDetails.locations)) {
(authorizationDetails.locations as string[]).push(this.serverMetadata.issuer);
} else {
authorizationDetails.locations = [authorizationDetails.locations as string, this.serverMetadata.issuer];
}
} else {
authorizationDetails.locations = this.serverMetadata.issuer;
}
}
return authorizationDetails;
}

public async acquireAccessToken({
pin,
clientId,
Expand Down
46 changes: 46 additions & 0 deletions packages/client/lib/__tests__/AuthorizationDetailsBuilder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AuthorizationDetailsBuilder } from '../AuthorizationDetailsBuilder';

describe('AuthorizationDetailsBuilder test', () => {
it('should create AuthorizationDetails object from arrays', () => {
const actual = new AuthorizationDetailsBuilder().withFormats('jwt_vc').withLocations(['test1', 'test2']).withType('openid_credential').build();
expect(actual).toEqual({
type: 'openid_credential',
format: 'jwt_vc',
locations: ['test1', 'test2'],
});
});
it('should create AuthorizationDetails object from single objects', () => {
const actual = new AuthorizationDetailsBuilder().withFormats('jwt_vc').withLocations(['test1']).withType('openid_credential').build();
expect(actual).toEqual({
type: 'openid_credential',
format: 'jwt_vc',
locations: ['test1'],
});
});
it('should create AuthorizationDetails object if locations is missing', () => {
const actual = new AuthorizationDetailsBuilder().withFormats('jwt_vc').withType('openid_credential').build();
expect(actual).toEqual({
type: 'openid_credential',
format: 'jwt_vc',
});
});
it('should fail if type is missing', () => {
expect(() => {
new AuthorizationDetailsBuilder().withFormats('jwt_vc').withLocations(['test1']).build();
}).toThrow(Error('Type and format are required properties'));
});
it('should fail if format is missing', () => {
expect(() => {
new AuthorizationDetailsBuilder().withType('openid_credential').withLocations(['test1']).build();
}).toThrow(Error('Type and format are required properties'));
});
it('should be able to add random field to the object', () => {
const actual = new AuthorizationDetailsBuilder().withFormats('jwt_vc').withType('openid_credential').build();
actual['random'] = 'test';
expect(actual).toEqual({
type: 'openid_credential',
format: 'jwt_vc',
random: 'test',
});
});
});
85 changes: 83 additions & 2 deletions packages/client/lib/__tests__/OpenID4VCIClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('OpenID4VCIClient should', () => {

expect(scope?.[0]).toBe('openid');
});
it('throw an error if no scope is provided', async () => {
it('throw an error if no scope and no authorization_details is provided', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
client._serverMetadata.openid4vci_metadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;
Expand All @@ -78,6 +78,87 @@ describe('OpenID4VCIClient should', () => {
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
redirectUri: 'http://localhost:8881/cb',
});
}).toThrow(Error('Please provide a scope. authorization_details based requests are not supported at this time'));
}).toThrow(Error('Please provide a scope or authorization_details'));
});
it('create an authorization request url with authorization_details array property', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
client._serverMetadata.openid4vci_metadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;

expect(
client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
authorizationDetails: [
{
type: 'openid_credential',
format: 'ldp_vc',
credential_definition: {
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
types: ['VerifiableCredential', 'UniversityDegreeCredential'],
},
},
{
type: 'openid_credential',
format: 'mso_mdoc',
doctype: 'org.iso.18013.5.1.mDL',
},
],
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%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%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb'
);
});
it('create an authorization request url with authorization_details object property', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
client._serverMetadata.openid4vci_metadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;

expect(
client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
authorizationDetails: {
type: 'openid_credential',
format: 'ldp_vc',
credential_definition: {
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
types: ['VerifiableCredential', 'UniversityDegreeCredential'],
},
},
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%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb'
);
});
it('create an authorization request url with authorization_details and scope', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
client._serverMetadata.openid4vci_metadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;

expect(
client.createAuthorizationRequestUrl({
clientId: 'test-client',
codeChallengeMethod: CodeChallengeMethod.SHA256,
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
authorizationDetails: {
type: 'openid_credential',
format: 'ldp_vc',
locations: ['https://test.com'],
credential_definition: {
'@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'],
types: ['VerifiableCredential', 'UniversityDegreeCredential'],
},
},
scope: 'openid',
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%22locations%22%3A%5B%22https%3A%2F%2Ftest%2Ecom%22%5D%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%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&scope=openid'
);
});
});
12 changes: 12 additions & 0 deletions packages/common/lib/types/Authorization.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { CredentialFormat } from '@sphereon/ssi-types';

import { IssuanceInitiationRequestPayload, IssuanceInitiationWithBaseUrl } from './CredentialIssuance.types';
import { EndpointMetadata, ErrorResponse, PRE_AUTH_CODE_LITERAL } from './Generic.types';

export interface AuthorizationDetails {
type: 'openid_credential' | string;
// If the Credential Issuer metadata contains an authorization_server parameter, the authorization detail's locations common data field MUST be set to the Credential Issuer Identifier value.
locations?: string[];
format: CredentialFormat;
[s: string]: unknown;
}

export enum GrantTypes {
AUTHORIZATION_CODE = 'authorization_code',
PRE_AUTHORIZED_CODE = 'urn:ietf:params:oauth:grant-type:pre-authorized_code',
Expand Down Expand Up @@ -49,6 +59,7 @@ export interface AuthorizationRequest {
client_id: string;
code_challenge: string;
code_challenge_method: CodeChallengeMethod;
authorization_details?: string;
redirect_uri: string;
scope?: string;
}
Expand All @@ -57,6 +68,7 @@ export interface AuthorizationRequestOpts {
clientId: string;
codeChallenge: string;
codeChallengeMethod: CodeChallengeMethod;
authorizationDetails?: AuthorizationDetails[];
redirectUri: string;
scope?: string;
}
Expand Down

0 comments on commit f5d1663

Please sign in to comment.