OAuth2 / social login
Zero-config OAuth2 / OpenID Connect login under middlewares/oauth2. Four
providers work out of the box โ Google, GitHub, Microsoft, Discord โ
with PKCE, CSRF-protected state, signed HttpOnly cookies (or JWT sessions),
and transparent token refresh. Only ClientID and ClientSecret are
required.
package main
import (
"github.com/nelthaarion/breeze/v2"
"github.com/nelthaarion/breeze/v2/middlewares/oauth2"
)
func main() {
app := breeze.New()
cfg := oauth2.Config{
Provider: oauth2.Google,
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
BaseURL: "https://app.example.com",
CookieSecret: "a-long-random-secret",
}
app.Router.Handle(breeze.GET, "/auth/google", oauth2.Login(cfg))
app.Router.Handle(breeze.GET, "/auth/google/callback", oauth2.Callback(cfg))
app.Router.Handle(breeze.GET, "/auth/logout", oauth2.Logout(cfg))
app.Router.Handle(breeze.GET, "/dashboard", dashboard, oauth2.Auth(cfg))
app.Listen(":3000")
}
func dashboard(ctx *breeze.Context) error {
user := oauth2.CurrentUser(ctx) // *oauth2.User
return ctx.JSON(user)
} Middlewares
| Function | Purpose |
|---|---|
Login(cfg) | begins the flow: random state + nonce + PKCE, redirect to the provider |
Callback(cfg) | validates state, exchanges the code, fetches the user, writes the session |
Auth(cfg) | requires a valid session; 401 / redirect otherwise |
Optional(cfg) | attaches the session if present, never blocks |
Refresh(cfg) | transparently refreshes an expiring access token, then rotates the cookie |
Logout(cfg) | clears the session and redirects |
Refresh layers before Auth on protected routes that need fresh access
tokens:
app.Router.Handle(breeze.GET, "/dashboard", dashboard,
oauth2.Refresh(cfg),
oauth2.Auth(cfg),
) Reading the user
From any handler that ran after Auth, Optional, or Callback:
user := oauth2.CurrentUser(ctx) // *User or nil
tok := oauth2.CurrentToken(ctx) // *Token or nil
if oauth2.IsAuthenticated(ctx) {
// ...
} UserFrom / TokenFrom are short aliases. The normalized User is
provider-independent:
type User struct {
ID string // stable provider id (Google sub, GitHub numeric id, ...)
Email string
Name string
Username string
Avatar string // URL
Provider Provider
} Sessions
Two modes, selected by Config.SessionMode:
SessionModeCookie(default) โ user + token serialized into a signed, HttpOnly cookie. Tamper-proof (HMAC), stateless server-side.SessionModeJWTโ an HS256 JWT carrying the claims, stored in the cookie. The algorithm is pinned to preventalg=none/ confusion attacks.
Every write rotates the session (a new JWT jti or a freshly signed
blob), so a pre-login cookie cannot be reused post-login.
Security
- PKCE (S256) on by default for every provider.
- CSRF โ
statelives in a signed, short-lived, single-use cookie, compared in constant time; the flow cookie is cleared on callback. - Open-redirect guard โ
?redirect=overrides accept same-site paths only. - HttpOnly + Secure + SameSite=Lax cookies.
Secureauto-enables forhttps://BaseURLs and can be forced. - Bounded provider calls โ every outbound token/userinfo request runs under a timeout so a slow provider cannot pin a worker.
Configuration
| Field | Default | Notes |
|---|---|---|
Provider | โ | Google, GitHub, Microsoft, Discord |
ClientID | โ (required) | |
ClientSecret | โ (required) | |
BaseURL | http://localhost:3000 | origin used to build RedirectURL |
RedirectURL | BaseURL/auth/{provider}/callback | the registered callback URL |
Scopes | provider defaults | override to request more scopes |
SessionMode | SessionModeCookie | or SessionModeJWT |
CookieName | breeze_oauth_session | |
SuccessRedirect | / | post-login destination |
FailureRedirect | "" โ 401 | set to redirect on failure instead |
CookieSecret | random per process | set in production (survives restarts) |
JWTSecret | falls back to CookieSecret | used in JWT mode |
SessionTTL | 24h | |
StateTTL | 10m | max time to complete a login |
ClockSkew | 1m | expiry tolerance |
Secure | true (auto for https BaseURL) | set false only for local http |
Diagnostics
The oauth2 probe reports through GET /dashboard/api/diagnostics?subsystem=oauth2.
See Diagnostics.