A secure, cryptographically-strong API key generation and verification service for multi-tenant applications. This service generates prefixed API keys with embedded tenant information and provides secure verification with timing attack protection.
- Secure Key Generation: Uses SHA3-512 hashing and cryptographically secure random bytes
- Multi-tenant Support: Each API key is associated with a tenant ID
- Prefixed Keys: Customizable prefixes for easy identification and organization
- Expiration Support: Optional expiration dates for keys
- Timing Attack Protection: Constant-time verification to prevent timing attacks
- Base58 Encoding: Human-readable, URL-safe key encoding
- UUIDv7 Integration: Time-sortable UUIDs for key identification
Generated API keys follow this format: {prefix}_{base58_encoded_data}
Example: myapp_api_2Z4j8K9mP3nQ7rS1tU5vW8xY2zA3b4C5d6E7f8G9h1J2k3L4m5N6p7Q8r9S1t2U3v4W5x6Y7z8A9b1C2
The encoded data contains:
- Key ID (16 bytes, UUIDv7)
- Tenant ID (16 bytes, UUID)
- Random secret (32 bytes)
npm install @noble/hashes
# or
yarn add @noble/hashesimport { createApiKey, verifyApiKey, type ApiKey } from './main.ts';
// Create an API key
const tenantId = "0198e890-b672-716f-ad02-e892662d818f";
const [apiKey, token] = createApiKey("myapp_api", tenantId, 30); // 30 days expiration
console.log('API Key:', apiKey);
console.log('Token:', token);
// Verify an API key
const getApiKeyFromDB = async (prefix: string, tenantId: string, keyId: string): Promise<ApiKey> => {
// Your database lookup logic here
return await db.apiKeys.findOne({ prefix, tenantId, id: keyId });
};
try {
const verifiedKey = await verifyApiKey(token, getApiKeyFromDB);
console.log('Valid key for tenant:', verifiedKey.tenantId);
} catch (error) {
console.error('Invalid or expired key:', error.message);
}// src/auth/api-key.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { createApiKey, verifyApiKey, type ApiKey as IApiKey } from 'apikey-service/main.ts';
import { ApiKey } from './entities/api-key.entity';
@Injectable()
export class ApiKeyService {
constructor(
@InjectRepository(ApiKey)
private apiKeyRepository: Repository<ApiKey>,
) {}
async generateApiKey(
prefix: string,
tenantId: string,
expirationDays: number | null = null,
description?: string,
): Promise<{ apiKey: ApiKey; token: string }> {
const [keyData, token] = createApiKey(prefix, tenantId, expirationDays);
const apiKey = this.apiKeyRepository.create({
id: keyData.id,
tenantId: keyData.tenantId,
prefix: keyData.prefix,
secretHashBase58: keyData.secret_hash_base58,
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
description,
isActive: true,
});
await this.apiKeyRepository.save(apiKey);
return { apiKey, token };
}
async verifyApiKey(token: string): Promise<ApiKey> {
const getApiKey = async (prefix: string, tenantId: string, keyId: string): Promise<IApiKey> => {
const apiKey = await this.apiKeyRepository.findOne({
where: { prefix, tenantId, id: keyId, isActive: true },
});
if (!apiKey) {
throw new Error('API key not found');
}
return {
id: apiKey.id,
tenantId: apiKey.tenantId,
prefix: apiKey.prefix,
secret_hash_base58: apiKey.secretHashBase58,
createdAt: apiKey.createdAt,
expiresAt: apiKey.expiresAt,
};
};
const verifiedKey = await verifyApiKey(token, getApiKey);
// Return the full entity from database
return await this.apiKeyRepository.findOne({
where: { id: verifiedKey.id, tenantId: verifiedKey.tenantId },
});
}
async revokeApiKey(keyId: string, tenantId: string): Promise<void> {
await this.apiKeyRepository.update(
{ id: keyId, tenantId },
{ isActive: false, revokedAt: new Date() }
);
}
async listApiKeys(tenantId: string): Promise<ApiKey[]> {
return await this.apiKeyRepository.find({
where: { tenantId, isActive: true },
select: ['id', 'prefix', 'description', 'createdAt', 'expiresAt'], // Don't expose hash
});
}
}// src/auth/entities/api-key.entity.ts
import { Entity, PrimaryColumn, Column, CreateDateColumn } from 'typeorm';
@Entity('api_keys')
export class ApiKey {
@PrimaryColumn('uuid')
id: string;
@Column('uuid')
tenantId: string;
@Column()
prefix: string;
@Column()
secretHashBase58: string;
@Column({ nullable: true })
description?: string;
@CreateDateColumn()
createdAt: Date;
@Column({ nullable: true })
expiresAt?: Date;
@Column({ nullable: true })
revokedAt?: Date;
@Column({ default: true })
isActive: boolean;
}// src/auth/guards/api-key.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { ApiKeyService } from '../api-key.service';
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly apiKeyService: ApiKeyService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const apiKey = this.extractApiKey(request);
if (!apiKey) {
throw new UnauthorizedException('API key is required');
}
try {
const verifiedKey = await this.apiKeyService.verifyApiKey(apiKey);
request.apiKey = verifiedKey;
request.tenantId = verifiedKey.tenantId;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid or expired API key');
}
}
private extractApiKey(request: any): string | null {
// Check Authorization header: "ApiKey {api_key}"
const authHeader = request.headers.authorization;
if (authHeader && authHeader.startsWith('ApiKey ')) {
return authHeader.substring(7);
}
// Check API-Key header
const apiKeyHeader = request.headers['api-key'];
if (apiKeyHeader) {
return apiKeyHeader;
}
return null;
}
}// src/auth/api-key.controller.ts
import { Controller, Post, Get, Delete, Body, Param, UseGuards, Req } from '@nestjs/common';
import { ApiKeyService } from './api-key.service';
import { ApiKeyGuard } from './guards/api-key.guard';
@Controller('api-keys')
export class ApiKeyController {
constructor(private readonly apiKeyService: ApiKeyService) {}
@Post()
@UseGuards(ApiKeyGuard) // Require existing API key to create new ones
async createApiKey(
@Body() createApiKeyDto: {
prefix: string;
expirationDays?: number;
description?: string;
},
@Req() req: any,
) {
const { apiKey, token } = await this.apiKeyService.generateApiKey(
createApiKeyDto.prefix,
req.tenantId,
createApiKeyDto.expirationDays,
createApiKeyDto.description,
);
return {
id: apiKey.id,
prefix: apiKey.prefix,
token, // Only returned once during creation
expiresAt: apiKey.expiresAt,
createdAt: apiKey.createdAt,
};
}
@Get()
@UseGuards(ApiKeyGuard)
async listApiKeys(@Req() req: any) {
return await this.apiKeyService.listApiKeys(req.tenantId);
}
@Delete(':keyId')
@UseGuards(ApiKeyGuard)
async revokeApiKey(@Param('keyId') keyId: string, @Req() req: any) {
await this.apiKeyService.revokeApiKey(keyId, req.tenantId);
return { message: 'API key revoked successfully' };
}
}// src/products/products.controller.ts
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { ApiKeyGuard } from '../auth/guards/api-key.guard';
@Controller('products')
@UseGuards(ApiKeyGuard)
export class ProductsController {
@Get()
async getProducts(@Req() req: any) {
// req.tenantId and req.apiKey are available here
return {
tenantId: req.tenantId,
message: 'Products for your tenant',
};
}
}// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApiKey } from './entities/api-key.entity';
import { ApiKeyService } from './api-key.service';
import { ApiKeyController } from './api-key.controller';
import { ApiKeyGuard } from './guards/api-key.guard';
@Module({
imports: [TypeOrmModule.forFeature([ApiKey])],
providers: [ApiKeyService, ApiKeyGuard],
controllers: [ApiKeyController],
exports: [ApiKeyService, ApiKeyGuard],
})
export class AuthModule {}POST /api-keys
Authorization: ApiKey existing_api_key_here
{
"prefix": "myapp_api",
"expirationDays": 90,
"description": "Production API key for service X"
}# Using Authorization header
GET /products
Authorization: ApiKey myapp_api_2Z4j8K9mP3nQ7rS1tU5vW8xY...
# Using API-Key header
GET /products
API-Key: myapp_api_2Z4j8K9mP3nQ7rS1tU5vW8xY...- Storage: Only store the hashed version (
secret_hash_base58) in your database - Transmission: Always use HTTPS when transmitting API keys
- Timing Attacks: The verification includes random delays to prevent timing attacks
- Key Rotation: Implement regular key rotation policies
- Logging: Never log the actual API key tokens
- Rate Limiting: Implement rate limiting on API key usage
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
prefix VARCHAR(50) NOT NULL,
secret_hash_base58 TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP,
revoked_at TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
INDEX idx_tenant_prefix (tenant_id, prefix),
INDEX idx_tenant_active (tenant_id, is_active),
INDEX idx_expires_at (expires_at)
);Creates a new API key.
- prefix: String identifier for the key (e.g., "myapp_api")
- tenantId: UUID string identifying the tenant
- lifeInDays: Optional number of days until expiration (null for no expiration)
Returns: [ApiKey, string] - The API key object and the token string
Verifies an API key token.
- token: The API key token to verify
- getApiKey: Async function to retrieve the stored API key from your database
Returns: Promise<ApiKey> - The verified API key object
Throws: Error if the key is invalid, expired, or not found
MIT