Last active
August 5, 2023 19:41
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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