Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: refactor packages to use byu-sdk jwt verification #158

Merged
merged 8 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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