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#Enabling it
Auth is enabled the way every Stone.js module is, with its decorator or with its blueprint. Either one registers the service provider and the kernel middleware that verifies the Bearer token carried by each request.
import { Auth } from '@stone-js/auth'
import { StoneApp } from '@stone-js/core'
@Auth()
@StoneApp({ name: 'my-app' })
export class Application {}import { defineStoneApp } from '@stone-js/core'
import { authBlueprint } from '@stone-js/auth'
export const Application = defineStoneApp({ name: 'my-app' }, [authBlueprint])#Configure the signing strategy
Nothing is verified until you say how tokens are signed. 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 { Configuration, IBlueprint, IConfiguration } from '@stone-js/core'
@Configuration()
export class AuthConfiguration implements IConfiguration {
configure (blueprint: IBlueprint): void {
blueprint
.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.getUser() // the authenticated principal
return this.tasks.ownedBy(user.id)
}#Turning a token into your own principal
Verification produces claims. Which user those claims mean is your application's question, so resolveUser answers it: it receives the verified claims and returns whatever your code should see, and the same requireAuth / requireScopes guards work on top of it unchanged.
@Auth({
resolveUser: async (claims) => await users.findById(claims.sub) // awaited: a store lookup
})
export class Application {}The principal is then read with event.getUser(). Not event.get('user'): it travels through a resolver rather than as metadata, so the generic accessor does not reach it.
const user = event.getUser<User>() // the authenticated principal, or undefined