AndrianBdn/oidc-auth-kit

★ 1Forks 0GoGitHub ↗Compare

README

oidc-auth-kit

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.

Install

go get github.com/andrianbdn/oidc-auth-kit

The package name is oidcauth:

import oidcauth "github.com/andrianbdn/oidc-auth-kit"

Quick start

Password-only mode

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))

OIDC mode

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))

Hybrid mode (password + OIDC)

Initialize with both Issuer and your own password check. Use CreateSessionCookie for password logins, and register OIDC handlers alongside your password form.

API reference

oidcauth.New(ctx, Config) (*Auth, error)

Creates an Auth instance. Performs OIDC discovery if Issuer is set. Returns error if SessionSecret is empty or OIDC discovery fails.

Config

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

Handlers

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.

Middleware

auth.Middleware(redirectPath string, checkers ...SessionChecker) func(http.HandlerFunc) http.HandlerFunc

Validates 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
})

Session management

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)

Helpers

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)

Session cookie format

The session cookie contains a base64url-encoded JSON payload signed with HMAC-SHA256:

base64url({"email":"...","exp":...,"rt":"...","rv":...}).hex(hmac-sha256)
  • rt (refresh token) and rv (revalidate-at timestamp) are present only for OIDC sessions.
  • Cookie attributes: HttpOnly, Secure, SameSite=Lax, Path=/.

OIDC revalidation

When RevalidateInterval is set and the OIDC provider returned a refresh token, the middleware periodically:

  1. Exchanges the refresh token for a new access token
  2. Calls the provider's userinfo endpoint to verify the user still exists
  3. Revokes the session on hard failures (401, 403, email mismatch)
  4. Retries in 5 minutes on transient failures (network, 5xx)
  5. Updates the cookie with a rotated refresh token if the provider issued one

Provider notes

  • Google: Uses access_type=offline instead of the offline_access scope. The library handles this automatically.
  • Other providers: The offline_access scope 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.

Integration example

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))
}

Testing

go test -v ./...

Contributors

AndrianBdn

Issues