Extensions
Auth
Authentication asks one question: who is calling. @stone-js/auth answers it statelessly, verifying a JWT or OAuth token at the boundary, so there is no session store to run and the same guard works on Node, on the edge, and in an agent call.
#Install
npm i @stone-js/auth#Configure the signing strategy
Auth is enabled from a @Configuration() that merges the auth blueprint (its service provider and the kernel middleware that verifies the Bearer token) and sets how tokens are signed and verified. Use a shared HMAC secret for symmetric JWT, or a publicKey/jwksUri to verify tokens minted by an external identity provider. Read secrets from the environment, never hard-code them.
import { getString } from '@stone-js/env'
import { authBlueprint } from '@stone-js/auth'
import { Configuration, IBlueprint, IConfiguration } from '@stone-js/core'
@Configuration()
export class AuthConfiguration implements IConfiguration {
configure (blueprint: IBlueprint): void {
blueprint
.set(authBlueprint) // provider + verify middleware
.set('stone.auth.secret', getString('JWT_SECRET')) // HMAC (HS256); or publicKey / jwksUri
.set('stone.auth.issuer', 'https://your-issuer.example')
.set('stone.auth.audience', 'your-api')
.set('stone.auth.ttl', '1h')
}
}#Identity at the boundary
The principle
Identity should be established once, at the edge, and carried as context, not re-derived deep in the code. Server-held state ties you to one machine; a token verified at the boundary travels wherever the request does.
In Stone.js
Guards are middleware. requireAuth() rejects anonymous calls with a 401; requireScopes(...) additionally demands OAuth scopes, rejecting a missing one with a 403. The verified principal is then available on the event.
import { EventHandler, Get, Post } from '@stone-js/router'
import { requireAuth, requireScopes } from '@stone-js/auth'
@EventHandler('/tasks')
export class TaskController {
@Get('/', { middleware: [requireAuth()] }) // must be authenticated
list () { return this.tasks.list() }
@Post('/', { middleware: [requireScopes('tasks:write')] }) // must hold the scope
create (event) { return this.tasks.add(event.get('title')) }
}import { defineEventHandler, defineRoutes } from '@stone-js/router'
import { requireAuth, requireScopes } from '@stone-js/auth'
export const routes = defineRoutes([
[defineEventHandler(TaskController, 'list'),
{ path: '/tasks', method: 'GET', middleware: [requireAuth()] }],
[defineEventHandler(TaskController, 'create'),
{ path: '/tasks', method: 'POST', middleware: [requireScopes('tasks:write')] }]
])#Guards
| Guard | Type | Description |
|---|---|---|
| requireAuth() | () => middleware | Require a valid token; 401 when anonymous. |
| requireScopes(...scopes) | (...string) => middleware | Require every listed OAuth scope; 401 when anonymous, 403 when a scope is missing. |
#Reading the principal
@Get('/mine', { middleware: [requireAuth()] })
mine (event: IncomingHttpEvent) {
const user = event.get('user') // the authenticated principal
return this.tasks.ownedBy(user.id)
}#Custom strategies
The built-in verification covers JWT and OAuth. When you need something else, an API key, a session, a bespoke provider, implement an Authenticator: it turns a request into a principal (or rejects it), and the same requireAuth/requireScopesguards work on top of it unchanged.
import { Authenticator } from '@stone-js/auth'
export class ApiKeyAuthenticator extends Authenticator {
async authenticate (event: IncomingHttpEvent) {
const key = event.getHeader('x-api-key')
const user = await this.keys.resolve(key)
if (user === undefined) throw new AuthenticationError('Invalid API key') // -> 401
return user // becomes event.get('user')
}
}