2026-07-24 21:26:15 +02:00
|
|
|
package auth
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-26 11:55:13 +02:00
|
|
|
"context"
|
2026-07-24 21:26:15 +02:00
|
|
|
"encoding/json"
|
2026-07-26 11:55:13 +02:00
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"net/url"
|
2026-07-24 21:26:15 +02:00
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-26 11:55:13 +02:00
|
|
|
// newTestOIDCVerifier spins up a fake OIDC discovery endpoint (just enough
|
|
|
|
|
// for oidc.NewProvider's discovery GET to succeed) and returns a real
|
|
|
|
|
// oidcVerifier backed by it, for tests that exercise EndSessionURL without
|
|
|
|
|
// a live Keycloak.
|
|
|
|
|
func newTestOIDCVerifier(t *testing.T) Verifier {
|
|
|
|
|
t.Helper()
|
|
|
|
|
var issuerURL string
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
fmt.Fprintf(w, `{
|
|
|
|
|
"issuer": %[1]q,
|
|
|
|
|
"authorization_endpoint": "%[1]s/auth",
|
|
|
|
|
"token_endpoint": "%[1]s/token",
|
|
|
|
|
"end_session_endpoint": "%[1]s/logout",
|
|
|
|
|
"jwks_uri": "%[1]s/certs"
|
|
|
|
|
}`, issuerURL)
|
|
|
|
|
})
|
|
|
|
|
mux.HandleFunc("/certs", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
fmt.Fprint(w, `{"keys":[]}`)
|
|
|
|
|
})
|
|
|
|
|
srv := httptest.NewServer(mux)
|
|
|
|
|
t.Cleanup(srv.Close)
|
|
|
|
|
issuerURL = srv.URL
|
|
|
|
|
|
|
|
|
|
verifier, err := NewOIDCVerifier(context.Background(), OIDCConfig{
|
|
|
|
|
IssuerURL: issuerURL, ClientID: "geniusrun", ClientSecret: "secret", RedirectURL: issuerURL + "/callback",
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("NewOIDCVerifier: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return verifier
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEndSessionURL_IncludesIDTokenHintWhenProvided(t *testing.T) {
|
|
|
|
|
verifier := newTestOIDCVerifier(t)
|
|
|
|
|
|
|
|
|
|
got := verifier.EndSessionURL("https://app.example.com/", "raw-id-token-jwt")
|
|
|
|
|
u, err := url.Parse(got)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("parse EndSessionURL result %q: %v", got, err)
|
|
|
|
|
}
|
|
|
|
|
q := u.Query()
|
|
|
|
|
if q.Get("id_token_hint") != "raw-id-token-jwt" {
|
|
|
|
|
t.Errorf("id_token_hint = %q, want %q", q.Get("id_token_hint"), "raw-id-token-jwt")
|
|
|
|
|
}
|
|
|
|
|
if q.Get("client_id") != "geniusrun" {
|
|
|
|
|
t.Errorf("client_id = %q, want geniusrun", q.Get("client_id"))
|
|
|
|
|
}
|
|
|
|
|
if q.Get("post_logout_redirect_uri") != "https://app.example.com/" {
|
|
|
|
|
t.Errorf("post_logout_redirect_uri = %q, want https://app.example.com/", q.Get("post_logout_redirect_uri"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEndSessionURL_OmitsIDTokenHintWhenEmpty(t *testing.T) {
|
|
|
|
|
verifier := newTestOIDCVerifier(t)
|
|
|
|
|
|
|
|
|
|
got := verifier.EndSessionURL("https://app.example.com/", "")
|
|
|
|
|
u, err := url.Parse(got)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("parse EndSessionURL result %q: %v", got, err)
|
|
|
|
|
}
|
|
|
|
|
if u.Query().Has("id_token_hint") {
|
|
|
|
|
t.Errorf("expected no id_token_hint param when idTokenHint is empty, got %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 21:26:15 +02:00
|
|
|
func TestIDTokenClaims_HasRole(t *testing.T) {
|
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
json string
|
|
|
|
|
role string
|
|
|
|
|
want bool
|
|
|
|
|
}{
|
|
|
|
|
{"role present among others", `{"realm_access":{"roles":["geniusrun-user","other"]}}`, "geniusrun-user", true},
|
|
|
|
|
{"role absent", `{"realm_access":{"roles":["other"]}}`, "geniusrun-user", false},
|
|
|
|
|
{"realm_access missing entirely", `{}`, "geniusrun-user", false},
|
|
|
|
|
{"roles array empty", `{"realm_access":{"roles":[]}}`, "geniusrun-user", false},
|
|
|
|
|
}
|
|
|
|
|
for _, tc := range cases {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
var c idTokenClaims
|
|
|
|
|
if err := json.Unmarshal([]byte(tc.json), &c); err != nil {
|
|
|
|
|
t.Fatalf("unmarshal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := c.hasRole(tc.role); got != tc.want {
|
|
|
|
|
t.Errorf("hasRole(%q) = %v, want %v", tc.role, got, tc.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|