Files
homelab-frontend/JWT_VS_RANDOM_TOKEN.md
T

11 KiB

JWT vs Random Tokens - Which is Better?

Analyzing token types for Authentik + SOPS workflow.


Quick Comparison

Aspect JWT Random Token
Stateless Yes Requires lookup
Self-contained Yes (claims inside) Opaque
Can verify locally Yes (signature) Must call Authentik
Size ⚠️ Larger (~500 bytes) Smaller (~32 bytes)
Immediate revocation Hard (token already signed) Easy
Contains user info Yes No
Standard OAuth2 ⚠️ Optional (Bearer tokens) Standard
Good for audit User/groups embedded ⚠️ Need to log lookup
Git-friendly Sign commits Sign commits

Use Case: Authentik + SOPS + Git Hooks

Scenario 1: Developer Pushes Secret Update

Developer:
  $ export SOPS_TOKEN=$token
  $ sops secrets/default/db-creds.enc.yaml
  $ git push

Git server hook runs:
  ├─ Receive token from commit metadata
  ├─ Need to validate token
  └─ Two options:

With JWT Token:

Git hook:
  1. Extract JWT from commit
  2. Verify signature locally (no API call)
  3. Read claims: {sub: "[email protected]", groups: ["k8s:secret-admin"]}
  4. Check: is user in k8s:secret-admin group?
  5. Allow/reject commit
  
Benefits:
  ✅ No need to call Authentik API
  ✅ Token self-validates
  ✅ Can check groups locally
  ✅ Instant verification
  ✅ Works offline

With Random Token:

Git hook:
  1. Extract token from commit
  2. Call Authentik API: GET /application/o/introspect/
     └─ "Is this token valid?"
  3. If valid, check groups: GET /api/v3/users/{id}/groups/
  4. If admin, allow commit
  
Problems:
  ❌ Need API call on every git push
  ❌ Slow (network latency)
  ❌ Requires network connectivity
  ❌ If Authentik is down, can't push
  ❌ Rate limiting risk (many API calls)

JWT Structure (What's Inside)

// Decoded JWT payload
{
  "sub": "[email protected]",
  "email": "[email protected]",
  "name": "John Doe",
  "groups": [
    "k8s:secret-admin",
    "k8s:namespace:default:editor"
  ],
  "iat": 1705336200,
  "exp": 1705422600,  // Expires in 24 hours
  "iss": "https://authentik.riotpiao.com",
  "aud": "secrets-management"
}

In git commit:

commit abc1234...
Author: [email protected] <[email protected]>
Date:   Tue Jan 16 14:30:00 2025 +0000

    chore(secret): update db-creds
    
    X-SOPS-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Git hook can:

1. Extract token from commit
2. Decode (no signature needed yet)
3. Read claims: sub, groups
4. Verify signature with Authentik's public key
5. Check groups locally
6. Decision: accept or reject

JWT Benefits for This Use Case

1. Local Token Verification (No API Calls)

// Git hook code (serverless, fast)
function validateJWT(token: string) {
  const decoded = jwt.verify(token, PUBLIC_KEY);
  // ✅ Done instantly
  // ✅ No network call
  // ✅ Can work offline
}

2. Embedded Claims (Audit Trail)

JWT contains:
  ├─ sub: who made the change
  ├─ groups: what permissions they had
  ├─ iat: when token was issued
  └─ exp: when it expires

Git commit metadata automatically includes:
  └─ Who, what groups, when

Complete audit trail without additional logging.

3. RBAC at Git Level

// Git hook can enforce RBAC locally
function canPushSecrets(token: JWT) {
  const groups = token.groups;
  
  // Only k8s:secret-admin can push to secrets/
  if (!groups.includes("k8s:secret-admin")) {
    return false;
  }
  
  return true;
}

4. No Dependency on Authentik Being Up

If Authentik is temporarily down:
  ✅ Developers can still push (JWT validates locally)
  ✅ Works offline
  ❌ With random tokens: push fails

5. Temporal Can Verify Tokens Locally

Temporal workflow (token rotation):
  └─ Reads old token from K8s
  └─ Decodes JWT: check exp field
  └─ "Is this token about to expire?"
  └─ Generate new token
  
No API call needed to validate old token.

Random Token Benefits

1. Standard OAuth2 Practice

Random tokens are what most OAuth2 providers use:
  ├─ Opaque (attacker doesn't know content)
  ├─ Standard bearer tokens
  └─ What Authentik probably generates by default

2. Immediate Revocation

If token is compromised:
  └─ Delete it from Authentik
  └─ All git pushes instantly fail
  
With JWT:
  └─ Token is still valid until exp time
  └─ Attacker has until expiry

3. Smaller Size

JWT: ~500 bytes (base64 encoded)
Random: ~32 bytes (hex)

Difference:
  ✅ Slightly smaller in git metadata
  ❌ But negligible for practical purposes

4. Simpler Conceptually

Developers understand random tokens:
  └─ "Here's a secret token, use it"
  
Developers don't understand JWT:
  └─ "What's this long base64 string?"
  └─ "Why do I need to decode it?"

My Recommendation: JWT

Here's why for YOUR use case:

1. Git Hook Verification

With JWT:
  Git hook validates token signature locally
  └─ No Authentik API call
  └─ Fast
  └─ Works offline
  └─ Scales infinitely

With random token:
  Git hook calls Authentik API
  └─ Network latency
  └─ Authentik bottleneck
  └─ Fails if Authentik down
  └─ Rate limiting risk

2. Embedded RBAC

With JWT:
  Git hook reads groups from token
  └─ Only k8s:secret-admin can push to secrets/
  └─ Enforced locally
  └─ No additional database lookups

With random token:
  Git hook calls Authentik API twice
  └─ Introspect token
  └─ Fetch groups
  └─ Slower

3. Audit Trail

With JWT:
  Git commit includes token claims
  └─ User, groups, timestamp all in commit metadata
  └─ Complete audit trail automatically

With random token:
  Git commit has only token
  └─ Need to log token → user mapping separately
  └─ Additional logging overhead

4. Temporal Token Rotation

With JWT:
  Temporal reads exp field
  └─ "Token expires in 2 hours"
  └─ No API call needed

With random token:
  Temporal calls Authentik API
  └─ "Is this token still valid?"
  └─ Network call needed

How to Get JWT from Authentik

Option 1: OpenID Connect (Standard)

Authentik already supports OIDC:

Developer requests token:
  $ curl -X POST https://authentik.../application/o/token/ \
    -d "grant_type=password&username=user&password=pwd&scope=openid profile groups"

Response:
  {
    "access_token": "eyJhbGc...",  ← JWT
    "token_type": "Bearer",
    "expires_in": 86400,
    "id_token": "eyJhbGc..."
  }

Option 2: OAuth2 with JWT

Authentik OAuth2 provider settings:
  ├─ Enable: "Use claims-based access tokens"
  ├─ Token claims: username, email, groups, preferred_username
  └─ Signature algorithm: RS256

Result: Access token is now JWT instead of random.

Implementation: SOPS + JWT

# Developer gets JWT token from Authentik
$ token=$(oidc-client get-token)
$ echo $token  # base64 encoded JWT, not random string

# SOPS commits with JWT
$ export SOPS_TOKEN=$token
$ sops secrets/default/db-creds.enc.yaml
$ git add . && git commit -m "update" && git push

# Git hook receives commit
$ git hook:
#   1. Extract SOPS_TOKEN from commit metadata
#   2. Decode JWT (no API call)
#   3. Verify signature with Authentik public key
#   4. Read claims: groups
#   5. Check: user in k8s:secret-admin?
#   6. Accept or reject push

Comparison in Your Architecture

Random Token Path

Developer → Authentik (get random token)
         → SOPS (encrypt + commit)
         → Git push
         → Git hook (calls Authentik API to validate)
         → ArgoCD
         → K8s
         
Network calls: 2 (Authentik for token + git hook validation)

JWT Token Path

Developer → Authentik (get JWT token)
         → SOPS (encrypt + commit)
         → Git push
         → Git hook (validates JWT locally, no API call)
         → ArgoCD
         → K8s
         
Network calls: 1 (only Authentik for token issuance)

Winner: JWT (fewer API calls)


Concerns with JWT

Q: Can JWT be revoked immediately?

A: Not easily. Options:

1. Token expires (set exp field to short time, e.g., 1 hour)
   └─ Temporal rotates daily anyway
   
2. Blacklist approach (store revoked JWTs)
   └─ Git hook checks blacklist before validating
   └─ But defeats purpose of stateless tokens
   
3. Accept that JWT lives until expiry
   └─ In your case: Temporal rotates daily
   └─ So max time a compromised token is valid: 24 hours
   └─ Acceptable for homelab

Q: Are JWTs vulnerable?

A: No, if configured right:

✅ Signed with RS256 (Authentik public key)
✅ Signature verified on every use
✅ Expiry checked (can't use expired token)
✅ Can't be forged (no private key to sign with)

Q: Does Authentik support JWT?

A: Yes, fully:

Authentik has:
  ✅ OIDC (returns JWT id_token + access_token)
  ✅ OAuth2 with claims (can return JWT)
  ✅ Configuration for JWT signing algorithm
  ✅ Public key endpoint for verification

Final Recommendation

Use JWT if:

✅ You want fast git hook validation (no API calls)
✅ You want embedded RBAC (groups in token)
✅ You want complete audit trail (claims in metadata)
✅ You want resilience (works if Authentik down)
✅ You want to scale (no API bottleneck)

Use Random Token if:

✅ Immediate revocation is critical
✅ You prefer standard OAuth2 approach
✅ Developers shouldn't see token contents
✅ Simplicity is more important than features

My Advice

Go with JWT.

Here's why:

  1. Authentik supports it natively
  2. Git hook verification is instant (no API calls)
  3. Embedded claims give you free audit trail
  4. Works offline (resilient)
  5. Perfect for homelab scale

Setup:

  1. Configure Authentik OIDC provider
  2. Enable "Use claims-based access tokens" option
  3. Add groups to token claims
  4. Developers get JWT instead of random string
  5. Git hook validates JWT locally (no API call)

Code example for git hook:

#!/bin/bash
# .git/hooks/update (server-side)

token=$(git log -1 --pretty=%B | grep "X-SOPS-Token" | cut -d' ' -f2)

# Verify JWT signature
jwt verify $token --key /path/to/authentik/public.key

# Decode and check groups
groups=$(jwt decode $token | jq .groups)
if [[ ! "$groups" =~ "k8s:secret-admin" ]]; then
  echo "Access denied: not in k8s:secret-admin group"
  exit 1
fi

# Allow push
exit 0

That's it. Simple, elegant, no redundancy.