authkit

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Mar 23, 2026 License: MIT Imports: 21 Imported by: 0

README

CI CodeQL Coverage Status Open Issues Go Report Card GitHub release (latest by date)

authkit

Plug-and-play authentication with YAML-based RBAC for Small Go HTTP services.

  • OAuth 2.0 via markbates/goth — supports GitHub, Google, GitLab out of the box
  • Email/password authentication with bcrypt hashing
  • API key authentication — plug in any key store via a single-method interface
  • Three modes: OAuth only, password only, or both simultaneously
  • Encrypted cookie sessions via gorilla/sessions
  • Role-Based Access Control via YAML file or a pluggable PolicyProvider interface
  • Layered RBAC — seed roles from YAML, then let a UI override individual users via a database
  • Live policy reload without restarts (WatchRBAC)
  • Works with Go 1.22+ stdlib net/http (no external router required)
  • Storage-agnostic — implement a 2-method interface for your database
  • Pluggable logger — bring your own (slog, zap, zerolog) or use the default

Installation

go get github.com/tlmanz/authkit

Quick Start

1. Define your policy (policy.yaml)
roles:
  admin:
    permissions: ["*"]         # wildcard grants every permission
    members:
      - alice@company.com

  developer:
    permissions: ["view", "upload"]
    members:
      - bob@company.com
      - carol@company.com

  viewer:
    permissions: ["view"]

# Fallback role for authenticated users not listed in any role.
# Omit to deny access to unlisted users entirely.
default_role: viewer
2. Choose your auth mode
OAuth only (default)
auth, err := authkit.New(authkit.Config{
    Providers: []authkit.ProviderConfig{
        {
            Name:         "github",
            ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
            ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
        },
    },
    CallbackBaseURL: "https://example.com",
    SessionSecret:   os.Getenv("SESSION_SECRET"),
    SecureCookie:    true,
    AfterLoginURL:   "/dashboard",
    RBAC:            authkit.RBACConfig{FilePath: "policy.yaml"},
})
Password only
auth, err := authkit.New(authkit.Config{
    Mode:          authkit.AuthModePassword,
    SessionSecret: os.Getenv("SESSION_SECRET"),
    SecureCookie:  true,
    AfterLoginURL: "/dashboard",
    UserStore:     myUserStore, // implements authkit.UserStore
    RBAC:          authkit.RBACConfig{FilePath: "policy.yaml"},
})
Both (OAuth + password)
auth, err := authkit.New(authkit.Config{
    Mode: authkit.AuthModeBoth,
    Providers: []authkit.ProviderConfig{
        {Name: "github", ClientID: "...", ClientSecret: "..."},
    },
    CallbackBaseURL: "https://example.com",
    SessionSecret:   os.Getenv("SESSION_SECRET"),
    SecureCookie:    true,
    AfterLoginURL:   "/dashboard",
    UserStore:       myUserStore,
    RBAC:            authkit.RBACConfig{FilePath: "policy.yaml"},
})
3. Wire up routes
mux := http.NewServeMux()

// OAuth routes (when OAuth is enabled)
mux.HandleFunc("GET /auth/{provider}",          auth.BeginAuth)
mux.HandleFunc("GET /auth/{provider}/callback", auth.Callback)

// Password routes (when password auth is enabled)
mux.HandleFunc("POST /auth/register", auth.Register)
mux.HandleFunc("POST /auth/login",    auth.Login)

// Common routes (work with all modes)
mux.HandleFunc("POST /auth/logout", auth.Logout)
mux.HandleFunc("GET /auth/me",      auth.Me)

// Protected routes
mux.Handle("GET /api/reports",   auth.RequireAuth(http.HandlerFunc(reportsHandler)))
mux.Handle("POST /api/projects", auth.Require("projects:write")(http.HandlerFunc(createHandler)))

Auth Modes

Mode Constant Providers required UserStore required Use case
OAuth only authkit.AuthModeOAuth Yes No SSO with GitHub/Google/GitLab
Password only authkit.AuthModePassword No Yes Traditional email/password
Both authkit.AuthModeBoth Yes Yes Let users choose their method

When Mode is not set, it defaults to AuthModeOAuth for backward compatibility.


Password Authentication

UserStore interface

Implement this 2-method interface with your database of choice:

type UserStore interface {
    CreateUser(ctx context.Context, email, name, hashedPassword string) error
    GetUserByEmail(ctx context.Context, email string) (*authkit.PasswordUser, error)
}
  • CreateUser must return authkit.ErrUserExists if the email is already taken.
  • GetUserByEmail must return authkit.ErrUserNotFound if no user matches.
  • Passwords are pre-hashed with bcrypt before being passed to CreateUser.
Password policy
authkit.Config{
    PasswordPolicy: &authkit.PasswordPolicy{
        MinLength: 12, // default: 8
    },
}
Hashing utility

HashPassword is exported for use in admin tooling or seed scripts:

hashed, err := authkit.HashPassword("user-password")
Security features
  • bcrypt cost 12 (~250ms per hash on modern hardware)
  • Constant-time responses — failed logins for unknown users take the same time as wrong-password failures, preventing user enumeration
  • Generic error messages — both wrong-user and wrong-password return "invalid email or password"
  • Rate limiting — not built in (to stay storage-agnostic). Apply your own middleware:
    mux.Handle("POST /auth/login", rateLimiter(http.HandlerFunc(auth.Login)))
    

Custom Logger

By default, authkit logs to Go's standard log package. You can plug in your own logger by implementing the Logger interface:

type Logger interface {
    Info(msg string, args ...any)
    Error(msg string, args ...any)
}
Using the default logger
// No Logger field needed — uses standard log package automatically.
auth, err := authkit.New(authkit.Config{...})
Using slog
type slogAdapter struct {
    l *slog.Logger
}

func (s slogAdapter) Info(msg string, args ...any)  { s.l.Info(fmt.Sprintf(msg, args...)) }
func (s slogAdapter) Error(msg string, args ...any) { s.l.Error(fmt.Sprintf(msg, args...)) }

auth, err := authkit.New(authkit.Config{
    Logger: slogAdapter{l: slog.Default()},
    // ...
})
Using zap
type zapAdapter struct {
    l *zap.SugaredLogger
}

func (z zapAdapter) Info(msg string, args ...any)  { z.l.Infof(msg, args...) }
func (z zapAdapter) Error(msg string, args ...any) { z.l.Errorf(msg, args...) }

auth, err := authkit.New(authkit.Config{
    Logger: zapAdapter{l: zapLogger.Sugar()},
    // ...
})

OAuth Providers

GitHub
authkit.ProviderConfig{
    Name:         "github",
    ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
    ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
    // Default scope: ["user:email"]
}

Where to register: github.com/settings/developers → New OAuth App

Field Value
Authorization callback URL https://example.com/auth/github/callback
Authorization callback URL (local dev) http://localhost:8080/auth/github/callback
Google
authkit.ProviderConfig{
    Name:         "google",
    ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
    ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
    // Default scopes: ["email", "profile"]
}

Where to register: Google Cloud Console → APIs & Services → Credentials → Create OAuth Client ID (type: Web application)

Field Value
Authorized redirect URIs https://example.com/auth/google/callback
Authorized redirect URIs (local dev) http://localhost:8080/auth/google/callback

Google allows multiple redirect URIs per client — add both production and localhost entries.

GitLab
authkit.ProviderConfig{
    Name:         "gitlab",
    ClientID:     os.Getenv("GITLAB_CLIENT_ID"),
    ClientSecret: os.Getenv("GITLAB_CLIENT_SECRET"),
    // Default scope: ["read_user"]
}

Where to register: GitLab → User Settings → Applications (or group/admin Applications for shared apps)

Field Value
Redirect URI https://example.com/auth/gitlab/callback
Redirect URI (local dev) http://localhost:8080/auth/gitlab/callback
Scopes to enable read_user
Multiple providers at once
Providers: []authkit.ProviderConfig{
    {Name: "github", ClientID: "...", ClientSecret: "..."},
    {Name: "google", ClientID: "...", ClientSecret: "..."},
    {Name: "gitlab", ClientID: "...", ClientSecret: "..."},
},

Users can then choose their provider via the login URL:

  • GET /auth/github
  • GET /auth/google
  • GET /auth/gitlab

HTTP Routes

Method Path Handler Mode Description
GET /auth/{provider} auth.BeginAuth OAuth / Both Starts the OAuth flow
GET /auth/{provider}/callback auth.Callback OAuth / Both Completes the OAuth handshake
POST /auth/register auth.Register Password / Both Creates account with email+password
POST /auth/login auth.Login Password / Both Authenticates with email+password
POST /auth/logout auth.Logout All Clears the session
GET /auth/me auth.Me All Returns the current user as JSON

/auth/me example response:

{
  "email": "alice@company.com",
  "name": "Alice",
  "avatarUrl": "https://avatars.githubusercontent.com/u/1234",
  "provider": "github",
  "role": "admin"
}

For password-auth users, provider will be "password" and avatarUrl will be empty.


Middleware

RequireAuth — enforce authentication (session or API key)
mux.Handle("GET /api/reports", auth.RequireAuth(http.HandlerFunc(reportsHandler)))

Returns 401 Unauthenticated if there is no valid credential. On success the current *authkit.User is available via authkit.UserFromCtx(r.Context()).

Accepts API keys when APIKeyValidator is configured. Works identically for OAuth, password-authenticated, and API key users.

Require(permission) — enforce authentication + permission (session or API key)
mux.Handle("POST /api/upload", auth.Require("upload")(http.HandlerFunc(handler)))

Returns 401 for missing credential, 403 Forbidden when the user lacks the permission. API keys are accepted when APIKeyValidator is configured.

RequireSessionAuth — enforce a valid session (rejects API keys)
mux.Handle("GET /auth/me", auth.RequireSessionAuth(http.HandlerFunc(meHandler)))

Same as RequireAuth but API key credentials are rejected even if APIKeyValidator is set. Use this for UI-only routes that should never be accessible from automated clients.

RequireSession(permission) — enforce session + permission (rejects API keys)
mux.Handle("DELETE /api/environments/{id}", auth.RequireSession("manage")(http.HandlerFunc(handler)))

Same as Require but API keys are always rejected. Use this for management routes that must only be operated by a logged-in human.

Reading the current user inside a handler
func reportsHandler(w http.ResponseWriter, r *http.Request) {
    u := authkit.UserFromCtx(r.Context())
    // u is always non-nil here because RequireAuth ran first.
    // Works for OAuth, password, and API key users.
    fmt.Fprintf(w, "Hello, %s (%s)", u.Name, u.Role)
}

API Key Authentication

For programmatic access (CI/CD pipelines, scripts, service-to-service calls) authkit can validate API keys alongside OAuth/password sessions. Implement the APIKeyValidator interface and pass it to Config:

type APIKeyValidator interface {
    ValidateKey(ctx context.Context, rawKey string) (*User, error)
}

Return nil, nil when the key is not found, inactive, or expired. Return nil, err only for unexpected infrastructure failures (e.g. a database connection error).

Wiring it up
auth, err := authkit.New(authkit.Config{
    // ... other fields ...
    APIKeyValidator: myKeyStore, // implements authkit.APIKeyValidator
})
How it works

When APIKeyValidator is set, the middleware checks the Authorization: Bearer <key> header (or X-API-Key: <key> as a fallback) before the session cookie on every request. On a valid key:

  1. ValidateKey returns a *User with Email, Name, Provider, and Role populated.
  2. Authkit resolves permissions from the RBAC policy based on the returned Role.
  3. The user is injected into the request context under the same key as session users — UserFromCtx works transparently for both.
Middleware variants
Middleware API keys Sessions Use for
auth.RequireAuth(next) General protected routes
auth.Require(perm)(next) Permission-gated routes open to CI/CD
auth.RequireSessionAuth(next) UI-only routes (e.g. /auth/me)
auth.RequireSession(perm)(next) Management routes that must not accept keys
mux.Handle("GET /api/reports",   auth.Require("view")(reportsHandler))   // API keys OK
mux.Handle("POST /api/projects", auth.RequireSession("manage")(createH)) // session only
mux.Handle("GET /auth/me",       auth.RequireSessionAuth(meHandler))     // session only
Example implementation

A minimal DB-backed key store:

type KeyStore struct{ db *sql.DB }

func (s *KeyStore) ValidateKey(ctx context.Context, rawKey string) (*authkit.User, error) {
    hash := sha256Hex(rawKey)
    var name, role string
    err := s.db.QueryRowContext(ctx,
        "SELECT name, role FROM api_keys WHERE key_hash = ? AND is_active = 1", hash,
    ).Scan(&name, &role)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }
    return &authkit.User{
        Email:    "apikey:" + name,
        Name:     name,
        Provider: "apikey",
        Role:     role,
    }, nil
}

The returned User.Role must match a role defined in policy.yaml for permissions to be resolved. If the role is not found in the policy the user is authenticated but has no permissions.


Permissions

Permissions are fully user-defined strings — authkit does not prescribe any specific set. You define them in policy.yaml and enforce them in code.

The only built-in constant is authkit.PermAll = "*", which is a wildcard that passes every permission check.

Define permissions in policy.yaml
roles:
  admin:
    permissions: ["*"]          # wildcard — passes every check

  editor:
    permissions: ["posts:write", "posts:publish", "media:upload"]

  reader:
    permissions: ["posts:read"]
Enforce on a route
mux.Handle("POST /posts",         auth.Require("posts:write")(http.HandlerFunc(createPost)))
mux.Handle("POST /posts/publish", auth.Require("posts:publish")(http.HandlerFunc(publishPost)))
mux.Handle("POST /media",         auth.Require("media:upload")(http.HandlerFunc(uploadMedia)))
Check inline in a handler
func handler(w http.ResponseWriter, r *http.Request) {
    u := authkit.UserFromCtx(r.Context())
    if !u.Can("posts:publish") {
        http.Error(w, "forbidden", http.StatusForbidden)
        return
    }
    // ...
}
Permission string format

Any alphanumeric string with . : - _ is valid. Common conventions:

Style Example
Simple "read", "write", "delete"
Namespaced "posts:read", "posts:write"
Dot-separated "reports.view", "reports.export"
Action-resource "create-project", "delete-user"

There is no hierarchy — "posts:read" does not automatically grant "posts". Each string is matched exactly.


Live Policy Reload

WatchRBAC reloads the policy on every tick. If the reload fails the old policy is kept — users are never accidentally locked out. Works with any PolicyProvider that implements PolicyReloader (both the built-in YAML provider and LayeredPolicyProvider support this).

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Reload every 60 seconds.
go auth.WatchRBAC(ctx, 60*time.Second)

For the YAML-only provider: edit policy.yaml and wait for the next tick. No restart needed.

For LayeredPolicyProvider: the YAML baseline is reloaded on each tick. Database overrides are always read live on each login request.

If you supply a custom PolicyProvider that manages its own cache, simply do not implement PolicyReloader and WatchRBAC will exit immediately — your provider controls its own refresh strategy.


Database-backed RBAC

By default, roles are read from a YAML file. For applications that need a management UI where operators can change user roles at runtime without touching files, authkit provides two additional mechanisms.

Option 1: Layered provider (YAML baseline + database overrides)

Roles and their initial members are defined in policy.yaml as usual. Any user whose role is changed through your UI writes to a UserRoleStore — authkit checks the store first, falling back to YAML for everyone else.

1. Implement UserRoleStore

type UserRoleStore interface {
    GetOverride(ctx context.Context, email string) (role string, permissions []string, found bool, err error)
    SetOverride(ctx context.Context, email, role string, permissions []string) error
    DeleteOverride(ctx context.Context, email string) error
}

A minimal Postgres implementation:

type RoleStore struct{ db *sql.DB }

func (s *RoleStore) GetOverride(ctx context.Context, email string) (string, []string, bool, error) {
    var role, permsJSON string
    err := s.db.QueryRowContext(ctx,
        "SELECT role, permissions FROM role_overrides WHERE email = $1", email,
    ).Scan(&role, &permsJSON)
    if errors.Is(err, sql.ErrNoRows) {
        return "", nil, false, nil
    }
    if err != nil {
        return "", nil, false, err
    }
    var perms []string
    json.Unmarshal([]byte(permsJSON), &perms)
    return role, perms, true, nil
}

func (s *RoleStore) SetOverride(ctx context.Context, email, role string, permissions []string) error {
    permsJSON, _ := json.Marshal(permissions)
    _, err := s.db.ExecContext(ctx,
        `INSERT INTO role_overrides (email, role, permissions)
         VALUES ($1, $2, $3)
         ON CONFLICT (email) DO UPDATE SET role = $2, permissions = $3`,
        email, role, string(permsJSON),
    )
    return err
}

func (s *RoleStore) DeleteOverride(ctx context.Context, email string) error {
    _, err := s.db.ExecContext(ctx, "DELETE FROM role_overrides WHERE email = $1", email)
    return err
}

Required table:

CREATE TABLE role_overrides (
    email       TEXT PRIMARY KEY,
    role        TEXT NOT NULL,
    permissions JSONB NOT NULL DEFAULT '[]'
);

2. Wire it up

roleStore := &RoleStore{db: db}

provider, err := authkit.NewLayeredProvider("policy.yaml", roleStore,
    authkit.WithLogger(myLogger), // optional: logs DB errors during role lookups
)
if err != nil {
    log.Fatal(err)
}

auth, err := authkit.New(authkit.Config{
    RBAC: authkit.RBACConfig{Provider: provider},
    // ... rest of config
})

go auth.WatchRBAC(ctx, 30*time.Second) // reloads the YAML baseline

3. Change a user's role from a management handler

Use provider.SetOverride — it validates the role name against the YAML policy and checks permission string format before writing to the store.

func setRoleHandler(w http.ResponseWriter, r *http.Request) {
    email := r.FormValue("email")
    role  := r.FormValue("role")
    perms := rolesMap[role] // your app's role→permissions lookup

    if err := provider.SetOverride(r.Context(), email, role, perms); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // Change takes effect on the user's next login.
}

Session note: role changes take effect on the next login only. Existing sessions keep their current permissions until they expire (7 days by default) or the user logs out. If you need immediate enforcement for demotions, implement session invalidation at the application level.

4. Reset a user to the YAML baseline

provider.DeleteOverride(ctx, "bob@example.com")
Option 2: Fully custom PolicyProvider

If neither YAML nor the layered approach fits your needs, implement the PolicyProvider interface directly:

type PolicyProvider interface {
    RoleFor(ctx context.Context, email string) (role string, permissions []string)
    PermissionsForRole(role string) []string
}

Pass your implementation via RBACConfig.Provider:

auth, err := authkit.New(authkit.Config{
    RBAC: authkit.RBACConfig{Provider: myCustomProvider},
})

Optionally implement PolicyReloader to participate in WatchRBAC:

type PolicyReloader interface {
    Reload() error
}

Policy YAML Reference

roles:
  <role-name>:
    # List of permission strings granted to this role.
    # Use "*" for superuser access.
    permissions: ["view", "upload"]

    # Emails assigned to this role (case-insensitive).
    members:
      - user@example.com

# Optional: role assigned to authenticated users whose email is not
# listed under any role. Omit to deny access to unknown users.
default_role: viewer

Role names must be alphanumeric (with hyphens/underscores). Permission names must be alphanumeric (with dots, colons, hyphens, underscores, or *). Member emails are validated for basic format.


Configuration Reference

authkit.Config{
    // Optional: auth mode. Default: authkit.AuthModeOAuth
    // Options: authkit.AuthModeOAuth, authkit.AuthModePassword, authkit.AuthModeBoth
    Mode: authkit.AuthModeOAuth,

    // Required when OAuth is enabled: providers to register.
    Providers: []authkit.ProviderConfig{...},

    // Required when OAuth is enabled: externally-reachable base URL (no trailing slash).
    CallbackBaseURL: "https://example.com",

    // Required: secret used to sign+encrypt session cookies.
    // Must be at least 32 bytes. Generate with: openssl rand -hex 32
    SessionSecret: "...",

    // Optional: set true for production HTTPS deployments. Default: false
    SecureCookie: true,

    // Optional: redirect target after successful login. Default: "/"
    AfterLoginURL: "/dashboard",

    // Optional: redirect target after logout. Default: "/"
    AfterLogoutURL: "/",

    // Optional: RBAC policy.
    // YAML only (default):
    RBAC: authkit.RBACConfig{FilePath: "policy.yaml"},
    // Layered (YAML baseline + database overrides):
    // RBAC: authkit.RBACConfig{Provider: authkit.NewLayeredProvider("policy.yaml", store)},
    // Fully custom:
    // RBAC: authkit.RBACConfig{Provider: myProvider},

    // Required when password auth is enabled: storage backend.
    UserStore: myUserStore,

    // Optional: password validation rules. Default: min 8 characters.
    PasswordPolicy: &authkit.PasswordPolicy{MinLength: 12},

    // Optional: custom logger. Default: standard log package.
    Logger: myLogger, // implements authkit.Logger

    // Optional: enables API key authentication alongside sessions.
    // When set, Require/RequireAuth accept Bearer tokens validated by this.
    // RequireSession/RequireSessionAuth always reject API keys regardless.
    APIKeyValidator: myKeyStore, // implements authkit.APIKeyValidator
}

Session Security

  • Sessions are stored in encrypted, signed cookies (gorilla/sessions + securecookie).
  • Cookie flags: HttpOnly, SameSite=Lax, 7-day MaxAge.
  • Set SecureCookie: true in production to add the Secure flag (HTTPS only).
  • The SessionSecret must be at least 32 bytes of cryptographically random data:
    openssl rand -hex 32
    
  • A startup warning is logged if SecureCookie is false.

Documentation

Overview

Package authkit provides plug-and-play OAuth authentication with RBAC for Go HTTP services. It wraps markbates/goth for the OAuth dance, gorilla/sessions for encrypted cookie sessions, and a YAML-based role policy for access control.

Quick start:

auth, err := authkit.New(authkit.Config{
    Providers: []authkit.ProviderConfig{
        {Name: "github", ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET")},
    },
    CallbackBaseURL: "https://example.com",
    SessionSecret:   os.Getenv("SESSION_SECRET"),
    RBAC: authkit.RBACConfig{FilePath: "policy.yaml"},
})

mux.Handle("GET /auth/{provider}",          http.HandlerFunc(auth.BeginAuth))
mux.Handle("GET /auth/{provider}/callback", http.HandlerFunc(auth.Callback))
mux.Handle("POST /auth/logout",             http.HandlerFunc(auth.Logout))
mux.Handle("GET /auth/me",                  http.HandlerFunc(auth.Me))

// Protected routes
mux.Handle("GET /api/reports", auth.RequireAuth(reportsHandler))
mux.Handle("POST /api/projects", auth.Require("projects:write")(createHandler))

Index

Constants

View Source
const (
	// PermAll grants every permission check. Use "*" in policy.yaml to assign
	// it to a role. All other permission strings are user-defined.
	PermAll = "*"
)

Variables

View Source
var (
	ErrUserExists   = errors.New("authkit: user already exists")
	ErrUserNotFound = errors.New("authkit: user not found")
)

Sentinel errors for UserStore implementations.

Functions

func CheckPassword

func CheckPassword(hashedPassword, password string) bool

CheckPassword compares a plaintext password against a bcrypt hash.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword hashes a plaintext password using bcrypt. Exported so consumers can use it in admin/seed tooling.

func WithLogger added in v0.0.4

func WithLogger(l Logger) func(*LayeredPolicyProvider)

WithLogger configures a Logger for LayeredPolicyProvider. When set, DB errors during role lookups are logged so operators can detect store outages.

Example:

provider, err := authkit.NewLayeredProvider("policy.yaml", store,
    authkit.WithLogger(myLogger),
)

Types

type APIKeyValidator added in v0.0.3

type APIKeyValidator interface {
	ValidateKey(ctx context.Context, rawKey string) (*User, error)
}

APIKeyValidator validates a raw API key string and returns the associated user. Implementations look up the key hash in a store and return a *User with Email, Name, Provider, and Role populated. Authkit then resolves the user's permissions from the RBAC policy based on the returned Role.

Return nil, nil when the key is not found, expired, or inactive. Return nil, err only for unexpected infrastructure failures.

type Auth

type Auth struct {
	// contains filtered or unexported fields
}

Auth is the central object. Create one with New() and attach its methods as HTTP handlers and middleware.

func New

func New(cfg Config) (*Auth, error)

New validates the config, registers the OAuth providers with goth, loads the RBAC policy, and returns a ready-to-use Auth instance.

func (*Auth) BeginAuth

func (a *Auth) BeginAuth(w http.ResponseWriter, r *http.Request)

BeginAuth starts the OAuth flow for the provider named in the URL path. Mount this on: GET /auth/{provider}

The provider name is extracted from the Go 1.22+ path value {provider}.

func (*Auth) Callback

func (a *Auth) Callback(w http.ResponseWriter, r *http.Request)

Callback completes the OAuth handshake, resolves the user's role from the RBAC policy, stores the user in the session, then redirects to AfterLoginURL. Mount this on: GET /auth/{provider}/callback

func (*Auth) Login

func (a *Auth) Login(w http.ResponseWriter, r *http.Request)

Login authenticates a user with email and password. Mount this on: POST /auth/login

Expects form values: email and password.

func (*Auth) Logout

func (a *Auth) Logout(w http.ResponseWriter, r *http.Request)

Logout clears the session and redirects to AfterLogoutURL. Mount this on: POST /auth/logout

func (*Auth) Me

func (a *Auth) Me(w http.ResponseWriter, r *http.Request)

Me returns the currently authenticated user as JSON. Returns 401 if the request has no valid session. Mount this on: GET /auth/me

func (*Auth) Register

func (a *Auth) Register(w http.ResponseWriter, r *http.Request)

Register creates a new user account with email and password, then logs them in automatically. Mount this on: POST /auth/register

Expects form values: email, password, and optionally name.

func (*Auth) Require

func (a *Auth) Require(permission string) func(http.Handler) http.Handler

Require is middleware that enforces both a valid credential (API key or OAuth session) AND that the authenticated user holds the given permission. Returns 401 when there is no credential, 403 when the user lacks the permission.

func (*Auth) RequireAuth

func (a *Auth) RequireAuth(next http.Handler) http.Handler

RequireAuth is middleware that enforces a valid credential — either an API key (via APIKeyValidator, if configured) or an OAuth session cookie. Responds 401 when neither is present or valid. On success it injects the User into the request context.

func (*Auth) RequireSession added in v0.0.3

func (a *Auth) RequireSession(permission string) func(http.Handler) http.Handler

RequireSession is like Require but rejects API key credentials. Use this for permission-gated management routes that must use OAuth sessions.

func (*Auth) RequireSessionAuth added in v0.0.3

func (a *Auth) RequireSessionAuth(next http.Handler) http.Handler

RequireSessionAuth is like RequireAuth but rejects API key credentials. Use this for routes that must only be accessed via an OAuth session (e.g. /auth/me, UI-only management actions).

func (*Auth) WatchRBAC

func (a *Auth) WatchRBAC(ctx context.Context, interval time.Duration)

WatchRBAC starts a background goroutine that reloads the RBAC policy file every interval. It stops when ctx is cancelled. This allows operators to update the policy without restarting the service.

Example:

go auth.WatchRBAC(ctx, 30*time.Second)

type AuthMode

type AuthMode string

AuthMode controls which authentication methods are enabled.

const (
	// AuthModeOAuth enables only OAuth providers (default).
	AuthModeOAuth AuthMode = "oauth"

	// AuthModePassword enables only email/password authentication.
	AuthModePassword AuthMode = "password"

	// AuthModeBoth enables both OAuth and email/password authentication.
	AuthModeBoth AuthMode = "both"
)

type Config

type Config struct {
	// Mode controls which authentication methods are enabled.
	// Defaults to AuthModeOAuth for backward compatibility.
	Mode AuthMode

	// Providers is the list of OAuth providers to enable.
	// Required when Mode is AuthModeOAuth or AuthModeBoth.
	Providers []ProviderConfig

	// CallbackBaseURL is the externally-reachable base URL of the service
	// (e.g. "https://example.com"). The OAuth callback URLs are derived as
	// {CallbackBaseURL}/auth/{provider}/callback.
	// Required when Mode is AuthModeOAuth or AuthModeBoth.
	CallbackBaseURL string

	// SessionSecret is used to sign and encrypt session cookies.
	// Must be at least 32 bytes of random data.
	SessionSecret string

	// SecureCookie controls the Secure flag on session cookies.
	// Set to true in production (HTTPS only). Defaults to false.
	SecureCookie bool

	// AfterLoginURL is the URL the user is redirected to after a successful login.
	// Defaults to "/".
	AfterLoginURL string

	// AfterLogoutURL is the URL the user is redirected to after logout.
	// Defaults to "/".
	AfterLogoutURL string

	// RBAC configures the role policy. If FilePath is empty, all authenticated
	// users receive an empty role with no permissions.
	RBAC RBACConfig

	// UserStore provides user persistence for password-based authentication.
	// Required when Mode is AuthModePassword or AuthModeBoth.
	UserStore UserStore

	// PasswordPolicy configures password validation rules.
	// If nil, defaults are used (minimum 8 characters).
	PasswordPolicy *PasswordPolicy

	// Logger is used for diagnostic output. If nil, logs are written to the
	// standard library log package.
	Logger Logger

	// APIKeyValidator enables API key authentication alongside OAuth sessions.
	// When set, Require and RequireAuth middleware check the Authorization:
	// Bearer (or X-API-Key) header first. RequireSession and RequireSessionAuth
	// skip API key auth entirely (session-only routes).
	// If nil, API key auth is disabled and only sessions are accepted.
	APIKeyValidator APIKeyValidator
}

Config holds all configuration needed to create an Auth instance.

type LayeredPolicyProvider added in v0.0.4

type LayeredPolicyProvider struct {
	// contains filtered or unexported fields
}

LayeredPolicyProvider combines a YAML baseline with per-user database overrides. The YAML file defines roles and their initial members. A management UI can call SetOverride to change individual users without editing the file.

Lookup order per request:

  1. Database override for this email (UI-managed, takes precedence)
  2. YAML baseline (file-managed, fallback)

Implements PolicyProvider and PolicyReloader.

func NewLayeredProvider added in v0.0.4

func NewLayeredProvider(filePath string, store UserRoleStore, opts ...func(*LayeredPolicyProvider)) (*LayeredPolicyProvider, error)

NewLayeredProvider creates a LayeredPolicyProvider that reads the initial policy from filePath and looks up per-user overrides from store.

Example:

provider, err := authkit.NewLayeredProvider("policy.yaml", myDBStore,
    authkit.WithLogger(myLogger),
)
auth, err := authkit.New(authkit.Config{
    RBAC: authkit.RBACConfig{Provider: provider},
    ...
})
go auth.WatchRBAC(ctx, 30*time.Second) // reloads YAML baseline

func (*LayeredPolicyProvider) DeleteOverride added in v0.0.4

func (l *LayeredPolicyProvider) DeleteOverride(ctx context.Context, email string) error

DeleteOverride removes the role override for email, reverting that user to the YAML baseline on their next login.

func (*LayeredPolicyProvider) PermissionsForRole added in v0.0.4

func (l *LayeredPolicyProvider) PermissionsForRole(role string) []string

PermissionsForRole implements PolicyProvider. Resolves permissions for a named role from the YAML baseline — role definitions are always file-managed.

func (*LayeredPolicyProvider) Reload added in v0.0.4

func (l *LayeredPolicyProvider) Reload() error

Reload implements PolicyReloader. Re-reads the YAML baseline without affecting any database overrides. Called automatically by WatchRBAC.

func (*LayeredPolicyProvider) RoleFor added in v0.0.4

func (l *LayeredPolicyProvider) RoleFor(ctx context.Context, email string) (string, []string)

RoleFor implements PolicyProvider. Checks the DB override first, then falls back to the YAML baseline. If the DB returns an error the fallback is used and the error is logged (if a Logger was configured). This prevents a store outage from locking users out, but means a user whose DB override was a demotion will temporarily regain their YAML role. Monitor store errors.

func (*LayeredPolicyProvider) SetOverride added in v0.0.4

func (l *LayeredPolicyProvider) SetOverride(ctx context.Context, email, role string, permissions []string) error

SetOverride validates and stores a per-user role override. Returns an error if the email format is invalid, the role name is not defined in the current YAML policy, or any permission string is invalid.

NOTE: role changes take effect on the user's next login — existing sessions retain their current permissions until they expire or the user logs out and back in. There is no built-in session invalidation.

func (*LayeredPolicyProvider) Store added in v0.0.4

Store returns the raw UserRoleStore for operations not covered by SetOverride/DeleteOverride (e.g. listing all overrides in a management UI).

type Logger

type Logger interface {
	// Info logs informational messages (e.g. startup warnings).
	Info(msg string, args ...any)

	// Error logs error messages (e.g. failed OAuth callbacks, session errors).
	Error(msg string, args ...any)
}

Logger is the interface authkit uses for diagnostic output. Provide your own implementation to route authkit logs into your application's logging system (e.g. slog, zap, zerolog). If not set in Config, a default logger that writes to the standard library log package is used.

type PasswordPolicy

type PasswordPolicy struct {
	// MinLength is the minimum password length. Defaults to 8.
	MinLength int
}

PasswordPolicy configures password validation constraints.

type PasswordUser

type PasswordUser struct {
	Email          string
	Name           string
	HashedPassword string
}

PasswordUser is the record returned by UserStore.GetUserByEmail.

type Policy

type Policy struct {
	// Roles maps role names to their definition.
	Roles map[string]RolePolicy `yaml:"roles"`

	// DefaultRole is assigned to authenticated users whose email is not listed
	// under any role. Leave empty to deny access to unlisted users.
	DefaultRole string `yaml:"default_role"`
}

Policy is the top-level structure of an rbac.yaml file.

type PolicyProvider added in v0.0.4

type PolicyProvider interface {
	// RoleFor returns the role name and permission list for the given email.
	// Called on every login and API key auth. Return empty strings/nil when
	// the user has no assigned role.
	RoleFor(ctx context.Context, email string) (role string, permissions []string)

	// PermissionsForRole returns the permissions for a named role.
	// Used to resolve permissions for API key users whose validator returns
	// a role name rather than an email lookup.
	PermissionsForRole(role string) []string
}

PolicyProvider resolves a user's role and permissions at login time. Implement this interface to back RBAC with any storage system. The built-in implementations are the YAML file provider (default) and LayeredPolicyProvider (YAML baseline + database overrides).

type PolicyReloader added in v0.0.4

type PolicyReloader interface {
	Reload() error
}

PolicyReloader is an optional interface that PolicyProvider implementations can satisfy to support live policy reloading via WatchRBAC.

type ProviderConfig

type ProviderConfig struct {
	// Name is the provider identifier: "github", "google", or "gitlab".
	Name string

	// ClientID and ClientSecret are the OAuth application credentials.
	ClientID     string
	ClientSecret string

	// Scopes overrides the default scopes for the provider.
	// Leave nil to use the sensible defaults (email + profile).
	Scopes []string
}

ProviderConfig holds the OAuth credentials for a single provider.

type RBACConfig

type RBACConfig struct {
	// FilePath is the path to the rbac.yaml policy file.
	// Used when Provider is nil.
	FilePath string

	// Provider supplies a custom PolicyProvider implementation (e.g. a
	// database-backed or layered provider). When set, FilePath is ignored.
	Provider PolicyProvider
}

RBACConfig tells the Auth instance how to load the role policy.

type RolePolicy

type RolePolicy struct {
	// Permissions is the list of allowed permissions for this role.
	// Use "*" to grant all permissions (admin).
	Permissions []string `yaml:"permissions"`

	// Members is the list of email addresses assigned to this role.
	Members []string `yaml:"members"`
}

RolePolicy defines the permissions and member emails for a single role.

type User

type User struct {
	Email     string `json:"email"`
	Name      string `json:"name"`
	AvatarURL string `json:"avatarUrl"`
	Provider  string `json:"provider"`
	Role      string `json:"role"`
	// contains filtered or unexported fields
}

User represents an authenticated user resolved from an OAuth session. It is injected into every request context after successful authentication.

func UserFromCtx

func UserFromCtx(ctx context.Context) *User

UserFromCtx returns the authenticated User stored in ctx, or nil if the request has not passed through RequireAuth middleware.

func (*User) Can

func (u *User) Can(permission string) bool

Can reports whether the user holds the given permission. A user with the "*" (PermAll) permission passes every check.

type UserRoleStore added in v0.0.4

type UserRoleStore interface {
	// GetOverride returns the role and permissions for email if a UI-managed
	// override exists. Return found=false when no override has been set for
	// this user — authkit will fall back to the YAML baseline.
	// The email argument is always lower-cased and trimmed before being passed.
	GetOverride(ctx context.Context, email string) (role string, permissions []string, found bool, err error)

	// SetOverride creates or replaces the role override for a user.
	// Prefer calling LayeredPolicyProvider.SetOverride instead — it validates
	// the role name and permission strings before writing to the store.
	SetOverride(ctx context.Context, email, role string, permissions []string) error

	// DeleteOverride removes the override for email, reverting that user to
	// whatever the YAML baseline assigns them.
	DeleteOverride(ctx context.Context, email string) error
}

UserRoleStore persists per-user role overrides to a database. Implement this interface against your preferred database (Postgres, SQLite, etc.) and pass it to NewLayeredProvider.

Only users whose roles have been changed via your UI need a row in the store. Everyone else falls through to the YAML baseline automatically.

type UserStore

type UserStore interface {
	// CreateUser persists a new user with the given email and bcrypt-hashed
	// password. Implementations MUST return ErrUserExists if the email is
	// already taken.
	CreateUser(ctx context.Context, email, name, hashedPassword string) error

	// GetUserByEmail retrieves a user by email. Returns ErrUserNotFound if
	// no user matches.
	GetUserByEmail(ctx context.Context, email string) (*PasswordUser, error)
}

UserStore is the interface consumers implement to provide user persistence for password-based authentication. authkit is storage-agnostic — the consumer chooses the backing store (PostgreSQL, SQLite, in-memory, etc.).

Jump to

Keyboard shortcuts

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