- PostgreSQL 3-replica HA with pgvector - Vault S3 storage backend (MinIO) - Authentik federated OIDC provider - Vault auto-unseal via postStart hook
91 lines
2.8 KiB
Go
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))
|
|
}
|