Feature: API Key Authentication for Programmatic Access #2

Open
opened 2026-01-02 19:00:53 +00:00 by padreug · 0 comments
Owner

Overview

Add API key authentication to lamassu-server's admin API, enabling programmatic access from external applications without going through the interactive login flow (username/password + 2FA).

Use Case: A web app needs to manage cash unit counts, sync membership data, and perform other operations via the GraphQL API.


Current State

The admin API currently only supports session-based authentication:

  • Username + password login
  • 2FA (TOTP or FIDO/WebAuthn)
  • Session cookie (lamassu_sid)

This makes it difficult for external services to integrate programmatically.


Proposed Solution

Add Bearer token authentication via API keys.

Database Schema

CREATE TABLE api_keys (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    key_hash VARCHAR(255) NOT NULL,
    key_prefix VARCHAR(8) NOT NULL,
    role VARCHAR(50) NOT NULL DEFAULT 'user',
    scopes TEXT[],
    created_by UUID REFERENCES users(id),
    created TIMESTAMPTZ DEFAULT NOW(),
    last_used TIMESTAMPTZ,
    expires_at TIMESTAMPTZ,
    enabled BOOLEAN DEFAULT TRUE
);

CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix);

API Key Format

lam_<random-32-chars>

Example: lam_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
  • Prefix lam_ identifies it as a Lamassu key
  • Only the hash is stored; full key shown once at creation

Implementation

1. CLI Command for Key Generation

Create bin/lamassu-create-api-key:

node packages/server/bin/lamassu-create-api-key "Cash Management App" --role user

# Output:
# API Key created successfully!
# Name: Cash Management App
# Key: lam_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
#
# IMPORTANT: Save this key now. It cannot be retrieved later.

2. Authentication Middleware

Create packages/server/lib/new-admin/middlewares/apiKeyAuth.js:

const argon2 = require('argon2')
const db = require('../../db')

async function validateApiKey(authHeader) {
    if (!authHeader?.startsWith('Bearer lam_')) {
        return null
    }

    const apiKey = authHeader.replace('Bearer ', '')
    const prefix = apiKey.substring(0, 8)

    const keyRecord = await db.oneOrNone(
        'SELECT * FROM api_keys WHERE key_prefix = $1 AND enabled = TRUE',
        [prefix]
    )

    if (!keyRecord) return null
    if (keyRecord.expires_at && new Date(keyRecord.expires_at) < new Date()) {
        return null
    }

    const isValid = await argon2.verify(keyRecord.key_hash, apiKey)
    if (!isValid) return null

    await db.none('UPDATE api_keys SET last_used = NOW() WHERE id = $1', [keyRecord.id])

    return {
        id: keyRecord.id,
        name: keyRecord.name,
        role: keyRecord.role,
        scopes: keyRecord.scopes
    }
}

module.exports = { validateApiKey }

3. Modify Auth Directive

Update packages/server/lib/new-admin/graphql/directives/auth.js:

// Check for both session auth and API key auth
const user = context.req.session?.user || context.apiKeyUser

if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) {
    throw new AuthenticationError('You do not have permission')
}

4. Update Apollo Context

const { validateApiKey } = require('./apiKeyAuth')

async function buildApolloContext({ req, res }) {
    const authHeader = req.headers.authorization
    const apiKeyUser = await validateApiKey(authHeader)

    return {
        req,
        res,
        apiKeyUser
    }
}

Usage Example

Update Cash Unit Counts

curl -k -X POST https://your-server:443/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer lam_your-api-key-here" \
  -d '{
    "query": "mutation SetCassetteBills($deviceId: ID!, $cashUnits: CashUnitsInput!) { machineAction(deviceId: $deviceId, action: setCassetteBills, cashUnits: $cashUnits) { deviceId cashUnits { cassette1 cassette2 } } }",
    "variables": {
      "deviceId": "machine-123",
      "cashUnits": { "cassette1": 300, "cassette2": 250 }
    }
  }'

Sync Membership Data

curl -k -X POST https://your-server:443/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer lam_your-api-key-here" \
  -d '{
    "query": "mutation UpsertMembership($externalUserId: String!, $tierName: String!, $lightningAddress: String) { upsertMembership(externalUserId: $externalUserId, tierName: $tierName, lightningAddress: $lightningAddress) { id tier { name } } }",
    "variables": {
      "externalUserId": "user_12345",
      "tierName": "gold",
      "lightningAddress": "user@getalby.com"
    }
  }'

Implementation Checklist

Phase 1: Core Infrastructure

  • Create database migration for api_keys table
  • Implement lamassu-create-api-key CLI command
  • Create apiKeyAuth.js middleware
  • Update buildApolloContext to check for API key
  • Modify @auth directive to accept API key authentication

Phase 2: Management Commands

  • Add lamassu-revoke-api-key CLI command
  • Add lamassu-list-api-keys CLI command

Phase 3: Testing

  • Test API key creation via CLI
  • Test authenticated GraphQL queries with API key
  • Test authenticated GraphQL mutations with API key
  • Test key revocation
  • Test expired key handling

Phase 4: Optional Enhancements

  • Admin UI for API key management
  • Rate limiting per API key
  • Audit logging for API key usage
  • Scoped permissions (limit keys to specific operations)

Security Considerations

  1. Key Storage: Only store hashed keys; never log full keys after creation
  2. HTTPS Required: API keys should only be transmitted over HTTPS
  3. Key Rotation: Implement expiration and encourage regular rotation
  4. Audit Trail: Log all API key usage for security monitoring
  5. Least Privilege: Use scopes to limit keys to only necessary operations
  6. Rate Limiting: Prevent brute-force attacks with rate limiting per key

  • #1 - Membership-Based Discount System (depends on this)

References

  • Full implementation plan: api-key-auth-plan.md in lamassu-stuff repo
## Overview Add API key authentication to lamassu-server's admin API, enabling programmatic access from external applications without going through the interactive login flow (username/password + 2FA). **Use Case:** A web app needs to manage cash unit counts, sync membership data, and perform other operations via the GraphQL API. --- ## Current State The admin API currently only supports session-based authentication: - Username + password login - 2FA (TOTP or FIDO/WebAuthn) - Session cookie (`lamassu_sid`) This makes it difficult for external services to integrate programmatically. --- ## Proposed Solution Add Bearer token authentication via API keys. ### Database Schema ```sql CREATE TABLE api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, key_hash VARCHAR(255) NOT NULL, key_prefix VARCHAR(8) NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'user', scopes TEXT[], created_by UUID REFERENCES users(id), created TIMESTAMPTZ DEFAULT NOW(), last_used TIMESTAMPTZ, expires_at TIMESTAMPTZ, enabled BOOLEAN DEFAULT TRUE ); CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix); ``` ### API Key Format ``` lam_<random-32-chars> Example: lam_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 ``` - Prefix `lam_` identifies it as a Lamassu key - Only the hash is stored; full key shown once at creation --- ## Implementation ### 1. CLI Command for Key Generation Create `bin/lamassu-create-api-key`: ```bash node packages/server/bin/lamassu-create-api-key "Cash Management App" --role user # Output: # API Key created successfully! # Name: Cash Management App # Key: lam_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 # # IMPORTANT: Save this key now. It cannot be retrieved later. ``` ### 2. Authentication Middleware Create `packages/server/lib/new-admin/middlewares/apiKeyAuth.js`: ```javascript const argon2 = require('argon2') const db = require('../../db') async function validateApiKey(authHeader) { if (!authHeader?.startsWith('Bearer lam_')) { return null } const apiKey = authHeader.replace('Bearer ', '') const prefix = apiKey.substring(0, 8) const keyRecord = await db.oneOrNone( 'SELECT * FROM api_keys WHERE key_prefix = $1 AND enabled = TRUE', [prefix] ) if (!keyRecord) return null if (keyRecord.expires_at && new Date(keyRecord.expires_at) < new Date()) { return null } const isValid = await argon2.verify(keyRecord.key_hash, apiKey) if (!isValid) return null await db.none('UPDATE api_keys SET last_used = NOW() WHERE id = $1', [keyRecord.id]) return { id: keyRecord.id, name: keyRecord.name, role: keyRecord.role, scopes: keyRecord.scopes } } module.exports = { validateApiKey } ``` ### 3. Modify Auth Directive Update `packages/server/lib/new-admin/graphql/directives/auth.js`: ```javascript // Check for both session auth and API key auth const user = context.req.session?.user || context.apiKeyUser if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) { throw new AuthenticationError('You do not have permission') } ``` ### 4. Update Apollo Context ```javascript const { validateApiKey } = require('./apiKeyAuth') async function buildApolloContext({ req, res }) { const authHeader = req.headers.authorization const apiKeyUser = await validateApiKey(authHeader) return { req, res, apiKeyUser } } ``` --- ## Usage Example ### Update Cash Unit Counts ```bash curl -k -X POST https://your-server:443/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer lam_your-api-key-here" \ -d '{ "query": "mutation SetCassetteBills($deviceId: ID!, $cashUnits: CashUnitsInput!) { machineAction(deviceId: $deviceId, action: setCassetteBills, cashUnits: $cashUnits) { deviceId cashUnits { cassette1 cassette2 } } }", "variables": { "deviceId": "machine-123", "cashUnits": { "cassette1": 300, "cassette2": 250 } } }' ``` ### Sync Membership Data ```bash curl -k -X POST https://your-server:443/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer lam_your-api-key-here" \ -d '{ "query": "mutation UpsertMembership($externalUserId: String!, $tierName: String!, $lightningAddress: String) { upsertMembership(externalUserId: $externalUserId, tierName: $tierName, lightningAddress: $lightningAddress) { id tier { name } } }", "variables": { "externalUserId": "user_12345", "tierName": "gold", "lightningAddress": "user@getalby.com" } }' ``` --- ## Implementation Checklist ### Phase 1: Core Infrastructure - [ ] Create database migration for `api_keys` table - [ ] Implement `lamassu-create-api-key` CLI command - [ ] Create `apiKeyAuth.js` middleware - [ ] Update `buildApolloContext` to check for API key - [ ] Modify `@auth` directive to accept API key authentication ### Phase 2: Management Commands - [ ] Add `lamassu-revoke-api-key` CLI command - [ ] Add `lamassu-list-api-keys` CLI command ### Phase 3: Testing - [ ] Test API key creation via CLI - [ ] Test authenticated GraphQL queries with API key - [ ] Test authenticated GraphQL mutations with API key - [ ] Test key revocation - [ ] Test expired key handling ### Phase 4: Optional Enhancements - [ ] Admin UI for API key management - [ ] Rate limiting per API key - [ ] Audit logging for API key usage - [ ] Scoped permissions (limit keys to specific operations) --- ## Security Considerations 1. **Key Storage**: Only store hashed keys; never log full keys after creation 2. **HTTPS Required**: API keys should only be transmitted over HTTPS 3. **Key Rotation**: Implement expiration and encourage regular rotation 4. **Audit Trail**: Log all API key usage for security monitoring 5. **Least Privilege**: Use scopes to limit keys to only necessary operations 6. **Rate Limiting**: Prevent brute-force attacks with rate limiting per key --- ## Related Issues - #1 - Membership-Based Discount System (depends on this) ## References - Full implementation plan: `api-key-auth-plan.md` in lamassu-stuff repo
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: aiolabs/lamassu-server#2
No description provided.