Skip to content

Instantly share code, notes, and snippets.

@marshallswain
Last active August 5, 2023 19:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marshallswain/ebb514a97b535ca1346ee7d418718fb8 to your computer and use it in GitHub Desktop.
Save marshallswain/ebb514a97b535ca1346ee7d418718fb8 to your computer and use it in GitHub Desktop.
Example FeathersJS auth code. I have not tested it with even a single request. I just didn't want to lose it.
import type { HookContext, NextFunction } from '@feathersjs/feathers'
type GetTokenFn = <H extends HookContext>(context: H) => Promise<string>
interface CheckAccessTokenHookOptions {
getToken?: GetTokenFn | GetTokenFn[]
}
const defaultGetTokenFn: GetTokenFn = async context => context.params.headers?.authorization?.split(' ')[1]
/**
* Sets up the `params.authentication` context for the request. `params.authentication.authenticated` is `false` by default.
*
* By default, it looks for a "Bearer" token in the authorization header.
* If the `getToken` option is provided as a function, it will use that function to get the token.
* If the `getToken` option is provided as an array of functions, it will use the first non-null value returned by the array of functions.
*
* Once a valid token is found and validated, the following happens:
* - `params.authentication.authenticated` is set to `true`
* - `params.authentication.payload` is set to the decoded JWT payload
*
* Note the following:
* - The user is not populated
* - The user's access is not verified. That must be done separately.
*/
export function checkAccessToken<H extends HookContext, N extends NextFunction>(options?: CheckAccessTokenHookOptions) {
return async (context: H, next: N) => {
// If params.authentication is undefined, set it to { authenticated: false }
context.params.authentication ??= { authenticated: false }
const getToken = options?.getToken || defaultGetTokenFn
// the token is the first non-null value returned by the array of async getToken functions
const tokenFns = Array.isArray(getToken) ? getToken : [getToken]
const token = await someToken(tokenFns, context)
// If a token was found, verify it and set params.authentication accordingly
if (token) {
const authService = context.app.service('authentication') as any
const isValid = await authService.verifyAccessToken(token)
if (isValid) {
context.params.authentication.authenticated = true
context.params.authentication.payload = authService.decodeAccessToken(token)
}
}
await next()
}
}
/**
* Returns the first truthy value returned by the array of async functions.
* Returns null if none of the functions return a truthy value.
* @param array array of GetTokenFn functions
* @param context Hook Context
* @returns accessToken or null
*/
export async function someToken(array: GetTokenFn[], context: HookContext): Promise<string | null> {
for (const asyncFn of array) {
const result = await asyncFn(context)
if (result)
return result
}
return null
}
import type { HookContext, NextFunction } from '@feathersjs/feathers'
import { NotAuthenticated } from '@feathersjs/errors'
type CheckAuthFn = <H extends HookContext>(context: H) => Promise<boolean>
/**
* Runs the `when` function to determine if auth is required.
* If `when` returns true, auth is required and the request requires to be authenticated.
* If `params.authentication.authenticated` is `false` a `NotAuthenticated` error is thrown.
*
* @param when the function that receives the context and returns a boolean indicating if authentication is required
*/
export function requireAuth<H extends HookContext>(when: CheckAuthFn) {
return async (context: H, next?: NextFunction) => {
// If params.authentication is undefined, set it to { authenticated: false }
context.params.authentication ??= { authenticated: false }
const isAuthRequired = await when(context)
if (isAuthRequired && !context.params.authentication.authenticated)
throw new NotAuthenticated()
if (next)
await next()
}
}
import { Type } from '@feathersjs/typebox'
import { resolve, resolveData, resolveQuery } from '@feathersjs/schema'
import { KyselyService } from '../../../feathers-kysely'
import type { Application, HookContext } from '../../declarations'
import { validateData, validateQuery } from '../../hooks/validate-schema'
import { querySyntax } from '../../query-syntax.schema'
import { checkAccessToken } from '../../authentication/hooks/auth-validate-jwt'
import { requireAuth } from '../../authentication/hooks/auth-require'
import { Service } from './users.service'
import { methods, path } from './users.shared'
import { type User, UserSchema } from './users.schema'
// Declare variables in outer scope to allow reuse between requests.
let adapter: any
let service: Service
export function usersService(app: Application) {
adapter = adapter ?? new KyselyService({ Model: app.get('database'), dialectType: 'sqlite', name: 'users' })
service = service ?? new Service({ adapter })
app.use(path, service, { methods })
app.service(path).hooks({
around: {
all: [
/**
* Get a token from any of these sources:
* 1. `authorization` header as Bearer token
* 2. Query string
* 3. `x-access-token` header
*
* If a token is found, validate it and set `context.params.authentication` accordingly.
*/
checkAccessToken({
getToken: [
// Check authorization header for a token
async (context) => {
if (context.params.headers?.authorization?.startsWith('Bearer '))
return context.params.headers.authorization.slice(7)
},
// Check query string for a token
async (context) => {
if (context.params.query?.access_token)
return context.params.query.access_token
},
// Check x-access-token header for a token
async (context) => {
if (context.params.provider === 'rest')
return context.params.headers?.['x-access-token']
},
],
}),
/**
* Alternatively, you can provide no options to `checkAccessToken`.
* In this case, the default is to check the authorization header for a Bearer token.
* If a token is found, validate it and set `context.params.authentication` accordingly.
*/
checkAccessToken(),
/**
* OPTIONAL:
* For stateful auth, load the user record if params.authentication.authenticated is `true`
*/
async (context, next) => {
const { authenticated, payload } = context.params.authentication ?? {}
if (authenticated && payload?.userId)
context.params.user = await app.service('users')._get(payload.userId)
await next()
},
/**
* Runs the `when` function to determine if auth is required.
* In this case, auth is required if context.provider is set (i.e. the request is external)
*/
requireAuth(async (context) => {
return !!context.provider
}),
],
find: [
validateQuery(Type.Intersect(
[
querySyntax(UserSchema),
Type.Object({}, { additionalProperties: false }),
],
{ additionalProperties: false },
)),
resolveQuery(resolve<User, HookContext>({
// users only see their own data
id: async (value, user, context) => {
if (context.params.user)
return context.params.user.id
return value
},
})),
],
get: [],
create: [
validateData(Type.Pick(UserSchema, ['email', 'password'])),
resolveData(resolve<User, HookContext>({})),
],
patch: [
validateData(Type.Pick(UserSchema, ['password'])),
resolveData(resolve<User, HookContext>({})),
],
remove: [],
},
})
}
declare module '../../declarations' {
interface ServiceTypes {
[path]: Service
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment