API Authentication
This guide covers how authentication works with the Voidkey API and the various ways to provide authentication tokens.
Authentication Overview
Section titled “Authentication Overview”The Voidkey API uses OIDC (OpenID Connect) tokens for authentication. These tokens are issued by configured identity providers and validated by the broker before granting access to credentials.
Authentication Flow
Section titled “Authentication Flow”sequenceDiagram
participant Client
participant IdP as Identity
Provider
participant Broker as Voidkey
Broker
participant Cloud as Cloud
Provider
Client->>IdP: 1. Authenticate & request token
IdP->>Client: 2. OIDC token
Client->>Broker: 3. API request with token
Broker->>IdP: 4. Validate token (JWKS)
IdP->>Broker: 5. Token valid + claims
Broker->>Cloud: 6. Request credentials
Cloud->>Broker: 7. Temporary credentials
Broker->>Client: 8. Credentials response
Providing Authentication Tokens
Section titled “Providing Authentication Tokens”1. Authorization Header (Recommended)
Section titled “1. Authorization Header (Recommended)”The preferred method is to include the OIDC token in the Authorization header:
GET /credentials/keys HTTP/1.1Host: broker.example.comAuthorization: Bearer eyJhbGciOiJSUzI1NiIs...Benefits:
- Standard HTTP authentication method
- Automatically handled by most HTTP libraries
- Not logged in access logs (unlike query parameters)
- Works with all endpoints
2. Request Body (POST only)
Section titled “2. Request Body (POST only)”For POST requests, you can include the token in the request body:
POST /credentials/mint HTTP/1.1Host: broker.example.comContent-Type: application/json
{ "oidcToken": "eyJhbGciOiJSUzI1NiIs...", "keys": ["AWS_DEPLOY"]}When to use:
- When you cannot modify HTTP headers
- Legacy integrations
- Certain client libraries
3. Query Parameter (GET only)
Section titled “3. Query Parameter (GET only)”For GET requests, you can pass the token as a query parameter:
GET /credentials/keys?token=eyJhbGciOiJSUzI1NiIs... HTTP/1.1Host: broker.example.comCaution:
- May be logged in server access logs
- Visible in browser history
- Limited URL length
- Only use when other methods aren’t available
Token Validation Process
Section titled “Token Validation Process”When you make an API request, the broker validates your token through these steps:
1. Token Extraction
Section titled “1. Token Extraction”The broker extracts the token from:
Authorization: Bearer <token>header (first priority)tokenquery parameter (GET requests)oidcTokenfield in request body (POST requests)
2. Token Structure Validation
Section titled “2. Token Structure Validation”// Token must be a valid JWT with three partsconst [header, payload, signature] = token.split('.');
// Header must specify algorithm and key IDconst headerObj = JSON.parse(base64Decode(header));// Required: alg, kid (if multiple keys)
// Payload must contain required claimsconst payloadObj = JSON.parse(base64Decode(payload));// Required: iss, aud, sub, exp, iat3. Signature Verification
Section titled “3. Signature Verification”// 1. Fetch JWKS from IdPconst jwks = await fetch(`${issuer}/.well-known/jwks.json`);
// 2. Find matching keyconst key = jwks.keys.find(k => k.kid === header.kid);
// 3. Verify signatureconst isValid = crypto.verify(algorithm, message, signature, key);4. Claims Validation
Section titled “4. Claims Validation”const now = Math.floor(Date.now() / 1000);
// Check expirationif (payload.exp <= now) { throw new Error('Token expired');}
// Check not beforeif (payload.nbf && payload.nbf > now) { throw new Error('Token not yet valid');}
// Check issuerif (payload.iss !== expectedIssuer) { throw new Error('Invalid issuer');}
// Check audienceif (!payload.aud.includes(expectedAudience)) { throw new Error('Invalid audience');}Token Requirements
Section titled “Token Requirements”Required Claims
Section titled “Required Claims”Your OIDC token must contain these standard claims:
| Claim | Description | Example |
|---|---|---|
iss | Token issuer | https://token.actions.githubusercontent.com |
aud | Intended audience | https://github.com/myorg |
sub | Subject identifier | repo:myorg/myapp:ref:refs/heads/main |
exp | Expiration time | 1705318200 (Unix timestamp) |
iat | Issued at time | 1705314600 (Unix timestamp) |
Optional Claims
Section titled “Optional Claims”These claims may be present and used for additional validation:
| Claim | Description | Example |
|---|---|---|
nbf | Not before time | 1705314600 |
jti | JWT ID (unique identifier) | uuid-string |
azp | Authorized party | client-id |
Subject Format Examples
Section titled “Subject Format Examples”The sub claim format depends on your identity provider:
GitHub Actions:
repo:owner/repository:ref:refs/heads/branchrepo:owner/repository:environment:environment_namerepo:owner/repository:pull_requestAuth0:
auth0|user_idgoogle-oauth2|user_idsamlp|connection|user_idKeycloak:
f47ac10b-58cc-4372-a567-0e02b2c3d479 # User UUIDservice-account-client-name # Service accountCommon Authentication Scenarios
Section titled “Common Authentication Scenarios”GitHub Actions
Section titled “GitHub Actions”permissions: id-token: write # Required for OIDC
jobs: deploy: steps: - name: Get OIDC token uses: actions/github-script@v7 id: get-token with: script: | const token = await core.getIDToken('https://github.com/myorg') core.setSecret(token) core.setOutput('token', token)
- name: Call Voidkey API run: | curl -H "Authorization: Bearer ${{ steps.get-token.outputs.token }}" \ https://broker.example.com/credentials/keysCLI Tools
Section titled “CLI Tools”# Using environment variableexport VOIDKEY_OIDC_TOKEN="$(get-token-command)"curl -H "Authorization: Bearer $VOIDKEY_OIDC_TOKEN" \ https://broker.example.com/credentials/keys
# Using query parameterTOKEN="$(get-token-command)"curl "https://broker.example.com/credentials/keys?token=$TOKEN"Service Accounts
Section titled “Service Accounts”// Node.js example with service accountconst { OAuth2Client } = require('google-auth-library');
async function getOIDCToken() { const client = new OAuth2Client(); const url = 'https://voidkey.example.com'; const audience = 'https://github.com/myorg';
const token = await client.fetchIdToken({ targetAudience: audience, serviceAccountEmail: 'service@project.iam.gserviceaccount.com' });
return token;}
async function callVoidkeyAPI() { const token = await getOIDCToken();
const response = await fetch('https://broker.example.com/credentials/keys', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } });
return response.json();}Token Caching and Refresh
Section titled “Token Caching and Refresh”Client-Side Caching
Section titled “Client-Side Caching”OIDC tokens are typically short-lived (5-15 minutes). Implement proper caching:
class TokenCache { constructor() { this.token = null; this.expiresAt = null; }
async getValidToken() { // Check if current token is still valid if (this.token && this.expiresAt > Date.now() + 60000) { // 1 minute buffer return this.token; }
// Fetch new token this.token = await this.fetchNewToken();
// Parse expiration from JWT const payload = JSON.parse(atob(this.token.split('.')[1])); this.expiresAt = payload.exp * 1000;
return this.token; }
async fetchNewToken() { // Implementation depends on your IdP // GitHub Actions: core.getIDToken() // Auth0: client credentials flow // etc. }}Server-Side Considerations
Section titled “Server-Side Considerations”Don’t cache tokens server-side - they’re meant to be short-lived and client-specific.
Authentication Errors
Section titled “Authentication Errors”Common Error Responses
Section titled “Common Error Responses”Missing Token (401):
{ "error": "UNAUTHORIZED", "message": "Missing authentication token", "details": { "reason": "no_token_provided" }}Invalid Token Format (401):
{ "error": "UNAUTHORIZED", "message": "Invalid token format", "details": { "reason": "malformed_jwt" }}Token Expired (401):
{ "error": "UNAUTHORIZED", "message": "Token has expired", "details": { "reason": "token_expired", "expiredAt": "2024-01-15T10:30:00Z", "currentTime": "2024-01-15T10:35:00Z" }}Invalid Signature (401):
{ "error": "UNAUTHORIZED", "message": "Token signature verification failed", "details": { "reason": "invalid_signature", "issuer": "https://token.actions.githubusercontent.com" }}Invalid Issuer (401):
{ "error": "UNAUTHORIZED", "message": "Token issuer not configured", "details": { "reason": "unknown_issuer", "issuer": "https://unknown-idp.com", "configuredIssuers": [ "https://token.actions.githubusercontent.com", "https://auth0.example.com/" ] }}Invalid Audience (401):
{ "error": "UNAUTHORIZED", "message": "Token audience validation failed", "details": { "reason": "invalid_audience", "tokenAudience": ["https://wrong-audience.com"], "expectedAudience": ["https://github.com/myorg"] }}Security Best Practices
Section titled “Security Best Practices”1. Use HTTPS Only
Section titled “1. Use HTTPS Only”# Goodcurl -H "Authorization: Bearer $TOKEN" https://broker.example.com/...
# Bad - token exposed in plaintextcurl -H "Authorization: Bearer $TOKEN" http://broker.example.com/...2. Secure Token Storage
Section titled “2. Secure Token Storage”# Good - environment variableexport VOIDKEY_OIDC_TOKEN="$(secure-token-source)"
# Bad - hardcoded in scriptTOKEN="eyJhbGciOiJSUzI1NiIs..." # Never do this3. Token Transmission
Section titled “3. Token Transmission”// Good - Authorization headerfetch('/api/endpoint', { headers: { 'Authorization': `Bearer ${token}` }});
// Avoid - query parameter (logged, cached, visible)fetch(`/api/endpoint?token=${token}`);4. Validate Token Locally
Section titled “4. Validate Token Locally”Before making API calls, validate tokens client-side when possible:
function isTokenExpired(token) { try { const payload = JSON.parse(atob(token.split('.')[1])); return payload.exp * 1000 < Date.now(); } catch (e) { return true; // Assume expired if can't parse }}
// Check before making API callif (isTokenExpired(token)) { token = await refreshToken();}5. Handle Errors Gracefully
Section titled “5. Handle Errors Gracefully”async function makeAuthenticatedRequest(url, token) { try { const response = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } });
if (response.status === 401) { // Token expired or invalid - refresh and retry const newToken = await refreshToken(); return makeAuthenticatedRequest(url, newToken); }
if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); }
return response.json(); } catch (error) { console.error('Authentication request failed:', error); throw error; }}Testing Authentication
Section titled “Testing Authentication”Local Development
Section titled “Local Development”# Get token from development IdPDEV_TOKEN=$(curl -s -X POST "http://localhost:8080/realms/client/protocol/openid-connect/token" \ -d "client_id=test-client" \ -d "username=test-user" \ -d "password=test-password" \ -d "grant_type=password" | jq -r '.access_token')
# Test API endpointcurl -H "Authorization: Bearer $DEV_TOKEN" \ http://localhost:3000/credentials/keysToken Debugging
Section titled “Token Debugging”# Decode token claims (without verification)echo "$TOKEN" | cut -d. -f2 | base64 -d | jq .
# Check token expirationecho "$TOKEN" | cut -d. -f2 | base64 -d | jq '.exp | todateiso'
# Validate token format[[ "$TOKEN" =~ ^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$ ]] && echo "Valid JWT format"Next Steps
Section titled “Next Steps”- API Endpoints - Detailed endpoint documentation
- REST API Overview - API usage examples
- CLI Commands - Using the CLI for authentication
- GitHub Actions Example - OIDC integration example