Skip to Content
🚀 APSO is now in public beta. Get started →
GuidesAuthAPI Keys

API Keys

API keys provide authentication for machine-to-machine communication, background jobs, and external integrations.

Overview

Use CaseRecommendation
User authenticationUse JWT tokens
Server-to-serverUse API keys
Background jobsUse API keys
External integrationsUse API keys
Mobile/Web appsUse JWT tokens

API Key Structure

apso_live_org123_abc123def456... └─┬─┘ └┬┘ └─┬─┘ └─────┬──────┘ │ │ │ │ prefix env org-id key

Implementation

API Key Entity

// src/entities/api-key.entity.ts import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; import { Organization } from './organization.entity'; @Entity('api_keys') export class ApiKey { @PrimaryGeneratedColumn('uuid') id: string; @Column() name: string; @Column({ unique: true }) keyHash: string; @Column() prefix: string; @ManyToOne(() => Organization) organization: Organization; @Column() organizationId: string; @Column({ type: 'simple-array', nullable: true }) scopes: string[]; @Column({ type: 'timestamp', nullable: true }) expiresAt: Date | null; @Column({ type: 'timestamp', nullable: true }) lastUsedAt: Date | null; @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) createdAt: Date; }

API Key Service

// src/modules/api-keys/api-keys.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ApiKey } from '../../entities/api-key.entity'; import * as crypto from 'crypto'; @Injectable() export class ApiKeysService { constructor( @InjectRepository(ApiKey) private readonly repository: Repository<ApiKey>, ) {} async create( organizationId: string, name: string, scopes?: string[], expiresAt?: Date, ): Promise<{ apiKey: ApiKey; rawKey: string }> { // Generate key const rawKey = this.generateKey(organizationId); const keyHash = this.hashKey(rawKey); const prefix = rawKey.substring(0, 12); const apiKey = this.repository.create({ name, keyHash, prefix, organizationId, scopes, expiresAt, }); await this.repository.save(apiKey); // Return raw key only once - it won't be retrievable later return { apiKey, rawKey }; } async validate(rawKey: string): Promise<ApiKey | null> { const keyHash = this.hashKey(rawKey); const apiKey = await this.repository.findOne({ where: { keyHash }, relations: ['organization'], }); if (!apiKey) return null; // Check expiration if (apiKey.expiresAt && apiKey.expiresAt < new Date()) { return null; } // Update last used apiKey.lastUsedAt = new Date(); await this.repository.save(apiKey); return apiKey; } async revoke(id: string, organizationId: string): Promise<void> { await this.repository.delete({ id, organizationId }); } private generateKey(organizationId: string): string { const env = process.env.NODE_ENV === 'production' ? 'live' : 'test'; const orgPrefix = organizationId.substring(0, 6); const randomPart = crypto.randomBytes(24).toString('hex'); return `apso_${env}_${orgPrefix}_${randomPart}`; } private hashKey(key: string): string { return crypto.createHash('sha256').update(key).digest('hex'); } }

API Key Guard

// src/common/guards/api-key.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { ApiKeysService } from '../../modules/api-keys/api-keys.service'; @Injectable() export class ApiKeyGuard implements CanActivate { constructor(private readonly apiKeysService: ApiKeysService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const apiKey = this.extractApiKey(request); if (!apiKey) { throw new UnauthorizedException('API key required'); } const validKey = await this.apiKeysService.validate(apiKey); if (!validKey) { throw new UnauthorizedException('Invalid or expired API key'); } // Set user context for downstream services request.user = { id: `apikey:${validKey.id}`, organizationId: validKey.organizationId, isApiKey: true, scopes: validKey.scopes, }; return true; } private extractApiKey(request: any): string | undefined { // Check header const headerKey = request.headers['x-api-key']; if (headerKey) return headerKey; // Check Authorization header const [type, token] = request.headers.authorization?.split(' ') ?? []; if (type === 'Bearer' && token?.startsWith('apso_')) { return token; } return undefined; } }

Combined Auth Guard

Support both JWT and API key authentication:

// src/common/guards/combined-auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { JwtAuthGuard } from './jwt-auth.guard'; import { ApiKeyGuard } from './api-key.guard'; @Injectable() export class CombinedAuthGuard implements CanActivate { constructor( private readonly jwtGuard: JwtAuthGuard, private readonly apiKeyGuard: ApiKeyGuard, ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const authHeader = request.headers.authorization; const apiKeyHeader = request.headers['x-api-key']; // API key takes precedence if (apiKeyHeader || authHeader?.includes('apso_')) { return this.apiKeyGuard.canActivate(context); } // Fall back to JWT if (authHeader) { return this.jwtGuard.canActivate(context); } throw new UnauthorizedException(); } }

API Key Controller

// src/modules/api-keys/api-keys.controller.ts import { Controller, Post, Get, Delete, Body, Param, UseGuards } from '@nestjs/common'; import { ApiKeysService } from './api-keys.service'; import { AuthGuard } from '../../common/guards/auth.guard'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; @Controller('api-keys') @UseGuards(AuthGuard) export class ApiKeysController { constructor(private readonly service: ApiKeysService) {} @Post() async create( @CurrentUser() user: { organizationId: string }, @Body() body: { name: string; scopes?: string[]; expiresAt?: string }, ) { const { apiKey, rawKey } = await this.service.create( user.organizationId, body.name, body.scopes, body.expiresAt ? new Date(body.expiresAt) : undefined, ); return { id: apiKey.id, name: apiKey.name, prefix: apiKey.prefix, key: rawKey, // Only returned once! scopes: apiKey.scopes, expiresAt: apiKey.expiresAt, createdAt: apiKey.createdAt, }; } @Get() async list(@CurrentUser() user: { organizationId: string }) { return this.service.findAll(user.organizationId); } @Delete(':id') async revoke( @Param('id') id: string, @CurrentUser() user: { organizationId: string }, ) { await this.service.revoke(id, user.organizationId); return { success: true }; } }

Usage Examples

cURL

# Using X-API-Key header curl -H "X-API-Key: apso_live_abc123_xyz789..." \ https://api.yourapp.com/api/v1/projects # Using Authorization header curl -H "Authorization: Bearer apso_live_abc123_xyz789..." \ https://api.yourapp.com/api/v1/projects

Node.js

const response = await fetch('https://api.yourapp.com/api/v1/projects', { headers: { 'X-API-Key': process.env.APSO_API_KEY, }, });

Python

import requests response = requests.get( 'https://api.yourapp.com/api/v1/projects', headers={'X-API-Key': os.environ['APSO_API_KEY']} )

Scopes

Restrict API key access with scopes:

// Available scopes const SCOPES = { 'projects:read': 'Read projects', 'projects:write': 'Create and update projects', 'tasks:read': 'Read tasks', 'tasks:write': 'Create and update tasks', '*': 'Full access', }; // Scope guard @Injectable() export class ScopeGuard implements CanActivate { constructor(private readonly requiredScopes: string[]) {} canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const user = request.user; if (!user.isApiKey) return true; // JWT users have full access const hasScope = this.requiredScopes.some( scope => user.scopes?.includes(scope) || user.scopes?.includes('*') ); if (!hasScope) { throw new ForbiddenException('Insufficient scope'); } return true; } }

Security Best Practices

  1. Never log API keys - Only log prefixes for debugging
  2. Hash keys in storage - Store SHA-256 hashes, not raw keys
  3. Set expiration - Require key rotation with expiry dates
  4. Use scopes - Limit keys to required permissions
  5. Monitor usage - Track lastUsedAt and watch for anomalies
  6. Revoke immediately - Remove compromised keys without delay
Last updated on