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
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:
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:
ScopeMiddlewaremust 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.- 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:
| Path | Endpoint |
|---|---|
/connect/authorize, /connect/authorize/callback | Authorize |
/connect/token | Token |
/.well-known/openid-configuration | Discovery document |
/.well-known/openid-configuration/jwks | JWKS |
/connect/userinfo | UserInfo |
/connect/introspect | Introspection |
/connect/endsession, /connect/endsession/callback | End session |
/connect/checksession | Check session iframe |
/connect/ciba | CIBA 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.
| When | What is checked | Failure 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 empty | panic, e.g. ConsentUrl is not configured |
| First use of a store | If Freeze was not called and the store was not injected: falls back to the in-memory implementation and logs one warn message | Runs 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
ServerConfigread-only, and laterSetXxxcalls 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 beforeFreeze(), and is for development only; see Persistence.
Lifecycle and Root.Close
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 implementsio.Closer, itsCloseis called; otherwise nothing happens.Closeis 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 callroot.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, soroot.Close()does not close thepgxpool.Pool.
Recommended shutdown order (illustrative):
_ = 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 poolRouter 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.
// 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.ServeMuxand chi'schi.Muxboth implementhttp.Handlerdirectly. - gin's
*gin.Enginealso implementshttp.Handlerand can be passed in asrouterabove.
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.
Login, consent, and error pages are implemented by the host
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):
| Field | Default | Notes |
|---|---|---|
LoginUrl | empty | Must be set by the host; see the warning below |
LoginReturnUrlParameter | empty | Recommended: "returnUrl" |
LogoutUrl | empty | The 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 |
CookieMessageThreshold | 2 |
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:
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.