# Custom Auth Providers 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: ```typescript 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 { apiUrl?: string; apiKey?: string; } export class MyAuthProvider extends MastraAuthProvider { 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 { 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 { // 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. ```typescript async authenticateToken(token: string, request: HonoRequest): Promise ``` | Parameter | Type | Description | | --------- | ------------- | ----------------------------------------------------------- | | `token` | `string` | The bearer token extracted from the `Authorization` header | | `request` | `HonoRequest` | The 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 ` 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. ```typescript async authorizeUser(user: TUser, request: HonoRequest): Promise | boolean ``` | Parameter | Type | Description | | --------- | ------------- | ----------------------------------------------- | | `user` | `TUser` | The user object returned by `authenticateToken` | | `request` | `HonoRequest` | The incoming request object | **Returns**: `true` to allow access, `false` to deny (returns 403 Forbidden). ## Configuration Options The `MastraAuthProviderOptions` interface supports these options: | Option | Type | Description | | --------------- | -------------------------------------------------------- | ----------------------------------- | | `name` | `string` | Provider name for logging/debugging | | `authorizeUser` | `(user, request) => Promise \| boolean` | Custom 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: ```typescript 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: ```typescript 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 ```typescript 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 ```typescript 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 { jwksUri?: string; issuer?: string; } export class MyJwksAuth extends MastraAuthProvider { 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 { 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 { // 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: ```typescript 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 ```typescript 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: ```typescript 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: 'test@example.com', 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: 'test@example.com', 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: 'test@example.com', 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: 'admin@example.com', roles: ['admin'] }; const regularUser = { id: 'user456', email: 'user@example.com', 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: ```typescript export class MyAuthProvider extends MastraAuthProvider { 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 { 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](https://github.com/mastra-ai/mastra/tree/main/auth) for implementation details. ## Related - [Auth Overview](https://mastra.ai/docs/server/auth) - Authentication concepts and configuration - [JWT Auth](https://mastra.ai/docs/server/auth/jwt) - Simple JWT authentication - [Clerk Auth](https://mastra.ai/docs/server/auth/clerk) - Clerk integration - [Custom API Routes](https://mastra.ai/docs/server/custom-api-routes) - Controlling authentication on custom endpoints