From a6af43b33286be92483235db7947a25623d26f68 Mon Sep 17 00:00:00 2001 From: Bernd Worsch Date: Wed, 25 Mar 2026 03:15:36 +0000 Subject: [PATCH] fix(authelia): use adapter's own client_id/redirect_uri in AuthorizeURL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adapter was forwarding the downstream client's client_id and redirect_uri to Authelia, which would always be rejected — Authelia only recognises client_id=keycape and its registered callback URI. Also removed downstream PKCE forwarding: KeyCape is a confidential OIDC client to Authelia and authenticates via client_secret instead. Co-Authored-By: Claude Sonnet 4.6 --- src/internal/adapters/authelia/adapter.go | 22 +++++++------------ .../adapters/authelia/adapter_test.go | 20 ++++++++++++----- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/internal/adapters/authelia/adapter.go b/src/internal/adapters/authelia/adapter.go index 146dc49..539200c 100644 --- a/src/internal/adapters/authelia/adapter.go +++ b/src/internal/adapters/authelia/adapter.go @@ -37,26 +37,20 @@ func New(cfg Config, httpClient HTTPClient) *AutheliaAdapter { // AuthorizeURL builds the Authelia OIDC authorization URL to which the user // should be redirected. +// +// KeyCape is a confidential OIDC client to Authelia. The adapter always uses +// its own registered client_id and redirect_uri — NOT the downstream client's +// values — and requests the full fixed scope set. PKCE is omitted because +// the confidential client_secret authenticates the token exchange instead. func (a *AutheliaAdapter) AuthorizeURL(_ context.Context, req domain.AuthRequest) (string, error) { base := strings.TrimRight(a.cfg.BaseURL, "/") + "/api/oidc/authorization" q := url.Values{} - q.Set("client_id", req.ClientID) - q.Set("redirect_uri", req.RedirectURI) + q.Set("client_id", a.cfg.ClientID) + q.Set("redirect_uri", a.cfg.RedirectURI) q.Set("response_type", "code") q.Set("state", req.State) - if req.Nonce != "" { - q.Set("nonce", req.Nonce) - } - if len(req.Scopes) > 0 { - q.Set("scope", strings.Join(req.Scopes, " ")) - } else { - q.Set("scope", "openid profile") - } - if req.PKCEChallenge != "" { - q.Set("code_challenge", req.PKCEChallenge) - q.Set("code_challenge_method", req.PKCEChallengeMethod) - } + q.Set("scope", "openid profile email groups") return base + "?" + q.Encode(), nil } diff --git a/src/internal/adapters/authelia/adapter_test.go b/src/internal/adapters/authelia/adapter_test.go index 1164857..e4f4180 100644 --- a/src/internal/adapters/authelia/adapter_test.go +++ b/src/internal/adapters/authelia/adapter_test.go @@ -75,6 +75,7 @@ func jsonResponse(body string) *http.Response { func TestAuthorizeURL_ContainsRequiredParams(t *testing.T) { adapter := authelia.New(testConfig(), &mockHTTPClient{}) + // Downstream client values — must NOT appear in the Authelia URL. req := domain.AuthRequest{ ClientID: "myapp", RedirectURI: "https://myapp.local/cb", @@ -90,22 +91,29 @@ func TestAuthorizeURL_ContainsRequiredParams(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - checks := []string{ - "client_id=myapp", + // Must use adapter's own client_id and redirect_uri, not the downstream client's. + required := []string{ + "client_id=keycape", "redirect_uri=", "response_type=code", "state=state-abc", - "nonce=nonce-xyz", - "code_challenge=challenge123", - "code_challenge_method=S256", "scope=", "openid", } - for _, want := range checks { + for _, want := range required { if !strings.Contains(u, want) { t.Errorf("AuthorizeURL missing %q in: %s", want, u) } } + + // Downstream client_id must NOT be forwarded to Authelia. + if strings.Contains(u, "client_id=myapp") { + t.Errorf("AuthorizeURL must not forward downstream client_id to Authelia, got: %s", u) + } + // PKCE must NOT be forwarded — confidential client uses client_secret instead. + if strings.Contains(u, "code_challenge") { + t.Errorf("AuthorizeURL must not include PKCE params for confidential client, got: %s", u) + } } func TestAuthorizeURL_UsesBaseURL(t *testing.T) {