Skip to content

Commit

Permalink
Merge pull request #28 from karimStekelenburg/feat/authorization-request
Browse files Browse the repository at this point in the history
feat: authorization request
  • Loading branch information
nklomp authored Mar 17, 2023
2 parents 8b16518 + dbd2ce5 commit 56b16a3
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 26 deletions.
70 changes: 61 additions & 9 deletions lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export class AccessTokenClient {
issuanceInitiation,
asOpts,
pin,
codeVerifier,
code,
redirectUri,
metadata,
}: AccessTokenRequestOpts): Promise<OpenIDResponse<AccessTokenResponse>> {
const { issuanceInitiationRequest } = issuanceInitiation;
Expand All @@ -34,6 +37,9 @@ export class AccessTokenClient {
accessTokenRequest: await this.createAccessTokenRequest({
issuanceInitiation,
asOpts,
codeVerifier,
code,
redirectUri,
pin,
}),
isPinRequired,
Expand Down Expand Up @@ -69,8 +75,16 @@ export class AccessTokenClient {
return this.sendAuthCode(requestTokenURL, accessTokenRequest);
}

public async createAccessTokenRequest({ issuanceInitiation, asOpts, pin }: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
public async createAccessTokenRequest({
issuanceInitiation,
asOpts,
pin,
codeVerifier,
code,
redirectUri,
}: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
const issuanceInitiationRequest = issuanceInitiation.issuanceInitiationRequest;
issuanceInitiationRequest;
const request: Partial<AccessTokenRequest> = {};
if (asOpts?.clientId) {
request.client_id = asOpts.clientId;
Expand All @@ -80,18 +94,24 @@ export class AccessTokenClient {
request.user_pin = pin;

if (issuanceInitiationRequest[PRE_AUTH_CODE_LITERAL]) {
if (codeVerifier) {
throw new Error('Cannot pass a code_verifier when flow type is pre-authorized');
}
request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE;
request[PRE_AUTH_CODE_LITERAL] = issuanceInitiationRequest[PRE_AUTH_CODE_LITERAL];
}
if (issuanceInitiationRequest.op_state) {
this.throwNotSupportedFlow();
/**
* Code is here for when we start to support this flow
*/
// if (issuanceInitiationRequest[PRE_AUTH_CODE_LITERAL]) {
// throw new Error('Cannot have both a pre_authorized_code and a op_state in the same initiation request');
// }
// request.grant_type = GrantTypes.AUTHORIZATION_CODE;
request.grant_type = GrantTypes.AUTHORIZATION_CODE;
}
if (codeVerifier) {
request.code_verifier = codeVerifier;
request.code = code;
request.redirect_uri = redirectUri;
request.grant_type = GrantTypes.AUTHORIZATION_CODE;
}
if (request.grant_type === GrantTypes.AUTHORIZATION_CODE && issuanceInitiationRequest[PRE_AUTH_CODE_LITERAL]) {
throw Error('A pre_authorized_code flow cannot have an op_state in the initiation request');
}

return request as AccessTokenRequest;
Expand All @@ -103,6 +123,12 @@ export class AccessTokenClient {
}
}

private assertAuthorizationGrantType(grantType: GrantTypes): void {
if (GrantTypes.AUTHORIZATION_CODE !== grantType) {
throw new Error("grant type must be 'authorization_code'");
}
}

private isPinRequiredValue(issuanceInitiationRequest: IssuanceInitiationRequestPayload): boolean {
let isPinRequired = false;
if (issuanceInitiationRequest !== undefined) {
Expand Down Expand Up @@ -135,13 +161,39 @@ export class AccessTokenClient {
}
}

private assertNonEmptyCodeVerifier(accessTokenRequest: AccessTokenRequest): void {
if (!accessTokenRequest.code_verifier) {
debug('No code_verifier present, whilst it is required');
throw new Error('Authorization flow requires the code_verifier to be present');
}
}

private assertNonEmptyCode(accessTokenRequest: AccessTokenRequest): void {
if (!accessTokenRequest.code) {
debug('No code present, whilst it is required');
throw new Error('Authorization flow requires the code to be present');
}
}

private assertNonEmptyRedirectUri(accessTokenRequest: AccessTokenRequest): void {
if (!accessTokenRequest.redirect_uri) {
debug('No redirect_uri present, whilst it is required');
throw new Error('Authorization flow requires the redirect_uri to be present');
}
}

private validate(accessTokenRequest: AccessTokenRequest, isPinRequired?: boolean): void {
if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {
this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type);
this.assertNonEmptyPreAuthorizedCode(accessTokenRequest);
this.assertNumericPin(isPinRequired, accessTokenRequest.user_pin);
} else if (accessTokenRequest.grant_type === GrantTypes.AUTHORIZATION_CODE) {
this.assertAuthorizationGrantType(accessTokenRequest.grant_type);
this.assertNonEmptyCodeVerifier(accessTokenRequest);
this.assertNonEmptyCode(accessTokenRequest);
this.assertNonEmptyRedirectUri(accessTokenRequest);
} else {
this.throwNotSupportedFlow();
this.throwNotSupportedFlow;
}
}

Expand Down
57 changes: 53 additions & 4 deletions lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder
import { IssuanceInitiation } from './IssuanceInitiation';
import { MetadataClient } from './MetadataClient';
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
import { convertJsonToURI } from './functions';
import {
AccessTokenResponse,
Alg,
AuthorizationRequest,
AuthorizationRequestOpts,
AuthzFlowType,
CredentialMetadata,
CredentialResponse,
CredentialsSupported,
EndpointMetadata,
IssuanceInitiationWithBaseUrl,
ProofOfPossessionCallbacks,
ResponseType,
} from './types';

const debug = Debug('sphereon:openid4vci:flow');
Expand All @@ -30,9 +34,6 @@ export class OpenID4VCIClient {
private _accessTokenResponse: AccessTokenResponse;

private constructor(initiation: IssuanceInitiationWithBaseUrl, flowType: AuthzFlowType, kid?: string, alg?: Alg | string, clientId?: string) {
if (flowType !== AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW) {
throw new Error(`Only pre-authorized code flow is support at present`);
}
this._flowType = flowType;
this._initiation = initiation;
this._kid = kid;
Expand Down Expand Up @@ -70,17 +71,64 @@ export class OpenID4VCIClient {
return this._serverMetadata;
}

public async acquireAccessToken({ pin, clientId }: { pin?: string; clientId?: string }): Promise<AccessTokenResponse> {
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');
}

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')) {
scope = `openid ${scope}`;
}

const queryObj: AuthorizationRequest = {
response_type: ResponseType.AUTH_CODE,
client_id: clientId,
code_challenge_method: codeChallengeMethod,
code_challenge: codeChallenge,
redirect_uri: redirectUri,
scope: scope,
};

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

return authRequestUrl;
}

public async acquireAccessToken({
pin,
clientId,
codeVerifier,
code,
redirectUri,
}: {
pin?: string;
clientId?: string;
codeVerifier?: string;
code?: string;
redirectUri?: string;
}): Promise<AccessTokenResponse> {
this.assertInitiation();
if (clientId) {
this._clientId = clientId;
}
if (!this._accessTokenResponse) {
const accessTokenClient = new AccessTokenClient();

const response = await accessTokenClient.acquireAccessTokenUsingIssuanceInitiation({
issuanceInitiation: this._initiation,
metadata: this._serverMetadata,
pin,
codeVerifier,
code,
redirectUri,
asOpts: { clientId: this.clientId },
});
if (response.errorBody) {
Expand All @@ -91,6 +139,7 @@ export class OpenID4VCIClient {
}
this._accessTokenResponse = response.successBody;
}

return this._accessTokenResponse;
}

Expand Down
19 changes: 19 additions & 0 deletions lib/types/Authorization.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export enum ResponseType {
AUTH_CODE = 'code',
}

export enum CodeChallengeMethod {
TEXT = 'text',
SHA256 = 'S256',
}

export interface AuthorizationServerOpts {
allowInsecureEndpoints?: boolean;
as?: string; // If not provided the issuer hostname will be used!
Expand All @@ -33,16 +38,29 @@ export interface AccessTokenRequestOpts {
issuanceInitiation: IssuanceInitiationWithBaseUrl;
asOpts?: AuthorizationServerOpts;
metadata?: EndpointMetadata;
codeVerifier?: string; // only required for authorization flow
code?: string; // only required for authorization flow
redirectUri?: string; // only required for authorization flow
pin?: string; // Pin-number. Only used when required
}

export interface AuthorizationRequest {
response_type: ResponseType.AUTH_CODE;
client_id: string;
code_challenge: string;
code_challenge_method: CodeChallengeMethod;
redirect_uri: string;
scope?: string;
}

export interface AuthorizationRequestOpts {
clientId: string;
codeChallenge: string;
codeChallengeMethod: CodeChallengeMethod;
redirectUri: string;
scope?: string;
}

export interface AuthorizationGrantResponse {
grant_type: string;
code: string;
Expand All @@ -52,6 +70,7 @@ export interface AuthorizationGrantResponse {

export interface AccessTokenRequest {
client_id?: string;
code?: string;
code_verifier?: string;
grant_type: GrantTypes;
'pre-authorized_code': string;
Expand Down
1 change: 1 addition & 0 deletions lib/types/Generic.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export interface EndpointMetadata {
issuer: string;
token_endpoint: string;
credential_endpoint: string;
authorization_endpoint?: string;
openid4vci_metadata?: OpenID4VCIServerMetadata;
}
2 changes: 2 additions & 0 deletions lib/types/OpenID4VCIServerMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface OpenID4VCIServerMetadata {
credential_issuer?: CredentialIssuer; // A JSON object containing display properties for the Credential issuer.
token_endpoint?: string; //NON-SPEC compliant, but used by several issuers. URL of the OP's Token Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components.
authorization_server?: string; //NON-SPEC compliant, but used by some issuers. URL of the AS. This URL MUST use the https scheme and MAY contain port, path and query parameter components.
// TODO: The above authorization_server being used in the wild, serves roughly the same purpose as the below spec compliant endpoint. Look at how to use authorization_server as authorization_endpoint in case it is present
authorization_endpoint?: string;
}

export type Oauth2ASWithOID4VCIMetadata = OAuth2ASMetadata & OpenID4VCIServerMetadata;
Expand Down
47 changes: 34 additions & 13 deletions tests/AccessTokenClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import nock from 'nock';

import { AccessTokenClient, AccessTokenRequest, AccessTokenResponse, GrantTypes, OpenIDResponse } from '../lib';
import { AccessTokenClient, AccessTokenRequest, AccessTokenRequestOpts, AccessTokenResponse, GrantTypes, OpenIDResponse } from '../lib';

import { UNIT_TEST_TIMEOUT } from './IT.spec';
import { INITIATION_TEST } from './MetadataMocks';
Expand All @@ -11,8 +11,9 @@ describe('AccessTokenClient should', () => {
beforeEach(() => {
nock.cleanAll();
});

it(
'get Access Token without resulting in errors',
'get Access Token for with pre-authorized code without resulting in errors',
async () => {
const accessTokenClient: AccessTokenClient = new AccessTokenClient();

Expand Down Expand Up @@ -43,22 +44,32 @@ describe('AccessTokenClient should', () => {
);

it(
'get error',
'get Access Token for authorization code without resulting in errors',
async () => {
const accessTokenClient: AccessTokenClient = new AccessTokenClient();

const accessTokenRequest: AccessTokenRequest = {
client_id: 'test-client',
code_verifier: 'F0Y2OGARX2ppIERYdSVuLCV3Zi95Ci5yWzAYNU8QQC0',
code: '9mq3kwIuNZ88czRjJ2-UDxtaNXulOfxHSXo-kM01MLV',
redirect_uri: 'http://test.com/cb',
grant_type: GrantTypes.AUTHORIZATION_CODE,
} as AccessTokenRequest;

nock(MOCK_URL).post(/.*/).reply(200, '');
const body: AccessTokenResponse = {
access_token: '6W-kZopGNBq8e-5KvnGf2u0p0iGSxWZ7jIGV86nO1Dn',
expires_in: 3600,
scope: 'TestCredential',
token_type: 'Bearer',
};
nock(MOCK_URL).post(/.*/).reply(200, JSON.stringify(body));

await expect(
accessTokenClient.acquireAccessTokenUsingRequest({
accessTokenRequest,
asOpts: { as: MOCK_URL },
})
).rejects.toThrow('Only pre-authorized-code flow is supported');
const accessTokenResponse: OpenIDResponse<AccessTokenResponse> = await accessTokenClient.acquireAccessTokenUsingRequest({
accessTokenRequest,
asOpts: { as: MOCK_URL },
});

expect(accessTokenResponse.successBody).toEqual(body);
},
UNIT_TEST_TIMEOUT
);
Expand Down Expand Up @@ -180,11 +191,21 @@ describe('AccessTokenClient should', () => {
).rejects.toThrow(Error('Cannot set a pin, when the pin is not required.'));
});

it('get error for unsupported flow type', async () => {
it('get error if code_verifier is present when flow type is pre-authorized', async () => {
const accessTokenClient: AccessTokenClient = new AccessTokenClient();

await expect(accessTokenClient.acquireAccessTokenUsingRequest({ accessTokenRequest: {} as never })).rejects.toThrow(
Error('Only pre-authorized-code flow is supported')
nock(MOCK_URL).post(/.*/).reply(200, {});

const requestOpts: AccessTokenRequestOpts = {
issuanceInitiation: INITIATION_TEST,
pin: undefined,
codeVerifier: 'RylyWGQ-dzpObnEcoMBDIH9cTAwZXk1wYzktKxsOFgA',
code: 'LWCt225yj7gzT2cWeMP4hXj4B4oIYkEiGs4T6pfez91',
redirectUri: 'http://example.com/cb',
};

await expect(() => accessTokenClient.acquireAccessTokenUsingIssuanceInitiation(requestOpts)).rejects.toThrow(
Error('Cannot pass a code_verifier when flow type is pre-authorized')
);
});

Expand Down
Loading

0 comments on commit 56b16a3

Please sign in to comment.