@occupop/lib-auth
v4.2.0
Published
...
Maintainers
Keywords
Readme
Package @occupop/lib-auth
This package is intended to be used with @occupop/lib-container
providing context function for GraphQL (or Express middleware) where AuthUser (or ScopedUser)
gets validated and signed to container.
This implementation refers to https://www.apollographql.com/docs/apollo-server/data/context!
NPM registry token required!
Install
# Yarn
yarn add @occupop/lib-auth
# NPM
npm install @occupop/lib-auth
# Bun
bun add @occupop/lib-authEnv Setup
ENV variables required
JWT_SECRET=randomTokenProvidedByAuthServiceCompatibility
- Node: an ES module package (
"type": "module"), loadable withimportand withrequire().require()of an ES module needsrequire(esm), which is on by default since Node 20.19 and 22.12; Node 22 and 24 are the versions tested. Since 4.1.0 the package takesjsonwebtoken, a CommonJS module, through its default export, so it loads in Node. 4.0.0 and earlier loaded only under Bun: Node failed with "Named export 'verify' not found". - Bun: loads with
import, as before. - Peers:
@occupop/lib-container^4.0.1, the first 4.x version that re-exports awilix'sasValue, which this package imports;typescript^5.6.3 || ^6.0.0.
bun run dist ends with bun run test:node, which loads the built dist/ with Node through both
import() and require().
Register
import {
makeAuthContextValueFunction, makeAuthContextMiddleware
} from '@occupop/lib-auth'
//... all other imports omited...
export const container = createTypedContainer({
container: { asFunction: () => container, singleton: true },
// ...
// for graphql:
authContextValueFunction: { asFunction: makeAuthContextValueFunction, singleton: true },
// for express:
authContextMiddleware: { asFunction: makeAuthContextMiddleware, singleton: true },
// ...
})
Note: container need to be self registered/referenced to be possible to context function/middleware register AuthUser and ScopedUser, that is extracted from request headers.
Usage (with graphql)
// server.ts
import { container } from '.container'
const { mongoClient, graphqlServer, graphqlStandalone, authContextValueFunction } = container.cradle
await mongoClient.connect()
console.log('Mongo connected')
const httpPort = process.env.API_PORT || 4000
await await graphqlStandalone(graphqlServer, {
context: authContextValueFunction,
listen: { port: httpPort },
})
console.log(`Server listening on http://localhost:${httpPort}`)// app/resolvers.ts
import { container } from '.container'
export const resolvers: GraphQLResolverMap<{ container: typeof container}> = {
Query: {
jobs: (source, { skip, take }, { container }, info) => {
const { getJobsUseCase } = container.cradle
return getJobsUseCase(skip, take)
},
},
}// app/use-cases/get-jobs-use-case.ts
import { type Deps } from '.container'
export function makeGetJobsUseCase({ authUser, jobService }: Deps) {
return (skip = 0, take = 15) => {
if (!authUser?.hiringCompanyUuid) throw 'not logged'
if (take > 100) take = 100
return jobService.getJobs(authUser.hiringCompanyUuid, skip, take)
}
}Usage (with express)
// server.ts
import { container } from '.container'
const { mongoClient, httpServer, appRouter, authContextMiddleware } = container.cradle
await mongoClient.connect()
console.log('Mongo connected')
const httpPort = process.env.API_PORT || 4000
httpServer.use(authContextMiddleware)
httpServer.use(appRouter)
await httpServer.listen(httpPort, () => {
console.log(`Http listening on http://localhost:${httpPort}`)
})
// ...The middleware attaches the request-scoped container as req.container, then calls next(). If
building the context fails (for example, a request carries a token while JWT_SECRET is not set),
it calls next(error) instead, and Express's error handling takes over. Before 4.1.0 it never
called next(), so a request passing through it hung.
// app/router.ts
export function makeAppRouter({ httpRouter }: Deps) {
return httpRouter
//...
.get('/jobs', (req, res) => {
const { getJobsUseCase } = req.container.cradle
res.json(getJobsUseCase(req.query.skip, req.query.take))
})
//...
}// app/use-cases/get-jobs-use-case.ts
import { type Deps } from '.container'
export function makeGetJobsUseCase({ authUser, jobService }: Deps) {
return (skip = 0, take = 15) => {
if (!authUser?.hiringCompanyUuid) throw 'not logged'
if (take > 100) take = 100
return jobService.getJobs(authUser.hiringCompanyUuid, skip, take)
}
}Signing a token for another service
signAuthUserToken(authUser) signs a user as a bearer token that another service verifies with
the same JWT_SECRET, for example to call the GraphQL gateway on that user's behalf:
import { signAuthUserToken } from '@occupop/lib-auth'
const token = signAuthUserToken(authUser) // a string, or null when authUser is null
const response = await fetch(gatewayUrl, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
body: JSON.stringify({ query }),
})- HS256 with
JWT_SECRET, read when the function is called. If it is not set, the function throws'Env var JWT_SECRET not set?', as verification does. - The payload is signed as given. A stored user that carries
iatandexpkeeps both; when it has noiat, jsonwebtoken adds the signing time. No expiry, issuer or audience is added, so a user withoutexpgives a token that does not expire. nullin,nullout.- The token is identical to the one
@occupop/lib-context2.0.1'smakeGetAuthUserTokenproduces for the same user, secret and time.src/index_test.tschecks this against tokens recorded from lib-context (src/auth-user-token-vectors.json).
Signing a scoped link
signScopedUserToken(scopedUser, jwtid) signs a scope — a grant over named entities and fields,
rather than a user — with jwtid as the token's jti, for a link a service hands out: an approval
link, a one-entity grant.
import { signScopedUserToken } from '@occupop/lib-auth'
const scope = { [jobUuid]: { allowedFields: ['signOffWorkflow', 'userAssignments'] } }
const token = signScopedUserToken(scope, approverUuid) // a string; throws for a null scope
const link = `${appUrl}/sign-off/${token}`getScopedUser reads such a token back, and getAuthUser returns null for it, so the two readers
stay disjoint.
- HS256 with
JWT_SECRET, read when the function is called. If it is not set, the function throws'Env var JWT_SECRET not set?', as verification does. jwtidbecomes the token'sjticlaim. Callers commonly use the uuid of the row the link grants access to, so a link can be traced back to it.- The payload is signed as given: no expiry, issuer or audience is added, and jsonwebtoken adds
iatonly when the payload has none — a payload that already carries one keeps it. - It throws
'scope not defined'for a null scope, wheresignAuthUserTokenreturnsnullfor a null user. That difference is deliberate, not an oversight: a caller minting a link has no use for a null token. - Passing an
AuthUserdoes not compile. The parameter isScopedUser | null, and anAuthUser'suserUuid: stringconflicts withScopedUser'suserUuid?: undefined. - The token is identical to the one
@occupop/lib-context2.0.1'smakeGetScopedUserTokenproduces for the same scope,jti, secret and time.src/index_test.tschecks this against recorded tokens (src/scoped-user-token-vectors.json).
