Skip to content

Sessions and logout ​

This page explains how users sign out of wicket and what the host logout page needs to do, and lists honestly how much of single logout is implemented in v0.1.4. The bottom line: logout via /connect/endsession works; front-channel and back-channel notifications and checksession have no actual effect in v0.1.4, even though the discovery document claims support.

The requests and results on this page come from an experimental program actually run on v0.1.4 (continuing in the environment from Authorization code + PKCE).

Implementation status at a glance ​

CapabilityDiscovery documentActual in v0.1.4
end_session_endpoint (/connect/endsession)ListedWorks: validates parameters, then redirects to the host logout page
post_logout_redirect_uri + state—Works, together with id_token_hint
frontchannel_logout_supportedtrueNo effect: in testing, SignOutIFrameUrl in the logout context is empty, so no front-channel notification is triggered
backchannel_logout_supportedtrueNot implemented: the service that sends notifications is a no-op, and nothing calls it
check_session_iframe (/connect/checksession)ListedThe page is reachable, but authorization responses do not return session_state, so it is unusable
Server-side sessions session.Store—Records can be written; revocation requires host cooperation (see below)

The direct reason front-channel logout doesn't work: the list of "which clients the user has signed in to" in the user session is not kept across requests, so the list is empty at logout.

1. Configuration ​

go
opts.UserInteraction.LogoutUrl = "/account/logout" // defaults to an empty string; you must set it
// LogoutIdParameter defaults to "logoutId"

web.PostLogoutRedirectUris = []string{"http://127.0.0.1:8080/signed-out"}

2. The client initiates logout ​

text
GET /connect/endsession
    ?id_token_hint=eyJhbGciOiJSUzI1NiIs...
    &post_logout_redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fsigned-out
    &state=bye

Both GET and POST (form) work. After validation, wicket redirects to the host logout page (observed):

text
HTTP/1.1 302 Found
Location: /account/logout?logoutId=ZXlKRGNtVmhkR1ZrSWpveE56azBOVE01TXpFeUxD...
ParameterNotes
id_token_hintA previously issued id_token, used to identify the client; its expiration is not checked, so an expired one also works. If the current browser is signed in with a different sub, the request is rejected
post_logout_redirect_uriTakes effect only with id_token_hint and when the address is in that client's PostLogoutRedirectUris; when it doesn't match, it is silently ignored (in testing, the field is empty in the logout context)
stateAppended to the redirect address only when post_logout_redirect_uri takes effect
ui_localesPassed through to the logout page

The endpoint also works with no parameters; in that case it redirects straight to /account/logout with no logoutId.

3. Host logout page ​

The logout page reads the context with logoutId, clears the host's login cookie, then decides whether to redirect back to the client:

go
app.HandleFunc("/account/logout", func(w http.ResponseWriter, r *http.Request) {
	sp := hosting.ScopeFrom(r.Context())
	lr, err := godi.GetRequired[services.InteractionService](sp).
		GetLogoutContext(r.Context(), r.URL.Query().Get("logoutId"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// When lr.ShowSignoutPrompt() is true (no identifiable client), ask the user to confirm first,
	// to prevent third-party pages from tricking users into signing out.

	if err := godi.GetRequired[auth.Service](sp).SignOut(r, w,
		wicket.DefaultCookieAuthenticationScheme, nil); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	if lr.PostLogoutRedirectUri != "" {
		http.Redirect(w, r, lr.PostLogoutRedirectUri, http.StatusFound)
		return
	}
	// Render a "signed out" page; when lr.SignOutIFrameUrl is non-empty, embed it in a hidden iframe (always empty in v0.1.4)
})

Observed logout context: ClientId is web-app, PostLogoutRedirectUri is http://127.0.0.1:8080/signed-out?state=bye (with state already appended), SignOutIFrameUrl, SubjectId, and SessionId are all empty, and ClientIds is an empty list. After the logout page runs SignOut, visiting /connect/authorize again redirects to the login page.

Logout is done by the host

/connect/endsession itself does not clear any cookies or sessions; it only validates parameters and hands the user to the logout page. The actual sign-out happens when you call auth.Service.SignOut (that is, your cookie handler's SignOut).

4. Front-channel, back-channel, and checksession ​

To help you assess the impact, here is the current state in v0.1.4:

  • Front-channel: Client.FrontChannelLogoutUri can be configured, and the /connect/endsession/callback?endSessionId=... endpoint is registered. The callback page itself works: when wicket's integration tests write a notification context directly, it outputs a hidden iframe with iss and sid parameters for each client. But in a real logout flow, this context needs "session ID + list of clients signed in to", and both are empty in v0.1.4 (the same in testing whether or not server-side sessions are enabled), so the SignOutIFrameUrl the logout page receives is empty and no front-channel notification is sent.
  • Back-channel: Client.BackChannelLogoutUri can be configured, and cfg.SetBackChannelLogoutHttpClient exists, but the service responsible for sending logout tokens just returns success without sending any request, and no code calls it.
  • checksession: GET /connect/checksession returns the session check iframe page, which reads a cookie named wicket.session. But authorization responses have no session_state parameter, so clients have nothing to compare against; and that cookie is only written when server-side sessions are enabled.

If you need single logout, the current approach is for the host to track "which clients the user has signed in to" itself and notify those clients from the logout page (for example, by calling each client's own logout API). On the client side, rely mainly on short access token lifetimes rather than on wicket's logout notifications.

5. Server-side sessions (session.Store) ​

When enabled, every sign-in (UserSession.CreateSessionId, the method the development cookie handler calls in SignIn) writes a record to session.Store and issues a wicket.session cookie.

go
opts.ServerSideSessions.Enabled = true
// Defaults: SessionLifetime 14 days; CookieName "wicket.session";
// CookieSecure true; CookieSameSite None. Set CookieSecure to false for local http debugging.

cfg.SetSessionStore(myStore) // implements session.Store; for development, use session.NewMemoryStore(clock)

session.Store provides create/read/update/delete by session ID, plus GetSessionsBySubjectID, DeleteSessionsBySubjectID (bulk revocation by user), and DeleteExpired (reclaims space; called on a schedule by the host — wicket does not start background goroutines of its own).

In v0.1.4, session records do not take effect automatically

In testing with server-side sessions enabled:

  • A record is written at sign-in (with the correct SubjectID), but its ClientIDs is always empty;
  • Later requests do not restore the session from the cookie, and issued tokens have no sid, so the session check during token validation never fires;
  • Server-side session records play no part in determining browser sign-in state: deleting a user's sessions (DeleteSessionsBySubjectID) does not by itself sign the browser out, and already issued tokens and refresh tokens are unaffected.

Whether the browser is "signed in" is determined entirely by your cookie handler's Authenticate.

To make "revoke by user" actually affect browser sign-in, have the cookie handler check in Authenticate whether the session still exists. Building on the development handler (see Authorization code + PKCE): save the session ID returned by CreateSessionId, and look it up in the store on each authentication. Below are the snippets that need to change (illustrative: the same logic was run and verified in the experimental program using a plain map; this sync.Map version has not been compiled on its own):

go
type ticket struct {
	p   *claims.ClaimsPrincipal
	sid string // return value of CreateSessionId
}

// In SignIn:
sid, err := h.session.CreateSessionId(h.r.Context(), p, props)
if err != nil {
	return err
}
tickets.Store(id, ticket{p: p, sid: sid})

// In Authenticate, after retrieving the ticket:
if h.sessions != nil && t.sid != "" {
	if _, err := h.sessions.GetSession(h.r.Context(), t.sid); err != nil {
		return auth.NoResult(), nil // session revoked or expired: treat as not signed in
	}
}

// Inject the store in the handler factory:
// h.sessions = godi.GetRequired[session.Store](p)

In testing, with this check added, calling DeleteSessionsBySubjectID(ctx, "carol") deleted 1 session, and when carol's browser visited /connect/authorize again, it was redirected to the login page. Correspondingly, calling h.sessions.DeleteSession(ctx, t.sid) in SignOut cleans up the record at logout (this step is illustrative and was not run on its own).

Note that this only affects browser sign-in state; already issued access tokens remain valid until they expire. For refresh tokens, see Refresh tokens.

Released under the Apache License 2.0 · Documentation © 2026 Wicket authors