{"id":15291831,"url":"https://github.com/catgoose/crooner","last_synced_at":"2026-04-01T22:34:43.969Z","repository":{"id":256275674,"uuid":"854801793","full_name":"catgoose/crooner","owner":"catgoose","description":"Crooner is a golang module for authenticating with an Azure app registration","archived":false,"fork":false,"pushed_at":"2026-01-31T16:22:45.000Z","size":104,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-01T04:52:36.754Z","etag":null,"topics":["azure","echo-framework","go","golang"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/catgoose.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-09-09T19:56:28.000Z","updated_at":"2026-01-31T16:20:07.000Z","dependencies_parsed_at":"2025-07-16T00:22:19.808Z","dependency_job_id":"083dc5a5-5b7b-4636-87fe-9f93af2e1ea9","html_url":"https://github.com/catgoose/crooner","commit_stats":null,"previous_names":["catgoose/crooner"],"tags_count":30,"template":false,"template_full_name":null,"purl":"pkg:github/catgoose/crooner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catgoose%2Fcrooner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catgoose%2Fcrooner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catgoose%2Fcrooner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catgoose%2Fcrooner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/catgoose","download_url":"https://codeload.github.com/catgoose/crooner/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/catgoose%2Fcrooner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29151681,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-06T02:39:25.012Z","status":"ssl_error","status_checked_at":"2026-02-06T02:37:22.784Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["azure","echo-framework","go","golang"],"created_at":"2024-09-30T16:14:43.531Z","updated_at":"2026-04-01T22:34:43.946Z","avatar_url":"https://github.com/catgoose.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Crooner\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/catgoose/crooner.svg)](https://pkg.go.dev/github.com/catgoose/crooner)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n![image](https://github.com/catgoose/screenshots/blob/fb17ed7cd8e989691447b0e7a755d93a677abbfd/crooner/crooner.png)\n\n\u003c!--toc:start--\u003e\n\n- [Crooner](#crooner)\n  - [What Is This?](#what-is-this)\n  - [Features](#features)\n  - [Installation](#installation)\n  - [Quick Start Example](#quick-start-example)\n  - [Configuration](#configuration)\n    - [Session Management (Best Practice)](#session-management-best-practice)\n    - [Content Security Policy (CSP) and Security Headers](#content-security-policy-csp-and-security-headers)\n      - [Default Security Header Values](#default-security-header-values)\n    - [Session Configuration: Functional Options](#session-configuration-functional-options)\n      - [Available Options](#available-options)\n      - [Example Usage](#example-usage)\n  - [Advanced Usage](#advanced-usage)\n    - [Bring Your Own Router](#bring-your-own-router)\n    - [Custom SessionManager](#custom-sessionmanager)\n      - [Example: Redis Implementation](#example-redis-implementation)\n  - [Security Best Practices](#security-best-practices)\n  - [Session Lifetime Recommendations](#session-lifetime-recommendations)\n  - [Setting Session Lifetime](#setting-session-lifetime)\n  - [Retrieving the Session Cookie Name](#retrieving-the-session-cookie-name)\n    - [Type-Specific Session Helper Functions](#type-specific-session-helper-functions)\n      - [Available Helpers](#available-helpers)\n      - [Usage Example](#usage-example)\n    - [Error Types](#error-types)\n  - [Development and the Makefile](#development-and-the-makefile)\n    - [About the example errors in docs](#about-the-example-errors-in-docs)\n  - [Testing](#testing)\n  - [Authentication Flow](#authentication-flow)\n    - [How the Flow Works](#how-the-flow-works)\n      - [Example Flow](#example-flow)\n      - [Note for Development](#note-for-development)\n  - [Questions? PRs?](#questions-prs)\n  - [License](#license)\n  \u003c!--toc:end--\u003e\n\nCrooner is an OIDC/OAuth2 client library for Go web applications using standard `net/http`. It handles PKCE login, callbacks, and session management with pluggable backends and secure defaults. Works with any OIDC-compliant provider -- Azure AD, Google, Okta, Auth0, Keycloak, etc.\n\n## Why\n\n**Without crooner:**\n\n```go\nprovider, _ := oidc.NewProvider(ctx, issuerURL)\noauth2Config := \u0026oauth2.Config{\n    ClientID:     os.Getenv(\"OIDC_CLIENT_ID\"),\n    ClientSecret: os.Getenv(\"OIDC_CLIENT_SECRET\"),\n    RedirectURL:  os.Getenv(\"OIDC_REDIRECT_URL\"),\n    Endpoint:     provider.Endpoint(),\n    Scopes:       []string{oidc.ScopeOpenID, \"profile\", \"email\"},\n}\nverifier := provider.Verifier(\u0026oidc.Config{ClientID: oauth2Config.ClientID})\n\n// Generate PKCE challenge\ncodeVerifier := generateRandomString(64)\ncodeChallenge := sha256URLEncode(codeVerifier)\n\n// Generate state, store in session, build auth URL...\nstate := generateRandomString(32)\nsession.Set(r, \"oauth_state\", state)\nsession.Set(r, \"pkce_verifier\", codeVerifier)\nhttp.Redirect(w, r, oauth2Config.AuthCodeURL(state,\n    oauth2.SetAuthURLParam(\"code_challenge\", codeChallenge),\n    oauth2.SetAuthURLParam(\"code_challenge_method\", \"S256\"),\n), http.StatusFound)\n\n// Then write the callback handler: validate state, exchange code with\n// verifier, verify ID token, extract claims, set session, redirect...\n```\n\n**With crooner:**\n\n```go\nsessionMgr, scsMgr, _ := crooner.NewSCSManager(\n    crooner.WithPersistentCookieName(secret, appName),\n    crooner.WithLifetime(12*time.Hour),\n)\nauthHandler, _ := crooner.NewAuthConfig(ctx, \u0026crooner.AuthConfigParams{\n    IssuerURL:    os.Getenv(\"OIDC_ISSUER_URL\"),\n    ClientID:     os.Getenv(\"OIDC_CLIENT_ID\"),\n    ClientSecret: os.Getenv(\"OIDC_CLIENT_SECRET\"),\n    RedirectURL:  os.Getenv(\"OIDC_REDIRECT_URL\"),\n    SessionMgr:   sessionMgr,\n})\nhandler = authHandler.Middleware()(mux)\nhandler = scsMgr.LoadAndSave(handler)\n// Login, callback, logout, PKCE, state, session -- all handled.\n```\n\n## What Is This?\n\nCrooner provides OIDC/OAuth2 authentication for Go web applications. It uses standard `net/http` patterns (`http.Handler`, `http.HandlerFunc`, middleware as `func(http.Handler) http.Handler`) with no framework dependencies beyond the Go standard library.\n\n## Features\n\n- **PKCE/OIDC login for any provider**\n- **Pluggable session management** (SCS, custom)\n- **Configurable Content Security Policy (CSP)**\n- **Secure, non-guessable session cookies**\n- **Standard net/http -- no framework dependency**\n- **Preserves original URLs (including query strings) through login and callback**\n- **Reverse proxy friendly authentication flow**\n- **Automatic recovery from lost session state** (e.g., after server restart)\n\n## Installation\n\n```bash\ngo get github.com/catgoose/crooner@latest\n```\n\n## Quick Start Example\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\tcrooner \"github.com/catgoose/crooner\"\n)\n\ntype AppConfig struct {\n\tSessionSecret string\n\tAppName       string\n\tCroonerConfig *crooner.AuthConfigParams\n\tSessionMgr    crooner.SessionManager\n}\n\nfunc LoadAppConfig() (*AppConfig, error) {\n\tsecret := os.Getenv(\"SESSION_SECRET\")\n\tif secret == \"\" {\n\t\treturn nil, fmt.Errorf(\"SESSION_SECRET is required\")\n\t}\n\tappName := \"myApp\"\n\n\tcroonerConfig := \u0026crooner.AuthConfigParams{\n\t\tIssuerURL:         os.Getenv(\"OIDC_ISSUER_URL\"),\n\t\tClientID:          os.Getenv(\"OIDC_CLIENT_ID\"),\n\t\tClientSecret:      os.Getenv(\"OIDC_CLIENT_SECRET\"),\n\t\tRedirectURL:       os.Getenv(\"OIDC_REDIRECT_URL\"),\n\t\tLogoutURLRedirect: os.Getenv(\"OIDC_LOGOUT_REDIRECT_URL\"),\n\t\tLoginURLRedirect:  os.Getenv(\"OIDC_LOGIN_REDIRECT_URL\"),\n\t\tAuthRoutes: \u0026crooner.AuthRoutes{\n\t\t\tLogin:    \"/login\",\n\t\t\tLogout:   \"/logout\",\n\t\t\tCallback: \"/callback\",\n\t\t},\n\t\tSecurityHeaders: \u0026crooner.SecurityHeadersConfig{\n\t\t\tContentSecurityPolicy:   \"default-src 'self'\",\n\t\t\tXFrameOptions:           \"DENY\",\n\t\t\tXContentTypeOptions:     \"nosniff\",\n\t\t\tReferrerPolicy:          \"strict-origin-when-cross-origin\",\n\t\t\tXXSSProtection:          \"1; mode=block\",\n\t\t\tStrictTransportSecurity: \"max-age=63072000; includeSubDomains; preload\",\n\t\t},\n\t}\n\n\treturn \u0026AppConfig{\n\t\tSessionSecret: secret,\n\t\tAppName:       appName,\n\t\tCroonerConfig: croonerConfig,\n\t}, nil\n}\n\nfunc main() {\n\tappConfig, err := LoadAppConfig()\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to load app config: %v\", err)\n\t}\n\n\tmux := http.NewServeMux()\n\n\tsessionMgr, scsMgr, err := crooner.NewSCSManager(\n\t\tcrooner.WithPersistentCookieName(appConfig.SessionSecret, appConfig.AppName),\n\t\tcrooner.WithLifetime(12*time.Hour),\n\t\tcrooner.WithCookieDomain(\"example.com\"),\n\t)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to initialize session manager: %v\", err)\n\t}\n\tappConfig.SessionMgr = sessionMgr\n\tappConfig.CroonerConfig.SessionMgr = sessionMgr\n\n\tctx := context.Background()\n\tauthHandler, err := crooner.NewAuthConfig(ctx, appConfig.CroonerConfig)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to initialize Crooner authentication: %v\", err)\n\t}\n\n\t// Register auth routes (login, callback, logout) on the mux\n\tauthHandler.SetupAuth(mux)\n\n\tmux.HandleFunc(\"GET /\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"Hello, Crooner!\"))\n\t})\n\n\t// Build middleware chain: session loading -\u003e auth middleware -\u003e mux\n\tvar handler http.Handler = mux\n\thandler = authHandler.Middleware()(handler)\n\thandler = scsMgr.LoadAndSave(handler)\n\n\tport := os.Getenv(\"PORT\")\n\tif port == \"\" {\n\t\tport = \"8080\"\n\t}\n\tlog.Fatal(http.ListenAndServe(\":\"+port, handler))\n}\n```\n\n## Configuration\n\n### Session Management (Best Practice)\n\n- Use a strong, random `SESSION_SECRET` (set via env/config).\n- Use a unique `AppName` per app.\n- Set a persistent, non-guessable cookie name using `crooner.WithPersistentCookieName(secret, appName)` when creating your session manager:\n\n  ```go\n  sessionMgr, scsMgr, err := crooner.NewSCSManager(\n  \tcrooner.WithPersistentCookieName(secret, appName),\n  \t// ...other options...\n  )\n  ```\n\n- `NewSCSManager` applies secure defaults. Only reach for advanced config when you have special requirements.\n\n### Content Security Policy (CSP) and Security Headers\n\nConfigure your security headers via `SecurityHeadersConfig`. Empty fields use secure defaults.\n\n```go\nparams := \u0026crooner.AuthConfigParams{\n\t// ... other config ...\n\tSecurityHeaders: \u0026crooner.SecurityHeadersConfig{\n\t\tContentSecurityPolicy:   \"default-src 'self'\",\n\t\tXFrameOptions:           \"DENY\",\n\t\tXContentTypeOptions:     \"nosniff\",\n\t\tReferrerPolicy:          \"strict-origin-when-cross-origin\",\n\t\tXXSSProtection:          \"1; mode=block\",\n\t\tStrictTransportSecurity: \"max-age=63072000; includeSubDomains; preload\",\n\t},\n}\n```\n\nThe `UserClaim` field controls which ID token claim is used as the session user -- default is `\"email\"`. If your provider does not provide email, use `\"preferred_username\"` or `\"upn\"`:\n\n```go\nparams := \u0026crooner.AuthConfigParams{\n\t// ... other config ...\n\tUserClaim: \"preferred_username\",\n}\n```\n\nCrooner tries your claim first, then falls back to `email` and `preferred_username`.\n\n#### Default Security Header Values\n\n| Header                    | Default Value                     |\n| ------------------------- | --------------------------------- |\n| Content-Security-Policy   | `default-src 'self'`              |\n| X-Frame-Options           | `DENY`                            |\n| X-Content-Type-Options    | `nosniff`                         |\n| Referrer-Policy           | `strict-origin-when-cross-origin` |\n| X-XSS-Protection          | `1; mode=block`                   |\n| Strict-Transport-Security | _(not set by default)_            |\n\n- Override a header by setting the field in `SecurityHeadersConfig`.\n- Set `Strict-Transport-Security` only when your app is always served over HTTPS.\n\n### Session Configuration: Functional Options\n\nCrooner uses idiomatic Go functional options for session config.\n\n#### Available Options\n\n| Option                                             | Description                                                                                               |\n| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `WithPersistentCookieName(secret, appName string)` | Sets a non-guessable, persistent cookie name using your secret and app name (recommended for production). |\n| `WithCookieName(name string)`                      | Sets a custom cookie name.                                                                                |\n| `WithCookieDomain(domain string)`                  | Sets the cookie domain.                                                                                   |\n| `WithCookiePath(path string)`                      | Sets the cookie path.                                                                                     |\n| `WithCookieSecure(secure bool)`                    | Sets the Secure flag.                                                                                     |\n| `WithCookieHTTPOnly(httpOnly bool)`                | Sets the HttpOnly flag.                                                                                   |\n| `WithCookieSameSite(sameSite http.SameSite)`       | Sets the SameSite mode.                                                                                   |\n| `WithLifetime(lifetime time.Duration)`             | Sets the session lifetime.                                                                                |\n| `WithStore(store scs.Store)`                       | Sets a custom session store backend (e.g., Redis).                                                        |\n\n#### Example Usage\n\n```go\nsessionMgr, scsMgr, err := crooner.NewSCSManager(\n\tcrooner.WithPersistentCookieName(appConfig.SessionSecret, appConfig.AppName),\n\tcrooner.WithLifetime(12*time.Hour),\n\tcrooner.WithCookieDomain(\"example.com\"),\n)\nif err != nil {\n\tlog.Fatalf(\"failed to initialize session manager: %v\", err)\n}\n```\n\n## Advanced Usage\n\nUse `crooner.DefaultSecureSessionConfig()` and pass it to `crooner.NewSCSManagerWithConfig(cfg)` for advanced session configuration.\n\nUse `crooner.RequireAuth(sessionMgr, routes)` as middleware to protect routes:\n\n```go\nhandler := crooner.RequireAuth(sessionMgr, routes)(mux)\n```\n\n```go\ncfg := crooner.DefaultSecureSessionConfig()\ncfg.CookieName = \"crooner-\" + myCustomSuffix\ncfg.Lifetime = 7 * 24 * time.Hour\ncfg.CookieDomain = \".example.com\"\ncfg.CookieSameSite = http.SameSiteStrictMode\ncfg.CookieSecure = true\nsessionMgr, scsMgr, err := crooner.NewSCSManagerWithConfig(cfg)\nif err != nil {\n\tlog.Fatalf(\"failed to initialize session manager: %v\", err)\n}\n```\n\n### Bring Your Own Router\n\nSince `NewAuthConfig` does not require a `*http.ServeMux`, you can use any router\nthat implements `http.Handler`. Call the individual handler methods directly:\n\n```go\nauthHandler, err := crooner.NewAuthConfig(ctx, params)\nif err != nil {\n\tlog.Fatal(err)\n}\n\n// chi, gorilla/mux, or any router\nr := chi.NewRouter()\nr.Get(params.AuthRoutes.Login, authHandler.LoginHandler())\nr.Get(params.AuthRoutes.Callback, authHandler.CallbackHandler())\nr.Post(params.AuthRoutes.Logout, authHandler.LogoutHandler())\n\n// Or for net/http, use the convenience method:\n// mux := http.NewServeMux()\n// authHandler.SetupAuth(mux)\n```\n\n### Custom SessionManager\n\nImplement the `SessionManager` interface for custom backends (DB, Redis, etc.).\n\n#### Example: Redis Implementation\n\n```go\npackage myapp\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\ntype RedisSessionManager struct {\n\tClient *redis.Client\n\tPrefix string\n\tTTL    time.Duration\n}\n\nfunc (r *RedisSessionManager) sessionKey(req *http.Request, key string) string {\n\tsessionID := req.Header.Get(\"X-Session-ID\")\n\treturn r.Prefix + sessionID + \":\" + key\n}\n\nfunc (r *RedisSessionManager) Get(req *http.Request, key string) (any, error) {\n\tctx := req.Context()\n\tval, err := r.Client.Get(ctx, r.sessionKey(req, key)).Result()\n\tif err == redis.Nil {\n\t\treturn nil, nil\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\tvar result any\n\tif err := json.Unmarshal([]byte(val), \u0026result); err != nil {\n\t\treturn nil, err\n\t}\n\treturn result, nil\n}\n\nfunc (r *RedisSessionManager) Set(req *http.Request, key string, value any) error {\n\tctx := req.Context()\n\tdata, err := json.Marshal(value)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn r.Client.Set(ctx, r.sessionKey(req, key), data, r.TTL).Err()\n}\n\nfunc (r *RedisSessionManager) Delete(req *http.Request, key string) error {\n\tctx := req.Context()\n\treturn r.Client.Del(ctx, r.sessionKey(req, key)).Err()\n}\n\nfunc (r *RedisSessionManager) Clear(req *http.Request) error {\n\treturn nil // Implement: clear all session keys for user\n}\n\nfunc (r *RedisSessionManager) Invalidate(req *http.Request) error {\n\treturn nil // Implement: invalidate session\n}\n\nfunc (r *RedisSessionManager) ClearInvalidate(req *http.Request) error {\n\tif err := r.Clear(req); err != nil {\n\t\treturn err\n\t}\n\treturn r.Invalidate(req)\n}\n```\n\nTo use your custom Redis session manager with Crooner:\n\n```go\nimport (\n\tcrooner \"github.com/catgoose/crooner\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"time\"\n)\n\nfunc main() {\n\tmux := http.NewServeMux()\n\tredisClient := redis.NewClient(\u0026redis.Options{\n\t\tAddr: \"localhost:6379\",\n\t})\n\tsessionMgr := \u0026myapp.RedisSessionManager{\n\t\tClient: redisClient,\n\t\tPrefix: \"crooner:\",\n\t\tTTL:    24 * time.Hour,\n\t}\n\tcroonerConfig := \u0026crooner.AuthConfigParams{\n\t\t// ...other config...\n\t\tSessionMgr: sessionMgr,\n\t}\n\t// ...rest of your setup...\n}\n```\n\n## Security Best Practices\n\n- Use a strong, random session secret (32+ bytes)\n- Use a unique, non-guessable cookie name per app (`crooner-\u003chash\u003e`)\n- Rotate the session secret when you need everybody out\n- HTTPS, HttpOnly, SameSite, Secure cookies\n- Configure CSP for your frontend's needs\n- Keep `ErrorConfig.ShowDetails` **false** in production so internal error details never hit the client\n- Behind a reverse proxy (TLS termination)? Trust proxy headers (`X-Forwarded-Proto`, `X-Forwarded-Host`) so your app gets the right scheme and host for redirects and HSTS\n\n## Session Lifetime Recommendations\n\n- **8-12 hours:** Sensitive stuff -- admin, finance, healthcare\n- **12-24 hours:** Good default for most business apps\n- **48 hours (2 days):** When convenience matters\n- **7+ days:** \"Remember me\" only -- use with caution\n\n## Setting Session Lifetime\n\n```go\ncfg := crooner.DefaultSecureSessionConfig()\nsuffix := crooner.PersistentCookieSuffix(appConfig.SessionSecret, appConfig.AppName)\ncfg.CookieName = \"crooner-\" + suffix\ncfg.Lifetime = 24 * time.Hour\n```\n\n- Destroy the session on logout\n- Regenerate the session on login or privilege change\n- The logout route is **POST** only. Use a form with `method=\"post\"` and `action=\"/logout\"` for your logout button.\n\n### CSRF Protection\n\nCrooner does not include CSRF middleware. The OAuth login flow is protected by the `state` parameter (stored in session and validated on callback). For CSRF protection on your own routes (POST, PUT, PATCH, DELETE), use [gorilla/csrf](https://github.com/gorilla/csrf) or a similar library.\n\n## Retrieving the Session Cookie Name\n\nSometimes the cookie name is generated for you (e.g. with `WithPersistentCookieName`). Use `GetCookieName()` on the session manager:\n\n```go\nsessionMgr, _, err := crooner.NewSCSManager(\n\tcrooner.WithPersistentCookieName(appConfig.SessionSecret, appConfig.AppName),\n\tcrooner.WithLifetime(24*time.Hour),\n)\nif err != nil {\n\t// handle error\n}\ncookieName := sessionMgr.GetCookieName()\n```\n\n### Type-Specific Session Helper Functions\n\nCrooner provides type-specific helpers for session values. They work with any `SessionManager` and return an error if the value is missing or wrong type.\n\n#### Available Helpers\n\n- `GetString(sm SessionManager, r *http.Request, key string) (string, error)`\n- `GetInt(sm SessionManager, r *http.Request, key string) (int, error)`\n- `GetBool(sm SessionManager, r *http.Request, key string) (bool, error)`\n\n#### Usage Example\n\n```go\nfunc myHandler(w http.ResponseWriter, r *http.Request) {\n\tusername, err := crooner.GetString(sessionMgr, r, \"username\")\n\tif err != nil {\n\t\thttp.Error(w, \"Unauthorized\", 401)\n\t\treturn\n\t}\n\tw.Write([]byte(\"Hello, \" + username))\n}\n```\n\nCrooner 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.\n\n### Error Types\n\nTyped errors for inspection with `errors.As` or `errors.Is`:\n\n- **ConfigError** -- something wrong with the setup (e.g. from `NewAuthConfig`). Check with `crooner.IsConfigError(err)` or `errors.As(err, \u0026cfgErr)`.\n- **AuthError** -- token exchange or ID token did not check out. Check with `crooner.IsAuthError(err)` or `crooner.AsAuthError(err)`.\n- **ChallengeError** -- PKCE or state generation failed. Check with `crooner.IsChallengeError(err)` or `crooner.AsChallengeError(err)`.\n- **SessionError** -- session get/set or wrong type. Check with `crooner.IsSessionError(err)` or `crooner.AsSessionError(err)`.\n- **State decode errors** -- invalid OAuth state. Use `errors.Is(err, crooner.ErrInvalidStateFormat)` or `errors.Is(err, crooner.ErrInvalidStateData)`.\n\nThe built-in auth routes respond with **RFC 7807 / RFC 9457 problem details**: JSON with `type`, `title`, `status`, and optional `detail`, plus extensions. **Content-Type** is `application/problem+json`. See [docs/errors.md](docs/errors.md) for the full list.\n\n| type URI                                                         | Meaning                                                      |\n| ---------------------------------------------------------------- | ------------------------------------------------------------ |\n| [docs/errors.md#config](docs/errors.md#config)                   | Configuration error                                          |\n| [docs/errors.md#auth](docs/errors.md#auth)                       | Auth / token / ID token error                                |\n| [docs/errors.md#challenge](docs/errors.md#challenge)             | PKCE or state generation error                               |\n| [docs/errors.md#session](docs/errors.md#session)                 | Session get/set or type error (includes `key`, `reason`)     |\n| [docs/errors.md#invalid_state](docs/errors.md#invalid_state)     | Invalid OAuth state payload                                  |\n| [docs/errors.md#invalid_request](docs/errors.md#invalid_request) | Invalid callback request (e.g. missing code, nonce mismatch) |\n| `about:blank`                                                    | Other or unknown error                                       |\n\n## Development and the Makefile\n\n| Target                    | What it does                                                                                                                                                                                                   |\n| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `help`                    | Default. Lists the real targets.                                                                                                                                                                               |\n| `build`                   | Builds `bin/oauth-server`, `bin/app`, `bin/simulate`.                                                                                                                                                          |\n| `test`                    | Runs `go test ./...`.                                                                                                                                                                                          |\n| `verify-docs`             | `git diff --exit-code docs/` -- fails if docs are dirty so CI keeps things real.                                                                                                                               |\n| `ci`                      | `build`, `test`, `verify-docs`.                                                                                                                                                                                |\n| `install-playwright`      | Install Playwright browsers for `simulate`.                                                                                                                                                                    |\n| `pkce-sim`                | Depends on `build`; runs the PKCE simulation script.                                                                                                                                                           |\n\n## Testing\n\nUsing Crooner in your app does **not** pull in Playwright. The main module has zero browser deps.\n\nThe PKCE simulation lives in the `simulate/` submodule. To run:\n\n```bash\ncd simulate \u0026\u0026 go run github.com/playwright-community/playwright-go/cmd/playwright install --with-deps\ncd .. \u0026\u0026 ./scripts/run-pkce-sim.sh\n```\n\n## Authentication Flow\n\n### How the Flow Works\n\n1. **You try to visit a protected page** -- Crooner checks your session. If not logged in, redirects to `/login?redirect=\u003cyour real destination\u003e`.\n2. **Login Handler** -- Encodes a secret state and your original destination into a base64-encoded package, stashes it in your session, and sends you to the OAuth provider.\n3. **Callback** -- After you sign in, the OAuth provider sends you back to `/callback` with your state. Crooner decodes it, checks credentials, and puts you back where you started.\n4. **If the Session is Gone** -- If the session state is missing or does not match, Crooner restarts the login flow, keeping your original destination safe.\n\n#### Example Flow\n\n1. You hit `/dashboard?id=42` (not logged in)\n2. Crooner sends you to `/login?redirect=/dashboard?id=42`\n3. You get sent to the OAuth provider with a state\n4. After login, you are back at `/callback?...\u0026state=...`\n5. Crooner decodes the state and puts you back at `/dashboard?id=42`\n\n#### Note for Development\n\nIf you are using an in-memory session store and you restart the server, your session is gone. Crooner will restart the login flow. For production, use a persistent session store (Redis, SQLite, etc.).\n\n## Architecture\n\n### OAuth2/OIDC flow\n\n```\n  browser                    crooner                     IdP\n  ───────                    ───────                     ───\n     │                          │                         │\n     ├── GET /login ──────────► │                         │\n     │                          ├── redirect ────────────►│\n     │                          │   (PKCE + state)        │\n     │   ◄── redirect ─────────┤ ◄── callback ───────────┤\n     │       to /callback       │    (code + state)       │\n     │                          │                         │\n     │                          ├── exchange code ───────►│\n     │                          │◄── tokens ──────────────┤\n     │                          │                         │\n     │                          ├── verify ID token       │\n     │                          ├── create session        │\n     │   ◄── redirect ─────────┤                         │\n     │       to original URL    │                         │\n```\n\n## Questions? PRs?\n\nOpen an issue or send a PR.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatgoose%2Fcrooner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcatgoose%2Fcrooner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcatgoose%2Fcrooner/lists"}