diff --git a/authentik.go b/authentik.go new file mode 100644 index 0000000..c78b146 --- /dev/null +++ b/authentik.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +// AuthentikConfig holds settings for the Authentik forward-auth integration. +type AuthentikConfig struct { + // Issuer is the OIDC issuer URL (e.g. https://auth.example.com/application/o/my-app/). + Issuer string + + // ClientID is the OAuth2 client identifier. + ClientID string + + // TrustedProxy enables reading X-authentik-* headers set by a reverse proxy. + // When false, headers are ignored to prevent spoofing from untrusted sources. + TrustedProxy bool + + // PublicPaths lists additional paths that do not require authentication. + // /health and /swagger are always public. + PublicPaths []string +} + +// AuthentikUser represents an authenticated user extracted from Authentik +// forward-auth headers or a validated JWT. +type AuthentikUser struct { + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + UID string `json:"uid"` + Groups []string `json:"groups,omitempty"` + Entitlements []string `json:"entitlements,omitempty"` + JWT string `json:"-"` +} + +// HasGroup reports whether the user belongs to the named group. +func (u *AuthentikUser) HasGroup(group string) bool { + for _, g := range u.Groups { + if g == group { + return true + } + } + return false +} diff --git a/authentik_test.go b/authentik_test.go new file mode 100644 index 0000000..e2b99c7 --- /dev/null +++ b/authentik_test.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api_test + +import ( + "testing" + + api "forge.lthn.ai/core/go-api" +) + +// ── AuthentikUser ────────────────────────────────────────────────────── + +func TestAuthentikUser_Good(t *testing.T) { + u := api.AuthentikUser{ + Username: "alice", + Email: "alice@example.com", + Name: "Alice Smith", + UID: "abc-123", + Groups: []string{"editors", "admins"}, + Entitlements: []string{"premium"}, + JWT: "tok.en.here", + } + + if u.Username != "alice" { + t.Fatalf("expected Username=%q, got %q", "alice", u.Username) + } + if u.Email != "alice@example.com" { + t.Fatalf("expected Email=%q, got %q", "alice@example.com", u.Email) + } + if u.Name != "Alice Smith" { + t.Fatalf("expected Name=%q, got %q", "Alice Smith", u.Name) + } + if u.UID != "abc-123" { + t.Fatalf("expected UID=%q, got %q", "abc-123", u.UID) + } + if len(u.Groups) != 2 || u.Groups[0] != "editors" { + t.Fatalf("expected Groups=[editors admins], got %v", u.Groups) + } + if len(u.Entitlements) != 1 || u.Entitlements[0] != "premium" { + t.Fatalf("expected Entitlements=[premium], got %v", u.Entitlements) + } + if u.JWT != "tok.en.here" { + t.Fatalf("expected JWT=%q, got %q", "tok.en.here", u.JWT) + } +} + +func TestAuthentikUserHasGroup_Good(t *testing.T) { + u := api.AuthentikUser{ + Groups: []string{"editors", "admins"}, + } + + if !u.HasGroup("admins") { + t.Fatal("expected HasGroup(admins) = true") + } + if !u.HasGroup("editors") { + t.Fatal("expected HasGroup(editors) = true") + } +} + +func TestAuthentikUserHasGroup_Bad_Empty(t *testing.T) { + u := api.AuthentikUser{} + + if u.HasGroup("admins") { + t.Fatal("expected HasGroup(admins) = false for empty user") + } +} + +func TestAuthentikConfig_Good(t *testing.T) { + cfg := api.AuthentikConfig{ + Issuer: "https://auth.example.com", + ClientID: "my-client", + TrustedProxy: true, + PublicPaths: []string{"/public", "/docs"}, + } + + if cfg.Issuer != "https://auth.example.com" { + t.Fatalf("expected Issuer=%q, got %q", "https://auth.example.com", cfg.Issuer) + } + if cfg.ClientID != "my-client" { + t.Fatalf("expected ClientID=%q, got %q", "my-client", cfg.ClientID) + } + if !cfg.TrustedProxy { + t.Fatal("expected TrustedProxy=true") + } + if len(cfg.PublicPaths) != 2 { + t.Fatalf("expected 2 public paths, got %d", len(cfg.PublicPaths)) + } +}