Quick start
Add Vestibule to a Gleam app and wire the OAuth request and callback phases.
Use middleware for the auth routes
Add core plus the middleware for your server layer. GitHub support is provided by vestibule_github; provider packages add Google, Microsoft, or Apple.
gleam add vestibule
gleam add vestibule_github
gleam add vestibule_wispRoute request and callback phases
Register strategies once, initialize the state store once, then pass request and callback routes to the middleware.
import gleam/http
import wisp
import vestibule/config
import vestibule/registry
import vestibule/state_store
import vestibule_wisp
import vestibule_github
let assert Ok(reg) =
registry.new()
|> registry.register(
vestibule_github.strategy(),
config.new(
"client_id",
"client_secret",
"http://localhost:8000/auth/github/callback",
),
)
let store = state_store.init()
case wisp.path_segments(req), req.method {
["auth", provider], http.Get ->
vestibule_wisp.request_phase(req, reg, provider, store)
["auth", provider, "callback"], http.Get
| ["auth", provider, "callback"], http.Post ->
vestibule_wisp.callback_phase(req, reg, provider, store, fn(auth) {
wisp.redirect("/dashboard")
})
_, _ ->
wisp.not_found()
}Using Mist? The route shape is the same; use vestibule_mist for plain Mist handlers.
Advanced pathNeed direct core control?Use this when your app owns routing, sessions, and callback storage.
Core gives you the authorization URL, state, and PKCE verifier, then validates the callback after your app supplies the stored values.
gleam add vestibule
gleam add vestibule_githubHandle the two phases yourself
Store auth_request.state and
auth_request.code_verifier before redirecting. Pass the
stored values back during callback validation and delete them after
success.
import gleam/dict
import vestibule
import vestibule/config
import vestibule_github
let strategy = vestibule_github.strategy()
let cfg =
config.new(
"client_id",
"client_secret",
"http://localhost:8000/auth/github/callback",
)
let assert Ok(auth_request) = vestibule.create_authorization_request(strategy, cfg)
// Store auth_request.state and auth_request.code_verifier server-side,
// bound to this user's session, with an expiration time.
// Redirect user to auth_request.url.
let params =
dict.from_list([
#("state", "state from callback"),
#("code", "authorization code from callback"),
])
let assert Ok(auth) =
vestibule.handle_callback(
strategy,
cfg,
params,
"expected state from session",
"code verifier from session",
)
// Delete the stored state and code_verifier after success.Before shipping
Keep OAuth callback data server-side, short-lived, and bound to the user’s session. Reject missing or mismatched callbacks.
- Production redirect URIs must use HTTPS.
- Bearer tokens should be redacted from logs and error reports.
- Cookie-secret rotation invalidates in-flight OAuth flows.