Skip to content

Commit

Permalink
feat!: refactor packages to use byu-sdk jwt verification (#158)
Browse files Browse the repository at this point in the history
* feat!: refactor jwt and fastify packages to use byu-sdk jwt verification (#154)

* chore(release): publish

 - @byu-oit/[email protected]
 - @byu-oit/[email protected]

* fix: update dependencies (#155)

* chore(release): publish

 - @byu-oit/[email protected]
 - @byu-oit/[email protected]

* fix(fastify): flatten ByuJwtAuthenticator options (#156)

* chore(release): publish

 - @byu-oit/[email protected]

* fix: update dependencies (#157)

* chore(release): publish

 - @byu-oit/[email protected]
 - @byu-oit/[email protected]

---------

Co-authored-by: tylerablackham <[email protected]>
  • Loading branch information
tylerablackham and tylerablackham authored Sep 25, 2023
1 parent 8d39a95 commit b0567c0
Show file tree
Hide file tree
Showing 25 changed files with 2,471 additions and 2,291 deletions.
3,278 changes: 2,078 additions & 1,200 deletions package-lock.json

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions packages/fastify/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [0.1.7-beta.3](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-09-25)


### Bug Fixes

* update dependencies ([#157](https://github.com/byu-oit/byu-jwt-nodejs/issues/157)) ([1a3229c](https://github.com/byu-oit/byu-jwt-nodejs/commit/1a3229c1e8e6baaee03ee29946a7a1d29f5009c6))





## [0.1.7-beta.2](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-09-18)


### Bug Fixes

* **fastify:** flatten ByuJwtAuthenticator options ([#156](https://github.com/byu-oit/byu-jwt-nodejs/issues/156)) ([b611bd0](https://github.com/byu-oit/byu-jwt-nodejs/commit/b611bd0d9584efce2e0ec19ac43bbf41a2174cea))





## [0.1.7-beta.1](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-09-14)


### Bug Fixes

* update dependencies ([#155](https://github.com/byu-oit/byu-jwt-nodejs/issues/155)) ([e20663e](https://github.com/byu-oit/byu-jwt-nodejs/commit/e20663ecfd7c6c42a09ee48fa272fee85e694cfb))





## [0.1.7-beta.0](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-09-11)

**Note:** Version bump only for package @byu-oit/fastify-jwt





## [0.1.6](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-07-10)


Expand Down
17 changes: 15 additions & 2 deletions packages/fastify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,23 @@ const fastify = Fastify({ logger })

fastify.register(ByuJwtProvider, {
/** Only authenticate routes matching this prefix */
prefix: '/example/v1',
prefix: '/example/v1',
development: process.env.NODE_ENV === 'development',
/** May pass in ByuJwt options from @byu-oit/jwt */
development: process.env.NODE_ENV === 'development'
issuer: 'https://api.byu.edu',
additionalValidations: [(jwt) => {
if(false) throw new Error('This will never happen')
}]
})

await fastify.listen({ port: 3000 }).catch(console.error)
```

## Options
In addition to the three properties below, you can also pass in any options that are defined in [BYU JWT](https://byu-oit.github.io/byu-jwt-nodejs/modules/BYU_JWT.html#md:options) documentation as well.

| property | type | default | description |
|------------------|---------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| prefix | string | `undefined` | Will only authenticate routes matching this prefix. |
| development | boolean | false | skips JWT verification for development purposes but will throw an error if NODE_ENV is set to `production`. |
| basePath | string | `undefined` | will validate that the audience starts with the provided basePath in production. |
6 changes: 4 additions & 2 deletions packages/fastify/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@byu-oit/fastify-jwt",
"version": "0.1.6",
"version": "0.1.7-beta.3",
"description": "A Fastify plugin for verifying callers JWTs with the BYU JWT package",
"keywords": [],
"author": "Spencer Tuft <[email protected]>",
Expand Down Expand Up @@ -40,7 +40,9 @@
},
"homepage": "https://github.com/byu-oit/byu-jwt-nodejs#readme",
"dependencies": {
"@byu-oit/jwt": "^0.0.6",
"@byu-oit-sdk/jwt": "^0.1.0",
"@byu-oit/jwt": "^0.0.7-beta.2",
"@sinclair/typebox": "^0.31.2",
"@types/node": "^18.16.2",
"fastify": "^4.17.0",
"fastify-plugin": "^4.5.0"
Expand Down
66 changes: 46 additions & 20 deletions packages/fastify/src/ByuJwtAuthenticator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { ByuJwt, type JwtPayload } from '@byu-oit/jwt'
import { ByuJwt, type JwtPayload, type CreateByuJwtOptions, type TransformedJwtPayload } from '@byu-oit/jwt'
import { TokenError } from 'fast-jwt'
import { type IncomingHttpHeaders } from 'http'
import { BYU_JWT_ERROR_CODES, ByuJwtError } from './ByuJwtError.js'

export class ByuJwtAuthenticator extends ByuJwt {
export interface ByuJwtAuthenticatorOptions extends CreateByuJwtOptions {
development?: boolean
basePath?: string
}

export class ByuJwtAuthenticator {
static HEADER_CURRENT = 'x-jwt-assertion'
static HEADER_ORIGINAL = 'x-jwt-assertion-original'

private readonly ByuJwt: typeof ByuJwt
private readonly development: boolean

constructor ({ development, basePath, ...byuJwtOptions }: ByuJwtAuthenticatorOptions = {}) {
this.development = development ?? false
/** Extra validation step if basePath is provided */
if (basePath != null) {
if (byuJwtOptions.additionalValidations == null) {
byuJwtOptions.additionalValidations = []
}
byuJwtOptions.additionalValidations.push(apiContextValidationFunction(basePath))
}
this.ByuJwt = ByuJwt.create(byuJwtOptions)
}

async authenticate (headers: IncomingHttpHeaders): Promise<JwtPayload> {
/** Verify any known JWT headers */
const JwtHeaders = [
Expand All @@ -17,7 +37,7 @@ export class ByuJwtAuthenticator extends ByuJwt {
const jwt = headers[header]
if (typeof jwt !== 'string') return undefined
try {
const { payload } = await this.verify(jwt)
const { payload } = this.development ? this.ByuJwt.decode(jwt) : await this.ByuJwt.verify(jwt)
return payload
} catch (e) {
if (e instanceof TokenError) {
Expand All @@ -32,24 +52,30 @@ export class ByuJwtAuthenticator extends ByuJwt {
throw new ByuJwtError(BYU_JWT_ERROR_CODES.missingExpectedJwt, 'Missing expected JWT')
}

/** Extra validation step if basePath is provided */
const basePath = this.basePath
if (basePath != null) {
const context = current.apiContext
if (!context.startsWith(basePath)) {
throw new ByuJwtError(BYU_JWT_ERROR_CODES.invalidApiContext, 'Invalid API context in JWT')
}
/** Check that the JWT is meant for the audience */
if (current.aud != null) {
const audiences = typeof current.aud === 'string' ? [current.aud] : current.aud
const hasAValidAudience = !audiences.some(audience => audience.startsWith(basePath))
if (hasAValidAudience) {
throw new ByuJwtError(BYU_JWT_ERROR_CODES.invalidAudience, 'Invalid aud in JWT')
}
}
}

/** Prioritize original caller over current */
return original ?? current
}
}

/**
* Returns a function that provides additional validation to the JWT
*
* @param basePath - will validate that the audience starts with the provided basePath in production.
* @returns - A function that validates the API context and audience.
*/
export function apiContextValidationFunction (basePath: string): (jwt: { payload: TransformedJwtPayload }) => void {
return (jwt) => {
const context = jwt.payload.apiContext
if (!context.startsWith(basePath)) {
throw new ByuJwtError(BYU_JWT_ERROR_CODES.invalidApiContext, 'Invalid API context in JWT')
}
/** Check that the JWT is meant for the audience */
if (jwt.payload.aud != null) {
const audiences = typeof jwt.payload.aud === 'string' ? [jwt.payload.aud] : jwt.payload.aud
const hasAValidAudience = !audiences.some((audience) => audience.startsWith(basePath))
if (hasAValidAudience) {
throw new ByuJwtError(BYU_JWT_ERROR_CODES.invalidAudience, 'Invalid aud in JWT')
}
}
}
}
15 changes: 9 additions & 6 deletions packages/fastify/src/ByuJwtProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import fp from 'fastify-plugin'
import type { ByuJwtOptions, JwtPayload } from '@byu-oit/jwt'
import { ByuJwtAuthenticator } from './ByuJwtAuthenticator.js'
import type { JwtPayload } from '@byu-oit/jwt'
import {
ByuJwtAuthenticator,
type ByuJwtAuthenticatorOptions
} from './ByuJwtAuthenticator.js'

/** Enhance the fastify request with the verified caller information */
declare module 'fastify' {
Expand All @@ -10,13 +13,13 @@ declare module 'fastify' {
}
}

export interface ByuJwtProviderOptions extends ByuJwtOptions {
export interface ByuJwtProviderOptions extends ByuJwtAuthenticatorOptions {
prefix?: string
}

const ByuJwtProviderPlugin: FastifyPluginAsync<ByuJwtProviderOptions> = async (fastify, options) => {
const authenticator = new ByuJwtAuthenticator(options)

const { prefix, ...opts } = options
const authenticator = new ByuJwtAuthenticator(opts)
async function ByuJwtAuthenticationHandler (request: FastifyRequest, reply: FastifyReply): Promise<void> {
try {
request.caller = await authenticator.authenticate(request.headers)
Expand All @@ -31,7 +34,7 @@ const ByuJwtProviderPlugin: FastifyPluginAsync<ByuJwtProviderOptions> = async (f
* under the specified prefix.
*/
fastify.addHook('onRoute', (route) => {
if (options.prefix != null && !route.path.startsWith(options.prefix)) {
if (prefix != null && !route.path.startsWith(prefix)) {
/** Don't add authentication to routes that don't match the specified prefix */
return
}
Expand Down
30 changes: 23 additions & 7 deletions packages/fastify/test/ByuJwtProvider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import test from 'ava'
import Fastify, { type FastifyReply, type FastifyRequest } from 'fastify'
import ByuJwtProvider, { type ByuJwtError } from '../src/index.js'
import { expiredJwt } from './assets/jwt.js'
import { expiredJwt, decodedJwt } from './assets/jwt.js'
import { apiContextValidationFunction } from '../src/index.js'

const issuer = 'https://example.com'
const development = true
Expand All @@ -18,6 +19,14 @@ test('authenticated user', async t => {
t.is(result.netId, 'stuft2')
})

test('cannot fetch key', async t => {
const fastify = Fastify()
await fastify.register(ByuJwtProvider, { issuer, basePath: '/test' })
fastify.get('/', (request) => request.caller)
const result = await fastify.inject({ url: '/', headers: { 'x-jwt-assertion': expiredJwt } }).then(res => res.json())
t.is(result.message, 'Cannot fetch key.')
})

test('missing expected JWT', async t => {
const fastify = Fastify()
fastify.setErrorHandler(errorHandler)
Expand All @@ -26,12 +35,19 @@ test('missing expected JWT', async t => {
const result = await fastify.inject('/').then(res => res.json<ByuJwtError>())
t.is(result.message, 'Missing expected JWT')
})

test('invalid API context in JWT', async t => {
const fastify = Fastify()
await fastify.register(ByuJwtProvider, { issuer, development, basePath: '/test' }) // fails because development:false forces jwt to be valid
fastify.get('/', (request) => request.caller)
const result = await fastify.inject({ url: '/', headers: { 'x-jwt-assertion': expiredJwt } }).then(res => res.json<ByuJwtError>())
t.is(result.message, 'Invalid API context in JWT')
const validate = apiContextValidationFunction('/test')
t.throws(() => {
validate(decodedJwt)
}, { instanceOf: Error, message: 'Invalid API context in JWT' })
})
test.todo('invalid audience in JWT') // Can't easily get aud from auth code token

test('invalid audience in JWT', async t => {
const validate = apiContextValidationFunction('/echo')
t.throws(() => {
validate(decodedJwt)
}, { instanceOf: Error, message: 'Invalid aud in JWT' })
})

test.todo('will return original instead of current')
38 changes: 38 additions & 0 deletions packages/fastify/test/assets/jwt.ts
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
export const expiredJwt = 'eyJraWQiOiJwdWJsaWM6YXBpLXNhbmRib3gtMiIsIng1dCI6Img1bnFJM2dBQm4wM3p2dGFfQVBMRVdZYm1LMCIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiJodHRwczovL2FwaS5ieXUuZWR1IiwiZXhwIjoxNjgyNjE1OTg0LCJodHRwOi8vd3NvMi5vcmcvY2xhaW1zL3N1YnNjcmliZXIiOiJCWVUvc3R1ZnQyIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy9hcHBsaWNhdGlvbmlkIjoiZWEyZWRlOWEtMTQ1Zi00YTZjLWE1MzEtNTViYTA0ODQ3ZTRiIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy9hcHBsaWNhdGlvbm5hbWUiOiJlYTJlZGU5YS0xNDVmLTRhNmMtYTUzMS01NWJhMDQ4NDdlNGIiLCJodHRwOi8vd3NvMi5vcmcvY2xhaW1zL2FwcGxpY2F0aW9udGllciI6IlVubGltaXRlZCIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvYXBpY29udGV4dCI6Ii9lY2hvL3YxIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy92ZXJzaW9uIjoidjEiLCJodHRwOi8vd3NvMi5vcmcvY2xhaW1zL3RpZXIiOiJVbmxpbWl0ZWQiLCJodHRwOi8vd3NvMi5vcmcvY2xhaW1zL2tleXR5cGUiOiJTQU5EQk9YIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy91c2VydHlwZSI6IkFQUExJQ0FUSU9OIiwiaHR0cDovL3dzbzIub3JnL2NsYWltcy9lbmR1c2VyIjoic3R1ZnQyQGNhcmJvbi5zdXBlciIsImh0dHA6Ly93c28yLm9yZy9jbGFpbXMvZW5kdXNlclRlbmFudElkIjoiLTEyMzQiLCJodHRwOi8vd3NvMi5vcmcvY2xhaW1zL2NsaWVudF9pZCI6ImVhMmVkZTlhLTE0NWYtNGE2Yy1hNTMxLTU1YmEwNDg0N2U0YiIsImh0dHA6Ly9ieXUuZWR1L2NsYWltcy9jbGllbnRfc3Vic2NyaWJlcl9uZXRfaWQiOiJzdHVmdDIiLCJodHRwOi8vYnl1LmVkdS9jbGFpbXMvY2xpZW50X3BlcnNvbl9pZCI6IjI5OTI3Njc4MiIsImh0dHA6Ly9ieXUuZWR1L2NsYWltcy9jbGllbnRfYnl1X2lkIjoiNzUwNzE3MDczIiwiaHR0cDovL2J5dS5lZHUvY2xhaW1zL2NsaWVudF9uZXRfaWQiOiJzdHVmdDIiLCJodHRwOi8vYnl1LmVkdS9jbGFpbXMvY2xpZW50X3N1cm5hbWUiOiJUdWZ0IiwiaHR0cDovL2J5dS5lZHUvY2xhaW1zL2NsaWVudF9zdXJuYW1lX3Bvc2l0aW9uIjoiTCIsImh0dHA6Ly9ieXUuZWR1L2NsYWltcy9jbGllbnRfcmVzdF9vZl9uYW1lIjoiU3BlbmNlciBKYW1lcyIsImh0dHA6Ly9ieXUuZWR1L2NsYWltcy9jbGllbnRfcHJlZmVycmVkX2ZpcnN0X25hbWUiOiJTcGVuY2VyIiwiaHR0cDovL2J5dS5lZHUvY2xhaW1zL2NsaWVudF9zb3J0X25hbWUiOiJUdWZ0LCBTcGVuY2VyIEphbWVzIiwiaHR0cDovL2J5dS5lZHUvY2xhaW1zL2NsaWVudF9uYW1lX3ByZWZpeCI6IiAiLCJodHRwOi8vYnl1LmVkdS9jbGFpbXMvY2xpZW50X25hbWVfc3VmZml4IjoiICIsImh0dHA6Ly9ieXUuZWR1L2NsYWltcy9jbGllbnRfY2xhaW1fc291cmNlIjoiQ0xJRU5UX1NVQlNDUklCRVIifQ.RoPdD6Inrdkovu0gQE1ozVf23HepuNLPqJJIJCnUe_0tY7r36ilKQmnER__bCNfHkGZUyVx0wjTinoPIG8ytIv-0pjP-LkR4dpZK9O3WMbb06qGyzWa0-OWOkb2iuH_qGcolM6brmco6eM8NROtn6jx3rVqbffJY1czpW5zJPzLo8YiiEDvTwk7efh99faVXQoRcUV0_LdFORwfgqKwBp8HmhXfZrMhCLQKbahTasIUUtpN8nyPk6BHb0GITXRjK4WQ5eHFf3CDRuGLVVQeTV6dbvIpa9jrzcKGtqkCDbJ-d9uxoPQ7OvLdouQ1D3oi2zj4Qalgs9ydKX3vpO4-4etm5T18qkJ9WlDe_Qra6hiD0CS0zT2umXh3MVFeU9qIpDNwBXRmXXMDbZTvQGWeWB14tK6x24gj-nNkOz_0JlwbykDN11mgsvCxDSWiSBBtYHndyO9B_YDKpLntP1ZafAIVLZtT9QYSljNKps1iIkotC7CG7FbMl9cr34yWuU7T4Sgn-_U1OE3DY6yojiDDtPqnkmTeTMWqmdqC2-ajqJ3JtQ1PFNdKWM5LIYbgmOXHsQB6DuaPIhwQlV0-reNX2_M9RRrNhklOhlUqpBz_A56ZKA5Su990YvDS6DcnW9wMERkxNDcW-tqGltxUwiN3PeCEhim5_ndul66DpBXndpSY'

export const decodedJwt = {
header: {
kid: 'public:api-sandbox-2',
x5t: 'h5nqI3gABn03zvta_APLEWYbmK0',
alg: 'RS256'
},
payload: {
apiContext: '/echo/v1',
application: {
id: 'ea2ede9a-145f-4a6c-a531-55ba04847e4b',
name: 'ea2ede9a-145f-4a6c-a531-55ba04847e4b',
tier: 'Unlimited'
},
aud: 'invalid aud',
byuId: '750717073',
clientId: 'ea2ede9a-145f-4a6c-a531-55ba04847e4b',
endUser: '[email protected]',
endUserTenantId: '-1234',
exp: 1682615984,
iss: 'https://api.byu.edu',
keyType: 'SANDBOX',
netId: 'stuft2',
personId: '299276782',
preferredFirstName: 'Spencer',
prefix: ' ',
restOfName: 'Spencer James',
sortName: 'Tuft, Spencer James',
subscriber: 'BYU/stuft2',
suffix: ' ',
surname: 'Tuft',
surnamePosition: 'L',
tier: 'Unlimited',
userType: 'APPLICATION',
version: 'v1'
},
signature: ''
}
30 changes: 30 additions & 0 deletions packages/jwt/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [0.0.7-beta.2](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-09-25)


### Bug Fixes

* update dependencies ([#157](https://github.com/byu-oit/byu-jwt-nodejs/issues/157)) ([1a3229c](https://github.com/byu-oit/byu-jwt-nodejs/commit/1a3229c1e8e6baaee03ee29946a7a1d29f5009c6))





## [0.0.7-beta.1](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-09-14)


### Bug Fixes

* update dependencies ([#155](https://github.com/byu-oit/byu-jwt-nodejs/issues/155)) ([e20663e](https://github.com/byu-oit/byu-jwt-nodejs/commit/e20663ecfd7c6c42a09ee48fa272fee85e694cfb))





## [0.0.7-beta.0](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-09-11)

**Note:** Version bump only for package @byu-oit/jwt





## [0.0.6](https://github.com/byu-oit/byu-jwt-nodejs/compare/@byu-oit/[email protected]...@byu-oit/[email protected]) (2023-07-10)


Expand Down
Loading

0 comments on commit b0567c0

Please sign in to comment.