Skip to content

Commit 5838421

Browse files
committed
style: create generic error creation functions
Signed-off-by: Andres Olave <arolave@gmail.com>
1 parent bc00ac4 commit 5838421

File tree

2 files changed

+78
-82
lines changed

2 files changed

+78
-82
lines changed

packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts

Lines changed: 48 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import {
99
CredentialRequest,
1010
getCredentialConfigurationsMatchingRequestFormat,
1111
} from '@openid4vc/openid4vci'
12-
1312
import createHttpError from 'http-errors'
1413
import { getCredentialConfigurationsSupportedForScopes } from '../../shared'
1514
import { CredoRouter, getRequestContext } from '../../shared/router'
1615
import { addSecondsToDate } from '../../shared/utils'
1716
import { OpenId4VcIssuanceSessionState } from '../OpenId4VcIssuanceSessionState'
1817
import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService'
1918
import { OpenId4VcIssuanceSessionRecord, OpenId4VcIssuanceSessionRepository } from '../repository'
19+
import { oauth2Error, oauth2UnauthorizedError } from '../util/errors'
2020

2121
export function configureCredentialEndpoint(router: CredoRouter, config: BaseOpenId4VcIssuerModuleConfig) {
2222
router.post(config.credentialEndpointPath, async (request: OpenId4VcIssuancePostRequest<CredentialRequest>) => {
@@ -41,21 +41,12 @@ export function configureCredentialEndpoint(router: CredoRouter, config: BaseOpe
4141
},
4242
})
4343
.catch((error: unknown) => {
44-
throw createHttpError(
45-
403,
46-
error instanceof Oauth2ResourceUnauthorizedError ? error.message : 'Unknown error occured',
47-
{
48-
headers: {
49-
'WWW-Authenticate':
50-
error instanceof Oauth2ResourceUnauthorizedError
51-
? error.toHeaderValue()
52-
: new Oauth2ResourceUnauthorizedError(
53-
'No credential configurations match credential request and access token scope',
54-
[{ scheme: SupportedAuthenticationScheme.DPoP }, { scheme: SupportedAuthenticationScheme.Bearer }]
55-
).toHeaderValue(),
56-
},
57-
}
58-
)
44+
throw error instanceof Oauth2ResourceUnauthorizedError
45+
? oauth2UnauthorizedError(error.message, error.wwwAuthenticateHeaders)
46+
: oauth2UnauthorizedError('Unknown error occured', [
47+
{ scheme: SupportedAuthenticationScheme.DPoP },
48+
{ scheme: SupportedAuthenticationScheme.Bearer },
49+
])
5950
})
6051
if (!resourceRequestResult) return
6152
const { tokenPayload, accessToken, scheme, authorizationServer } = resourceRequestResult
@@ -75,34 +66,25 @@ export function configureCredentialEndpoint(router: CredoRouter, config: BaseOpe
7566

7667
const subject = tokenPayload.sub
7768
if (!subject) {
78-
throw createHttpError(
79-
400,
80-
`Received token without 'sub' claim. Subject is required for binding issuance session`,
81-
{
82-
type: 'oauth2_error',
83-
errorResponse: { error: Oauth2ErrorCodes.ServerError },
84-
}
69+
throw oauth2Error(
70+
Oauth2ErrorCodes.ServerError,
71+
`Received token without 'sub' claim. Subject is required for binding issuance session`
8572
)
8673
}
8774

8875
// Already handle request without format. Simplifies next code sections
8976
if (!parsedCredentialRequest.format) {
90-
throw createHttpError(
91-
400,
92-
parsedCredentialRequest.credentialIdentifier
93-
? `Credential request containing 'credential_identifier' not supported`
94-
: parsedCredentialRequest.credentialConfigurationId
95-
? `Credential configuration '${parsedCredentialRequest.credentialConfigurationId}' not supported`
96-
: `Credential format '${parsedCredentialRequest.credentialRequest.format}' not supported`,
97-
{
98-
type: 'oauth2_error',
99-
errorResponse: {
100-
error: parsedCredentialRequest.credentialIdentifier
101-
? Oauth2ErrorCodes.InvalidCredentialRequest
102-
: Oauth2ErrorCodes.UnsupportedCredentialFormat,
103-
},
104-
}
105-
)
77+
throw parsedCredentialRequest.credentialIdentifier
78+
? oauth2Error(
79+
Oauth2ErrorCodes.InvalidCredentialRequest,
80+
`Credential request containing 'credential_identifier' not supported`
81+
)
82+
: oauth2Error(
83+
Oauth2ErrorCodes.UnsupportedCredentialFormat,
84+
parsedCredentialRequest.credentialConfigurationId
85+
? `Credential configuration '${parsedCredentialRequest.credentialConfigurationId}' not supported`
86+
: `Credential format '${parsedCredentialRequest.credentialRequest.format}' not supported`
87+
)
10688
}
10789

10890
if (preAuthorizedCode || issuerState) {
@@ -124,34 +106,28 @@ export function configureCredentialEndpoint(router: CredoRouter, config: BaseOpe
124106
}
125107
)
126108

127-
throw createHttpError(
128-
400,
129-
`No issuance session found for incoming credential request for issuer ${issuer.issuerId} and access token data`,
130-
{
131-
type: 'oauth2_error',
132-
errorResponse: { error: Oauth2ErrorCodes.CredentialRequestDenied },
133-
}
109+
throw oauth2Error(
110+
Oauth2ErrorCodes.CredentialRequestDenied,
111+
`No issuance session found for incoming credential request for issuer ${issuer.issuerId} and access token data`
134112
)
135113
}
136114

137115
// Use issuance session dpop config
138116
if (issuanceSession.dpop?.required && !resourceRequestResult.dpop) {
139-
return createHttpError(401, 'Missing required DPoP proof', agentContext.config.logger, {
140-
tyoe: 'oauth2_error',
141-
errorResponse: { error: Oauth2ErrorCodes.InvalidDpopProof },
142-
})
117+
return oauth2UnauthorizedError('Missing required DPoP proof', [
118+
{
119+
scheme,
120+
error: Oauth2ErrorCodes.InvalidDpopProof,
121+
},
122+
])
143123
}
144124

145125
// Verify the issuance session subject
146126
if (issuanceSession.authorization?.subject) {
147127
if (issuanceSession.authorization.subject !== tokenPayload.sub) {
148-
throw createHttpError(
149-
400,
150-
`Issuance session authorization subject does not match with the token payload subject for issuance session '${issuanceSession.id}'. Returning error response`,
151-
{
152-
type: 'oauth2_error',
153-
errorResponse: { error: Oauth2ErrorCodes.CredentialRequestDenied },
154-
}
128+
throw oauth2Error(
129+
Oauth2ErrorCodes.CredentialRequestDenied,
130+
`Issuance session authorization subject does not match with the token payload subject for issuance session '${issuanceSession.id}'. Returning error response`
155131
)
156132
}
157133
}
@@ -163,10 +139,7 @@ export function configureCredentialEndpoint(router: CredoRouter, config: BaseOpe
163139
) {
164140
issuanceSession.errorMessage = 'Credential offer has expired'
165141
await openId4VcIssuerService.updateState(agentContext, issuanceSession, OpenId4VcIssuanceSessionState.Error)
166-
throw createHttpError(400, 'Session expired', {
167-
type: 'oauth2_error',
168-
errorResponse: { error: Oauth2ErrorCodes.CredentialRequestDenied },
169-
})
142+
throw oauth2Error(Oauth2ErrorCodes.CredentialRequestDenied, 'Session expired')
170143
} else {
171144
issuanceSession.authorization = {
172145
...issuanceSession.authorization,
@@ -186,10 +159,12 @@ export function configureCredentialEndpoint(router: CredoRouter, config: BaseOpe
186159

187160
// Use global config when creating a dynamic session
188161
if (config.dpopRequired && !resourceRequestResult.dpop) {
189-
return createHttpError(401, 'Missing required DPoP proof', agentContext.config.logger, {
190-
tyoe: 'oauth2_error',
191-
errorResponse: { error: Oauth2ErrorCodes.InvalidDpopProof },
192-
})
162+
throw oauth2UnauthorizedError('Missing required DPoP proof', [
163+
{
164+
scheme: scheme,
165+
error: Oauth2ErrorCodes.InvalidDpopProof,
166+
},
167+
])
193168
}
194169

195170
const configurationsForScope = getCredentialConfigurationsSupportedForScopes(
@@ -215,17 +190,12 @@ export function configureCredentialEndpoint(router: CredoRouter, config: BaseOpe
215190
}
216191

217192
if (Object.keys(configurationsForToken).length === 0) {
218-
throw createHttpError(403, 'No credential configurations match credential request and access token scope', {
219-
headers: {
220-
'WWW-Authenticate': new Oauth2ResourceUnauthorizedError(
221-
'No credential configurationss match credential request and access token scope',
222-
{
223-
scheme,
224-
error: Oauth2ErrorCodes.InsufficientScope,
225-
}
226-
).toHeaderValue(),
193+
throw oauth2UnauthorizedError('No credential configurations match credential request and access token scope', [
194+
{
195+
scheme,
196+
error: Oauth2ErrorCodes.InsufficientScope,
227197
},
228-
})
198+
])
229199
}
230200

231201
issuanceSession = new OpenId4VcIssuanceSessionRecord({
@@ -251,13 +221,9 @@ export function configureCredentialEndpoint(router: CredoRouter, config: BaseOpe
251221
await issuanceSessionRepository.save(agentContext, issuanceSession)
252222
openId4VcIssuerService.emitStateChangedEvent(agentContext, issuanceSession, null)
253223
} else if (!issuanceSession) {
254-
throw createHttpError(
255-
400,
256-
`Access token without 'issuer_state' or 'pre-authorized_code' issued by external authorization server provided, but 'allowDynamicIssuanceSessions' is disabled. Either bind the access token to a stateful credential offer, or enable 'allowDynamicIssuanceSessions'.`,
257-
{
258-
type: 'oauth2_error',
259-
errorResponse: { error: Oauth2ErrorCodes.CredentialRequestDenied },
260-
}
224+
throw oauth2Error(
225+
Oauth2ErrorCodes.CredentialRequestDenied,
226+
`Access token without 'issuer_state' or 'pre-authorized_code' issued by external authorization server provided, but 'allowDynamicIssuanceSessions' is disabled. Either bind the access token to a stateful credential offer, or enable 'allowDynamicIssuanceSessions'.`
261227
)
262228
}
263229

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { encodeWwwAuthenticateHeader } from '@openid4vc/utils'
2+
3+
import { WwwAuthenticateHeaderChallenge } from '@openid4vc/oauth2'
4+
import createHttpError from 'http-errors'
5+
6+
export function oauth2Error(errorCode: string, message: string, status = 400) {
7+
return createHttpError(status, message, {
8+
type: 'oauth2_error',
9+
errorResponse: { error: errorCode },
10+
})
11+
}
12+
13+
export function oauth2UnauthorizedError(message: string, wwwAuthenticateHeaders: WwwAuthenticateHeaderChallenge[]) {
14+
return createHttpError(403, message, {
15+
type: 'oauth2_error',
16+
headers: {
17+
'WWW-Authenticate': encodeWwwAuthenticateHeader(
18+
wwwAuthenticateHeaders.map((header) => ({
19+
scheme: header.scheme,
20+
payload: {
21+
error: header.error ?? null,
22+
error_description: header.error_description ?? null,
23+
scope: header.scope ?? null,
24+
},
25+
...header.additionalPayload,
26+
}))
27+
),
28+
},
29+
})
30+
}

0 commit comments

Comments
 (0)