Skip to content

Instantly share code, notes, and snippets.

@frhack
Created February 6, 2026 17:10
Show Gist options
  • Select an option

  • Save frhack/f5664c652e9e0f65a48cae969c97ff56 to your computer and use it in GitHub Desktop.

Select an option

Save frhack/f5664c652e9e0f65a48cae969c97ff56 to your computer and use it in GitHub Desktop.
Fix #1945: invalidate session on fatal refresh error (invalid_grant/Session not active) - OAuth2-Proxy v7.14.2
# Stage 1: Build the patched oauth2-proxy binary
FROM golang:1.22-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git make
# Set working directory
WORKDIR /build
# Allow Go to download the required toolchain version
ENV GOTOOLCHAIN=auto
# Copy go mod files first for better caching
COPY go.mod go.sum ./
RUN go mod download
# Copy the entire source code (including the patched stored_session.go)
COPY . .
# Build the binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o oauth2-proxy .
# Stage 2: Create the final image
FROM alpine:3.19
# Install runtime dependencies
RUN apk add --no-cache ca-certificates curl
# Copy the binary from builder
COPY --from=builder /build/oauth2-proxy /bin/oauth2-proxy
# Use non-root user for security
RUN addgroup -g 2000 -S oauth2-proxy && \
adduser -u 2000 -S oauth2-proxy -G oauth2-proxy
USER oauth2-proxy
ENTRYPOINT ["/bin/oauth2-proxy"]
OAuth2-Proxy Session Invalidation on Fatal Refresh Errors
==========================================================
Description:
Fix for GitHub Issue #1945: Invalidate session when refresh token fails
with fatal errors like "invalid_grant" or "Session not active" from OIDC providers.
Problem:
When a session is invalidated at the provider level (e.g., admin revokes session
in Keycloak), OAuth2-Proxy continues serving the local cached session indefinitely,
keeping users logged in even though their provider session no longer exists.
Solution:
This patch distinguishes between fatal errors (revoked/invalid sessions) and
transient errors (network issues). When a fatal error is detected during token
refresh, the session is immediately cleared from storage (Redis/Cookie) and the
user is redirected to login.
Fatal Errors (session cleared):
- "invalid_grant"
- "Session not active"
- "Token is not active"
- "invalid_client"
Transient Errors (session kept):
- Network timeouts
- Provider temporarily unreachable (5xx errors)
- DNS errors
Target Version: v7.14.2
Tested With: Keycloak OIDC provider, Redis session store
Issue: https://github.com/oauth2-proxy/oauth2-proxy/issues/1945
Date: 2026-02-06
How to Apply:
cd oauth2-proxy
git checkout v7.14.2
git apply oauth2-proxy-fix-1945.patch
# Or using patch command:
patch -p1 < oauth2-proxy-fix-1945.patch
Files Modified:
pkg/middleware/stored_session.go
Changes:
- Added import: "strings"
- Added helper function: isFatalRefreshError()
- Modified refreshSessionIfNeeded() to clear session on fatal errors
---
diff --git a/pkg/middleware/stored_session.go b/pkg/middleware/stored_session.go
index f861c756..f5e9125d 100644
--- a/pkg/middleware/stored_session.go
+++ b/pkg/middleware/stored_session.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
+ "strings"
"time"
"github.com/justinas/alice"
@@ -31,6 +32,31 @@ const (
sessionRefreshRetryPeriod = 10 * time.Millisecond
)
+// isFatalRefreshError checks if a refresh error indicates a revoked or
+// non-existent session that should be immediately invalidated.
+// Fatal errors indicate the session is no longer valid at the provider level.
+// Non-fatal errors (network issues, timeouts) should not invalidate the session.
+func isFatalRefreshError(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ errStr := err.Error()
+ fatalErrors := []string{
+ "invalid_grant",
+ "Session not active",
+ "Token is not active",
+ "invalid_client",
+ }
+
+ for _, fe := range fatalErrors {
+ if strings.Contains(errStr, fe) {
+ return true
+ }
+ }
+ return false
+}
+
// StoredSessionLoaderOptions contains all of the requirements to construct
// a stored session loader.
// All options must be provided.
@@ -188,9 +214,25 @@ func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req
// We are holding the lock and the session needs a refresh
logger.Printf("Refreshing session - User: %s; SessionAge: %s", session.User, session.Age())
if err := s.refreshSession(rw, req, session); err != nil {
- // If a preemptive refresh fails, we still keep the session
- // if validateSession succeeds.
logger.Errorf("Unable to refresh session: %v", err)
+
+ // Check if this is a fatal error that indicates the session is revoked
+ // or no longer valid at the provider level
+ if isFatalRefreshError(err) {
+ logger.Printf("Fatal refresh error detected (session revoked or invalid), clearing session for user: %s", session.User)
+
+ // Clear the session from storage (Redis) and remove the cookie
+ clearErr := s.store.Clear(rw, req)
+ if clearErr != nil {
+ logger.Errorf("Error clearing session: %v", clearErr)
+ }
+
+ // Return error immediately to force re-authentication
+ return fmt.Errorf("session invalidated due to fatal refresh error: %w", err)
+ }
+
+ // For non-fatal errors (network issues, timeouts), keep the session
+ // and let validateSession determine if it's still usable
}
// Validate all sessions after any Redeem/Refresh operation (fail or success)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment