Skip to content

khaledez/nodejs-apikey-service

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

API Key Service

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.

Features

  • 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

Key Structure

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)

Installation

npm install @noble/hashes
# or
yarn add @noble/hashes

Usage

Basic Usage

import { 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);
}

NestJS Integration

1. Create API Key Service

// 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
    });
  }
}

2. Create Entity

// 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;
}

3. Create Auth Guard

// 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;
  }
}

4. Create Controller

// 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' };
  }
}

5. Protect Routes

// 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',
    };
  }
}

6. Module Configuration

// 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 {}

API Usage Examples

Creating an API Key

POST /api-keys
Authorization: ApiKey existing_api_key_here

{
  "prefix": "myapp_api",
  "expirationDays": 90,
  "description": "Production API key for service X"
}

Using an API Key

# Using Authorization header
GET /products
Authorization: ApiKey myapp_api_2Z4j8K9mP3nQ7rS1tU5vW8xY...

# Using API-Key header
GET /products
API-Key: myapp_api_2Z4j8K9mP3nQ7rS1tU5vW8xY...

Security Considerations

  1. Storage: Only store the hashed version (secret_hash_base58) in your database
  2. Transmission: Always use HTTPS when transmitting API keys
  3. Timing Attacks: The verification includes random delays to prevent timing attacks
  4. Key Rotation: Implement regular key rotation policies
  5. Logging: Never log the actual API key tokens
  6. Rate Limiting: Implement rate limiting on API key usage

Database Schema

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)
);

API Reference

createApiKey(prefix, tenantId, lifeInDays?)

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

verifyApiKey(token, getApiKey)

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

License

MIT

About

Simple & Secure multi-tenant API key management service written in TypeScript for Node.js

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published