Drop-in OIDC and password authentication for Go web apps. Provides HMAC-signed session cookies, OIDC login/callback handlers, auth middleware, and periodic session revalidation.
go get github.com/andrianbdn/oidc-auth-kit
The package name is oidcauth:
import oidcauth "github.com/andrianbdn/oidc-auth-kit"If you only need signed session cookies (no OIDC), omit the Issuer:
auth, err := oidcauth.New(context.Background(), oidcauth.Config{
SessionSecret: oidcauth.GenerateSecret(32),
})Create a session after verifying credentials yourself:
http.SetCookie(w, auth.CreateSessionCookie(email))Protect routes with middleware:
http.HandleFunc("GET /", auth.Middleware("/login")(dashboardHandler))auth, err := oidcauth.New(ctx, oidcauth.Config{
Issuer: "https://accounts.google.com",
ClientID: os.Getenv("OIDC_CLIENT_ID"),
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
BaseURL: "https://myapp.example.com",
SessionSecret: secretBytes,
RevalidateInterval: time.Hour,
AllowedUsers: oidcauth.ParseAllowedUsers("*@mycompany.com [email protected]"),
})Register the handlers:
http.HandleFunc("GET /auth/oidc", auth.LoginHandler())
http.HandleFunc("GET /auth/oidc/callback", auth.CallbackHandler())
http.HandleFunc("POST /logout", auth.LogoutHandler())
http.HandleFunc("GET /", auth.Middleware("/auth")(protectedHandler))Initialize with both Issuer and your own password check. Use CreateSessionCookie for password logins, and register OIDC handlers alongside your password form.
Creates an Auth instance. Performs OIDC discovery if Issuer is set. Returns error if SessionSecret is empty or OIDC discovery fails.
| Field | Type | Default | Description |
|---|---|---|---|
Issuer |
string |
"" |
OIDC provider URL. Empty = password-only mode. |
ClientID |
string |
OAuth2 client ID (required when Issuer is set) | |
ClientSecret |
string |
OAuth2 client secret (required when Issuer is set) | |
BaseURL |
string |
App's external URL, e.g. https://myapp.com |
|
CallbackPath |
string |
/auth/oidc/callback |
OAuth2 redirect path |
AllowedUsers |
[]string |
nil (allow all) |
Email patterns: [email protected], *@corp.com |
SessionSecret |
[]byte |
Required. HMAC-SHA256 signing key. Use GenerateSecret(32) for random. |
|
SessionMaxAge |
time.Duration |
1 year | Cookie lifetime |
RevalidateInterval |
time.Duration |
0 (disabled) |
How often to check user still exists at provider |
| Method | Description |
|---|---|
auth.LoginHandler() |
Redirects to OIDC provider. Mount at GET /auth/oidc. |
auth.CallbackHandler() |
Handles OIDC callback. Mount at GET {CallbackPath}. |
auth.LogoutHandler() |
Clears session cookie. Mount at POST /logout. |
auth.Middleware(redirectPath string, checkers ...SessionChecker) func(http.HandlerFunc) http.HandlerFuncValidates the session cookie and injects the email into the request context. Redirects to redirectPath on failure.
SessionChecker — optional callback for additional authorization checks on every request:
auth.Middleware("/login", func(email string) error {
if _, ok := allowedUsers[email]; !ok {
return fmt.Errorf("user removed")
}
return nil
})| Method | Description |
|---|---|
auth.CreateSessionCookie(email) |
Signed cookie for password auth (no refresh token) |
auth.CreateOIDCSessionCookie(email, refreshToken) |
Signed cookie for OIDC auth (with revalidation support) |
auth.ValidateSession(r) |
Returns (email, error) from the request's session cookie |
oidcauth.EmailFromContext(ctx) |
Returns the email injected by middleware (empty string if absent) |
| Function | Description |
|---|---|
oidcauth.GenerateSecret(n) |
Returns n cryptographically random bytes |
oidcauth.ParseAllowedUsers(s) |
Parses space-separated email patterns into a slice |
oidcauth.MatchAllowedUser(email, allowed) |
Checks email against an allowed list (supports *@domain wildcards) |
The session cookie contains a base64url-encoded JSON payload signed with HMAC-SHA256:
base64url({"email":"...","exp":...,"rt":"...","rv":...}).hex(hmac-sha256)
rt(refresh token) andrv(revalidate-at timestamp) are present only for OIDC sessions.- Cookie attributes:
HttpOnly,Secure,SameSite=Lax,Path=/.
When RevalidateInterval is set and the OIDC provider returned a refresh token, the middleware periodically:
- Exchanges the refresh token for a new access token
- Calls the provider's userinfo endpoint to verify the user still exists
- Revokes the session on hard failures (401, 403, email mismatch)
- Retries in 5 minutes on transient failures (network, 5xx)
- Updates the cookie with a rotated refresh token if the provider issued one
- Google: Uses
access_type=offlineinstead of theoffline_accessscope. The library handles this automatically. - Other providers: The
offline_accessscope is requested for refresh token support. Some providers may not return a refresh token on the first login — the library logs a warning in this case.
Full example showing password + OIDC hybrid mode:
package main
import (
"context"
"crypto/subtle"
"fmt"
"log"
"net/http"
"os"
oidcauth "github.com/andrianbdn/oidc-auth-kit"
)
func main() {
auth, err := oidcauth.New(context.Background(), oidcauth.Config{
Issuer: os.Getenv("OIDC_ISSUER"),
ClientID: os.Getenv("OIDC_CLIENT_ID"),
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
BaseURL: os.Getenv("BASE_URL"),
SessionSecret: []byte(os.Getenv("SESSION_SECRET")),
AllowedUsers: oidcauth.ParseAllowedUsers(os.Getenv("OIDC_ALLOWED_USERS")),
})
if err != nil {
log.Fatal(err)
}
// WARNING: Plaintext passwords are used here for brevity only.
// In production, store hashed passwords (e.g. bcrypt) and compare
// with a constant-time hash check instead.
passwords := map[string]string{"[email protected]": "secret123"}
oidcEnabled := os.Getenv("OIDC_ISSUER") != ""
// Login page — shows password form and/or SSO link
http.HandleFunc("GET /auth", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<!DOCTYPE html><html><body>
<h1>Login</h1>
<form method="POST" action="/auth">
<input name="email" type="email" placeholder="Email" required>
<input name="password" type="password" placeholder="Password" required>
<button type="submit">Sign in</button>
</form>`)
if oidcEnabled {
fmt.Fprint(w, `<p>— or —</p><a href="/auth/oidc">Sign in with SSO</a>`)
}
fmt.Fprint(w, `</body></html>`)
})
// Password login handler
http.HandleFunc("POST /auth", func(w http.ResponseWriter, r *http.Request) {
email, pw := r.FormValue("email"), r.FormValue("password")
stored, ok := passwords[email]
if !ok || subtle.ConstantTimeCompare([]byte(pw), []byte(stored)) != 1 {
http.Error(w, "Invalid credentials", 401)
return
}
http.SetCookie(w, auth.CreateSessionCookie(email))
http.Redirect(w, r, "/", http.StatusFound)
})
// OIDC handlers
if oidcEnabled {
http.HandleFunc("GET /auth/oidc", auth.LoginHandler())
http.HandleFunc("GET /auth/oidc/callback", auth.CallbackHandler())
}
http.HandleFunc("POST /logout", auth.LogoutHandler())
// Protected route
http.HandleFunc("GET /", auth.Middleware("/auth")(func(w http.ResponseWriter, r *http.Request) {
email := oidcauth.EmailFromContext(r.Context())
fmt.Fprintf(w, "Hello, %s!", email)
}))
log.Fatal(http.ListenAndServe(":8080", nil))
}go test -v ./...