feat(auth): pass id_token_hint on Keycloak logout
Carries the raw ID token in the session cookie so logout can hand it back to Keycloak as id_token_hint, letting it skip its own logout-confirmation prompt -- otherwise a user could cancel out of it and land back in the app with a Keycloak SSO session but no geniusrun profile (e.g. right after deleting their account).
This commit is contained in:
@@ -1,10 +1,83 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIDTokenClaims_HasRole(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user