Skip to content

Commit

Permalink
feat: full issuer and authorization server
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Nov 4, 2024
1 parent d9ad328 commit 2e81cf1
Show file tree
Hide file tree
Showing 49 changed files with 2,763 additions and 322 deletions.
25 changes: 24 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noRestrictedGlobals": {
"level": "error",
"options": {
"deniedGlobals": ["URL", "URLSearchParams", "fetch"]
}
}
},
"correctness": {
"recommended": true,
"noUnusedImports": "error"
Expand All @@ -41,5 +49,20 @@
"useIgnoreFile": true,
"clientKind": "git",
"enabled": true
}
},
"overrides": [
{
"include": ["*.test.ts", "**/tests/*"],
"linter": {
"rules": {
"style": {
"noRestrictedGlobals": {
"level": "info",
"options": {}
}
}
}
}
}
]
}
17 changes: 15 additions & 2 deletions packages/oauth2/src/Oauth2AuthorizationServer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseWithErrorHandling } from '@animo-id/oid4vc-utils'
import { type CreateAccessTokenOptions, createAccessTokenJwt } from './access-token/create-access-token'
import {
type CreateAccessTokenResponseOptions,
Expand All @@ -11,6 +12,10 @@ import {
verifyPreAuthorizedCodeAccessTokenRequest,
} from './access-token/verify-access-token-request'
import type { CallbackContext } from './callbacks'
import {
type AuthorizationServerMetadata,
vAuthorizationServerMetadata,
} from './metadata/authorization-server/v-authorization-server-metadata'

export interface Oauth2AuthorizationServerOptions {
/**
Expand All @@ -22,6 +27,14 @@ export interface Oauth2AuthorizationServerOptions {
export class Oauth2AuthorizationServer {
public constructor(private options: Oauth2AuthorizationServerOptions) {}

public createAuthorizationServerMetadata(authorizationServerMetadata: AuthorizationServerMetadata) {
return parseWithErrorHandling(
vAuthorizationServerMetadata,
authorizationServerMetadata,
'Error validating authorization server metadata'
)
}

/**
* Parse access token request and extract the grant specific properties.
*
Expand Down Expand Up @@ -52,13 +65,13 @@ export class Oauth2AuthorizationServer {
}

/**
* Create an access token.
* Create an access token response.
*
* The `sub` claim can be used to identify the resource owner is subsequent requests.
* For pre-auth flow this can be the pre-authorized_code but there are no requirements
* on the value.
*/
public async createAccessToken(
public async createAccessTokenResponse(
options: Pick<
CreateAccessTokenOptions,
| 'expiresInSeconds'
Expand Down
10 changes: 5 additions & 5 deletions packages/oauth2/src/__tests__/Oauth2AuthorizationServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('Oauth2AuthorizationServer', () => {
test('parse, verify and grant pre authorized code access token request', async () => {
const callbacks = {
...partialCallbacks,
signJwt: await getSignJwtCallback([dpopSignerJwk, accessTokenSignerJwk]),
signJwt: getSignJwtCallback([dpopSignerJwk, accessTokenSignerJwk]),
}

const createdDpopJwt = await createDpopJwt({
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('Oauth2AuthorizationServer', () => {
jwt: dpopJwt,
},

expectPreAuthorizedCode: grant.preAuthorizedCode,
expectedPreAuthorizedCode: grant.preAuthorizedCode,
expectedTxCode: grant.txCode,

now: new Date('2024-01-01'),
Expand All @@ -101,7 +101,7 @@ describe('Oauth2AuthorizationServer', () => {

expect(dpopJwk).toEqual(dpopSignerJwkPublic)

const accessTokenResponse = await authorizationServer.createAccessToken({
const accessTokenResponse = await authorizationServer.createAccessTokenResponse({
audience: 'https://credential-issuer.com',
authorizationServer: 'https://authorization-server.com',
expiresInSeconds: 300,
Expand Down Expand Up @@ -158,7 +158,7 @@ describe('Oauth2AuthorizationServer', () => {
test('parse, verify and grant authorization code access token request', async () => {
const callbacks = {
...partialCallbacks,
signJwt: await getSignJwtCallback([dpopSignerJwk, accessTokenSignerJwk]),
signJwt: getSignJwtCallback([dpopSignerJwk, accessTokenSignerJwk]),
}

const pkce = await createPkce({
Expand Down Expand Up @@ -237,7 +237,7 @@ describe('Oauth2AuthorizationServer', () => {

expect(dpopJwk).toEqual(dpopSignerJwkPublic)

const accessTokenResponse = await authorizationServer.createAccessToken({
const accessTokenResponse = await authorizationServer.createAccessTokenResponse({
audience: 'https://credential-issuer.com',
authorizationServer: 'https://authorization-server.com',
expiresInSeconds: 300,
Expand Down
8 changes: 4 additions & 4 deletions packages/oauth2/src/__tests__/Oauth2ResourceServer.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'
import { Oauth2ResourceServer } from '../Oauth2ResourceServer'
import { callbacks, getSignJwtCallback } from '../../tests/util'
import { Oauth2ResourceServer } from '../Oauth2ResourceServer'
import { createAccessTokenJwt } from '../access-token/create-access-token'
import type { Jwk, JwkSet } from '../common/jwk/v-jwk'
import { createDpopJwt } from '../dpop/dpop'
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('Oauth2ResourceServer', () => {
authorizationServer: authorizationServerMetadata.issuer,
callbacks: {
...callbacks,
signJwt: await getSignJwtCallback([dpopSignerJwk, accessTokenSignerJwk]),
signJwt: getSignJwtCallback([dpopSignerJwk, accessTokenSignerJwk]),
},
expiresInSeconds: 300,
signer: {
Expand All @@ -85,7 +85,7 @@ describe('Oauth2ResourceServer', () => {
const dpopJwt = await createDpopJwt({
callbacks: {
...callbacks,
signJwt: await getSignJwtCallback([dpopSignerJwk, accessTokenSignerJwk]),
signJwt: getSignJwtCallback([dpopSignerJwk, accessTokenSignerJwk]),
},
request: {
method: 'POST',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
preAuthorizedCode: 'hello2',
},
callbacks,
expectPreAuthorizedCode: 'hello',
expectedPreAuthorizedCode: 'hello',
request,
})
).rejects.toThrow(`Invalid 'pre-authorized_code' provided`)
Expand All @@ -47,7 +47,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
txCode: 'not-expected',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
request,
})
).rejects.toThrow(`Request contains 'tx_code' that was not expected`)
Expand All @@ -65,7 +65,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
preAuthorizedCode: 'hello2',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
expectedTxCode: 'expected',
request,
})
Expand All @@ -86,7 +86,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
txCode: 'provided-tx-code',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
expectedTxCode: 'expected',
request,
})
Expand All @@ -106,7 +106,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
preAuthorizedCode: 'hello2',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
request,
preAuthorizedCodeExpiresAt: new Date(now.getTime() - 100),
now,
Expand All @@ -126,7 +126,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
preAuthorizedCode: 'hello2',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
request,
pkce: {
codeChallenge: 'someting',
Expand All @@ -148,7 +148,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
preAuthorizedCode: 'hello2',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
request,
pkce: {
codeChallenge: 'someting',
Expand All @@ -170,7 +170,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
preAuthorizedCode: 'hello2',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
request,
pkce: {
codeVerifier: 'something',
Expand All @@ -195,7 +195,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
preAuthorizedCode: 'hello2',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
request,
dpop: {
required: true,
Expand All @@ -216,7 +216,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
preAuthorizedCode: 'hello2',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
request: {
...request,
headers: new Headers({
Expand Down Expand Up @@ -244,7 +244,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
const dpopJwt = await createDpopJwt({
callbacks: {
...callbacks,
signJwt: await getSignJwtCallback([dpopPrivateJwk]),
signJwt: getSignJwtCallback([dpopPrivateJwk]),
},
request,
signer: {
Expand All @@ -268,7 +268,7 @@ describe('Verify Pre Auhthorized Code Access Token Request', () => {
txCode: 'some-tx-code',
},
callbacks,
expectPreAuthorizedCode: 'hello2',
expectedPreAuthorizedCode: 'hello2',
expectedTxCode: 'some-tx-code',
// 1 minute
preAuthorizedCodeExpiresAt: new Date(now.getTime() + 60000),
Expand Down Expand Up @@ -465,7 +465,7 @@ describe('Verify Authorization Code Access Token Request', () => {
const dpopJwt = await createDpopJwt({
callbacks: {
...callbacks,
signJwt: await getSignJwtCallback([dpopPrivateJwk]),
signJwt: getSignJwtCallback([dpopPrivateJwk]),
},
request,
signer: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { valibotRecursiveFlattenIssues } from '@animo-id/oid4vc-utils'
import * as v from 'valibot'
import type { RequestLike } from '../common/v-common'
import { Oauth2ErrorCodes } from '../common/v-oauth2-error'
Expand Down Expand Up @@ -63,7 +64,7 @@ export function parseAccessTokenRequest(options: ParseAccessTokenRequestOptions)
if (!parsedAccessTokenRequest.success) {
throw new Oauth2ServerErrorResponseError({
error: Oauth2ErrorCodes.InvalidRequest,
error_description: `Error occured during validation of authorization request.\n${JSON.stringify(v.flatten(parsedAccessTokenRequest.issues), null, 2)}`,
error_description: `Error occured during validation of authorization request.\n${JSON.stringify(valibotRecursiveFlattenIssues(parsedAccessTokenRequest.issues), null, 2)}`,
})
}

Expand Down
27 changes: 10 additions & 17 deletions packages/oauth2/src/access-token/retrieve-access-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import * as v from 'valibot'
import { ValidationError } from '../../../utils/src/error/ValidationError'
import type { CallbackContext } from '../callbacks'
import { ContentType } from '../common/content-type'
import {
type RequestDpopOptions,
type ResponseDpopReturn,
createDpopJwt,
extractDpopNonceFromHeaders,
} from '../dpop/dpop'
import { type RequestDpopOptions, createDpopJwt, extractDpopNonceFromHeaders } from '../dpop/dpop'
import { shouldRetryTokenRequestWithDPoPNonce } from '../dpop/dpop-retry'
import { Oauth2ClientErrorResponseError } from '../error/Oauth2ClientErrorResponseError'
import { Oauth2InvalidFetchResponseError } from '../error/Oauth2InvalidFetchResponseError'
Expand All @@ -24,7 +19,7 @@ import {

export interface RetrieveAccessTokenReturn {
accessTokenResponse: AccessTokenResponse
dpop?: ResponseDpopReturn
dpop?: RequestDpopOptions
}

interface RetrieveAccessTokenBaseOptions {
Expand Down Expand Up @@ -154,16 +149,13 @@ async function retrieveAccessToken(options: RetrieveAccessTokenOptions): Promise
options.request,
'Error validating access token request'
)
// if (options.issuerMetadata.originalDraftVersion === Oid4vciDraftVersion.Draft11) {
// accessTokenRequest = parseWithErrorHandling(
// vAccessTokenRequestDraft14To11,
// accessTokenRequest,
// 'Error transforming draft 14 access token request into draft 11 request'
// )
// }

const requestQueryParams = objectToQueryParams(accessTokenRequest)
// For backwards compat with draft 11 (we send both)
if (accessTokenRequest.tx_code) {
accessTokenRequest.user_pin = accessTokenRequest.tx_code
}

const requestQueryParams = objectToQueryParams(accessTokenRequest)
const { response, result } = await fetchWithValibot(
vAccessTokenResponse,
options.authorizationServerMetadata.token_endpoint,
Expand Down Expand Up @@ -204,11 +196,12 @@ async function retrieveAccessToken(options: RetrieveAccessTokenOptions): Promise
throw new ValidationError('Error validating access token response', result.issues)
}

const dpopNonce = extractDpopNonceFromHeaders(response.headers)
const dpopNonce = extractDpopNonceFromHeaders(response.headers) ?? undefined
return {
dpop: dpopNonce
dpop: options.dpop
? {
nonce: dpopNonce,
signer: options.dpop.signer,
}
: undefined,
accessTokenResponse: result.output,
Expand Down
Loading

0 comments on commit 2e81cf1

Please sign in to comment.