# OAuth App Setup & Management Guide

This guide provides a standardized workflow for registering new services with the homelab OIDC provider (Authentik) and wiring them for Vault JWT authentication.

> **TL;DR:** Run `register_oauth_app.py` with your app's details; it handles Authentik + Vault + K8s secrets automatically.

---

## Architecture Overview

The homelab uses a three-tier authentication stack:

```
User / Service
  ↓
Authentik (OIDC IdP)
  ├─ Grafana
  ├─ MinIO
  ├─ Forgejo
  ├─ Argo CD
  ├─ Your New Service
  └─ ...
  ↓ (device code / authorization code flow)
Vault (KV + JWT auth)
  ├─ JWT role mapping (sub/groups → policies)
  ├─ OIDC browser login (vault-browser provider)
  └─ KV secrets (secret/cluster/*, secret/mcp/*)
```

**Key concepts:**
- **Authentik** is the OIDC identity provider — it issues tokens and manages users/groups.
- **Vault** validates Authentik's JWT tokens and maps them to policies & secret access.
- **Kubernetes secrets** store OAuth credentials that services read at startup (ConfigMap-based).
- **Groups** in Authentik control access policies and Vault role assignment.

---

## Prerequisites

Before registering a new OAuth service, ensure:

1. **Authentik is running** (in `iam` namespace)
   ```bash
   kubectl get pods -n iam | grep authentik-server
   ```

2. **Vault is running** (in `storage` namespace)
   ```bash
   kubectl get pods -n storage | grep vault-0
   ```

3. **SSH/API access to Authentik** — port-forward available
   ```bash
   kubectl port-forward -n iam svc/authentik-server 7000:80
   ```

4. **Vault bootstrap is complete** (run `setup_vault.sh` if not already done)

5. **Required environment variables** — populate these in `~/.authentik/.env`:
   ```bash
   # Token for Authentik API (from bootstrap)
   AUTHENTIK_BOOTSTRAP_TOKEN=<your-token>
   
   # Service-specific client secret (generate via openssl rand -hex 32)
   # Example: MY_SERVICE_OIDC_CLIENT_SECRET=<generated-secret>
   ```

---

## One-Line Setup (Recommended)

For most services, use the automated registration script:

```bash
cd /Users/rockliang/workplace/homelab

# Register an OAuth app with automatic Vault JWT wiring
python3 k8s/talos-iam/register_oauth_app.py \
  --service-name my-app \
  --namespace my-namespace \
  --redirect-uri "https://my-app.riotpiao.com/oauth2/callback" \
  --service-name-in-vault "MY_SERVICE" \
  --vault-jwt-policy "shell-secrets"
```

The script will:
1. ✅ Create an OAuth2 provider in Authentik (from credentials in Vault)
2. ✅ Create an Application in Authentik
3. ✅ Create/bind groups for RBAC (optional)
4. ✅ Create a Kubernetes secret with the client credentials
5. ✅ Create a Vault JWT role for the service's JWT auth method
6. ✅ Write policies for the service's scope in Vault

---

## Manual Setup (Step-by-Step)

If you prefer manual control, or if the script doesn't fit your use case:

### Step 1: Generate Credentials

```bash
# Generate a new client secret
CLIENT_SECRET=$(openssl rand -hex 32)
echo "CLIENT_SECRET=$CLIENT_SECRET"

# Store in Vault (required for service initialization)
# Convention: YOURSERVICE_OIDC_CLIENT_SECRET
talos put cluster/YOURSERVICE_OIDC_CLIENT_SECRET YOURSERVICE_OIDC_CLIENT_SECRET="$CLIENT_SECRET"

# Or set in .env temporarily
export YOURSERVICE_OIDC_CLIENT_SECRET="$CLIENT_SECRET"
```

### Step 2: Create Authentik OAuth2 Provider

Access Authentik UI via port-forward:

```bash
kubectl port-forward -n iam svc/authentik-server 7000:80 &
# http://localhost:7000/if/admin/ → login with akadmin
```

**In Authentik UI:**
1. Navigate to **Applications → Providers → Create → OpenID Connect (OAuth2) Provider**
2. Fill in:
   - **Name:** `yourservice`
   - **Client ID:** `yourservice` (or custom)
   - **Client Secret:** (paste from `$CLIENT_SECRET` above)
   - **Redirect URIs:** `https://your-app.riotpiao.com/oauth2/callback` (or your app's callback URL)
   - **Sub Mode:** `Hashed User ID`
   - **Include claims in ID Token:** ✓ Enabled
   - **Grant Types:** `Authorization Code`, `Refresh Token`
   - **Signing Key:** (select the homelab-oidc key)

3. **Save** and note the **provider slug** (usually auto-generated from Name).

### Step 3: Create Authentik Application

**In Authentik UI:**
1. Navigate to **Applications → Applications → Create**
2. Fill in:
   - **Name:** `Your App Display Name`
   - **Slug:** `yourservice` (must match provider slug)
   - **Provider:** (select the provider created above)
   - **Meta Launch URL:** `https://your-app.riotpiao.com` (optional, for app launcher)

3. **Save**

### Step 4: Configure OIDC in Your Service

Pass the following environment variables to your service:

```bash
# OIDC endpoint (in-cluster: authentik-server.iam.svc.cluster.local)
OIDC_ISSUER_URL=http://authentik-server.iam.svc.cluster.local/application/o/yourservice/

# OAuth2 credentials (from step 1-2)
OIDC_CLIENT_ID=yourservice
OIDC_CLIENT_SECRET=$CLIENT_SECRET

# Redirect URI (must match what you configured in step 2)
OIDC_REDIRECT_URI=https://your-app.riotpiao.com/oauth2/callback

# Optionally, token validation endpoint
OIDC_TOKEN_URL=http://authentik-server.iam.svc.cluster.local/application/o/yourservice/token/
OIDC_USERINFO_URL=http://authentik-server.iam.svc.cluster.local/application/o/yourservice/userinfo/

# Optional: JWKS endpoint for offline token validation
OIDC_JWKS_URL=http://authentik-server.iam.svc.cluster.local/application/o/yourservice/jwks/
```

**Store secrets in Vault:**
```bash
# Store the client secret for runtime retrieval
talos put cluster/YOURSERVICE_OIDC_CLIENT_SECRET YOURSERVICE_OIDC_CLIENT_SECRET="$CLIENT_SECRET"

# Store other config if needed
talos put cluster/yourservice-oidc \
  client_id=yourservice \
  issuer_url="http://authentik-server.iam.svc.cluster.local/application/o/yourservice/" \
  callback_uri="https://your-app.riotpiao.com/oauth2/callback"
```

### Step 5: Mount Credentials in Your Service

**Option A: Kubernetes Secret (ConfigMap-based)**

Create a Kubernetes secret with the credentials:

```bash
kubectl create secret generic yourservice-oidc \
  --from-literal=client_id=yourservice \
  --from-literal=client_secret="$CLIENT_SECRET" \
  -n your-namespace
```

Reference in your Helm values or Pod spec:

```yaml
# In Helm values
env:
  - name: OIDC_CLIENT_ID
    valueFrom:
      secretKeyRef:
        name: yourservice-oidc
        key: client_id
  - name: OIDC_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: yourservice-oidc
        key: client_secret
```

**Option B: Vault KV Secret (Runtime)**

Store in Vault and retrieve at startup:

```bash
# Store full config
vault kv put secret/services/yourservice \
  client_id=yourservice \
  client_secret="$CLIENT_SECRET" \
  issuer_url="http://authentik-server.iam.svc.cluster.local/application/o/yourservice/"

# Service reads at startup:
# curl -H "Authorization: Bearer $VAULT_TOKEN" \
#   http://vault.storage.svc.cluster.local:8200/v1/secret/data/services/yourservice
```

---

## Vault JWT Authentication (For Services)

If your service needs to authenticate to Vault directly (beyond just reading secrets), create a JWT role:

### 1. Create Vault JWT Role

```bash
# Authenticate to Vault (as cluster-admin or root)
kubectl port-forward -n storage svc/vault 8200:8200 &
export VAULT_ADDR=http://127.0.0.1:8200
vault login -method=oidc role=homelab

# Create a service-specific JWT role
vault write auth/jwt/role/yourservice \
  role_type=jwt \
  bound_audiences="vault" \
  user_claim="sub" \
  bound_claims='{"client_id":["yourservice"]}' \
  policies="shell-secrets,service-read-secrets" \
  ttl=4h \
  max_ttl=8h
```

### 2. Create Service Policy in Vault

```bash
# Policy that allows the service to read its own secrets
vault policy write service-read-yourservice - <<'EOF'
path "secret/data/services/yourservice" {
  capabilities = ["read"]
}
path "secret/data/cluster/minio" {
  capabilities = ["read"]
}
EOF

# Update the role to use this policy
vault write auth/jwt/role/yourservice \
  policies="service-read-secrets,service-read-yourservice"
```

### 3. Service JWT Authentication Flow

Your service obtains a Vault token from Authentik's JWT:

```python
# Example service code (Python)
import requests
import json

# 1. Authenticate to Authentik OIDC (get ID token)
oidc_response = requests.post(
    "http://authentik-server.iam.svc.cluster.local/application/o/yourservice/token/",
    data={
        "grant_type": "client_credentials",
        "client_id": "yourservice",
        "client_secret": os.environ["OIDC_CLIENT_SECRET"],
        "audience": "vault"  # Important: request JWT for Vault
    }
)
id_token = oidc_response.json()["access_token"]

# 2. Authenticate to Vault using the JWT
vault_response = requests.post(
    "http://vault.storage.svc.cluster.local:8200/v1/auth/jwt/login",
    data=json.dumps({
        "role": "yourservice",
        "jwt": id_token
    }),
    headers={"Content-Type": "application/json"}
)
vault_token = vault_response.json()["auth"]["client_token"]

# 3. Use Vault token to read secrets
secret_response = requests.get(
    "http://vault.storage.svc.cluster.local:8200/v1/secret/data/services/yourservice",
    headers={"X-Vault-Token": vault_token}
)
secrets = secret_response.json()["data"]["data"]
```

---

## RBAC & Groups

### Adding Group-Based Access Control

Groups in Authentik map to Vault policies and app bindings:

```bash
# Create a group for your service's administrators
python3 k8s/talos-iam/provision_oidc.py --add-group "yourservice-admins"

# Add users to the group (Authentik UI)
# Applications → Groups → yourservice-admins → Users

# Bind group to app (Authentik UI)
# Applications → Applications → yourservice → Policies → Binding
# Select the group and enable the binding
```

### Group-Based Vault Policy Mapping

Create a Vault policy that checks Authentik group membership:

```bash
vault policy write service-admin-yourservice - <<'EOF'
# Full access for admins
path "secret/data/services/yourservice/*" {
  capabilities = ["read", "create", "update", "delete"]
}
path "secret/data/mcp/*" {
  capabilities = ["read"]
}
EOF

# Create a separate JWT role for admins
vault write auth/jwt/role/yourservice-admin \
  role_type=jwt \
  bound_audiences="vault" \
  user_claim="sub" \
  bound_claims='{"groups":["yourservice-admins"]}' \
  policies="shell-secrets,service-admin-yourservice" \
  ttl=4h
```

---

## Verification Checklist

After registration, verify everything works:

```bash
# 1. Verify Authentik provider exists
curl -H "Authorization: Bearer $AUTHENTIK_BOOTSTRAP_TOKEN" \
  http://localhost:7000/api/v3/providers/oauth2/?name=yourservice

# 2. Verify Authentik application exists
curl -H "Authorization: Bearer $AUTHENTIK_BOOTSTRAP_TOKEN" \
  http://localhost:7000/api/v3/core/applications/?slug=yourservice

# 3. Verify K8s secret is mounted
kubectl get secret yourservice-oidc -n your-namespace -o jsonpath='{.data}' | base64 -d

# 4. Verify Vault JWT role
vault read auth/jwt/role/yourservice

# 5. Test OAuth login (requires port-forward to your app)
# Open http://your-app.riotpiao.com/login
# Should redirect to Authentik, then back to your app

# 6. Test JWT auth to Vault (if applicable)
# Service obtains ID token and authenticates to Vault
vault login -method=jwt role=yourservice jwt=$ID_TOKEN
```

---

## Troubleshooting

### "JWKS URL not found" error in Vault

**Cause:** Authentik is not reachable from Vault pod at the configured URL.

**Fix:**
1. Check Authentik is running: `kubectl get pods -n iam`
2. Verify URL uses in-cluster address: `http://authentik-server.iam.svc.cluster.local`
3. Test DNS from Vault pod:
   ```bash
   kubectl exec -n storage vault-0 -- nslookup authentik-server.iam.svc.cluster.local
   ```

### "OAuth callback failed" or redirect loop

**Cause:** Redirect URI in Authentik does not match what the service sends.

**Fix:**
1. Check Authentik provider's Redirect URIs: UI → Applications → Providers → Edit
2. Must exactly match the URI your service redirects to (protocol, domain, port, path)
3. Common mistake: `https://` in service but `http://` in Authentik

### Service can't read K8s secret

**Cause:** Secret is in wrong namespace or service account lacks permissions.

**Fix:**
1. Verify secret exists: `kubectl get secret yourservice-oidc -n your-namespace`
2. Check RBAC for service account:
   ```bash
   kubectl describe sa yourservice -n your-namespace
   ```

### "client_id mismatch" or "client authentication failed"

**Cause:** Client secret in K8s secret does not match what's in Authentik.

**Fix:**
1. Rotate the secret in Authentik (delete and recreate)
2. Update the K8s secret with the new value
3. Restart the service pod(s)

---

## Advanced: Custom Property Mappings

For services that need custom JWT claims (e.g., MinIO's `policy` claim), use property mappings:

```bash
# Create a custom scope with expression
curl -X POST http://localhost:7000/api/v3/propertymappings/provider/scope/ \
  -H "Authorization: Bearer $AUTHENTIK_BOOTSTRAP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "yourservice-claims",
    "scope_name": "yourservice-custom",
    "expression": "return {\"department\": request.user.attributes.get(\"department\", \"unknown\")}"
  }'

# Add to provider's property_mappings:
# Authentik UI → Applications → Providers → Edit yourservice
# Select the custom scope in the Property Mappings list
```

---

## Integration Examples

### Integrate with Grafana

See `k8s/logging/grafana-values.yaml` for an example of configuring generic OAuth in Grafana.

### Integrate with MinIO

See `provision_oidc.py` for MinIO's group-based policy claim injection.

### Integrate with Custom Go App

See `k8s/talos-iam/go-example-oidc/` for a minimal Authorization Code flow example.

---

## Related Documentation

- [`README.md`](README.md) — Authentik deployment & key rotation
- [`provision_oidc.py`](provision_oidc.py) — Automated OIDC provisioning for core apps
- [`setup_vault.sh`](setup_vault.sh) — Vault initialization & JWT auth wiring
- [`CLAUDE.md`](../CLAUDE.md) — Homelab architecture & secrets management
