crooner

package module
v1.3.23 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 22, 2026 License: MIT Imports: 21 Imported by: 0

README

🎩 Crooner: You Gotta Be Right Next to Me for It to Look Real, Baby

Go Reference

image

Fuck! He's trying to steal my decals! Fuck! They're trying to make it look fake! Goddammit! You gotta give! The hat and the cigar. You're driving with the Driving Crooner, baby.

Crooner is an Azure AD OIDC/OAuth2 client for Go Echo apps. It handles PKCE login, callbacks, and session management with pluggable backends and secure defaults.

What Is This? Why Do People Hate It?

I don't know. Some people hate this, James. I don't know what it is, but they fuckin' hate it. There's people that wanna kill me, James. But I gotta figure out how to make money on this thing. It's simply too good. Crooner is for Go web apps using Echo, and it's the real deal. Not like those other guys, with their fake decals and their fake logins. This is the real Crooner. The hat and the cigar.

Ever want to authenticate with Azure in your Go project but MSAL has no examples for a hosted HTTP service? MSAL Issue #468

Features (Don't Try to Steal My Decals)

What you get when you ride with the Crooner—none of this fake stuff:

  • Azure AD PKCE/OIDC login — the hat and the cigar
  • Pluggable session management (SCS, custom) — you pick who's in the car
  • Configurable Content Security Policy (CSP) — don't let them make it look fake
  • Secure, non-guessable session cookies — no decal theft
  • Designed for Echo, but extensible — right next to me, baby
  • Preserves original URLs (including query strings) through login and callback — your destination stays real
  • Reverse proxy friendly authentication flow — keeps you on the road
  • Automatic recovery from lost session state (e.g., after server restart) — hit a pothole? Crooner gets you back

Installation (You Gotta Give!)

go get github.com/catgoose/crooner@latest

Quick Start Example (You Gotta Be Right Next to Me)

Here's how you get the show on the road:

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	crooner "github.com/catgoose/crooner"
	"github.com/labstack/echo/v4"
)

type AppConfig struct {
	SessionSecret string
	AppName       string
	CroonerConfig *crooner.AuthConfigParams
	SessionMgr    crooner.SessionManager
}

func LoadAppConfig() (*AppConfig, error) {
	// Load secrets/config from environment variables or your preferred config system
	secret := os.Getenv("SESSION_SECRET")
	if secret == "" {
		return nil, fmt.Errorf("SESSION_SECRET is required")
	}
	appName := "myApp" // or load from env/config

	// Fill in your Azure AD and Crooner config
	croonerConfig := &crooner.AuthConfigParams{
		ClientID:          os.Getenv("AZURE_CLIENT_ID"),
		ClientSecret:      os.Getenv("AZURE_CLIENT_SECRET"),
		TenantID:          os.Getenv("AZURE_TENANT_ID"),
		RedirectURL:       os.Getenv("AZURE_REDIRECT_URL"),
		LogoutURLRedirect: os.Getenv("AZURE_LOGOUT_REDIRECT_URL"),
		LoginURLRedirect:  os.Getenv("AZURE_LOGIN_REDIRECT_URL"),
		AuthRoutes: &crooner.AuthRoutes{
			Login:    "/login",
			Logout:   "/logout",
			Callback: "/callback",
		},
		SecurityHeaders: &crooner.SecurityHeadersConfig{
			ContentSecurityPolicy:   "img-src 'self' data: https://login.microsoftonline.com;",
			XFrameOptions:           "DENY",
			XContentTypeOptions:     "nosniff",
			ReferrerPolicy:          "strict-origin-when-cross-origin",
			XXSSProtection:          "1; mode=block",
			StrictTransportSecurity: "max-age=63072000; includeSubDomains; preload", // set only if HTTPS
		},
		// ...other config as needed...
	}

	return &AppConfig{
		SessionSecret: secret,
		AppName:       appName,
		CroonerConfig: croonerConfig,
	}, nil
}

func main() {
	appConfig, err := LoadAppConfig()
	if err != nil {
		log.Fatalf("failed to load app config: %v", err)
	}

	e := echo.New()

	sessionMgr, scsMgr, err := crooner.NewSCSManager(
		crooner.WithPersistentCookieName(appConfig.SessionSecret, appConfig.AppName),
		crooner.WithLifetime(12*time.Hour),
		crooner.WithCookieDomain("example.com"), // optional
		// ...add other options as needed
	)
	if err != nil {
		log.Fatalf("failed to initialize session manager: %v", err)
	}
	e.Use(echo.WrapMiddleware(scsMgr.LoadAndSave))
	appConfig.SessionMgr = sessionMgr
	appConfig.CroonerConfig.SessionMgr = sessionMgr

	ctx := context.Background()
	if err := crooner.NewAuthConfig(ctx, e, appConfig.CroonerConfig); err != nil {
		log.Fatalf("failed to initialize Crooner authentication: %v", err)
	}

	e.GET("/", func(c echo.Context) error {
		return c.String(200, "Hello, Crooner!")
	})

	// Start server
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	e.Logger.Fatal(e.Start(":" + port))
}

Configuration (Don't Let Them Make It Look Fake)

Session Management (Best Practice)

Don't let them make it look fake. Do it right:

  • Use a strong, random SESSION_SECRET (set via env/config)—you gotta give.

  • Use a unique AppName per app. No sharing decals.

  • Set a persistent, non-guessable cookie name using crooner.WithPersistentCookieName(secret, appName) when creating your session manager:

    sessionMgr, scsMgr, err := crooner.NewSCSManager(
    	crooner.WithPersistentCookieName(secret, appName),
    	// ...other options...
    )
    
    
  • NewSCSManager applies secure defaults so the show looks real. Only reach for advanced config when you've got special requirements.

Content Security Policy (CSP) and Security Headers

Configure your security headers via SecurityHeadersConfig. Empty field? Crooner slaps a secure default on it so nobody makes it look fake.

params := &crooner.AuthConfigParams{
	// ... other config ...
	SecurityHeaders: &crooner.SecurityHeadersConfig{
		ContentSecurityPolicy:   "img-src 'self' data: https://login.microsoftonline.com;",
		XFrameOptions:           "DENY",
		XContentTypeOptions:     "nosniff",
		ReferrerPolicy:          "strict-origin-when-cross-origin",
		XXSSProtection:          "1; mode=block",
		StrictTransportSecurity: "max-age=63072000; includeSubDomains; preload", // set only if HTTPS
	},
}

The Crooner don't fake who's in the car. You pick which ID token claim rides shotgun as the session user—default's "email". If your Azure AD app ain't giving you email, use "preferred_username" or "upn":

params := &crooner.AuthConfigParams{
	// ... other config ...
	UserClaim: "preferred_username", // or "upn" for some tenants
}

Crooner tries your claim first, then falls back to email and preferred_username so nobody gets left at the curb.

Default Security Header Values
Header Default Value
Content-Security-Policy default-src 'self'
X-Frame-Options DENY
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
X-XSS-Protection 1; mode=block
Strict-Transport-Security (not set by default)
  • Override a header by setting the field in SecurityHeadersConfig.
  • Set Strict-Transport-Security only when your app is always served over HTTPS—otherwise you're just pretending, James.
Session Configuration: Functional Options

Crooner uses idiomatic Go functional options for session config. Compose 'em how you want—you gotta be right next to me for it to look real.

Available Options
Option Description
WithPersistentCookieName(secret, appName string) Sets a non-guessable, persistent cookie name using your secret and app name (recommended for production).
WithCookieName(name string) Sets a custom cookie name.
WithCookieDomain(domain string) Sets the cookie domain.
WithCookiePath(path string) Sets the cookie path.
WithCookieSecure(secure bool) Sets the Secure flag.
WithCookieHTTPOnly(httpOnly bool) Sets the HttpOnly flag.
WithCookieSameSite(sameSite http.SameSite) Sets the SameSite mode.
WithLifetime(lifetime time.Duration) Sets the session lifetime.
WithStore(store scs.Store) Sets a custom session store backend (e.g., Redis).
Example Usage
sessionMgr, scsMgr, err := crooner.NewSCSManager(
	crooner.WithPersistentCookieName(appConfig.SessionSecret, appConfig.AppName),
	crooner.WithLifetime(12*time.Hour),
	crooner.WithCookieDomain("example.com"),
	// Add other options as needed
)
if err != nil {
	log.Fatalf("failed to initialize session manager: %v", err)
}
  • Combine as many options as you need. No fake limits.
  • If you use both WithPersistentCookieName and WithCookieName, the last one wins—don't let 'em confuse the decals.
  • Not set? Secure defaults. The Crooner don't leave the car open.

Advanced Usage (You Gotta Be Right Next to Me)

Need to run the whole show your way? Use crooner.DefaultSecureSessionConfig() and pass it to crooner.NewSCSManagerWithConfig(cfg). Advanced only—don't reach for this unless you really gotta.

Sometimes you don't need the full show—just "who's in the car." Use crooner.RequireAuth(sessionMgr, routes) as middleware: e.Use(crooner.RequireAuth(sessionMgr, routes)) or slap it on a group. Only the real ones get through. No fake passengers, baby.

cfg := crooner.DefaultSecureSessionConfig()
// Customize as needed
cfg.CookieName = "crooner-" + myCustomSuffix
cfg.Lifetime = 7 * 24 * time.Hour // 7 days
cfg.CookieDomain = ".example.com"
cfg.CookieSameSite = http.SameSiteStrictMode
cfg.CookieSecure = true // (default is true)
// Advanced: use Redis or another backend
// cfg.Store = myRedisStore
sessionMgr, scsMgr, err := crooner.NewSCSManagerWithConfig(cfg)
if err != nil {
	log.Fatalf("failed to initialize session manager: %v", err)
}
Custom SessionManager

Implement the SessionManager interface and bring your own ride—DB, Redis, whatever keeps your decals safe.

Example: Redis Implementation

Any backend works as long as it implements SessionManager. Here's how you might do it with Redis so the session stays real:

package myapp

import (
	"context"
	"encoding/json"
	"github.com/catgoose/crooner"
	"github.com/go-redis/redis/v8"
	"github.com/labstack/echo/v4"
	"time"
)

type RedisSessionManager struct {
	Client *redis.Client
	Prefix string // optional, for namespacing session keys
	TTL    time.Duration
}

func (r *RedisSessionManager) sessionKey(c echo.Context, key string) string {
	// You can use a cookie, header, or other identifier for session scoping
	sessionID := c.Request().Header.Get("X-Session-ID") // Example only
	return r.Prefix + sessionID + ":" + key
}

func (r *RedisSessionManager) Get(c echo.Context, key string) (any, error) {
	ctx := c.Request().Context()
	val, err := r.Client.Get(ctx, r.sessionKey(c, key)).Result()
	if err == redis.Nil {
		return nil, nil
	} else if err != nil {
		return nil, err
	}
	var result any
	if err := json.Unmarshal([]byte(val), &result); err != nil {
		return nil, err
	}
	return result, nil
}

func (r *RedisSessionManager) Set(c echo.Context, key string, value any) error {
	ctx := c.Request().Context()
	data, err := json.Marshal(value)
	if err != nil {
		return err
	}
	return r.Client.Set(ctx, r.sessionKey(c, key), data, r.TTL).Err()
}

func (r *RedisSessionManager) Delete(c echo.Context, key string) error {
	ctx := c.Request().Context()
	return r.Client.Del(ctx, r.sessionKey(c, key)).Err()
}

func (r *RedisSessionManager) Clear(c echo.Context) error {
	// Implement logic to clear all session keys for the user/session
	return nil // Example: not implemented
}

func (r *RedisSessionManager) Invalidate(c echo.Context) error {
	// Implement logic to invalidate the session (e.g., delete all keys)
	return nil // Example: not implemented
}

func (r *RedisSessionManager) ClearInvalidate(c echo.Context) error {
	if err := r.Clear(c); err != nil {
		return err
	}
	return r.Invalidate(c)
}

To use your custom Redis session manager with Crooner:

import (
	crooner "github.com/catgoose/crooner"
	"github.com/go-redis/redis/v8"
	"github.com/labstack/echo/v4"
	"time"
)

func main() {
	e := echo.New()
	redisClient := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
		// ...other options...
	})
	sessionMgr := &myapp.RedisSessionManager{
		Client: redisClient,
		Prefix: "crooner:",
		TTL:    24 * time.Hour,
	}
	croonerConfig := &crooner.AuthConfigParams{
		// ...other config...
		SessionMgr: sessionMgr,
	}
	// ...rest of your setup...
}

That's it. Redis—or any backend that satisfies SessionManager—and you're driving with the Crooner. No fake storage, baby.

Security Best Practices (Don't Let Them Make It Look Fake)

They're gonna try to make it look fake. Don't let 'em:

  • Use a strong, random session secret (32+ bytes)—you gotta give.
  • Use a unique, non-guessable cookie name per app (crooner-<hash>, not something they can guess)
  • Rotate the session secret when you need everybody out—forces logout, keeps decals safe
  • HTTPS, HttpOnly, SameSite, Secure cookies. The real deal.
  • Configure CSP for your frontend’s needs
  • Don't give the crowd your dirty laundry—keep ErrorConfig.ShowDetails false in production so internal error details (OAuth, ID token, etc.) never hit the client. They'll try to make it look fake.
  • Behind a reverse proxy (TLS termination)? You gotta trust proxy headers (X-Forwarded-Proto, X-Forwarded-Host) so your app gets the right Scheme and Host for redirects and HSTS. Otherwise it looks fake, baby.

Session Lifetime Recommendations (How Long's the Set?)

How long's the set before they gotta show their face again? Pick something that doesn't make it look fake:

  • 8–12 hours: Sensitive stuff—admin, finance, healthcare. Short set, real security.
  • 12–24 hours: Good default for most business apps. Right next to me, baby.
  • 48 hours (2 days): When convenience matters and you're okay with a longer ride
  • 7+ days: "Remember me" only—use with caution or they'll think it's fake

Shorter set = more secure. Longer set = more convenient. You gotta give somewhere.

Setting Session Lifetime

cfg := crooner.DefaultSecureSessionConfig()
suffix := crooner.PersistentCookieSuffix(appConfig.SessionSecret, appConfig.AppName)
cfg.CookieName = "crooner-" + suffix
cfg.Lifetime = 24 * time.Hour // 1 day is a good default
// For more convenience:
// cfg.Lifetime = 48 * time.Hour // 2 days
  • Destroy the session on logout. No ghost passengers.
  • Regenerate the session on login or privilege change—fresh decals.
  • The logout route is POST only so some fake link can't boot you. Use a form with method="post" and action="/logout" (or a button that submits it) for your logout button.

Sometimes the cookie name is generated for you (e.g. with WithPersistentCookieName). You gotta know the real name for middleware and the rest of your app. Use GetCookieName() on the session manager:

sessionMgr, _, err := crooner.NewSCSManager(
	crooner.WithPersistentCookieName(appConfig.SessionSecret, appConfig.AppName),
	crooner.WithLifetime(24*time.Hour),
)
if err != nil {
	// handle error
}
cookieName := sessionMgr.GetCookieName()

// Use cookieName in your middleware setup
// e.g., e.Use(middleware.AzureClaims(cookieName))

Then your middleware and everything else use the real cookie name—no guessing, no fake decals.

Type-Specific Session Helper Functions

Crooner's got type-specific helpers so you pull session values the right way—no fake types. They work with any SessionManager and return an error if the value's missing or wrong. You gotta be right next to me for it to look real.

Available Helpers
  • GetString(sm SessionManager, c echo.Context, key string) (string, error)
  • GetInt(sm SessionManager, c echo.Context, key string) (int, error)
  • GetBool(sm SessionManager, c echo.Context, key string) (bool, error)
Usage Example
import (
	crooner "github.com/catgoose/crooner"
	"github.com/labstack/echo/v4"
)

func myHandler(c echo.Context) error {
	// Assume sessionMgr is your SessionManager implementation
	username, err := crooner.GetString(sessionMgr, c, "username")
	if err != nil {
		return c.String(401, "Unauthorized")
	}
	return c.String(200, "Hello, "+username)
}

Robust error handling, any backend that implements SessionManager—the real deal, baby.

Crooner does not store the OAuth2 access token or refresh token in the session by default. Only the user identifier (and any claims you map via SessionValueClaims) are persisted. If your app needs to call APIs on behalf of the user, you must persist tokens yourself (e.g. in session or a store) in a custom callback or post-login step.

Error Types

When something goes wrong, the Crooner don't leave you guessing. We use typed errors so you can check with errors.As or errors.Is:

  • ConfigError — something's wrong with the setup (e.g. from NewAuthConfig). Check with crooner.IsConfigError(err) or errors.As(err, &cfgErr) where var cfgErr *crooner.ConfigError.
  • AuthError — token exchange or ID token didn't check out. Check with crooner.IsAuthError(err) or crooner.AsAuthError(err).
  • ChallengeError — PKCE or state got messed up. Check with crooner.IsChallengeError(err) or crooner.AsChallengeError(err).
  • SessionError — session get/set or wrong type (e.g. from GetString, GetInt, GetBool). Check with crooner.IsSessionError(err) or crooner.AsSessionError(err).
  • State decode errors — invalid OAuth state (bad base64 or malformed payload). Use errors.Is(err, crooner.ErrInvalidStateFormat) or errors.Is(err, crooner.ErrInvalidStateData).

That's for when you're driving the Crooner yourself—your code, your handlers. When the Crooner hits a pothole on login, callback, or logout, he don't hand you some fake error—he gives you the real deal. The built-in auth routes respond with RFC 7807 / RFC 9457 problem details: JSON with type, title, status, and optional detail, plus extensions where it matters (e.g. session key, reason). Content-Type is application/problem+json. Every type URI in the response links to real documentation so you know what went wrong. See docs/errors.md for the full list and when each type is returned. Don't let them make it look fake.

type URI Meaning
docs/errors.md#config Configuration error
docs/errors.md#auth Auth / token / ID token error
docs/errors.md#challenge PKCE or state generation error
docs/errors.md#session Session get/set or type error (includes key, reason)
docs/errors.md#invalid_state Invalid OAuth state payload
docs/errors.md#invalid_request Invalid callback request (e.g. missing code, nonce mismatch)
about:blank Other or unknown error

Don't let them make it look fake. Handle your errors.

Development and the Makefile (You Gotta Be Right Next to Me)

Target What it does
help Default. Lists the real targets—no fake menus.
build Builds bin/oauth-server, bin/app, bin/simulate.
test Runs go test ./....
generate-error-examples Runs scripts/gen-error-examples.sh: starts app and oauth-server, curls __error_examples__/*, writes docs/error-examples/*.json and rewrites the "Generated example responses" block in docs/errors.md.
verify-docs git diff --exit-code docs/ — fails if docs are dirty so CI keeps decals real.
ci build, test, generate-error-examples, verify-docs.
install-playwright Install Playwright browsers for simulate. You gotta be right next to the browser.
pkce-sim Depends on build; runs the PKCE simulation script.
About the example errors in docs

docs/errors.md lists every problem-detail type (config, auth, challenge, session, invalid_state, invalid_request, about:blank), when each is returned, and the Generated example responses section. docs/error-examples/*.json are live JSON samples for each type. They're generated—not hand-written. Run make generate-error-examples and the script hits the app's __error_examples__ routes, writes those JSON files, then rewrites the markdown code blocks in docs/errors.md from them. The Crooner don't leave you with fake examples; the docs stay real. CI runs generate-error-examples and verify-docs, so if you change code that affects error responses and forget to regenerate, the build fails. Don't let them make it look fake.

Testing (You Gotta Be Right Next to Me)

Using Crooner in your app does not pull in Playwright—no fake passengers, baby. The main module has zero browser deps.

The PKCE simulation lives in the simulate/ submodule. That module has Playwright as a dependency and the install CLI as a tool, so the version is pinned and you're not chasing @latest like some guy in a hot dog suit. To run the simulation from the repo root:

cd simulate && go run github.com/playwright-community/playwright-go/cmd/playwright install --with-deps
cd .. && ./scripts/run-pkce-sim.sh

The script builds the app and mock OAuth server from the root module and the simulate binary from simulate/; starts the servers; and drives the browser through the happy path and security checks. If you skip the Playwright install, the simulation will fail—you gotta be right next to the browser for it to look real.

Driving Crooner Authentication Flow (Don't Let Them Make It Look Fake)

You ever try to log in behind a reverse proxy and it just dumps you on the wrong page? Not with the Driving Crooner, baby. This authentication flow is so real, it’ll keep your decals safe and your redirects looking legit—even if some guy in a hot dog suit is trying to make it look fake.

How the Crooner Keeps You on the Road
  1. You try to visit a protected page
    • The Crooner checks your credentials. If you’re not logged in, he throws you in the sidecar and redirects you to /login?redirect=<your real destination, decals and all>. That means the full path, query string, the works. No fake detours.
  2. Login Handler: The Hat and the Cigar
    • Crooner encodes a secret state and your original destination into a base64-encoded package, stashes it in your session, and sends you off to the OAuth provider. Nobody’s stealing your spot in line.
  3. Callback: You Gotta Be Right Next to Me
    • After you sign in, the OAuth provider sends you back to /callback with your state. Crooner decodes it, checks your credentials, and puts you right back where you started—no matter how many fake login pages you drove through.
  4. If the Session’s Gone (You Hit a Pothole)
    • Maybe you live reloaded, maybe the server restarted, maybe you just got bumped out by a fish. If the session state is missing or doesn’t match, Crooner doesn’t freak out. He just restarts the login flow, keeping your original destination safe. No “Invalid state” errors, no fake-outs.
Example: The Real Crooner Flow
  1. You hit /dashboard?id=42 (not logged in)
  2. Crooner sends you to /login?redirect=/dashboard?id=42 (decals intact)
  3. You get sent to the OAuth provider with a state that’s got your back
  4. After login, you’re back at /callback?...&state=...
  5. Crooner decodes the state and puts you right back at /dashboard?id=42—no detours, no fake logins

If you hit a pothole (like a live reload), Crooner just restarts the login flow. You never see an error, you never lose your place. That’s the real deal.

Note for Development (Don’t Let the Session Look Fake)

If you’re using an in-memory session store and you restart the server, your session’s gone. But Crooner’s got you: he’ll just restart the login flow and keep you moving. For production, use a persistent session store (Redis, SQLite, whatever keeps your decals safe).

Questions? PRs? Hecklers?

Open an issue, send a PR, or just shout “Crooner!” into the night. We'll hear you. But you gotta be right next to me for it to look real.

When I was a kid, I fell into a river and a fish bumped me out. I was supposed to die. But a fish bumped me out with its nose. That was the earth telling me I'm supposed to do something great. And I know that's the Driving Crooner. It has to be. You know what I mean, James?

License

MIT, baby! Use it, fork it, remix it—just don't try to make it look fake.

Documentation

Overview

Package crooner provides secure session management and authentication helpers for Go web applications. It offers a flexible, config-first approach to session configuration, secure cookie handling, and integration with authentication providers such as Azure AD. Main features include:

  • Secure, customizable session cookie management
  • Helpers for non-predictable cookie names
  • Pluggable session backends (SCS, Redis, etc.)
  • Easy integration with Echo and other web frameworks
  • Security-focused defaults and best practices

Index

Constants

View Source
const (
	ReasonNotFound    = "not found"
	ReasonInvalidType = "invalid type"
)

Standard reasons for SessionError.

View Source
const (
	SessionKeyUser         = "user"
	SessionKeyOAuthState   = "oauth_state"
	SessionKeyCodeVerifier = "code_verifier"
	SessionKeyOAuthNonce   = "oauth_nonce"
)

Common session key constants for consistency

Variables

View Source
var (
	ErrAuthorizationCodeNotProvided = errors.New("authorization code not provided")
	ErrNonceMismatch                = errors.New("nonce mismatch")
)
View Source
var (
	ErrInvalidStateFormat = errors.New("invalid state format")
	ErrInvalidStateData   = errors.New("invalid state data")
)

Sentinel errors for state encoding/decoding.

Functions

func DecodeStatePayload added in v1.3.7

func DecodeStatePayload(state string) (originalPath string, err error)

DecodeStatePayload decodes the OAuth state to extract the original path for post-auth redirect. State format is base64(csrfState|originalPath).

func EncodeStatePayload added in v1.3.7

func EncodeStatePayload(csrfState, originalPath string) string

EncodeStatePayload produces the OAuth state value. Format is base64(csrfState|originalPath).

func GenerateCodeChallenge

func GenerateCodeChallenge(verifier string) string

GenerateCodeChallenge generates a SHA256 code challenge from the verifier

func GenerateCodeVerifier

func GenerateCodeVerifier() (string, error)

GenerateCodeVerifier generates a random PKCE code verifier

func GenerateState

func GenerateState() (string, error)

GenerateState generates a cryptographically secure random state parameter for OAuth2

func GetBool added in v1.1.0

func GetBool(sm SessionManager, c echo.Context, key string) (bool, error)

GetBool retrieves a bool value from the session by key. Returns a *SessionError if the key is missing or the value is not a bool.

func GetInt added in v1.1.0

func GetInt(sm SessionManager, c echo.Context, key string) (int, error)

GetInt retrieves an int value from the session by key. Returns a *SessionError if the key is missing or the value is not an int.

func GetString added in v1.1.0

func GetString(sm SessionManager, c echo.Context, key string) (string, error)

GetString retrieves a string value from the session by key. Returns a *SessionError if the key is missing or the value is not a string.

func IsAuthError added in v1.3.2

func IsAuthError(err error) bool

IsAuthError checks if an error is an AuthError

func IsAuthExemptPath added in v1.3.7

func IsAuthExemptPath(path string, routes *AuthRoutes) bool

IsAuthExemptPath reports whether path is an auth route or listed in AuthExempt (prefix match).

func IsChallengeError added in v1.3.2

func IsChallengeError(err error) bool

IsChallengeError checks if an error is a ChallengeError

func IsConfigError added in v1.3.2

func IsConfigError(err error) bool

IsConfigError checks if an error is a ConfigError

func IsSessionError added in v1.3.2

func IsSessionError(err error) bool

IsSessionError checks if an error is a SessionError

func NewAuthConfig

func NewAuthConfig(ctx context.Context, e *echo.Echo, params *AuthConfigParams) error

func PersistentCookieSuffix

func PersistentCookieSuffix(secret, appName string) string

PersistentCookieSuffix returns a non-guessable, persistent hash for use as a cookie name suffix. Use a strong session secret and (optionally) an app name for uniqueness.

func RequireAuth added in v1.3.7

func RequireAuth(sm SessionManager, routes *AuthRoutes) echo.MiddlewareFunc

RequireAuth returns Echo middleware that requires a session user. Exempt paths (login, callback, logout, AuthExempt) skip the check. Unauthenticated requests are redirected to the login route with a redirect parameter.

func SaveSessionValueClaims added in v1.3.7

func SaveSessionValueClaims(sm SessionManager, c echo.Context, claims map[string]any, valueClaims []map[string]string) error

SaveSessionValueClaims stores configured claims from the ID token into the session. valueClaims is a slice of maps: each map has one entry (session key -> claim name). Slice/array claim values are normalized to []string.

func SecurityHeadersMiddleware added in v1.3.7

func SecurityHeadersMiddleware(cfg *SecurityHeadersConfig) echo.MiddlewareFunc

SecurityHeadersMiddleware returns Echo middleware that applies SecurityHeadersConfig to responses. If cfg is nil, defaults are used for all headers.

func SessionErrorResponse added in v1.3.2

func SessionErrorResponse(err error) map[string]any

SessionErrorResponse creates a JSON-friendly response from a SessionError for app-level use (e.g. your own routes). Auth routes use RFC 7807/9457 ProblemDetails instead.

func ValidatePostLoginRedirect added in v1.3.12

func ValidatePostLoginRedirect(originalPath string, baseURL string, config *URLValidationConfig) (safePath string, err error)

ValidatePostLoginRedirect validates a relative, same-origin path used as the post-login redirect target (e.g. the ?redirect= query parameter or the path encoded in the OAuth state). It requires a leading "/", rejects protocol-relative ("//") and absolute URLs, and normalizes the path via path.Clean to prevent directory traversal. The returned safePath should be used for the actual redirect.

baseURL and config are reserved for future use (e.g. allowlisting absolute URLs).

For validating absolute configuration URLs (e.g. RedirectURL, LogoutURLRedirect), use ValidateRedirectURL instead.

func ValidateRedirectURL added in v1.3.7

func ValidateRedirectURL(rawURL string, uv *URLValidationConfig) error

ValidateRedirectURL validates an absolute redirect URL used in configuration (e.g. RedirectURL, LogoutURLRedirect, LoginURLRedirect). It checks format, scheme (http/https only), and host, then applies optional URLValidationConfig constraints (RequireHTTPS, AllowedSchemes, AllowedDomains).

For validating relative, same-origin paths after login (e.g. the ?redirect= query parameter), use ValidatePostLoginRedirect instead.

Types

type AuthConfig

type AuthConfig struct {
	OAuth2Config       *oauth2.Config         // OAuth2 configuration
	Provider           *oidc.Provider         // OIDC Provider for Azure AD
	Verifier           *oidc.IDTokenVerifier  // Verifier to verify ID tokens
	AuthRoutes         *AuthRoutes            // Routes for authentication
	TenantID           string                 // Azure AD Tenant ID
	LogoutURLRedirect  string                 // URL to redirect after logout
	LoginURLRedirect   string                 // URL to redirect after login (fallback when state decode fails)
	EndSessionEndpoint string                 // OIDC end_session_endpoint (when IssuerURL is set and discovery provides it)
	CookieName         string                 // Reserved for custom SessionManager; built-in flow uses SessionManager.GetCookieName()
	SessionSecurity    *SessionSecurityConfig // Reserved for custom SessionManager; built-in flow uses SessionConfig
	URLValidation      *URLValidationConfig   // URL validation configuration
	ErrorConfig        *ErrorConfig           // Error handling configuration
	SecurityHeaders    *SecurityHeadersConfig // Security headers configuration
	UserClaim          string                 // Claim name for session user (default "email"); use "preferred_username" or "upn" if email absent
}

AuthConfig is the runtime config built by NewAuthConfig; it holds OAuth2/OIDC and security settings.

func (*AuthConfig) ExchangeToken

func (c *AuthConfig) ExchangeToken(ctx context.Context, code, codeVerifier string) (*oauth2.Token, error)

ExchangeToken exchanges the authorization code for an access token

func (*AuthConfig) GetLoginURL

func (c *AuthConfig) GetLoginURL(state, codeChallenge, nonce string) string

GetLoginURL constructs and returns the Azure AD login URL

func (*AuthConfig) VerifyIDToken

func (c *AuthConfig) VerifyIDToken(ctx context.Context, idToken string) (map[string]any, error)

VerifyIDToken verifies the provided ID token using the OIDC provider

type AuthConfigParams

type AuthConfigParams struct {
	SessionMgr         SessionManager
	AuthRoutes         *AuthRoutes
	SecurityHeaders    *SecurityHeadersConfig
	ErrorConfig        *ErrorConfig
	URLValidation      *URLValidationConfig
	SessionSecurity    *SessionSecurityConfig
	LogoutURLRedirect  string
	CookieName         string
	LoginURLRedirect   string
	ClientID           string
	RedirectURL        string
	TenantID           string
	ClientSecret       string
	IssuerURL          string
	UserClaim          string
	AdditionalScopes   []string
	SessionValueClaims []map[string]string
}

AuthConfigParams is the input for NewAuthConfig; do not reuse as runtime config.

type AuthError

type AuthError struct {
	Err    error
	Op     string
	Reason string
}

AuthError represents an error related to authentication or OIDC operations.

func AsAuthError added in v1.3.2

func AsAuthError(err error) (*AuthError, bool)

AsAuthError attempts to convert an error to AuthError

func (*AuthError) Error

func (e *AuthError) Error() string

func (*AuthError) Unwrap

func (e *AuthError) Unwrap() error

type AuthHandlerConfig

type AuthHandlerConfig struct {
	SessionMgr SessionManager
	*AuthConfig
	SessionValueClaims []map[string]string
}

AuthHandlerConfig is internal: it ties AuthConfig to a SessionManager and optional claim mapping; created by NewAuthConfig.

func (*AuthHandlerConfig) SetupAuth

func (a *AuthHandlerConfig) SetupAuth(e *echo.Echo)

SetupAuth initializes the authentication middleware and routes

type AuthRoutes

type AuthRoutes struct {
	Login      string   // Login route
	Logout     string   // Logout route
	Callback   string   // Callback route for receiving authorization code
	AuthExempt []string // Routes to be exempt from auth
}

AuthRoutes contains the routes for authentication

type ChallengeError

type ChallengeError struct {
	Err error
	Op  string
}

ChallengeError represents an error during PKCE challenge or state generation.

func AsChallengeError added in v1.3.2

func AsChallengeError(err error) (*ChallengeError, bool)

AsChallengeError attempts to convert an error to ChallengeError

func (*ChallengeError) Error

func (e *ChallengeError) Error() string

func (*ChallengeError) Unwrap

func (e *ChallengeError) Unwrap() error

type ConfigError

type ConfigError struct {
	Err    error
	Field  string
	Reason string
}

ConfigError represents an error related to configuration loading or validation.

func AsConfigError added in v1.3.2

func AsConfigError(err error) (*ConfigError, bool)

AsConfigError attempts to convert an error to ConfigError

func (*ConfigError) Error

func (e *ConfigError) Error() string

func (*ConfigError) Unwrap

func (e *ConfigError) Unwrap() error

type ErrorConfig

type ErrorConfig struct {
	LogLevel    string
	ShowDetails bool
}

ErrorConfig contains error handling configuration

type ProblemDetails added in v1.3.17

type ProblemDetails struct {
	Type     string `json:"type,omitempty"`
	Title    string `json:"title"`
	Detail   string `json:"detail,omitempty"`
	Instance string `json:"instance,omitempty"`
	Key      string `json:"key,omitempty"`
	Reason   string `json:"reason,omitempty"`
	Op       string `json:"op,omitempty"`
	Field    string `json:"field,omitempty"`
	Status   int    `json:"status"`
}

ProblemDetails represents RFC 7807 / RFC 9457 problem details for HTTP API errors. Auth handlers return this with Content-Type application/problem+json.

type SCSManager

type SCSManager struct {
	Session *scs.SessionManager
	// contains filtered or unexported fields
}

SCSManager implements SessionManager using SCS (github.com/alexedwards/scs/v2)

func NewSCSManager

func NewSCSManager(opts ...SessionOption) (*SCSManager, *scs.SessionManager, error)

NewSCSManager creates a new SCSManager using functional options. Returns an error if required configuration is missing.

func NewSCSManagerWithConfig

func NewSCSManagerWithConfig(cfg SessionConfig) (*SCSManager, *scs.SessionManager, error)

NewSCSManagerWithConfig returns a configured SCSManager and the underlying *scs.SessionManager. Returns an error if CookieName is not set.

func (*SCSManager) Clear

func (s *SCSManager) Clear(c echo.Context) error

func (*SCSManager) ClearInvalidate added in v1.0.13

func (s *SCSManager) ClearInvalidate(c echo.Context) error

ClearInvalidate removes all values and invalidates the session (expires cookie).

func (*SCSManager) Delete

func (s *SCSManager) Delete(c echo.Context, key string) error

func (*SCSManager) Get

func (s *SCSManager) Get(c echo.Context, key string) (any, error)

func (*SCSManager) GetCookieName added in v1.0.14

func (s *SCSManager) GetCookieName() string

GetCookieName returns the session cookie name used by this manager.

func (*SCSManager) GetSCSManager added in v1.3.2

func (s *SCSManager) GetSCSManager() *scs.SessionManager

GetSCSManager returns the underlying SCS session manager for advanced usage. This allows users to access SCS-specific features when needed.

func (*SCSManager) Invalidate

func (s *SCSManager) Invalidate(c echo.Context) error

func (*SCSManager) RenewToken added in v1.3.12

func (s *SCSManager) RenewToken(c echo.Context) error

RenewToken regenerates the session token to prevent session fixation. Call after privilege-level change (e.g. login).

func (*SCSManager) Set

func (s *SCSManager) Set(c echo.Context, key string, value any) error

type SecurityHeadersConfig

type SecurityHeadersConfig struct {
	ContentSecurityPolicy   string // Content-Security-Policy header
	XFrameOptions           string // X-Frame-Options header
	XContentTypeOptions     string // X-Content-Type-Options header
	ReferrerPolicy          string // Referrer-Policy header
	XXSSProtection          string // X-XSS-Protection header
	StrictTransportSecurity string // Strict-Transport-Security header (set only if HTTPS)
}

SecurityHeadersConfig contains configuration for security headers.

All fields are optional. If a field is empty, a secure default will be used.

ContentSecurityPolicy:    default-src 'self'
XFrameOptions:            DENY
XContentTypeOptions:      nosniff
ReferrerPolicy:           strict-origin-when-cross-origin
XXSSProtection:           1; mode=block
StrictTransportSecurity:  (not set by default)

type SessionConfig

type SessionConfig struct {
	Store          scs.Store
	CookieName     string
	CookieDomain   string
	CookiePath     string
	CookieSameSite http.SameSite
	Lifetime       time.Duration
	CookieSecure   bool
	CookieHTTPOnly bool
}

SessionConfig holds configuration for the SCS session manager factory.

func DefaultSecureSessionConfig

func DefaultSecureSessionConfig() SessionConfig

DefaultSecureSessionConfig returns a config with secure defaults.

type SessionError

type SessionError struct {
	Err    error  // Optional wrapped error
	Key    string // The session key involved
	Reason string // A human-readable reason for the error
}

SessionError represents an error related to session operations.

func AsSessionError added in v1.3.2

func AsSessionError(err error) (*SessionError, bool)

AsSessionError attempts to convert an error to SessionError

func (*SessionError) Error

func (e *SessionError) Error() string

func (*SessionError) Unwrap added in v1.3.2

func (e *SessionError) Unwrap() error

Unwrap returns the wrapped error, if any.

type SessionManager

type SessionManager interface {
	// Get retrieves a value from the session by key
	Get(c echo.Context, key string) (any, error)
	// Set sets a value in the session
	Set(c echo.Context, key string, value any) error
	// Delete removes a value from the session
	Delete(c echo.Context, key string) error
	// Clear removes all values from the session
	Clear(c echo.Context) error
	// Invalidate invalidates the session (expires cookie)
	Invalidate(c echo.Context) error
	// ClearInvalidate removes all values and invalidates the session (expires cookie)
	ClearInvalidate(c echo.Context) error
}

SessionManager abstracts session operations for pluggable backends (SCS, etc.)

type SessionOption

type SessionOption func(*SessionConfig)

SessionOption defines a functional option for SessionConfig.

Use these with NewSCSManager to customize session behavior.

func WithCookieDomain

func WithCookieDomain(domain string) SessionOption

WithCookieDomain sets the session cookie domain.

func WithCookieHTTPOnly

func WithCookieHTTPOnly(httpOnly bool) SessionOption

WithCookieHTTPOnly sets the session cookie HttpOnly flag.

func WithCookieName

func WithCookieName(name string) SessionOption

WithCookieName sets the session cookie name.

func WithCookiePath

func WithCookiePath(path string) SessionOption

WithCookiePath sets the session cookie path.

func WithCookieSameSite

func WithCookieSameSite(sameSite http.SameSite) SessionOption

WithCookieSameSite sets the session cookie SameSite mode.

func WithCookieSecure

func WithCookieSecure(secure bool) SessionOption

WithCookieSecure sets the session cookie Secure flag.

func WithLifetime

func WithLifetime(lifetime time.Duration) SessionOption

WithLifetime sets the session lifetime.

func WithPersistentCookieName

func WithPersistentCookieName(secret, appName string) SessionOption

WithPersistentCookieName sets the cookie name using a persistent, non-guessable hash derived from the provided secret and app name.

func WithStore

func WithStore(store scs.Store) SessionOption

WithStore sets the session store backend.

type SessionSecurityConfig

type SessionSecurityConfig struct {
	Domain   string
	Path     string
	SameSite http.SameSite
	MaxAge   int
	HTTPOnly bool
	Secure   bool
}

SessionSecurityConfig contains session security configuration

type SessionTokenRenewer added in v1.3.12

type SessionTokenRenewer interface {
	RenewToken(c echo.Context) error
}

SessionTokenRenewer is an optional interface for session backends that can regenerate the session token (e.g. to prevent session fixation). If SessionManager implements this, it will be called after successful login before storing user data.

type URLValidationConfig

type URLValidationConfig struct {
	AllowedSchemes []string
	AllowedDomains []string
	RequireHTTPS   bool
}

URLValidationConfig contains URL validation configuration

Directories

Path Synopsis
cmd
app command
oauth-server command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL