Repository: goauthentik/authentik
File: internal/outpost/proxyv2/application/oauth.go (Go)

Code before:

const (
	redirectParam     = "rd"
	CallbackSignature = "X-authentik-auth-callback"
	LogoutSignature   = "X-authentik-logout"
)

func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) {
	state, err := a.createState(r, rw, fwd)
	if err != nil {
		a.log.WithError(err).Warning("failed to create state")
		if !strings.HasPrefix(err.Error(), "failed to get session") {
			return
		}

		// Client has a cookie but we're unable to load the session from
		// storage (TMPDIR=/dev/shm). This can happen if the session file
		// was deleted due to container restart or session invalidation
		// (e.g., logout on auth server).
		//
		// Create a new session/state and continue with redirect flow.

		// The registry caches the previous attempt to open the session so
		// it needs to be cleared in order to create a new session/state
		// below.
		*r = *r.WithContext(context.Background())

		newSession, err := a.sessions.New(r, a.SessionName())
		if err != nil && !strings.HasSuffix(err.Error(), "no such file or directory") {
			a.log.WithError(err).Warning("failed to create new session")
			return
		}
		err = a.sessions.Save(r, rw, newSession)
		if err != nil {
			a.log.WithError(err).Warning("failed to save new session")
			return
		}
		state, err = a.createState(r, rw, fwd)
		if err != nil {
			a.log.WithError(err).Warning("failed to create state on retry")
			return
		}
	}
	http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(state), http.StatusFound)
}

func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
	s, err := a.sessions.Get(r, a.SessionName())
	if err != nil {
		a.log.WithError(err).Warning("failed to decode session")
	}

Review comment (bug):
Creating a new session as you mentioned above involves generating and using a new cookie, but this logic would not work:

Summarizing what's getting modified:

  1. `createState()` fails because session file is missing (container restart cleared /dev/shm or whatnot)
  2. Clear the gorilla/sessions registry cache by replacing the request context
  3. Create a new session with `sessions.New()`
  4. Save the new session with `sessions.Save()`
  5. Retry `createState()`, expecting it to use the newly created session

 This doesn't work because he HTTP request object is read-only for incoming data. When you save a new session: `err = a.sessions.Save(r, rw, newSession)`

The new session cookie is written to `rw (the http.ResponseWriter) `which will be sent to the client in the HTTP response. But the request object `r` still contains the original cookie in its headers.

This means when the code wants to retry `createState()`: `state, err = a.createState(r, rw, fwd)  // Line 49 in this diff`

Inside `createState()`at [oauth_state.go:72](https://github.com/goauthentik/authentik/blob/c37ef6d72840e89406ab86dcbd9b24b45137e2b9/internal/outpost/proxyv2/application/oauth_state.go#L72): `s, err := a.sessions.Get(r, a.SessionName())`

The `Get() ` method reads the session cookie from the request headers, not from the `ResponseWriter`. It finds the old cookie referencing the deleted session file.

Code after:

const (
	redirectParam     = "rd"
	CallbackSignature = "X-authentik-auth-callback"
	LogoutSignature   = "X-authentik-logout"
)

func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, fwd string) {
	state, err := a.createState(r, rw, fwd)
	if err != nil {
		a.log.WithError(err).Warning("failed to create state")
		if !strings.HasPrefix(err.Error(), "failed to get session") {
			rw.WriteHeader(400)
			return
		}

		// Client has a cookie but we're unable to load the session from
		// storage (TMPDIR=/dev/shm). This can happen if the session file
		// was deleted due to container restart or session invalidation
		// (e.g., logout on auth server).
		//
		// Re-save an empty session and try again.

		session, err := a.sessions.Get(r, a.SessionName())
		if err != nil && !strings.HasSuffix(err.Error(), "no such file or directory") {
			a.log.WithError(err).Warning("failed to get session")
			rw.WriteHeader(400)
			return
		}
		err = a.sessions.Save(r, rw, session)
		if err != nil {
			a.log.WithError(err).Warning("failed to save session")
			rw.WriteHeader(400)
			return
		}

		// The registry caches the previous attempt to open the session so it
		// needs to be cleared in order to get the session in createState().
		*r = *r.WithContext(context.Background())

		state, err = a.createState(r, rw, fwd)
		if err != nil {
			a.log.WithError(err).Warning("failed to create state on retry")
			rw.WriteHeader(400)
			return
		}
	}
	http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(state), http.StatusFound)
}

func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {