fix: invalidate session on fatal OAuth2 refresh errors (#3333)

* fix: invalidate session on fatal OAuth2 refresh errors

When a token refresh fails with a fatal OAuth2 error (invalid_grant,
invalid_client), the session is now cleared from the session store
and the cookie is removed, forcing re-authentication.

Previously, fatal refresh errors were logged but the stale session
continued to be served, leaving users logged in indefinitely after
their session was revoked at the provider level.

Transient errors (network timeouts, server errors) continue to
preserve the existing session as before.

Fixes #1945

Signed-off-by: Francesco Pasqualini <frapas@gmail.com>

* fix: apply review nits and add CHANGELOG entry

Signed-off-by: Francesco Pasqualini <frapas@gmail.com>
Signed-off-by: Jan Larwig <jan@larwig.com>

---------

Signed-off-by: Francesco Pasqualini <frapas@gmail.com>
Signed-off-by: Jan Larwig <jan@larwig.com>
This commit is contained in:
Francesco Pasqualini
2026-04-12 14:48:55 +02:00
committed by GitHub
parent 26de082a78
commit 2e1261c4be
3 changed files with 99 additions and 4 deletions
+51
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
@@ -801,3 +802,53 @@ func (f *fakeSessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
func (f *fakeSessionStore) VerifyConnection(_ context.Context) error {
return nil
}
// TestIsFatalRefreshError tests the isFatalRefreshError function to ensure
// it correctly identifies fatal OAuth2 errors that should invalidate a session.
func TestIsFatalRefreshError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "nil error",
err: nil,
expected: false,
},
{
name: "invalid_grant error",
err: fmt.Errorf("failed to get token: oauth2: \"invalid_grant\" \"Session not active\""),
expected: true,
},
{
name: "invalid_client error",
err: fmt.Errorf("invalid_client: client not found"),
expected: true,
},
{
name: "network timeout - not fatal",
err: fmt.Errorf("Post \"https://keycloak/token\": dial tcp: connect: connection refused"),
expected: false,
},
{
name: "server error - not fatal",
err: fmt.Errorf("unexpected status code 500"),
expected: false,
},
{
name: "generic refresh error - not fatal",
err: fmt.Errorf("error refreshing tokens: context deadline exceeded"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isFatalRefreshError(tt.err)
if result != tt.expected {
t.Errorf("isFatalRefreshError(%v) = %v, want %v", tt.err, result, tt.expected)
}
})
}
}