Protect your API
Add the OAuth guard
Section titled “Add the OAuth guard”For a Lucid application with a User model at #models/user, keep the session guard as the default and add oauth:
import { defineConfig } from '@adonisjs/auth'import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session'import type { InferAuthenticators, InferAuthEvents, Authenticators } from '@adonisjs/auth/types'import { oauthGuard } from '@julr/sesame/guard'import { oauthUserProvider } from '@julr/sesame/guard/lucid'
const authConfig = defineConfig({ default: 'web', guards: { web: sessionGuard({ useRememberMeTokens: false, provider: sessionUserProvider({ model: () => import('#models/user') }), }), oauth: oauthGuard({ provider: oauthUserProvider({ model: () => import('#models/user') }), }), },})
export default authConfig
declare module '@adonisjs/auth/types' { interface Authenticators extends InferAuthenticators<typeof authConfig> {}}
declare module '@adonisjs/core/types' { interface EventsList extends InferAuthEvents<Authenticators> {}}For Kysely users, replace the Lucid provider with your custom user provider.
Require a token and a scope
Section titled “Require a token and a scope”Authenticate the OAuth guard before checking its scopes:
import router from '@adonisjs/core/services/router'
router.get('/api/me', async ({ auth }) => { const guard = auth.use('oauth') const user = await guard.authenticate({ scopes: ['read'] })
if (!guard.hasScope('read')) throw guard.insufficientScopeError(['read'])
return { id: user.id, clientId: guard.clientId }})Declare read in your scope config. A request without a valid Bearer token returns 401. A valid token without read returns 403.
Use guard.hasScope('read', 'write') for all listed scopes. Use guard.hasAnyScope('read', 'write') for any listed scope.
Accept both browser sessions and tokens
Section titled “Accept both browser sessions and tokens”For a route that also accepts first-party session users, use the named middleware installed by Sésame:
import router from '@adonisjs/core/services/router'import { middleware } from '#start/kernel'
router.get('/data', async () => ({ ok: true })) .use(middleware.scopes({ scopes: ['read', 'write'] }))
router.get('/summary', async () => ({ ok: true })) .use(middleware.anyScope({ scopes: ['read', 'write'] }))scopes requires all listed scopes. anyScope requires at least one. With an Authorization header, both authenticate the OAuth guard. Without that header, a valid default session passes without an OAuth scope check.
After authentication, guard.accessToken identifies the token record without its raw value or hash. guard.grantId, guard.context, and guard.audience describe its authorization and target resource.
For a route that requires a token, always authenticate auth.use('oauth') before the scope middleware, or use the explicit guard check above.
Enforce a resource audience
Section titled “Enforce a resource audience”To restrict tokens to one API, register its resource and configure a matching guard. This example uses /api/mcp; replace it with your API path.
import sesame from '@julr/sesame/services/main'
sesame.registerProtectedResource({ resource: '/api/mcp', scopes: ['read'] })Resource registration publishes discovery and records the target for audience matching. It does not protect the handler. Add a guard to your existing Auth config:
mcp: oauthGuard({ provider: oauthUserProvider({ model: () => import('#models/user') }), resource: '/api/mcp', requireAudience: true,}),Authenticate auth.use('mcp') on that resource, using the token and scope checks above. For scope middleware, pass guard: 'mcp'.
requireAudience: true rejects unbound tokens. Omit it during a transition to accept older unbound tokens while rejecting tokens bound elsewhere. A guard without resource does not check audience.
Request a bound token
Section titled “Request a bound token”Add resource=https://auth.example.com/api/mcp to the authorization request and its code exchange. On refresh, omit the parameter or send the same value. A bound token cannot change targets; an unbound code or refresh token can acquire a target at exchange.
Call the API with a token bound to that resource. It succeeds and guard.audience contains the target URL. A token bound to another resource returns 401.
Register the exact guard path. An unregistered path falls back to the closest registered resource and logs a warning once. See resource matching for normalization and errors.