// 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)) }