Custom Auth Provider Implementation Guide - Mastra Framework

Trust: ★★★☆☆ (0.90) · 0 validations · factual

Published: 2026-05-09 · Source: crawler_authoritative

Tình huống

Developer needing to implement custom authentication for identity systems not supported by built-in Mastra providers (v0.x). Use cases include self-hosted identity systems, custom token formats, specialized authorization rules, and enterprise SSO integrations.

Insight

MastraAuthProvider is the base class for authentication in Mastra framework. Custom providers must implement two required methods: authenticateToken() receives the bearer token from Authorization header and returns user object or null, while authorizeUser() checks if authenticated user is allowed to access resources (returns true/false or 403 Forbidden). The base class accepts MastraAuthProviderOptions with configuration including name for logging, custom authorizeUser function, protected paths array (RegExp/string/[string, Methods]), and public paths array. Path patterns support wildcards like ‘/api/’, regex like /^/secure/./, and method-specific rules like [‘/api/webhook’, ‘POST’]. Token extraction is automatic from ‘Authorization: Bearer ’ header, but request object allows access to other headers and cookies. Configuration values can be provided via constructor options or environment variables with fallback logic. The @mastra/auth package provides verifyHmac(), verifyJwks(), decodeToken(), and getTokenIssuer() helper utilities for common JWT verification patterns. Error handling should provide descriptive messages during initialization but return null for authentication failures to avoid exposing details. Built-in providers include MastraJwtAuth, MastraAuthClerk, MastraAuthAuth0, MastraAuthSupabase, MastraAuthFirebase, MastraAuthWorkOS, MastraAuthBetterAuth, and SimpleAuth for development.

Hành động

  1. Define user type interface extending the requirements (id, email, roles). 2. Define auth options interface extending MastraAuthProviderOptions with optional custom fields like apiUrl, apiKey. 3. Create class extending MastraAuthProvider. 4. In constructor: call super({name: ‘your-provider-name’}), retrieve config from options or environment variables, throw descriptive errors if required config missing, call this.registerOptions(options). 5. Implement authenticateToken(token, request): make API call to verification endpoint, return user object if response.ok, return null otherwise. 6. Implement authorizeUser(user, request): return true if user has valid id, or implement custom logic like role-based checks. 7. Register provider with Mastra: new Mastra({ server: { auth: new YourAuthProvider({config}) } }). 8. For testing with Vitest: mock global.fetch, test initialization (valid options, missing config throws), authenticateToken (valid/invalid tokens), authorizeUser (with/without id), custom authorization, route configuration storage.

Điều kiện áp dụng

Requires Mastra framework v0.x with TypeScript. Applicable when using self-hosted identity systems, custom token formats, specialized authorization rules, or enterprise SSO not covered by built-in providers (Clerk, Auth0, Supabase, Firebase, WorkOS, Better Auth). The verifyJwks() helper requires @mastra/auth package.


Nội dung gốc (Original)

Custom auth provider

Custom auth providers allow you to implement authentication for identity systems that aren’t covered by the built-in providers. Extend the MastraAuthProvider base class to integrate with any authentication system.

Overview

Auth providers handle authentication and authorization for incoming requests:

  • Token verification and user extraction
  • User authorization logic
  • Path-based access control (public/protected routes)

Create custom auth providers to support:

  • Self-hosted identity systems
  • Custom token formats or verification logic
  • Specialized authorization rules
  • Enterprise SSO integrations

Creating a custom auth provider

Extend the MastraAuthProvider class and implement the required methods:

import { MastraAuthProvider } from '@mastra/core/server'
import type { MastraAuthProviderOptions } from '@mastra/core/server'
import type { HonoRequest } from 'hono'
 
// Define your user type
type MyUser = {
  id: string
  email: string
  roles: string[]
}
 
// Define options for your provider
interface MyAuthOptions extends MastraAuthProviderOptions<MyUser> {
  apiUrl?: string
  apiKey?: string
}
 
export class MyAuthProvider extends MastraAuthProvider<MyUser> {
  protected apiUrl: string
  protected apiKey: string
 
  constructor(options?: MyAuthOptions) {
    // Call super with a name for logging/debugging
    super({ name: options?.name ?? 'my-auth' })
 
    const apiUrl = options?.apiUrl ?? process.env.MY_AUTH_API_URL
    const apiKey = options?.apiKey ?? process.env.MY_AUTH_API_KEY
 
    if (!apiUrl || !apiKey) {
      throw new Error(
        'Auth API URL and API key are required. Provide them in options or set MY_AUTH_API_URL and MY_AUTH_API_KEY environment variables.',
      )
    }
 
    this.apiUrl = apiUrl
    this.apiKey = apiKey
 
    // Register any custom options (authorizeUser override, public/protected paths)
    this.registerOptions(options)
  }
 
  /**
   * Verify the token and return the user
   * Return null if authentication fails
   */
  async authenticateToken(token: string, request: HonoRequest): Promise<MyUser | null> {
    try {
      const response = await fetch(`${this.apiUrl}/verify`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': this.apiKey,
        },
        body: JSON.stringify({ token }),
      })
 
      if (!response.ok) {
        return null
      }
 
      const user = await response.json()
      return user
    } catch (error) {
      console.error('Token verification failed:', error)
      return null
    }
  }
 
  /**
   * Check if the authenticated user is authorized
   * Return true to allow access, false to deny
   */
  async authorizeUser(user: MyUser, request: HonoRequest): Promise<boolean> {
    // Basic authorization: user must exist and have an ID
    return !!user?.id
  }
}

Required methods

authenticateToken()

Verify the incoming token and return the user object if valid, or null if authentication fails.

async authenticateToken(token: string, request: HonoRequest): Promise<TUser | null>
ParameterTypeDescription
tokenstringThe bearer token extracted from the Authorization header
requestHonoRequestThe incoming request object (access headers, cookies, etc.)

Returns: The user object if authentication succeeds, or null if it fails.

The token is automatically extracted from the Authorization: Bearer <token> header. If you need to access other headers or cookies, use the request parameter.

authorizeUser()

Determine if the authenticated user is allowed to access the resource.

async authorizeUser(user: TUser, request: HonoRequest): Promise<boolean> | boolean
ParameterTypeDescription
userTUserThe user object returned by authenticateToken
requestHonoRequestThe incoming request object

Returns: true to allow access, false to deny (returns 403 Forbidden).

Configuration options

The MastraAuthProviderOptions interface supports these options:

OptionTypeDescription
namestringProvider name for logging/debugging
authorizeUser(user, request) => Promise<boolean> | booleanCustom authorization function
protected(RegExp | string | [string, Methods | Methods[]])[]Paths that require authentication
public(RegExp | string | [string, Methods | Methods[]])[]Paths that bypass authentication

Path Patterns

Configure which paths require authentication using pattern matching:

const auth = new MyAuthProvider({
  // Paths that require authentication
  protected: [
    '/api/*', // Wildcard: all /api routes
    '/admin/*', // Wildcard: all /admin routes
    /^\/secure\/.*/, // Regex pattern
  ],
 
  // Paths that bypass authentication
  public: [
    '/health', // Exact match
    '/api/status', // Exact match
    ['/api/webhook', 'POST'], // Only POST requests to /api/webhook
  ],
})

Using your auth provider

Register your custom auth provider with the Mastra instance:

import { Mastra } from '@mastra/core'
import { MyAuthProvider } from './my-auth-provider'
 
export const mastra = new Mastra({
  server: {
    auth: new MyAuthProvider({
      apiUrl: process.env.MY_AUTH_API_URL,
      apiKey: process.env.MY_AUTH_API_KEY,
    }),
  },
})

Helper utilities

The @mastra/auth package provides utilities for common token verification patterns:

JWT Verification

import { verifyHmac, verifyJwks, decodeToken, getTokenIssuer } from '@mastra/auth'
 
// Verify HMAC-signed JWT
const payload = await verifyHmac(token, 'your-secret-key')
 
// Verify with JWKS (for OAuth providers)
const payload = await verifyJwks(token, 'https://provider.com/.well-known/jwks.json')
 
// Decode without verification (for inspection)
const decoded = await decodeToken(token)
 
// Get the issuer from a decoded token
const issuer = getTokenIssuer(decoded)

Example: JWKS-based Provider

import { MastraAuthProvider } from '@mastra/core/server'
import type { MastraAuthProviderOptions } from '@mastra/core/server'
import { verifyJwks } from '@mastra/auth'
import type { JwtPayload } from '@mastra/auth'
 
type MyUser = JwtPayload
 
interface MyJwksAuthOptions extends MastraAuthProviderOptions<MyUser> {
  jwksUri?: string
  issuer?: string
}
 
export class MyJwksAuth extends MastraAuthProvider<MyUser> {
  protected jwksUri: string
  protected issuer: string
 
  constructor(options?: MyJwksAuthOptions) {
    super({ name: options?.name ?? 'my-jwks-auth' })
 
    const jwksUri = options?.jwksUri ?? process.env.MY_JWKS_URI
    const issuer = options?.issuer ?? process.env.MY_AUTH_ISSUER
 
    if (!jwksUri) {
      throw new Error('JWKS URI is required')
    }
 
    this.jwksUri = jwksUri
    this.issuer = issuer ?? ''
 
    this.registerOptions(options)
  }
 
  async authenticateToken(token: string): Promise<MyUser | null> {
    try {
      const payload = await verifyJwks(token, this.jwksUri)
 
      // Optionally validate issuer
      if (this.issuer && payload.iss !== this.issuer) {
        return null
      }
 
      return payload
    } catch {
      return null
    }
  }
 
  async authorizeUser(user: MyUser): Promise<boolean> {
    // Check token hasn't expired
    if (user.exp && user.exp * 1000 < Date.now()) {
      return false
    }
    return !!user.sub
  }
}

Custom authorization logic

Override the default authorization by providing a custom authorizeUser function:

const auth = new MyAuthProvider({
  apiUrl: process.env.MY_AUTH_API_URL,
  apiKey: process.env.MY_AUTH_API_KEY,
 
  // Custom authorization: require admin role for all requests
  async authorizeUser(user, request) {
    return user.roles.includes('admin')
  },
})

Role-based Authorization

const auth = new MyAuthProvider({
  async authorizeUser(user, request) {
    const path = request.url
    const method = request.method
 
    // Admin routes require admin role
    if (path.startsWith('/admin/')) {
      return user.roles.includes('admin')
    }
 
    // Write operations require write role
    if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
      return user.roles.includes('write') || user.roles.includes('admin')
    }
 
    // Read operations allowed for all authenticated users
    return true
  },
})

Testing custom auth providers

Example test structure using Vitest:

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { MyAuthProvider } from './my-auth-provider'
 
// Mock fetch for API calls
global.fetch = vi.fn()
 
describe('MyAuthProvider', () => {
  const mockOptions = {
    apiUrl: 'https://auth.example.com',
    apiKey: 'test-api-key',
  }
 
  beforeEach(() => {
    vi.clearAllMocks()
  })
 
  describe('initialization', () => {
    it('should initialize with provided options', () => {
      const auth = new MyAuthProvider(mockOptions)
      expect(auth).toBeInstanceOf(MyAuthProvider)
    })
 
    it('should throw error when required options are missing', () => {
      expect(() => new MyAuthProvider({})).toThrow('Auth API URL and API key are required')
    })
  })
 
  describe('authenticateToken', () => {
    it('should return user when token is valid', async () => {
      const mockUser = { id: 'user123', email: '[email protected]', roles: ['read'] }
      ;(fetch as any).mockResolvedValue({
        ok: true,
        json: () => Promise.resolve(mockUser),
      })
 
      const auth = new MyAuthProvider(mockOptions)
      const result = await auth.authenticateToken('valid-token', {} as any)
 
      expect(fetch).toHaveBeenCalledWith(
        'https://auth.example.com/verify',
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify({ token: 'valid-token' }),
        }),
      )
      expect(result).toEqual(mockUser)
    })
 
    it('should return null when token is invalid', async () => {
      ;(fetch as any).mockResolvedValue({ ok: false })
 
      const auth = new MyAuthProvider(mockOptions)
      const result = await auth.authenticateToken('invalid-token', {} as any)
 
      expect(result).toBeNull()
    })
  })
 
  describe('authorizeUser', () => {
    it('should return true when user has valid id', async () => {
      const auth = new MyAuthProvider(mockOptions)
      const result = await auth.authorizeUser(
        { id: 'user123', email: '[email protected]', roles: [] },
        {} as any,
      )
 
      expect(result).toBe(true)
    })
 
    it('should return false when user has no id', async () => {
      const auth = new MyAuthProvider(mockOptions)
      const result = await auth.authorizeUser(
        { id: '', email: '[email protected]', roles: [] },
        {} as any,
      )
 
      expect(result).toBe(false)
    })
  })
 
  describe('custom authorization', () => {
    it('should use custom authorizeUser when provided', async () => {
      const auth = new MyAuthProvider({
        ...mockOptions,
        authorizeUser: user => user.roles.includes('admin'),
      })
 
      const adminUser = { id: 'user123', email: '[email protected]', roles: ['admin'] }
      const regularUser = { id: 'user456', email: '[email protected]', roles: ['read'] }
 
      expect(await auth.authorizeUser(adminUser, {} as any)).toBe(true)
      expect(await auth.authorizeUser(regularUser, {} as any)).toBe(false)
    })
  })
 
  describe('route configuration', () => {
    it('should store public routes configuration', () => {
      const publicRoutes = ['/health', '/api/status']
      const auth = new MyAuthProvider({
        ...mockOptions,
        public: publicRoutes,
      })
 
      expect(auth.public).toEqual(publicRoutes)
    })
 
    it('should store protected routes configuration', () => {
      const protectedRoutes = ['/api/*', '/admin/*']
      const auth = new MyAuthProvider({
        ...mockOptions,
        protected: protectedRoutes,
      })
 
      expect(auth.protected).toEqual(protectedRoutes)
    })
  })
})

Error handling

Provide descriptive errors for common failure scenarios:

export class MyAuthProvider extends MastraAuthProvider<MyUser> {
  constructor(options?: MyAuthOptions) {
    super({ name: options?.name ?? 'my-auth' })
 
    const apiUrl = options?.apiUrl ?? process.env.MY_AUTH_API_URL
    const apiKey = options?.apiKey ?? process.env.MY_AUTH_API_KEY
 
    if (!apiUrl) {
      throw new Error(
        'Missing MY_AUTH_API_URL. Set the environment variable or pass apiUrl in options.',
      )
    }
 
    if (!apiKey) {
      throw new Error(
        'Missing MY_AUTH_API_KEY. Set the environment variable or pass apiKey in options.',
      )
    }
 
    this.apiUrl = apiUrl
    this.apiKey = apiKey
    this.registerOptions(options)
  }
 
  async authenticateToken(token: string): Promise<MyUser | null> {
    if (!token || typeof token !== 'string') {
      return null // Immediate safe fail
    }
 
    try {
      const response = await fetch(`${this.apiUrl}/verify`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': this.apiKey,
        },
        body: JSON.stringify({ token }),
      })
 
      if (!response.ok) {
        return null
      }
 
      return await response.json()
    } catch (error) {
      // Log error for debugging, but don't expose details to client
      console.error('Auth verification error:', error)
      return null
    }
  }
}

Built-in providers

Mastra includes these auth providers as reference implementations:

  • MastraJwtAuth: Simple JWT verification with HMAC secrets (@mastra/auth)
  • MastraAuthClerk: Clerk authentication (@mastra/auth-clerk)
  • MastraAuthAuth0: Auth0 authentication (@mastra/auth-auth0)
  • MastraAuthSupabase: Supabase authentication (@mastra/auth-supabase)
  • MastraAuthFirebase: Firebase authentication (@mastra/auth-firebase)
  • MastraAuthWorkOS: WorkOS authentication (@mastra/auth-workos)
  • MastraAuthBetterAuth: Better Auth integration (@mastra/auth-better-auth)
  • SimpleAuth: Token-to-user mapping for development (@mastra/core/server)

See the source code for implementation details.

Liên kết