Files
homelab/k8s/talos-iam/go-example-oidc/main.go
T
Story Crater Bot 674c8f0d66 k8s/iam: add cloudnativepg postgres and vault + authentik
- PostgreSQL 3-replica HA with pgvector
- Vault S3 storage backend (MinIO)
- Authentik federated OIDC provider
- Vault auto-unseal via postStart hook
2026-07-11 19:17:22 -07:00

91 lines
2.8 KiB
Go

// Minimal OIDC Authorization Code flow against Authentik.
//
// Setup:
// 1. In Authentik create a provider + app (slug "go-example", redirect URI http://localhost:8080/callback).
// 2. Fill in ~/.authentik/.env (see .env.example).
// 3. go mod tidy && go run .
// 4. Open http://localhost:8080/login
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/joho/godotenv"
"golang.org/x/oauth2"
)
// oauthState is a fixed random value for this process — good enough for a local demo.
// In production, generate a per-request random state and store it in a cookie.
var oauthState = "homelab-oidc-example"
func main() {
// Load ~/.authentik/.env; shell env vars already set take precedence.
home, _ := os.UserHomeDir()
godotenv.Load(filepath.Join(home, ".authentik", ".env"))
ctx := context.Background()
// go-oidc discovers the token endpoint, auth endpoint, and JWKS URI automatically
// from Authentik's /.well-known/openid-configuration.
issuer := os.Getenv("AUTHENTIK_BASE_URL") + "/application/o/" + os.Getenv("APP_SLUG")
provider, err := oidc.NewProvider(ctx, issuer)
if err != nil {
log.Fatalf("OIDC discovery failed (%s): %v", issuer, err)
}
cfg := &oauth2.Config{
ClientID: os.Getenv("OIDC_CLIENT_ID"),
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
RedirectURL: os.Getenv("REDIRECT_URL"),
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "email", "profile"},
}
verifier := provider.Verifier(&oidc.Config{ClientID: cfg.ClientID})
// /login — redirect the browser to Authentik's authorization endpoint
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, cfg.AuthCodeURL(oauthState), http.StatusFound)
})
// /callback — Authentik redirects here with ?code=...&state=...
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("state") != oauthState {
http.Error(w, "state mismatch", http.StatusBadRequest)
return
}
// Exchange the authorization code for tokens
token, err := cfg.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "token exchange: "+err.Error(), http.StatusInternalServerError)
return
}
// Verify the ID token signature against Authentik's JWKS, then extract claims
rawID, _ := token.Extra("id_token").(string)
idToken, err := verifier.Verify(ctx, rawID)
if err != nil {
http.Error(w, "id_token verify: "+err.Error(), http.StatusInternalServerError)
return
}
var claims map[string]any
idToken.Claims(&claims)
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.Encode(claims)
})
fmt.Println("open http://localhost:8080/login")
log.Fatal(http.ListenAndServe(":8080", nil))
}