mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Serve legacy 3-segment OIDC SSO login and callback paths
An OIDC provider upgraded from v5 keeps its v5 redirect URL (/api/oidc/callback, the DefaultOIDCCallbackPath), so the IdP redirects the browser back to the 3-segment legacy path carrying only code and state, with no session cookie and no API token yet. The global auth middleware only marked the 4-segment per-provider path public, so in API-token-only mode (AuthUser=="" && AuthPass=="" && HasAPITokens()) the callback was rejected with "API token required via Authorization header or X-API-Token header", and the route dispatcher 404ed the same path. The handler layer (extractOIDCProviderID) already maps the legacy /api/oidc/login and /api/oidc/callback paths to the migrated legacy provider, so only the public-path allowlist and the route dispatcher needed to recognise the 3-segment form. Adds a regression test covering both legacy paths in API-token-only mode, the exact configuration that emitted the error. Refs #1533
This commit is contained in:
parent
74131e56e3
commit
590f8c34db
5 changed files with 74 additions and 4974 deletions
|
|
@ -4031,11 +4031,17 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Per-provider SSO OIDC routes are public (login initiation + callback)
|
||||
// Per-provider SSO OIDC routes are public (login initiation + callback).
|
||||
// The legacy v5 single-provider paths /api/oidc/login and
|
||||
// /api/oidc/callback stay public too: an upgraded provider keeps its v5
|
||||
// redirect URL, so the IdP redirects the browser back to the 3-part path
|
||||
// carrying only the code/state, with no session cookie or API token yet.
|
||||
if strings.HasPrefix(normalizedPath, "/api/oidc/") {
|
||||
oidcParts := strings.Split(strings.TrimPrefix(normalizedPath, "/"), "/")
|
||||
if len(oidcParts) >= 4 && (oidcParts[3] == "login" || oidcParts[3] == "callback") {
|
||||
isPublic = true
|
||||
} else if len(oidcParts) == 3 && (oidcParts[2] == "login" || oidcParts[2] == "callback") {
|
||||
isPublic = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,14 +38,23 @@ func (r *Router) registerAuthSecurityInstallRoutes() {
|
|||
// Use a prefix handler since Go 1.x ServeMux doesn't support path params.
|
||||
// Requests matching /api/oidc/{something}/ are dispatched here.
|
||||
r.mux.HandleFunc("/api/oidc/", func(w http.ResponseWriter, req *http.Request) {
|
||||
// Determine which sub-endpoint was requested
|
||||
// Determine which sub-endpoint was requested.
|
||||
parts := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/")
|
||||
// Expected: ["api", "oidc", "{providerID}", "{endpoint}"]
|
||||
if len(parts) < 4 {
|
||||
// Per-provider: ["api", "oidc", "{providerID}", "{endpoint}"]
|
||||
// Legacy v5: ["api", "oidc", "{endpoint}"] -> mapped to the migrated
|
||||
// legacy provider by handleSSOOIDC*. An upgraded provider
|
||||
// keeps its v5 redirect URL (/api/oidc/callback), so the IdP
|
||||
// still redirects the browser to the 3-part path.
|
||||
endpoint := ""
|
||||
switch {
|
||||
case len(parts) >= 4:
|
||||
endpoint = parts[3]
|
||||
case len(parts) == 3:
|
||||
endpoint = parts[2]
|
||||
default:
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
endpoint := parts[3]
|
||||
switch endpoint {
|
||||
case "login":
|
||||
r.handleSSOOIDCLogin(w, req)
|
||||
|
|
|
|||
|
|
@ -4478,6 +4478,60 @@ func TestSSOOIDCCallbackBypassesAuth(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestSSOOIDCLegacyCallbackBypassesAuthTokenOnly reproduces the v5->v6 upgrade
|
||||
// bug from issue #1533. An upgraded OIDC provider keeps its v5 redirect URL
|
||||
// (/api/oidc/callback, the 3-segment DefaultOIDCCallbackPath), so the IdP
|
||||
// redirects the browser back to the legacy path with only code/state, no
|
||||
// session cookie and no API token. In API-token-only mode
|
||||
// (AuthUser=="" && AuthPass=="" && HasAPITokens()) the global auth middleware
|
||||
// used to reject that inbound redirect with
|
||||
// "API token required via Authorization header or X-API-Token header"
|
||||
// because the public-path allowlist only recognised the 4-segment
|
||||
// per-provider path. The legacy login and callback paths must bypass auth and
|
||||
// reach handleSSOOIDC*, which map them to the migrated legacy provider.
|
||||
func TestSSOOIDCLegacyCallbackBypassesAuthTokenOnly(t *testing.T) {
|
||||
// Token-only mode: this is the exact config that emits the reported error.
|
||||
record := newTokenRecord(t, "legacy-oidc-token-123.12345678", []string{config.ScopeMonitoringRead}, nil)
|
||||
cfg := newTestConfigWithTokens(t, record)
|
||||
if cfg.AuthUser != "" || cfg.AuthPass != "" || !cfg.HasAPITokens() {
|
||||
t.Fatalf("test setup must reproduce token-only mode: authUser=%q authPass set=%v hasTokens=%v", cfg.AuthUser, cfg.AuthPass != "", cfg.HasAPITokens())
|
||||
}
|
||||
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
||||
|
||||
// No legacy provider is configured, so the callback handler redirects with a
|
||||
// provider_not_found error (302) and the login handler returns 404. Either
|
||||
// way the request reached the handler, proving auth was bypassed. Before the
|
||||
// fix both returned 401 with the API-token error.
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
clientIP string
|
||||
wantCode int
|
||||
}{
|
||||
{"legacy callback", "/api/oidc/callback?code=abc&state=xyz", "203.0.113.61", http.StatusFound},
|
||||
{"legacy login", "/api/oidc/login", "203.0.113.62", http.StatusNotFound},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ResetRateLimitForIP(tc.clientIP)
|
||||
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
|
||||
req.RemoteAddr = tc.clientIP + ":1234"
|
||||
rec := httptest.NewRecorder()
|
||||
router.Handler().ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code == http.StatusUnauthorized {
|
||||
t.Fatalf("legacy OIDC path %q was rejected by API-token auth (401): %s", tc.path, strings.TrimSpace(rec.Body.String()))
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "API token required") {
|
||||
t.Fatalf("legacy OIDC path %q returned the API-token error body: %s", tc.path, strings.TrimSpace(rec.Body.String()))
|
||||
}
|
||||
if rec.Code != tc.wantCode {
|
||||
t.Fatalf("legacy OIDC path %q: expected status %d (auth bypassed, reached handler), got %d", tc.path, tc.wantCode, rec.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIOAuthCallbackBypassesAuth(t *testing.T) {
|
||||
cfg := newTestConfigWithTokens(t)
|
||||
cfg.AuthUser = "admin"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue