Skip to main content

How to Create a Custom Auth Adapter

This guide explains how to create custom authentication adapters for IDPass DataCollect to integrate with different authentication providers.

Overview​

Auth adapters implement the AuthAdapter interface to provide authentication functionality for specific providers. The system supports multiple authentication methods including OAuth/OIDC, basic authentication, and custom token-based systems.

AuthAdapter Interface​

All auth adapters must implement the AuthAdapter interface:

export interface AuthAdapter {
initialize(): Promise<void>;
isAuthenticated(): Promise<boolean>;
login(credentials: PasswordCredentials | TokenCredentials | null): Promise<{ username: string; token: string }>;
logout(): Promise<void>;
validateToken(token: string): Promise<boolean>;
handleCallback(): Promise<void>;
}

Implementation Pattern​

For simple authentication systems, follow the MockAuthAdapter pattern:

import { AuthAdapter, AuthConfig, SingleAuthStorage } from "../packages/datacollect/api/interfaces/types";

export class CustomBasicAuthAdapter implements AuthAdapter {
private authenticated = false;

constructor(
private authStorage: SingleAuthStorage | null,
public config: AuthConfig,
) {}

async initialize(): Promise<void> {
if (this.authStorage) {
const token = await this.authStorage.getToken();
if (token) {
this.authenticated = await this.validateToken(token);
}
}
}

async isAuthenticated(): Promise<boolean> {
return this.authenticated;
}

async login(credentials: PasswordCredentials | TokenCredentials | null): Promise<{ username: string; token: string }> {
if (!this.authStorage) {
throw new Error("Auth storage is not set");
}

if (!credentials) {
throw new Error("Credentials are required");
}

let response: { username: string; token: string };

if ("username" in credentials) {
// Handle password credentials
response = await this.authenticateWithProvider(credentials);
} else if ("token" in credentials) {
// Handle token credentials
const isValid = await this.validateToken(credentials.token);
if (!isValid) {
throw new Error("Invalid token");
}
response = { username: "token-user", token: credentials.token };
} else {
throw new Error("Invalid credentials format");
}

if (this.authStorage) {
await this.authStorage.setToken(response.token);
}

this.authenticated = true;
return response;
}

async logout(): Promise<void> {
if (this.authStorage) {
await this.authStorage.removeToken();
}
this.authenticated = false;
}

async validateToken(token: string): Promise<boolean> {
// Implement token validation logic
return this.verifyTokenWithProvider(token);
}

async handleCallback(): Promise<void> {
// Not needed for basic auth
}

private async authenticateWithProvider(credentials: PasswordCredentials): Promise<{ username: string; token: string }> {
// Your custom authentication logic
const response = await fetch(`${this.config.fields.url}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});

if (!response.ok) {
throw new Error('Authentication failed');
}

return await response.json();
}

private async verifyTokenWithProvider(token: string): Promise<boolean> {
try {
const response = await fetch(`${this.config.fields.url}/auth/verify`, {
headers: { 'Authorization': `Bearer ${token}` }
});
return response.ok;
} catch {
return false;
}
}
}

Step-by-Step Implementation​

Step 1: Create Adapter Class​

Create your adapter class in packages/datacollect/src/components/authentication/:

touch packages/datacollect/src/components/authentication/MyCustomAuthAdapter.ts

Step 2: Implement Interface​

import { AuthAdapter, AuthConfig, SingleAuthStorage } from "../packages/datacollect/api/interfaces/types";

export class MyCustomAuthAdapter implements AuthAdapter {
constructor(
private authStorage: SingleAuthStorage | null,
public config: AuthConfig,
) {}

// Implement all required methods
async initialize(): Promise<void> { /* ... */ }
async isAuthenticated(): Promise<boolean> { /* ... */ }
async login(credentials: any): Promise<{ username: string; token: string }> { /* ... */ }
async logout(): Promise<void> { /* ... */ }
async validateToken(token: string): Promise<boolean> { /* ... */ }
async handleCallback(): Promise<void> { /* ... */ }
}

Step 3: Register Adapter​

Add your adapter to the adaptersMapping in AuthManager.ts:

import { MyCustomAuthAdapter } from "./authentication/MyCustomAuthAdapter";

const adaptersMapping = {
auth0: Auth0AuthAdapter,
keycloak: KeycloakAuthAdapter,
mycustom: MyCustomAuthAdapter, // Add your adapter
};

Step 4: Configure Authentication​

Use your adapter in the configuration:

{
"type": "mycustom",
"fields": {
"url": "https://auth.example.com",
"client_id": "your-client-id",
"api_key": "your-api-key"
}
}

Configuration Patterns​

OAuth Configuration​

{
"type": "custom-oauth",
"fields": {
"authority": "https://auth.example.com",
"client_id": "your-client-id",
"redirect_uri": "http://localhost:3000/callback",
"scope": "openid profile email"
}
}

Best Practices​

Error Handling​

async validateToken(token: string): Promise<boolean> {
try {
const response = await this.callAuthProvider(token);
return response.ok;
} catch (error) {
console.error("Token validation failed:", error);
return false;
}
}

Environment Detection​

constructor(authStorage: SingleAuthStorage | null, config: AuthConfig) {
this.appType = typeof window !== 'undefined' ? 'frontend' : 'backend';
// Use different validation strategies based on environment
}

Token Storage​

async login(credentials: any): Promise<{ username: string; token: string }> {
const result = await this.authenticate(credentials);

// Always store tokens when available
if (this.authStorage && result.token) {
await this.authStorage.setToken(result.token);
}

return result;
}

Config Validation​

constructor(authStorage: SingleAuthStorage | null, config: AuthConfig) {
if (!config.fields.url) {
throw new Error("URL is required for custom auth adapter");
}
this.config = config;
}

Testing Your Adapter​

Create tests following the pattern in AuthManager.test.ts:

import { MyCustomAuthAdapter } from "../authentication/MyCustomAuthAdapter";

describe("MyCustomAuthAdapter", () => {
let adapter: MyCustomAuthAdapter;
let mockAuthStorage: jest.Mocked<SingleAuthStorage>;

beforeEach(() => {
mockAuthStorage = {
getToken: jest.fn(),
setToken: jest.fn(),
removeToken: jest.fn(),
};

adapter = new MyCustomAuthAdapter(mockAuthStorage, {
type: "mycustom",
fields: { url: "https://test.example.com" }
});
});

it("should authenticate successfully", async () => {
// Test implementation
});
});

Common Use Cases​

1. API Key Authentication​

async login(credentials: PasswordCredentials | TokenCredentials | null): Promise<{ username: string; token: string }> {
if (!this.authStorage) {
throw new Error("Auth storage is not set");
}

if (!credentials || !("token" in credentials)) {
throw new Error("API key token is required");
}

const apiKey = credentials.token;
const isValid = await this.validateApiKey(apiKey);

if (!isValid) {
throw new Error("Invalid API key");
}

await this.authStorage.setToken(apiKey);
this.authenticated = true;
return { username: "api-user", token: apiKey };
}

private async validateApiKey(apiKey: string): Promise<boolean> {
try {
const response = await fetch(`${this.config.fields.url}/validate`, {
headers: { 'X-API-Key': apiKey }
});
return response.ok;
} catch {
return false;
}
}

2. JWT Token Validation​

async validateToken(token: string): Promise<boolean> {
try {
// Simple JWT validation without signature verification
const parts = token.split('.');
if (parts.length !== 3) {
return false;
}

const payload = JSON.parse(atob(parts[1]));
const now = Math.floor(Date.now() / 1000);

// Check if token is expired
if (payload.exp && payload.exp < now) {
return false;
}

// Additional validation with your auth server
const response = await fetch(`${this.config.fields.url}/verify`, {
headers: { 'Authorization': `Bearer ${token}` }
});

return response.ok;
} catch {
return false;
}
}

3. Custom Headers and Authentication​

export class CustomHeaderAuthAdapter implements AuthAdapter {
private authenticated = false;

constructor(
private authStorage: SingleAuthStorage | null,
public config: AuthConfig,
) {}

async login(credentials: PasswordCredentials | TokenCredentials | null): Promise<{ username: string; token: string }> {
if (!this.authStorage) {
throw new Error("Auth storage is not set");
}

if (!credentials || !("username" in credentials)) {
throw new Error("Username and password required");
}

const response = await this.authenticateWithCustomHeaders(credentials);
await this.authStorage.setToken(response.token);
this.authenticated = true;
return response;
}

private async authenticateWithCustomHeaders(credentials: PasswordCredentials): Promise<{ username: string; token: string }> {
const response = await fetch(`${this.config.fields.url}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-ID': this.config.fields.clientId,
'X-API-Version': this.config.fields.apiVersion || '1.0',
},
body: JSON.stringify(credentials)
});

if (!response.ok) {
throw new Error('Authentication failed');
}

return await response.json();
}

async validateToken(token: string): Promise<boolean> {
try {
const response = await fetch(`${this.config.fields.url}/verify`, {
headers: {
'Authorization': `Bearer ${token}`,
'X-Client-ID': this.config.fields.clientId,
}
});
return response.ok;
} catch {
return false;
}
}

// ... other required methods
}

Troubleshooting​

Common Issues​

  1. Adapter not found: Ensure it's registered in adaptersMapping
  2. Token validation fails: Check endpoint URLs and request format
  3. Storage errors: Verify SingleAuthStorage is properly injected
  4. CORS issues: Configure your auth provider for cross-origin requests

Alternative Solutions​

  • Extend existing adapters for similar OAuth providers
  • Use MockAuthAdapter for development/testing
  • Implement multiple adapters for different environments