Skip to content

Embedding in a host application ​

This page explains how Wicket is layered inside the host process, which requests it handles and which it hands back to you, and what the host needs to do at startup and shutdown.

Wiring skeleton ​

go
root, err := di.NewRoot()
if err != nil {
	log.Fatal(err)
}
defer root.Close()

opts := godi.GetRequired[*options.WicketOptions](root)
opts.IssuerUri = "https://id.example.com"

cfg := root.ServerConfig()
// ... cfg.SetClients / SetApiScopes / SetSigningCredentialStores / the various stores ...
cfg.Freeze() // checks store injection; panics on failure

srv, err := server.DefaultServer(root, server.WithLogger(logger))
if err != nil {
	log.Fatal(err)
}

app := http.NewServeMux() // your application routes
handler := hosting.ScopeMiddleware(root)(srv.UseWicket()(app))
log.Fatal(http.ListenAndServe(":5000", handler))

For a complete version you can run directly, see Quick start.

Middleware chain order ​

A request passes through the following layers, from outside in:

text
hosting.ScopeMiddleware(root)        creates a DI scope for this request and puts it in the context
 └─ middleware returned by srv.UseWicket()
     ├─ dynamic scheme middleware    handles external login schemes registered at runtime (passes through when not enabled)
     ├─ core authentication          identifies the current user by authentication scheme
     └─ endpoint router              path matches a protocol endpoint → wicket handles it and stops
                                     no match → hands off to next (your app)

Two hard requirements:

  1. ScopeMiddleware must be the outermost layer. The endpoint router takes the scope from the context; if it can't find one, it does not return an error but passes every request through to the host unchanged. The symptom is "all protocol endpoints return 404 / land in your application routes". Forgetting this layer is the most common wiring mistake.
  2. Call UseWicket() only once, after wiring is complete. It runs the startup checks (see below) at the moment it is called, and resolves singletons such as the router.

Which paths Wicket handles ​

The endpoint router performs a case-insensitive exact match on r.URL.Path and handles the request only if it matches one of these paths:

PathEndpoint
/connect/authorize, /connect/authorize/callbackAuthorize
/connect/tokenToken
/.well-known/openid-configurationDiscovery document
/.well-known/openid-configuration/jwksJWKS
/connect/userinfoUserInfo
/connect/introspectIntrospection
/connect/endsession, /connect/endsession/callbackEnd session
/connect/checksessionCheck session iframe
/connect/cibaCIBA backchannel authentication

Every other path goes to the host handler, including /connect/revocation, /connect/deviceauthorization, and the mTLS paths, which in v0.1.4 exist only as constants with no handler. This is also how your login page, consent page, and application APIs coexist with Wicket.

Root path only

The URLs Wicket generates (the endpoints in the discovery document, the callback address in login redirects) are all relative to the site root, and BasePath is always empty. Don't mount it under a subpath with http.StripPrefix.

Freeze and startup failures ​

Wicket's startup checks all surface as panics; they don't return errors. Do all wiring during process startup so that problems show up at deployment time.

WhenWhat is checkedFailure behavior
cfg.Freeze()Whether all 11 store seams have been injected, or the host has called cfg.AllowInMemoryFallback()panic: store seam "..." not injected by host and in-memory fallback not declared
srv.UseWicket()ErrorUrl, ConsentUrl, ConsentReturnUrlParameter, LogoutIdParameter, ErrorIdParameter, CustomRedirectReturnUrlParameter, CheckSessionCookieName, and CorsPolicyName must not be emptypanic, e.g. ConsentUrl is not configured
First use of a storeIf Freeze was not called and the store was not injected: falls back to the in-memory implementation and logs one warn messageRuns silently in memory

What you need to know about Freeze:

  • It only runs the injection check once and sets a flag; it does not make ServerConfig read-only, and later SetXxx calls still take effect. Treat it as a "wiring complete" checkpoint, not as concurrency protection.
  • Wicket runs without Freeze, but any store that wasn't injected quietly falls back to process memory, which is guaranteed to break in a multi-replica deployment. Always call it in production wiring.
  • AllowInMemoryFallback() only has an effect when called before Freeze(), and is for development only; see Persistence.

Lifecycle and Root.Close ​

go
root, err := di.NewRoot()
// ...
defer root.Close()
  • Root.Close() releases the store wrappers tracked by the container in reverse order of construction: if an adapter injected by the host implements io.Closer, its Close is called; otherwise nothing happens.
  • Close is idempotent: a second call returns the result of the first. Resolving services from the container after closing panics.
  • The host must stop accepting requests first (for example with http.Server.Shutdown) and then call root.Close(). Requests still using an adapter during shutdown are a host bug; Wicket does no synchronization.
  • Resources the host creates itself (database connection pools, the authorization parameter message store, etc.) are closed by the host. For example, the wicket-pg stores don't implement Close, so root.Close() does not close the pgxpool.Pool.

Recommended shutdown order (illustrative):

go
_ = httpServer.Shutdown(ctx) // 1. stop accepting new requests and wait for in-flight ones to finish
cancelCleanup()              // 2. stop the host's expired-data cleanup loop
_ = root.Close()             // 3. release the wicket container
pool.Close()                 // 4. close the host's own connection pool

Router integration ​

UseWicket() returns a standard func(http.Handler) http.Handler, and so does ScopeMiddleware. Any router that accepts an http.Handler can therefore be integrated, with one rule: wrap the router in Wicket, so that protocol paths are intercepted before they reach the router.

go
// Illustrative: buildMyRouter returns the host's router
var router http.Handler = buildMyRouter() // standard library mux, chi, a gin engine... any http.Handler

handler := hosting.ScopeMiddleware(root)(srv.UseWicket()(router))
http.ListenAndServe(":5000", handler)
  • The standard library http.ServeMux and chi's chi.Mux both implement http.Handler directly.
  • gin's *gin.Engine also implements http.Handler and can be passed in as router above.

Registering Wicket as a sub-route inside the router is not recommended: Wicket needs to take over both the /connect/* and /.well-known/* path groups, and relies on the root path to compute URLs.

Wicket renders no interaction pages. When an authorization request needs user interaction, it redirects the browser with a 302 to the address configured in WicketOptions.UserInteraction, appending a return URL parameter; once the host page is done, it sends the browser back to that return URL.

Actual field values after NewRoot() (verified against v0.1.4):

FieldDefaultNotes
LoginUrlemptyMust be set by the host; see the warning below
LoginReturnUrlParameteremptyRecommended: "returnUrl"
LogoutUrlemptyThe host logout page redirected to at end session
LogoutIdParameter"logoutId"
ConsentUrl"/consent"
ConsentReturnUrlParameter"returnUrl"
ErrorUrl"/home/error"
ErrorIdParameter"errorId"
CustomRedirectReturnUrlParameter"returnUrl"
DeviceVerificationUrl"/device"v0.1.4 has no device authorization endpoint, so this has no effect for now
CookieMessageThreshold2

LoginUrl has no default

In v0.1.4, LoginUrl and LoginReturnUrlParameter both default to the empty string, and UseWicket() does not check them. If you don't set them, an unauthenticated user hitting the authorize endpoint is redirected to an invalid address like /connect/?=.... Any host that uses the authorization code flow should set them explicitly:

go
ui := opts.UserInteraction
ui.LoginUrl = "/account/login"
ui.LoginReturnUrlParameter = "returnUrl"
ui.LogoutUrl = "/account/logout"

Once set, unauthenticated authorization requests are redirected to /account/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3F....

The host login page's job: validate the user's credentials → write the login session through the wicket authentication scheme → redirect back to returnUrl (accept only same-site relative addresses, to prevent open redirects). As described in Core concepts, the host must provide the cookie handler for the wicket scheme itself. For the full interaction flow, see Authorization code + PKCE.

Logging ​

server.WithLogger(logger) applies only to the Server itself (startup messages, etc.). The logger used by the services inside the container is fixed at di.NewRoot() time:

Known limitation in v0.1.4

Calling cfg.SetLogger(logger) after NewRoot() does not change the logger of services already constructed. In testing, the container's internal logs are always written to standard output as JSON, at Debug level, with source locations. In v0.1.4 the host cannot adjust their level or destination through the API; filter out Debug level on the log collection side. See Logging and observability for details.

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