Send the public origin on the in-cluster Authelia token request.
The token POST stays on tokenBaseURL. When that origin is not the browser-facing HTTPS origin, the request sets X-Forwarded-Proto and X-Forwarded-Host from that origin. Browser redirects are unchanged. Assistant: grok Assistant-Session: 01a0e29c-2e57-7342-a500-8f3b43b5fefd
This commit is contained in:
parent
df823d3950
commit
3b0446e0ef
4 changed files with 125 additions and 25 deletions
|
|
@ -155,21 +155,11 @@ type tokenResponse struct {
|
|||
|
||||
// exchangeCode sends a POST to Authelia's token endpoint and returns the
|
||||
// parsed token response. On any HTTP or status error it returns a non-nil error.
|
||||
func (a *AutheliaAdapter) exchangeCode(_ context.Context, code string) (*tokenResponse, error) {
|
||||
tokenURL := strings.TrimRight(a.tokenBaseURL(), "/") + "/api/oidc/token"
|
||||
|
||||
body := url.Values{}
|
||||
body.Set("grant_type", "authorization_code")
|
||||
body.Set("code", code)
|
||||
body.Set("redirect_uri", a.cfg.RedirectURI)
|
||||
body.Set("client_id", a.cfg.ClientID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, tokenURL, strings.NewReader(body.Encode()))
|
||||
func (a *AutheliaAdapter) exchangeCode(ctx context.Context, code string) (*tokenResponse, error) {
|
||||
req, err := a.newTokenRequest(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authelia: build token request: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(a.cfg.ClientID, a.cfg.ClientSecret)
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -232,6 +222,49 @@ func (a *AutheliaAdapter) tokenBaseURL() string {
|
|||
return a.cfg.BaseURL
|
||||
}
|
||||
|
||||
// newTokenRequest is the back-channel authorization-code exchange. The URL
|
||||
// stays on tokenBaseURL. When that origin is not the browser-facing HTTPS
|
||||
// origin, the request carries that origin in X-Forwarded-Proto and
|
||||
// X-Forwarded-Host so Authelia can derive the public issuer. Authelia 4.39
|
||||
// rejects a plain-http scheme. Browser redirects are unchanged.
|
||||
func (a *AutheliaAdapter) newTokenRequest(ctx context.Context, code string) (*http.Request, error) {
|
||||
tokenURL := strings.TrimRight(a.tokenBaseURL(), "/") + "/api/oidc/token"
|
||||
|
||||
body := url.Values{}
|
||||
body.Set("grant_type", "authorization_code")
|
||||
body.Set("code", code)
|
||||
body.Set("redirect_uri", a.cfg.RedirectURI)
|
||||
body.Set("client_id", a.cfg.ClientID)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(body.Encode()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("authelia: build token request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(a.cfg.ClientID, a.cfg.ClientSecret)
|
||||
a.setPublicOriginHeaders(req)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// setPublicOriginHeaders annotates a split-horizon token request. A public
|
||||
// origin that is not HTTPS is left unset: sending X-Forwarded-Proto: http is
|
||||
// the value Authelia 4.39 refuses. Same-origin calls are left unset too.
|
||||
func (a *AutheliaAdapter) setPublicOriginHeaders(req *http.Request) {
|
||||
public, err := url.Parse(a.authorizeBaseURL())
|
||||
if err != nil || public.Scheme != "https" || public.Host == "" {
|
||||
return
|
||||
}
|
||||
token, err := url.Parse(req.URL.String())
|
||||
if err != nil || token.Host == "" {
|
||||
return
|
||||
}
|
||||
if token.Scheme == public.Scheme && strings.EqualFold(token.Host, public.Host) {
|
||||
return
|
||||
}
|
||||
req.Header.Set("X-Forwarded-Proto", public.Scheme)
|
||||
req.Header.Set("X-Forwarded-Host", public.Host)
|
||||
}
|
||||
|
||||
// parseIDTokenClaims extracts the JWT payload claims without verifying
|
||||
// anything. It is NOT part of the authentication path: HandleCallback verifies
|
||||
// through idTokenVerifier. Kept for tests and diagnostics that need to read a
|
||||
|
|
|
|||
|
|
@ -295,24 +295,91 @@ func TestHandleCallback_UsesTokenBaseURLWhenConfigured(t *testing.T) {
|
|||
"sub": "user-uuid-123",
|
||||
"preferred_username": "alice",
|
||||
})
|
||||
var tokenURL string
|
||||
var tokenReq *http.Request
|
||||
client := &mockHTTPClient{
|
||||
doFn: func(req *http.Request) (*http.Response, error) {
|
||||
tokenURL = req.URL.String()
|
||||
if req.URL.Path == "/api/oidc/token" {
|
||||
tokenReq = req
|
||||
}
|
||||
return jsonResponse(tokenBody), nil
|
||||
},
|
||||
}
|
||||
|
||||
cfg := testConfig()
|
||||
cfg.BaseURL = "https://auth.coulomb.social"
|
||||
cfg.BaseURL = "http://authelia.sso.svc.cluster.local:9091"
|
||||
cfg.BrowserBaseURL = "https://auth.coulomb.social"
|
||||
cfg.TokenBaseURL = "http://authelia.sso.svc.cluster.local:9091"
|
||||
|
||||
adapter := authelia.New(cfg, client)
|
||||
if _, err := adapter.HandleCallback(context.Background(), domain.CallbackParams{Code: "code"}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(tokenURL, "http://authelia.sso.svc.cluster.local:9091") {
|
||||
t.Errorf("expected token exchange to use TokenBaseURL, got: %s", tokenURL)
|
||||
if tokenReq == nil {
|
||||
t.Fatal("token endpoint was not called")
|
||||
}
|
||||
if tokenReq.URL.String() != "http://authelia.sso.svc.cluster.local:9091/api/oidc/token" {
|
||||
t.Errorf("token exchange URL: got %s", tokenReq.URL.String())
|
||||
}
|
||||
if tokenReq.Host != "authelia.sso.svc.cluster.local:9091" {
|
||||
t.Errorf("token request host: got %s", tokenReq.Host)
|
||||
}
|
||||
if tokenReq.Header.Get("X-Forwarded-Proto") != "https" {
|
||||
t.Errorf("X-Forwarded-Proto: got %q", tokenReq.Header.Get("X-Forwarded-Proto"))
|
||||
}
|
||||
if tokenReq.Header.Get("X-Forwarded-Host") != "auth.coulomb.social" {
|
||||
t.Errorf("X-Forwarded-Host: got %q", tokenReq.Header.Get("X-Forwarded-Host"))
|
||||
}
|
||||
redirect, err := adapter.AuthorizeURL(context.Background(), domain.AuthRequest{
|
||||
State: "s",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("AuthorizeURL: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(redirect, "https://auth.coulomb.social/api/oidc/authorization?") {
|
||||
t.Errorf("browser redirect: got %s", redirect)
|
||||
}
|
||||
if strings.Contains(redirect, "authelia.sso.svc.cluster.local") {
|
||||
t.Errorf("browser redirect must stay on the public origin, got %s", redirect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenRequestOmitsForwardedHeadersForSameOriginAndPlainHTTP(t *testing.T) {
|
||||
cases := []authelia.Config{
|
||||
testConfig(),
|
||||
{
|
||||
BaseURL: "http://authelia:9091",
|
||||
BrowserBaseURL: "http://localhost:9091",
|
||||
TokenBaseURL: "http://authelia:9091",
|
||||
ClientID: "keycape",
|
||||
ClientSecret: "test-secret",
|
||||
RedirectURI: "http://localhost:8080/authorize/callback",
|
||||
},
|
||||
}
|
||||
for _, cfg := range cases {
|
||||
t.Run(cfg.BrowserBaseURL+" "+cfg.BaseURL, func(t *testing.T) {
|
||||
var tokenReq *http.Request
|
||||
client := &mockHTTPClient{
|
||||
doFn: func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.Path == "/api/oidc/token" {
|
||||
tokenReq = req
|
||||
}
|
||||
return jsonResponse(buildTokenResponse(map[string]interface{}{
|
||||
"sub": "user-uuid-123",
|
||||
"preferred_username": "alice",
|
||||
})), nil
|
||||
},
|
||||
}
|
||||
adapter := authelia.New(cfg, client)
|
||||
if _, err := adapter.HandleCallback(context.Background(), domain.CallbackParams{Code: "code"}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if tokenReq == nil {
|
||||
t.Fatal("token endpoint was not called")
|
||||
}
|
||||
if proto, host := tokenReq.Header.Get("X-Forwarded-Proto"), tokenReq.Header.Get("X-Forwarded-Host"); proto != "" || host != "" {
|
||||
t.Fatalf("forwarded headers set on %s: proto %q host %q", tokenReq.URL.String(), proto, host)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -166,16 +166,13 @@ func (p *IssuerProbe) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (p *IssuerProbe) verifyCode(ctx context.Context, code string) (string, string) {
|
||||
// Exchange under the same tokenBaseURL/client/redirect as the production
|
||||
// adapter. Use a bounded reader and cancellation; never relay error bodies.
|
||||
tokenURL := strings.TrimRight(p.adapter.tokenBaseURL(), "/") + "/api/oidc/token"
|
||||
form := url.Values{"grant_type": {"authorization_code"}, "code": {code}, "redirect_uri": {p.adapter.cfg.RedirectURI}, "client_id": {p.adapter.cfg.ClientID}}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
|
||||
// Exchange under the same token request as the production adapter, including
|
||||
// the public-origin headers. Use a bounded reader and cancellation; never
|
||||
// relay error bodies.
|
||||
req, err := p.adapter.newTokenRequest(ctx, code)
|
||||
if err != nil {
|
||||
return "", "token_exchange_error"
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(p.adapter.cfg.ClientID, p.adapter.cfg.ClientSecret)
|
||||
response, err := p.adapter.client.Do(req)
|
||||
if err != nil {
|
||||
return "", "token_exchange_error"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ type probeProvider struct {
|
|||
func (p *probeProvider) Do(r *http.Request) (*http.Response, error) {
|
||||
if r.URL.Path == "/api/oidc/token" {
|
||||
p.calls++
|
||||
if r.Header.Get("X-Forwarded-Proto") != "https" || r.Header.Get("X-Forwarded-Host") != "auth.example.com" {
|
||||
p.t.Fatalf("token forwarded headers: proto %q host %q", r.Header.Get("X-Forwarded-Proto"), r.Header.Get("X-Forwarded-Host"))
|
||||
}
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != testConfig().ClientID || pass != testConfig().ClientSecret {
|
||||
p.t.Fatal("wrong client authentication")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue