Authentication Providers#
Tinyauth supports five authentication provider types, each modelled as a ProviderType constant: ProviderLocal, ProviderBasicAuth, ProviderOAuth, ProviderLDAP, and ProviderTailscale. A UserContext is populated per-request and carries the active provider plus a provider-specific sub-struct (LocalContext, OAuthContext, LDAPContext, TailscaleContext).
The AuthService in internal/service/auth_service.go is the central orchestrator: it holds references to the optional LdapService, OAuthBrokerService, and TailscaleService, and manages all sessions, rate-limiting, and password verification. Provider-specific configuration lives in the top-level Config struct under Auth, OAuth, OIDC, LDAP, and Tailscale sections.
The ContextMiddleware resolves the active provider on every request by checking, in order: session cookie β HTTP Basic Auth β Tailscale IP whois.
Local & Basic Auth#
Local users are defined via Auth.Users (comma-separated username:hashed_password pairs) or an external Auth.UsersFile. Passwords are verified with bcrypt in CheckUserPassword(). Per-user OIDC attributes (name, email, locale, address, etc.) are configurable via Auth.UserAttributes.
Basic Auth (provider type ProviderBasicAuth) is resolved directly from the Authorization: Basic β¦ header inside the context middleware . It applies to both local and LDAP users β the basicAuth() function calls SearchUser() first to determine the user type, then delegates password checking accordingly. Rate-limiting and global lockdown apply equally. One restriction: users with a TOTP secret cannot authenticate via Basic Auth , since there is no second-factor channel in that flow.
LDAP#
LDAP is activated by setting LDAP.Address; the LdapService is an optional dependency injected into AuthService .
Connection & mTLS. If LDAP.AuthCert and LDAP.AuthKey are set, the service loads an X.509 key pair and uses it for mTLS authentication . Otherwise, plain or InsecureSkipVerify TLS is used based on the LDAP.Insecure flag. A background heartbeat pings the server every 5 minutes and reconnects with exponential backoff on failure .
User search. GetUserInfo() searches using LDAP.SearchFilter (default (uid=%s)) against LDAP.BaseDN, and escapes the username to prevent LDAP injection. It returns the user's DN and email.
Authentication flow. The service account binds first, then user credentials are verified with a direct bind via Bind(). After user authentication, the service account is rebound to restore the connection state .
Group membership. GetUserGroups() searches for groupOfUniqueNames entries containing the user as a uniquemember. Results are cached per-DN for LDAP.GroupCacheTTL seconds (default 900 s / 15 min) in AuthService . LDAP groups are passed through to OIDC userinfo claims when Tinyauth acts as an OIDC provider .
OAuth#
OAuth providers are registered and looked up through OAuthBrokerService. Built-in presets exist for github and google ; any other provider can be configured with explicit AuthURL, TokenURL, and UserinfoURL fields . The IOAuthService interface defines the contract for all providers.
Google preset configures openid email profile scopes and the Google OpenID Connect endpoints . GitHub preset uses read:user user:email and attaches a custom userinfo extractor because GitHub does not expose a standard userinfo URL.
PKCE flow. NewOAuthSession() generates a random state and PKCE verifier and stores a pending session in the oauth CacheStore (TTL 10 min, hard cap 256 entries). The callback handler verifies the state parameter, exchanges the code, fetches userinfo, and enforces the email whitelist before creating a session .
Email whitelist. A global OAuth.Whitelist applies to all providers; a per-provider Providers[name].Whitelist overrides it when set . The whitelist is re-evaluated on every cookie-based session validation, so removing an email immediately revokes access .
OAuth redirect URI validation. For non-OIDC redirects, isRedirectSafe() requires: (1) a valid, parseable URI with non-empty scheme and host; (2) scheme and effective port must match the app URL; (3) host must either exactly match the app URL host or, when subdomains are enabled, be a subdomain of the configured cookie domain. Unsafe URIs are silently dropped.
Tailscale#
Tailscale integration is activated by Tailscale.Enabled = true, with required Tailnet name and APIToken (or APITokenFile). It provides private network authentication: rather than a login flow, the client's IP address is resolved to a Tailscale identity.
TailscaleService.Whois(addr) queries the Tailscale API for the device list and user list, caches results for Tailscale.CacheDuration seconds (default 300 s), and matches the request IP against device addresses. Tagged devices are skipped . The device's owner is resolved against the user list by loginName, returning DisplayName, LoginName, and NodeName.
In the context middleware, a Tailscale match sets a UserContext with Authenticated: false β the user still needs to complete a session-based login. On subsequent requests with a valid session cookie, the middleware re-validates the Tailscale context and checks that the device owner matches the session email . Both the device cache and user cache are swept every minute for expired entries .
OIDC Server Mode#
Tinyauth can act as an OpenID Connect provider for downstream applications. The mode activates when OIDC.Clients is non-empty; the app URL must be HTTPS . An RSA 2048-bit key pair is auto-generated on first run if the configured key files are absent .
Supported scopes: openid, profile, email, phone, address, groups . Supported grant types: authorization_code, refresh_token . PKCE with S256 or plain challenge methods is supported .
Redirect URI validation. ValidateAuthorizeParams() checks that the redirect_uri is in the per-client TrustedRedirectURIs allowlist (strict slice contains check). Any unlisted URI is rejected with invalid_request_uri.
Token issuance. ID tokens are RS256-signed JWTs. Although the spec recommends against embedding user claims in ID tokens, Tinyauth does so for compatibility with apps that skip the userinfo endpoint . Access tokens expire at Auth.SessionExpiry; refresh tokens last twice as long . Authorization codes are single-use: they are deleted from the cache on retrieval and tracked in a usedCode cache for 2 minutes .
Subject identifier. The OIDC sub is a deterministic UUID derived from username:clientId, meaning it is stable across sessions but changes if the username or client ID changes .
User info sources. userinfoFromContext() builds claims from whichever provider authenticated the user: full attribute map for local users; group list pass-through for LDAP and OAuth users .
Cross-Provider Security Controls#
Rate limiting & lockdown. RecordLoginAttempt() tracks failed attempts per identifier. After Auth.LoginMaxRetries failures, the account is locked for Auth.LoginTimeout seconds. When Auth.LockdownEnabled is true and the login cache reaches a calculated limit (based on total user count Γ max retries, with a minimum of 256 and random jitter), the server enters global lockdown mode β blocking all login attempts for the timeout duration .
Session cookies. All sessions use HttpOnly: true, SameSite: Lax, optional Secure, and are stored in the database with a configurable SessionExpiry (default 86400 s / 1 day). An optional SessionMaxLifetime provides a hard cap regardless of refreshes . Sessions are refreshed automatically when their remaining TTL drops below a threshold .
OAuth pending session cap. MaxOAuthPendingSessions is hard-coded at 256; a cleanup of 16 entries fires on overflow to prevent unbounded memory growth .
Reverse proxy headers. Authenticated user context is forwarded to upstream services as Remote-User, Remote-Name, Remote-Email, Remote-Groups, and Remote-Sub headers . Per-app BasicAuth response credentials can be injected via AppResponse.BasicAuth configuration .