Skip to content

Commit

Permalink
chore: added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Brummos committed Jan 15, 2025
1 parent 1eef34b commit c6c5df2
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 11 deletions.
10 changes: 10 additions & 0 deletions packages/client/lib/MetadataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class MetadataClient {
let credential_endpoint: string | undefined;
let deferred_credential_endpoint: string | undefined;
let authorization_endpoint: string | undefined;
let authorization_challenge_endpoint: string | undefined;
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
let authorization_servers: string[] | undefined = [issuer];
let authorization_server: string | undefined = undefined;
Expand Down Expand Up @@ -130,6 +131,14 @@ export class MetadataClient {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (!authMetadata.authorization_challenge_endpoint) {
throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`);
} else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) {
throw Error(
`Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`,
);
}
authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint;
if (!authMetadata.token_endpoint) {
throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`);
} else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
Expand Down Expand Up @@ -193,6 +202,7 @@ export class MetadataClient {
deferred_credential_endpoint,
...(authorization_server ? { authorization_server } : { authorization_servers: authorization_servers }),
authorization_endpoint,
authorization_challenge_endpoint,
authorizationServerType,
credentialIssuerMetadata: authorization_server
? (credentialIssuerMetadata as IssuerMetadataV1_0_08 & Partial<AuthorizationServerMetadata>)
Expand Down
10 changes: 10 additions & 0 deletions packages/client/lib/MetadataClientV1_0_11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class MetadataClientV1_0_11 {
let credential_endpoint: string | undefined;
let deferred_credential_endpoint: string | undefined;
let authorization_endpoint: string | undefined;
let authorization_challenge_endpoint: string | undefined;
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
let authorization_server: string = issuer;
const oid4vciResponse = await MetadataClientV1_0_11.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations
Expand Down Expand Up @@ -105,6 +106,14 @@ export class MetadataClientV1_0_11 {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (!authMetadata.authorization_challenge_endpoint) {
throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`);
} else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) {
throw Error(
`Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`,
);
}
authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint;
if (!authMetadata.token_endpoint) {
throw Error(`Authorization Sever ${authorization_server} did not provide a token_endpoint`);
} else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
Expand Down Expand Up @@ -165,6 +174,7 @@ export class MetadataClientV1_0_11 {
deferred_credential_endpoint,
authorization_server,
authorization_endpoint,
authorization_challenge_endpoint,
authorizationServerType,
credentialIssuerMetadata: credentialIssuerMetadata as unknown as Partial<AuthorizationServerMetadata> & IssuerMetadataV1_0_08,
authorizationServerMetadata: authMetadata,
Expand Down
10 changes: 10 additions & 0 deletions packages/client/lib/MetadataClientV1_0_13.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class MetadataClientV1_0_13 {
let credential_endpoint: string | undefined;
let deferred_credential_endpoint: string | undefined;
let authorization_endpoint: string | undefined;
let authorization_challenge_endpoint: string | undefined;
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
let authorization_servers: string[] = [issuer];
const oid4vciResponse = await MetadataClientV1_0_13.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations
Expand Down Expand Up @@ -104,6 +105,14 @@ export class MetadataClientV1_0_13 {
);
}
authorization_endpoint = authMetadata.authorization_endpoint;
if (!authMetadata.authorization_challenge_endpoint) {
throw Error(`Authorization Sever ${authorization_challenge_endpoint} did not provide a authorization_challenge_endpoint`);
} else if (authorization_challenge_endpoint && authMetadata.authorization_challenge_endpoint !== authorization_challenge_endpoint) {
throw Error(
`Credential issuer has a different authorization_challenge_endpoint (${authorization_challenge_endpoint}) from the Authorization Server (${authMetadata.authorization_challenge_endpoint})`,
);
}
authorization_challenge_endpoint = authMetadata.authorization_challenge_endpoint;
if (!authMetadata.token_endpoint) {
throw Error(`Authorization Sever ${authorization_servers} did not provide a token_endpoint`);
} else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
Expand Down Expand Up @@ -164,6 +173,7 @@ export class MetadataClientV1_0_13 {
deferred_credential_endpoint,
authorization_server: authorization_servers[0],
authorization_endpoint,
authorization_challenge_endpoint,
authorizationServerType,
credentialIssuerMetadata: credentialIssuerMetadata,
authorizationServerMetadata: authMetadata,
Expand Down
5 changes: 5 additions & 0 deletions packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('VcIssuer', () => {
.withCredentialEndpoint('http://localhost:3456/test/credential-endpoint')
.withTokenEndpoint('http://localhost:3456/test/token')
.withAuthorizationEndpoint('https://token-endpoint.example.com/authorize')
.withAuthorizationChallengeEndpoint('http://localhost:3456/test/authorize-challenge')
.withTokenEndpointAuthMethodsSupported(['none', 'client_secret_basic', 'client_secret_jwt', 'client_secret_post'])
.withResponseTypesSupported(['code', 'token', 'id_token'])
.withScopesSupported(['openid', 'abcdef'])
Expand Down Expand Up @@ -266,6 +267,7 @@ describe('VcIssuer', () => {
it('should retrieve server metadata', async () => {
await expect(client.retrieveServerMetadata()).resolves.toEqual({
authorizationServerMetadata: {
authorization_challenge_endpoint: 'http://localhost:3456/test/authorize-challenge',
authorization_endpoint: 'https://token-endpoint.example.com/authorize',
credential_endpoint: 'http://localhost:3456/test/credential-endpoint',
issuer: 'http://localhost:3456/test',
Expand All @@ -275,6 +277,7 @@ describe('VcIssuer', () => {
token_endpoint_auth_methods_supported: ['none', 'client_secret_basic', 'client_secret_jwt', 'client_secret_post'],
},
authorizationServerType: 'OID4VCI',
authorization_challenge_endpoint: 'http://localhost:3456/test/authorize-challenge',
authorization_endpoint: 'https://token-endpoint.example.com/authorize',
deferred_credential_endpoint: undefined,
authorization_server: 'http://localhost:3456/test',
Expand Down Expand Up @@ -316,6 +319,7 @@ describe('VcIssuer', () => {
token_endpoint: 'http://localhost:3456/test/token',
})
})

it('should get state on server side', async () => {
const preAuthCode = client.credentialOffer!.credential_offer.grants?.[PRE_AUTH_GRANT_LITERAL]?.[PRE_AUTH_CODE_LITERAL]
expect(preAuthCode).toBeDefined()
Expand Down Expand Up @@ -382,4 +386,5 @@ describe('VcIssuer', () => {
format: 'jwt_vc_json',
})
})

})
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { uuidv4 } from '@sphereon/oid4vc-common'
import {
AuthorizationChallengeError,
CNonceState,
CredentialIssuerMetadataOptsV1_0_13,
CredentialOfferSession,
IssueStatus
} from '@sphereon/oid4vci-common'
import { VcIssuer } from '@sphereon/oid4vci-issuer'
import { AuthorizationServerMetadataBuilder } from '@sphereon/oid4vci-issuer'
import { MemoryStates } from '@sphereon/oid4vci-issuer/dist/state-manager'
import { ExpressBuilder, ExpressSupport } from '@sphereon/ssi-express-support'
import { DIDDocument } from 'did-resolver'
import { Express } from 'express'
import requests from 'supertest'

import { OID4VCIServer } from '../OID4VCIServer'

const authorizationServerMetadata = new AuthorizationServerMetadataBuilder()
.withIssuer('test-issuer')
.withAuthorizationChallengeEndpoint('http://localhost:3456/test/authorize-challenge')
.withResponseTypesSupported(['code', 'token', 'id_token'])
.build()

describe('OID4VCIServer', () => {
let app: Express
let expressSupport: ExpressSupport
const sessionId = 'c1413695-8744-4369-845b-c2bd0ee8d5e4'

beforeAll(async () => {
const credentialOfferState1: CredentialOfferSession = {
// preAuthorizedCode: preAuthorizedCode1,
txCode: '493536',
notification_id: uuidv4(),
createdAt: +new Date(),
lastUpdatedAt: +new Date(),
status: IssueStatus.OFFER_CREATED,
credentialOffer: {
credential_offer: {
credential_issuer: 'test_issuer',
credentials: [
{
format: 'ldp_vc',
credential_definition: {
'@context': ['test_context'],
types: ['VerifiableCredential'],
credentialSubject: {},
},
},
],

// grants: {
// 'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
// tx_code: {
// length: 6,
// input_mode: 'numeric',
// description: 'Please enter the 6 digit code you received on your phone',
// },
// //'pre-authorized_code': preAuthorizedCode1,
// },
// },
},
},
}
const credentialOfferSessions = new MemoryStates<CredentialOfferSession>()
await credentialOfferSessions.set(sessionId, credentialOfferState1)

const vcIssuer: VcIssuer<DIDDocument> = new VcIssuer<DIDDocument>(
{
credential_endpoint: 'http://localhost:9000',
authorization_challenge_endpoint: 'http://localhost:9000/authorize-challenge',
} as CredentialIssuerMetadataOptsV1_0_13,
authorizationServerMetadata,
{
cNonceExpiresIn: 300,
credentialOfferSessions,
cNonces: new MemoryStates<CNonceState>(),
},
)

expressSupport = ExpressBuilder.fromServerOpts({
startListening: false,
port: 9000,
hostname: '0.0.0.0',
}).build({ startListening: false })
const vcIssuerServer = new OID4VCIServer(expressSupport, {
issuer: vcIssuer,
baseUrl: 'http://localhost:9000',
endpointOpts: {
tokenEndpointOpts: {
tokenEndpointDisabled: true
},
authorizationChallengeOpts: {
enabled: true,
verifyAuthResponseCallback: async () => true,
createAuthRequestUriCallback: async () => '/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234'
}
},
})
expressSupport.start()
app = vcIssuerServer.app
})

afterAll(async () => {
if (expressSupport) {
await expressSupport.stop()
}
await new Promise((resolve) => setTimeout((v: void) => resolve(v), 500))
})

it('should return http code 400 with error invalid_request', async () => {
const res = await requests(app)
.post('/authorize-challenge')
.send(`client_id=${uuidv4()}`)
expect(res.statusCode).toEqual(400)
const actual = JSON.parse(res.text)
expect(actual).toEqual({
error: AuthorizationChallengeError.invalid_request
})
})

it('should return http code 400 with message No client id or auth session present', async () => {
const res = await requests(app)
.post('/authorize-challenge')
.send()
expect(res.statusCode).toEqual(400)
const actual = JSON.parse(res.text)
expect(actual).toEqual({
error: AuthorizationChallengeError.invalid_request,
error_description: 'No client id or auth session present'
})
})

it('should return http code 400 with message Session is invalid with invalid issuer_state', async () => {
const res = await requests(app)
.post('/authorize-challenge')
.send(`client_id=${uuidv4()}&issuer_state=${uuidv4()}`)
expect(res.statusCode).toEqual(400)
const actual = JSON.parse(res.text)
expect(actual).toEqual({
error: AuthorizationChallengeError.invalid_session,
error_description: 'Session is invalid'
})
})

it('should return http code 400 with message No definition id present', async () => {
const res = await requests(app)
.post('/authorize-challenge')
.send(`client_id=${uuidv4()}&issuer_state=${sessionId}`)
expect(res.statusCode).toEqual(400)
const actual = JSON.parse(res.text)
expect(actual).toEqual({
error: AuthorizationChallengeError.invalid_request,
error_description: 'No definition id present'
})
})

it('should return http code 400 with error insufficient_authorization', async () => {
const res = await requests(app)
.post('/authorize-challenge')
.send(`client_id=${uuidv4()}&issuer_state=${sessionId}&definition_id=${'testValue'}`)
expect(res.statusCode).toEqual(400)
const actual = JSON.parse(res.text)
expect(actual).toEqual({
error: AuthorizationChallengeError.insufficient_authorization,
auth_session: "c1413695-8744-4369-845b-c2bd0ee8d5e4",
presentation: "/authorize?client_id=..&request_uri=https://rp.example.com/oidc/request/1234"
})
})

it('should return http code 400 with message Session is invalid with invalid auth_session', async () => {
const res = await requests(app)
.post('/authorize-challenge')
.send(`auth_session=${uuidv4()}&presentation_during_issuance_session=${uuidv4()}&definition_id=testDefinitionId`)
expect(res.statusCode).toEqual(400)
const actual = JSON.parse(res.text)
expect(actual).toEqual({
error: AuthorizationChallengeError.invalid_session,
error_description: 'Session is invalid'
})
})

it('should return http code 200 with authorization_code', async () => {
const res = await requests(app)
.post('/authorize-challenge')
.send(`auth_session=${sessionId}&presentation_during_issuance_session=${uuidv4()}&definition_id=testDefinitionId`)
expect(res.statusCode).toEqual(200)
const actual = JSON.parse(res.text)
expect(actual).toBeDefined()
expect(actual.authorization_code).toBeDefined()
})

})
26 changes: 15 additions & 11 deletions packages/issuer-rest/lib/oid4vci-api-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,25 +123,28 @@ export function authorizationChallengeEndpoint<DIDDoc extends object>(
try {
if (!client_id && !auth_session) {
const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {
error: AuthorizationChallengeError.invalid_request
}
return Promise.reject(authorizationChallengeErrorResponse)
error: AuthorizationChallengeError.invalid_request,
error_description: 'No client id or auth session present'
} as AuthorizationChallengeErrorResponse
throw authorizationChallengeErrorResponse
}

if (!auth_session && issuer_state) {
const session = await issuer.credentialOfferSessions.get(issuer_state)
if (!session) {
const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {
error: AuthorizationChallengeError.invalid_session
error: AuthorizationChallengeError.invalid_session,
error_description: 'Session is invalid'
}
return Promise.reject(authorizationChallengeErrorResponse)
throw authorizationChallengeErrorResponse
}

if (!definition_id) {
const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {
error: AuthorizationChallengeError.invalid_request
error: AuthorizationChallengeError.invalid_request,
error_description: 'No definition id present'
}
return Promise.reject(authorizationChallengeErrorResponse)
throw authorizationChallengeErrorResponse
}

const authRequestURI = await opts.createAuthRequestUriCallback(definition_id, issuer_state)
Expand All @@ -150,16 +153,17 @@ export function authorizationChallengeEndpoint<DIDDoc extends object>(
auth_session: issuer_state,
presentation: authRequestURI
}
return Promise.reject(authorizationChallengeErrorResponse)
throw authorizationChallengeErrorResponse
}

if (auth_session && presentation_during_issuance_session && definition_id) {
const session = await issuer.credentialOfferSessions.get(auth_session)
if (!session) {
const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {
error: AuthorizationChallengeError.invalid_session
error: AuthorizationChallengeError.invalid_session,
error_description: 'Session is invalid'
}
return Promise.reject(authorizationChallengeErrorResponse)
throw authorizationChallengeErrorResponse
}

const verifiedResponse = await opts.verifyAuthResponseCallback(definition_id, presentation_during_issuance_session)
Expand All @@ -177,7 +181,7 @@ export function authorizationChallengeEndpoint<DIDDoc extends object>(
const authorizationChallengeErrorResponse: AuthorizationChallengeErrorResponse = {
error: AuthorizationChallengeError.invalid_request
}
return Promise.reject(authorizationChallengeErrorResponse)
throw authorizationChallengeErrorResponse
} catch (e) {
return sendErrorResponse(
response,
Expand Down
1 change: 1 addition & 0 deletions packages/oid4vci-common/lib/types/ServerMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export interface AuthorizationServerMetadata extends DynamicRegistrationClientMe
export const authorizationServerMetadataFieldNames: Array<keyof AuthorizationServerMetadata> = [
'issuer',
'authorization_endpoint',
'authorization_challenge_endpoint',
'token_endpoint',
'jwks_uri',
'registration_endpoint',
Expand Down

0 comments on commit c6c5df2

Please sign in to comment.