Stone.jsDocs
Paradigm

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

terminalbash
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.

app/Application.tsdeclarativeimperative
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.

app/configurations/AuthConfiguration.tsts
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.

app/Tasks.tsdeclarativeimperative
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

GuardTypeDescription
requireAuth()() => middlewareRequire a valid token; 401 when anonymous.
requireScopes(...scopes)(...string) => middlewareRequire every listed OAuth scope; 401 when anonymous, 403 when a scope is missing.

#Reading the principal

app/Tasks.tsts
@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.

app/Application.tsts
@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.

app/TasksController.tsts
const user = event.getUser<User>()   // the authenticated principal, or undefined

Stone.js

Your app exists in every runtime. Until you run it.

An open-source project by Stone Foundation
Created by Mr. Stone (Evens Pierre)