Skip to main content
Mastra 1.0 is available 🎉 Read announcement

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
Direct link to 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
Direct link to 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
Direct link to Required Methods

authenticateToken()
Direct link to 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()
Direct link to 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
Direct link to 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
Direct link to 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
Direct link to 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
Direct link to Helper Utilities

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

JWT Verification
Direct link to 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
Direct link to 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
Direct link to 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
Direct link to 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
Direct link to 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: '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
Direct link to 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
Direct link to 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.