auth

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 31 Imported by: 0

README

awesome-go-auth

awesome-go-auth is a Go authentication library with stateful sessions and access/refresh tokens.

Installation

go get github.com/nik2208/awesome-go-auth

Quick start

package main

import (
	"context"
	"log"

	auth "github.com/nik2208/awesome-go-auth"
)

func main() {
	cfg := auth.DefaultConfig("replace-with-at-least-32-random-chars")
	service, err := auth.NewService(cfg, auth.NewMemoryUserStore(), auth.NewMemorySessionStore())
	if err != nil {
		log.Fatal(err)
	}

	_, tokens, err := service.Register(context.Background(), auth.RegisterInput{
		Email:    "alice@example.com",
		Password: "supersecurepassword",
		TenantID: "tenant-1",
	})
	if err != nil {
		log.Fatal(err)
	}

	log.Println("access token issued", tokens.AccessToken != "")
}

Features

  • Secure configuration with validation.
  • Register/login with password hashing (bcrypt).
  • Signed access token + refresh token (HMAC-SHA256) with expiry.
  • Stateful sessions with refresh token rotation and revocation (logout).
  • Configurable session check policy (allcalls / refresh / none).
  • Authenticated user retrieval (Me).
  • Account management (UpdateProfile, DeleteAccount) in addition to password/email lifecycle.
  • CSRF double-submit middleware (cookie + header) for browser flows.
  • Password reset (ForgotPassword, ResetPassword, ChangePassword).
  • Passwordless magic link (SendMagicLink, VerifyMagicLink).
  • SMS OTP login (SendSMSCode, VerifySMSCode).
  • TOTP 2FA (SetupTOTP, VerifyTOTPSetup, VerifyTOTP, DisableTOTP).
  • Email verification (SendVerificationEmailToken, VerifyEmail) and email change (RequestEmailChange, ConfirmEmailChange).
  • Session admin helpers (ListSessions, RevokeSessionByID, CleanupExpiredSessions).
  • User metadata store and service helpers.
  • Multi-tenant in-memory RBAC (MemoryRolesPermissionsStore) with roles and permissions.
  • In-memory tenant store (MemoryTenantStore) with user↔tenant membership.
  • Custom token claims via Config.BuildTokenClaims, also reflected in the enriched profile.
  • API key service and HTTP middleware (APIKeyService, APIKeyMiddleware).
  • In-process event bus (EventBus) for event-driven integrations.
  • Extended storage interfaces and thread-safe in-memory implementations for all the above flows.

Idiomatic entrypoint and HTTP adapters

  • auth.New(...) entrypoint with functional options (WithSecret, WithTokenTTLs, WithUserStore, WithSessionStore, etc.).
  • Framework-agnostic adapters available:
    • adapter/nethttp (Middleware, Mount, MountWithConfig)
    • adapter/chi (Middleware, Mount, MountWithConfig)
    • adapter/gin (Middleware, Mount, MountWithConfig)
    • adapter/echo (Middleware, Mount, MountWithConfig)
  • All four adapters serve the same wire contract, configured by a single auth.HTTPConfig (mount prefix, cookie policy, CSRF) and written through the shared helpers in wire.go.

HTTP wire conventions

The HTTP surface follows the awesome-node-auth contract the family clients (Angular, Flutter, the served auth.js) are pinned to:

  • GET <prefix>/me returns the user object unwrapped.
  • Cookie mode (default) answers {"success": true} and sets the cookies; X-Auth-Strategy: bearer (exact, case-sensitive) answers with top-level accessToken/refreshToken and sets no cookies.
  • Cookie names resolve to __Host- / __Secure- / bare from the cookie policy, and every read tries __Host- → __Secure- → bare.
  • Errors are {"error": "<message>", "code": "<CODE>"}; a revoked session is 401 {"code": "SESSION_REVOKED"}, the signal both browser clients use to stop refreshing and log out.
  • CSRF is double-submit on X-CSRF-Token, enforced for cookie-authenticated unsafe methods only. There is no /csrf endpoint: the cookie is distributed by the router-level middleware.

Parity Snapshot vs awesome-node-auth

Capability Status in awesome-go-auth Notes
Auth strategies (email/password, magic link, SMS OTP, TOTP 2FA, OAuth linking) ✅ Implemented OAuth + account linking in oauth.go.
Token management (cookie/bearer, access/refresh rotation, secure cookies) ✅ Implemented Adapter HTTP supportano cookie e bearer; rotation attiva.
Identity Provider (IdP) mode (RS256 + JWKS + resource server validation) ✅ Implemented OIDC IdP con discovery, authorize, token, userinfo e JWKS.
Stateful sessions ✅ Implemented Revoca/rotation + Config.SessionCheckOn (allcalls/refresh/none).
Dynamic email templates + UI i18n fallback ✅ Implemented Template mail + fallback i18n built-in.
CSRF protection ✅ Implemented CSRFMiddleware cookie+header double-submit.
Account management ✅ Implemented Register, UpdateProfile, DeleteAccount, password/email lifecycle.
Account linking ✅ Implemented Pending link + linked account store.
RBAC ✅ Implemented RolesPermissionsStore tenant-aware.
Multi-tenancy ✅ Implemented TenantStore e membership utente↔tenant.
Admin panel ✅ Implemented ServeAdminUI() embedded.
Built-in UI + auth runtime (auth.js) ✅ Implemented ServeAuthUI() + ServeAuthJS().
Client libraries compatibility (Angular + Flutter) ✅ Implemented Cookie+CSRF browser e bearer per native client.
Event-driven tooling (event bus, SSE, inbound/outbound webhooks, telemetry, notify channels) ✅ Implemented Event bus, SSE, webhooks, telemetry, notify.
API keys (M2M) ✅ Implemented APIKeyService + middleware API key.
OpenAPI / Swagger docs ✅ Implemented GenerateOpenAPISpec.
MCP server (awesome-node-auth-mcp-server) ➖ Out of scope Fuori scope di parity per questa libreria.

Documentation

Index

Constants

View Source
const (
	SessionCheckOnAllCalls = "allcalls"
	SessionCheckOnRefresh  = "refresh"
	SessionCheckOnNone     = "none"
)
View Source
const (
	EmailVerificationModeNone   = "none"
	EmailVerificationModeLazy   = "lazy"
	EmailVerificationModeStrict = "strict"
)

Email verification modes accepted by Config.EmailVerificationMode.

none   : Register marks the address verified, so verification never comes up
         on the self-registration path. An address left unverified by some
         other route (admin provisioning, a data import, a custom UserStore
         whose column defaults to false) is still refused at login. This is
         the default and is exactly how the service behaved before the mode
         existed.
lazy   : Register leaves the address unverified and login is allowed anyway.
         This is the only mode in which an unverified address may log in.
strict : Register leaves the address unverified and login is refused until
         the address is confirmed.
View Source
const (
	CodeOAuthStateInvalid      = "OAUTH_STATE_INVALID"
	CodeOAuthTokenExchangeFail = "OAUTH_TOKEN_EXCHANGE_FAILED"
	CodeOAuthProfileFailed     = "OAUTH_PROFILE_FAILED"
	CodeEmailRequired          = "EMAIL_REQUIRED"
	CodeTokenRequired          = "TOKEN_REQUIRED"
	CodeInvalidLinkToken       = "INVALID_LINK_TOKEN"
	CodeLinkTokenExpired       = "LINK_TOKEN_EXPIRED"
	CodeUnauthorized           = "UNAUTHORIZED"
)

Error codes for this route group. The first five are the reference's own literals; OAUTH_STATE_INVALID has no counterpart because the reference never rejects a state.

View Source
const (
	CodeTempTokenRequired = "TEMP_TOKEN_REQUIRED"
	CodeInvalidTempToken  = "INVALID_TEMP_TOKEN"
	// CodeInvalidAccessToken is what /2fa/verify answers for a bad step-up
	// token where its siblings answer INVALID_TEMP_TOKEN. See
	// HTTPErrInvalidStepUpToken.
	CodeInvalidAccessToken = "INVALID_ACCESS_TOKEN"
	CodeInvalidMagicLink   = "INVALID_MAGIC_LINK"
	CodeTokenMismatch      = "TOKEN_MISMATCH"
	CodePhoneNotSet        = "PHONE_NOT_SET"
)

Error codes for this section of the contract.

View Source
const (
	// AuthStrategyHeader opts a caller into bearer delivery. The reference reads
	// this header in exactly one place and compares the value exactly, so the
	// match here is case-sensitive on purpose.
	AuthStrategyHeader = "X-Auth-Strategy"
	// AuthStrategyBearer is the only recognised AuthStrategyHeader value.
	AuthStrategyBearer = "bearer"
	// CSRFHeaderName carries the double-submit token.
	CSRFHeaderName = "X-CSRF-Token"
)
View Source
const (
	AccessTokenCookieName  = "accessToken"
	RefreshTokenCookieName = "refreshToken"
	CSRFTokenCookieName    = "csrf-token"
)

Cookie base names. The prefix a cookie actually carries is resolved by CookieOptions.CookieName.

View Source
const (
	CodeInvalidCredentials  = "INVALID_CREDENTIALS"
	CodeEmailNotVerified    = "EMAIL_NOT_VERIFIED"
	CodeSessionRevoked      = "SESSION_REVOKED"
	CodeInvalidToken        = "INVALID_TOKEN"
	CodeInvalidRefreshToken = "INVALID_REFRESH_TOKEN"
	CodeCSRFInvalid         = "CSRF_INVALID"
	CodeTwoFactorRequired   = "2FA_REQUIRED"
	CodeUserNotFound        = "USER_NOT_FOUND"
	CodeNotImplemented      = "NOT_IMPLEMENTED"
)

Error codes emitted by the HTTP layer. The first block is the reference catalog; a client keying on any of these keeps working across the family.

View Source
const (
	CodeUserExists   = "USER_EXISTS"
	CodeWeakPassword = "WEAK_PASSWORD"
	CodeInvalidBody  = "INVALID_BODY"
)

Codes with no counterpart in the reference catalog. The reference delegates registration to a host-supplied callback and therefore has no code of its own for a duplicate address or a rejected password; these fill that hole. No shipped client branches on them.

View Source
const CodePasswordRequired = "PASSWORD_REQUIRED"

CodePasswordRequired is the one coded error in this group. Every other failure here is code-less in the reference, and a client is documented not to pattern-match on an absent code.

View Source
const DefaultAPIPrefix = "/auth"

DefaultAPIPrefix is where the auth routes mount unless told otherwise.

View Source
const StepUpMode = "2fa"

StepUpMode is the only value of the `mode` field the routes special-case.

Variables

View Source
var (
	ErrInvalidCredentials  = errors.New("auth: invalid credentials")
	ErrUserExists          = errors.New("auth: user already exists")
	ErrInvalidToken        = errors.New("auth: invalid token")
	ErrSessionNotFound     = errors.New("auth: session not found")
	ErrSessionRevoked      = errors.New("auth: session revoked")
	ErrWeakPassword        = errors.New("auth: password is too weak")
	ErrFeatureNotSupported = errors.New("auth: feature not supported by configured stores")
	ErrEmailNotVerified    = errors.New("auth: email not verified")
	ErrInvalidCode         = errors.New("auth: invalid code")
	ErrTwoFactorRequired   = errors.New("auth: two-factor authentication required")
	ErrAlreadyExists       = errors.New("auth: resource already exists")
	ErrTenantNotFound      = errors.New("auth: tenant not found")
	ErrRoleNotFound        = errors.New("auth: role not found")
)
View Source
var (
	// HTTPErrOAuthState is the fix for #10. It is a 401 with a code because a
	// browser mid-flow has to be able to tell a tampered state from a provider
	// failure, and because the reference's own choice on this route is a JSON
	// error rather than a redirect (wire-contract.md §4, gap 5).
	HTTPErrOAuthState = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired OAuth state", Code: CodeOAuthStateInvalid}

	// Coded, unlike its passwordless sibling. The reference throws AuthError with
	// EMAIL_REQUIRED here (auth.router.ts:1498) but answers uncoded on
	// /magic-link/send (auth.router.ts:1111), so the two are not interchangeable.
	// See HTTPErrPasswordlessEmailRequired.
	HTTPErrLinkEmailRequired  = HTTPError{Status: http.StatusBadRequest, Message: "email is required", Code: CodeEmailRequired}
	HTTPErrTokenRequired      = HTTPError{Status: http.StatusBadRequest, Message: "token is required", Code: CodeTokenRequired}
	HTTPErrInvalidLinkToken   = HTTPError{Status: http.StatusBadRequest, Message: "Invalid account-link token", Code: CodeInvalidLinkToken}
	HTTPErrLinkTokenExpired   = HTTPError{Status: http.StatusBadRequest, Message: "Account-link token has expired", Code: CodeLinkTokenExpired}
	HTTPErrTargetUserNotFound = HTTPError{Status: http.StatusNotFound, Message: "Target user not found", Code: CodeUserNotFound}

	// HTTPErrLinkUnauthorized is the reference's second identity-resolution
	// failure. Its first one ("Authentication required", emitted when no pending
	// link store is configured at all) is unreachable here: this port resolves
	// both the stash and the link-token storage through the same
	// PendingLinkStore, so an absent store answers NOT_IMPLEMENTED before
	// identity resolution is ever attempted.
	HTTPErrLinkUnauthorized = HTTPError{Status: http.StatusUnauthorized, Message: "Authentication required or no pending link found", Code: CodeUnauthorized}
)

The error catalog for this group. Messages are the reference's strings where the reference has one.

View Source
var (
	HTTPErrTempTokenRequired = HTTPError{Status: http.StatusBadRequest, Message: "tempToken is required for 2FA mode", Code: CodeTempTokenRequired}
	HTTPErrInvalidTempToken  = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired temp token", Code: CodeInvalidTempToken}

	// HTTPErrInvalidStepUpToken is /2fa/verify's answer to the failure the
	// magic-link and SMS routes answer with HTTPErrInvalidTempToken. The
	// reference verifies the tempToken there through verifyAccessToken and lets
	// its AuthError through unchanged (auth.router.ts:862 vs :1096, :1144,
	// :1201, :1264), so the same failure has two codes depending on the 2FA
	// method. It is reproduced rather than harmonised: a client that special-
	// cases TOTP today would break if this route started agreeing with its
	// siblings.
	HTTPErrInvalidStepUpToken = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired access token", Code: CodeInvalidAccessToken}

	// Uncoded on purpose. The reference emits this same message WITHOUT a code on
	// /magic-link/send (auth.router.ts:1111) but WITH code EMAIL_REQUIRED on
	// /link-request (auth.router.ts:1498), so this and HTTPErrLinkEmailRequired are
	// not interchangeable. Do not merge them into one entry.
	HTTPErrPasswordlessEmailRequired = HTTPError{Status: http.StatusBadRequest, Message: "email is required"}
	HTTPErrUserIDOrEmailRequired     = HTTPError{Status: http.StatusBadRequest, Message: "userId or email is required"}
	HTTPErrUserIDRequired            = HTTPError{Status: http.StatusBadRequest, Message: "userId is required"}
	HTTPErrPhoneNotSet               = HTTPError{Status: http.StatusBadRequest, Message: "User does not have a phone number configured", Code: CodePhoneNotSet}

	HTTPErrInvalidSMSCode = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired SMS code"}

	// HTTPErrInvalidMagicLink covers both of the reference's magic-link
	// failures. The reference splits them — INVALID_MAGIC_LINK for a token it
	// cannot find, MAGIC_LINK_EXPIRED for one past its TTL
	// (magic-link.strategy.ts:41-49) — but Service.VerifyMagicLink collapses
	// the two into ErrInvalidToken, so the expiry code is not reachable from
	// here. Splitting the sentinel is the fix; both are 401 either way.
	HTTPErrInvalidMagicLink = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid magic link token", Code: CodeInvalidMagicLink}
	HTTPErrTokenMismatch    = HTTPError{Status: http.StatusUnauthorized, Message: "Token mismatch", Code: CodeTokenMismatch}

	// The two "Invalid TOTP code" entries differ only in status: /2fa/verify
	// answers 401 (auth.router.ts:869-872), /2fa/verify-setup answers 400
	// (:847-850).
	HTTPErrInvalidTOTPCode      = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid TOTP code"}
	HTTPErrInvalidTOTPSetupCode = HTTPError{Status: http.StatusBadRequest, Message: "Invalid TOTP code"}
	HTTPErrTOTPNotSetUp         = HTTPError{Status: http.StatusBadRequest, Message: "User not found or 2FA not set up"}

	HTTPErrTwoFactorRequiredForUser  = HTTPError{Status: http.StatusForbidden, Message: "Cannot disable 2FA: required for your account", Code: CodeTwoFactorRequired}
	HTTPErrTwoFactorRequiredByPolicy = HTTPError{Status: http.StatusForbidden, Message: "Cannot disable 2FA: required by system policy", Code: CodeTwoFactorRequired}
)

The catalog entries these routes write. Several carry no code: the reference answers those inline rather than through its AuthError path, and the omission is exact, not an oversight (wire-contract §3, "Shared mechanics").

View Source
var (
	// HTTPErrNoAccessToken and HTTPErrInvalidAccessToken are 403 and carry no
	// code. That looks wrong next to the 401s around them, but it is what the
	// reference middleware emits and the clients are built against it.
	HTTPErrNoAccessToken      = HTTPError{Status: http.StatusForbidden, Message: "No access token provided"}
	HTTPErrInvalidAccessToken = HTTPError{Status: http.StatusForbidden, Message: "Invalid or expired access token"}

	// HTTPErrSessionRevoked is the family's fast-logout signal: both browser
	// clients stop refreshing and log out on this exact code, so nothing else
	// may be returned for a revoked session.
	HTTPErrSessionRevoked = HTTPError{Status: http.StatusUnauthorized, Message: "Session has been revoked", Code: CodeSessionRevoked}

	HTTPErrNoRefreshToken      = HTTPError{Status: http.StatusUnauthorized, Message: "No refresh token provided"}
	HTTPErrInvalidRefreshToken = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid refresh token"}
	HTTPErrExpiredRefreshToken = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired refresh token", Code: CodeInvalidRefreshToken}

	HTTPErrInvalidCredentials = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid credentials", Code: CodeInvalidCredentials}
	HTTPErrEmailNotVerified   = HTTPError{Status: http.StatusForbidden, Message: "Email address is not verified", Code: CodeEmailNotVerified}
	HTTPErrTwoFactorRequired  = HTTPError{Status: http.StatusForbidden, Message: "Two-factor authentication required", Code: CodeTwoFactorRequired}
	HTTPErrInvalidToken       = HTTPError{Status: http.StatusUnauthorized, Message: "Invalid or expired token", Code: CodeInvalidToken}
	HTTPErrCSRFInvalid        = HTTPError{Status: http.StatusForbidden, Message: "CSRF token validation failed", Code: CodeCSRFInvalid}
	// HTTPErrUserNotFound carries no code: every reference site that emits the
	// literal "User not found" emits it code-less (GET /me, /change-password,
	// /sms/send, the magic-link 2fa branch). The one coded USER_NOT_FOUND in the
	// reference is a different message ("Target user not found", /link-request),
	// so a route reusing this value must not gain a code by accident.
	HTTPErrUserNotFound   = HTTPError{Status: http.StatusNotFound, Message: "User not found"}
	HTTPErrNotImplemented = HTTPError{Status: http.StatusNotImplemented, Message: "Feature not supported by the configured stores", Code: CodeNotImplemented}

	// HTTPErrInternal deliberately carries neither a code nor the underlying
	// error text: an unexpected failure must not describe itself to the caller.
	HTTPErrInternal = HTTPError{Status: http.StatusInternalServerError, Message: "Internal server error"}

	HTTPErrUserExists   = HTTPError{Status: http.StatusConflict, Message: "User already exists", Code: CodeUserExists}
	HTTPErrWeakPassword = HTTPError{Status: http.StatusBadRequest, Message: "Password is too weak", Code: CodeWeakPassword}
	HTTPErrInvalidBody  = HTTPError{Status: http.StatusBadRequest, Message: "Invalid request body", Code: CodeInvalidBody}
)

The error catalog. Messages are the reference's own strings, because a client that surfaces the server message must not read differently per implementation.

View Source
var (
	// HTTPErrResetTokenStoreMissing and the two below it are 500, not 501: the
	// reference reports a store that cannot perform the flow as an internal
	// error (auth.router.ts:805-808, 937-940, 1000-1003, 1042-1045). A client
	// that treats 501 as "feature absent" and 500 as "server broken" therefore
	// has to see 500 here, which is why these routes do not go through
	// HTTPErrorFor's ErrFeatureNotSupported → 501 mapping.
	HTTPErrResetTokenStoreMissing        = HTTPError{Status: http.StatusInternalServerError, Message: "UserStore does not implement findByResetToken"}
	HTTPErrEmailVerificationStoreMissing = HTTPError{Status: http.StatusInternalServerError, Message: "UserStore does not implement email verification"}
	HTTPErrChangeEmailStoreMissing       = HTTPError{Status: http.StatusInternalServerError, Message: "UserStore does not implement change-email"}

	HTTPErrInvalidResetToken       = HTTPError{Status: http.StatusBadRequest, Message: "Invalid reset token"}
	HTTPErrInvalidVerifyToken      = HTTPError{Status: http.StatusBadRequest, Message: "Invalid verification token"}
	HTTPErrInvalidEmailChangeToken = HTTPError{Status: http.StatusBadRequest, Message: "Invalid email-change token"}
	HTTPErrVerifyTokenRequired     = HTTPError{Status: http.StatusBadRequest, Message: "Token is required"}
	HTTPErrEmailAlreadyVerified    = HTTPError{Status: http.StatusBadRequest, Message: "Email is already verified"}

	// HTTPErrCurrentPasswordIncorrect is a 401 with no code, unlike the coded
	// INVALID_CREDENTIALS a failed login returns. Reproduced as-is even though it
	// is a known hazard: /change-password is absent from the Angular
	// interceptor's no-retry list, so a wrong current password costs a pointless
	// refresh-and-replay round trip (wire-contract §2, [MISMATCH]).
	HTTPErrCurrentPasswordIncorrect = HTTPError{Status: http.StatusUnauthorized, Message: "Current password is incorrect"}
	HTTPErrNewPasswordRequired      = HTTPError{Status: http.StatusBadRequest, Message: "New password is required"}

	// HTTPErrEmailInUse carries no code, so a client cannot branch on it — only
	// on the 409 status.
	HTTPErrEmailInUse = HTTPError{Status: http.StatusConflict, Message: "Email address is already in use"}

	HTTPErrPasswordRequired = HTTPError{
		Status:  http.StatusForbidden,
		Message: "You must set a password before you can change your email address.",
		Code:    CodePasswordRequired,
	}
)

The §2 error catalog. Messages are the reference's own strings, down to the "UserStore does not implement …" wording it uses for an incapable store.

View Source
var ErrMagicLinkOwnerMismatch = errors.New("auth: magic link belongs to another user")

ErrMagicLinkOwnerMismatch is a valid magic link presented as a second factor for somebody else: the link verified, but it does not belong to the user the step-up token names. It is a distinct sentinel rather than ErrInvalidToken because the wire answer differs (TOKEN_MISMATCH, not INVALID_MAGIC_LINK) and because the two failures mean different things — one is a bad link, the other is a good link on the wrong account.

View Source
var HTTPErrSessionNotFound = HTTPError{Status: http.StatusNotFound, Message: "Session not found"}

HTTPErrSessionNotFound is the answer to DELETE /sessions/{handle} for a handle that does not exist and for one that belongs to somebody else — deliberately the same body, so the route cannot be used to probe for live sessions (auth.router.ts:762-767). It is written by the route rather than mapped in HTTPErrorFor, because ErrSessionNotFound means "unusable refresh token" (401) everywhere else.

Functions

func APIKeyMiddleware

func APIKeyMiddleware(store APIKeyStore, requiredScopes []string) func(http.Handler) http.Handler

func AccessTokenFromRequest added in v0.2.0

func AccessTokenFromRequest(r *http.Request) string

AccessTokenFromRequest applies the reference's extraction order: the Authorization header first, then the access-token cookie.

func BearerToken added in v0.2.0

func BearerToken(header string) string

BearerToken extracts the token from an Authorization header.

func CSRFMiddleware

func CSRFMiddleware(cfg HTTPConfig) func(http.Handler) http.Handler

CSRFMiddleware distributes and enforces the double-submit token.

Distribution mirrors the reference's router-level auto-init: a request that arrives without a readable CSRF cookie gets a fresh one.

Enforcement follows the reference matrix. A request is checked when it is an unsafe method, on a mounted route, that route is not exempt, the request presents no bearer credential, and either it carries an access-token cookie or the route does its own manual check in the reference:

enforced = mutating && mounted && !exempt && !usingBearerCredential
           && (accessTokenCookiePresent || manualCheckRoute)

Safe methods, bearer requests, the unauthenticated routes in csrfExemptRoutes and — for every route but csrfManualCheckRoutes — requests with no cookie credential are passed through. The last of those is the reference's ordering: see csrfEnforced.

func CookieValue added in v0.2.0

func CookieValue(r *http.Request, base string) string

CookieValue reads a cookie honouring the family's __Host- → __Secure- → bare priority. Every read site must go through it, otherwise a deployment that switches Secure on stops recognising its own cookies.

func DecodeOptionalJSON added in v0.2.0

func DecodeOptionalJSON(w http.ResponseWriter, r *http.Request, dst any) bool

DecodeOptionalJSON decodes a JSON request body into dst, tolerating an absent or empty one, and writes the error envelope when the body is present but malformed.

Every route in this group accepts an empty body: the reference runs express.json(), which leaves req.body as {} rather than failing, and the Flutter client posts /send-verification-email with no body at all. Rejecting a malformed body with 400 INVALID_BODY is the port's own convention, already applied by /register and /login.

func DecodeOptionalJSONBody added in v0.2.0

func DecodeOptionalJSONBody(r *http.Request, dst any) error

DecodeOptionalJSONBody decodes r's JSON body into dst, treating an absent or zero-length body as the empty object rather than as an error.

The reference runs behind express.json(), which leaves req.body = {} when the request carried no body at all; PATCH /profile and POST /add-phone then read optional fields off it, so a bodyless call is a 200 no-op there and not a 400 (§3.5, §3.6). Without a shared decoder the four adapters disagree about exactly this: echo's Bind returns nil for a zero-length body, while encoding/json and gin's ShouldBindJSON both surface io.EOF as a decode error.

A body that is present but is not valid JSON is still an error: express.json() rejects that too, and tolerating it would let a typo'd payload be silently applied as "change nothing".

func GenerateOpenAPISpec

func GenerateOpenAPISpec(info OpenAPIInfo) map[string]any

GenerateOpenAPISpec returns an OpenAPI 3.0 spec for awesome-go-auth endpoints.

func IsBearerRequest added in v0.2.0

func IsBearerRequest(r *http.Request) bool

IsBearerRequest reports whether the caller asked for token delivery in the response body instead of cookies.

func OAuthNotConfiguredError added in v0.2.0

func OAuthNotConfiguredError(provider string) error

OAuthNotConfiguredError is the "<Provider> OAuth not configured" stub as an error, so an adapter can answer it without reaching into the flow methods.

func RefreshTokenFromRequest added in v0.2.0

func RefreshTokenFromRequest(r *http.Request) string

RefreshTokenFromRequest applies the reference's acceptance order for /refresh: a refreshToken in the body wins, otherwise the cookie. A missing or empty body is not an error — cookie-mode refresh sends no body at all.

func ServeAdminUI

func ServeAdminUI() http.Handler

ServeAdminUI returns an http.Handler serving the embedded admin dashboard HTML.

func ServeAuthJS

func ServeAuthJS() http.Handler

ServeAuthJS returns an http.Handler serving the embedded auth.js browser SDK.

func ServeAuthUI

func ServeAuthUI() http.Handler

ServeAuthUI returns an http.Handler serving the embedded auth UI HTML (login/register).

func ServeSSE

func ServeSSE(hub *SseHub, channel string) http.HandlerFunc

ServeSSE is an http.HandlerFunc that streams SSE to a connected client. channel is typically the authenticated user's ID.

func SessionHandleParam added in v0.2.0

func SessionHandleParam(r *http.Request, raw string) string

SessionHandleParam normalises the {handle} path parameter: exactly one percent-decode, the way the reference's decodeURIComponent does (auth.router.ts:761). One family client URI-escapes the handle and the other posts it verbatim, so both forms have to name the same session.

The decode deliberately does not start from the router's own path parameter, because the four routers disagree about what they hand over: chi (via r.PathValue, which chi populates from its escaped route path) and echo (which routes on r.URL.RawPath when it is set) pass the segment still encoded, while net/http and gin pass it already decoded. Unescaping whatever arrives is therefore one decode on two adapters and two on the other two, and no inspection of the string can tell the cases apart — "ses_50%25off" is both a valid decoded handle and the encoded form of "ses_50%off".

r.URL.EscapedPath() is router-independent (RawPath when the request carried one, the re-encoded Path otherwise), so taking the last segment of it and decoding once gives all four adapters the same handle. raw is kept as the fallback for a path that yields no segment.

A handle containing an encoded slash is still not recoverable — the path has already been split on it — but the handles this package issues are "ses_" plus hex.

func TwoFactorMode added in v0.2.0

func TwoFactorMode(mode string) bool

TwoFactorMode reports whether a magic-link or SMS request asked for the second-factor branch.

The comparison is literal and case-sensitive because the reference's is (`mode === '2fa'`, auth.router.ts:1087, :1134, :1192, :1255): "login", the empty string, and anything else all take the login branch. That is what makes the two shipped clients interchangeable — Angular sends mode:"login", Flutter omits the field — so loosening this comparison silently reroutes one of them.

func VerifyEmailToken added in v0.2.0

func VerifyEmailToken(r *http.Request) string

VerifyEmailToken reads the token GET /verify-email carries. It is a query parameter, not a body field, and the route is the only GET in this group.

The value is used exactly as sent: the reference tests it with `if (!token)` (auth.router.ts:972), so a token of one space is truthy there and fails the lookup with 400 "Invalid verification token" rather than 400 "Token is required". Trimming here would be friendlier but would move that request into the other error, so the reference's answer wins.

func VerifyWebhookSignature

func VerifyWebhookSignature(secret string, body []byte, sigHeader string) error

VerifyWebhookSignature validates an inbound webhook signature.

func WriteHTTPError added in v0.2.0

func WriteHTTPError(w http.ResponseWriter, e HTTPError)

WriteHTTPError writes the error envelope.

func WriteJSON added in v0.2.0

func WriteJSON(w http.ResponseWriter, status int, payload any)

WriteJSON writes payload as the entire response body.

func WriteServiceError added in v0.2.0

func WriteServiceError(w http.ResponseWriter, err error)

WriteServiceError maps a service sentinel and writes it.

func WriteSuccess added in v0.2.0

func WriteSuccess(w http.ResponseWriter, status int, extra map[string]any)

WriteSuccess writes the {"success": true} envelope, merging any extra top-level fields a route contributes.

Types

type APIKeyRecord

type APIKeyRecord struct {
	ID         string
	Prefix     string
	Name       string
	ServiceID  string
	KeyHash    string
	Scopes     []string
	AllowedIPs []string
	IsActive   bool
	ExpiresAt  *time.Time
	LastUsedAt *time.Time
}

APIKeyRecord stores hashed key material and policy constraints.

type APIKeyService

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

APIKeyService issues and verifies API keys. Create stores a bcrypt hash of the key, so it carries the same cost knob as Config.BcryptCost — an operator who raises the password cost would otherwise still get key hashes pinned at the default, with no way to reach them.

func NewAPIKeyService

func NewAPIKeyService(bcryptCost int) *APIKeyService

NewAPIKeyService returns a service that hashes new keys at bcryptCost.

As with Config.BcryptCost, zero means unset and resolves to bcrypt.DefaultCost. Verify does not hash, so a service used only for verification may be built with 0 regardless of the cost its keys were created at: bcrypt reads the cost out of the stored hash.

func (*APIKeyService) Create

func (s *APIKeyService) Create(ctx context.Context, store APIKeyStore, name, serviceID string, scopes, allowedIPs []string, expiresAt *time.Time) (rawKey string, record APIKeyRecord, err error)

func (*APIKeyService) Verify

func (s *APIKeyService) Verify(ctx context.Context, store APIKeyStore, rawKey string, ip string, requiredScopes []string) (APIKeyRecord, error)

type APIKeyStore

type APIKeyStore interface {
	Save(ctx context.Context, key APIKeyRecord) error
	FindByPrefix(ctx context.Context, prefix string) (APIKeyRecord, error)
	Revoke(ctx context.Context, id string) error
	UpdateLastUsed(ctx context.Context, id string, when time.Time) error
}

APIKeyStore persists and resolves API keys.

type AddPhoneInput added in v0.2.0

type AddPhoneInput struct {
	UserID      string
	TenantID    string
	PhoneNumber string
}

AddPhoneInput is the payload behind POST /add-phone. An empty PhoneNumber clears the number, as the reference's nullable phoneNumber does.

type Auth

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

Auth is the top-level entrypoint configured with functional options.

func New

func New(opts ...Option) (*Auth, error)

New creates a configured Auth instance from the package defaults.

func NewWithConfig added in v0.2.0

func NewWithConfig(cfg Config, opts ...Option) (*Auth, error)

NewWithConfig creates a configured Auth instance from a pre-populated Config, with any Options applied in order on top of it.

New builds its Config internally and the exported Option set has always been narrower than Config, so fields with no matching Option — the individual TTLs, EmailVerificationMode, and until now BcryptCost — were unreachable from outside the package (#25). Accepting the struct closes that whole class of gap at once and cannot drift as Config grows, which one-Option-per-field would. The Options remain as sugar and still compose with a supplied Config.

cfg is validated by NewService exactly as New's is; a hand-built Config that omits, say, the TTLs is rejected rather than silently defaulted.

func (*Auth) ChangePassword added in v0.2.0

func (a *Auth) ChangePassword(ctx context.Context, in ChangePasswordInput) error

ChangePassword performs POST /change-password.

A passwordless account — OAuth-only, or magic-link-only — skips the current-password comparison entirely and may set an initial password by supplying newPassword alone: in the reference the compare sits inside `if (user.password)` and the `else if` only fires when *both* fields are falsy, so such a caller falls through to the hash-and-store (wire-contract §2 "Passwordless-account path", auth.router.ts:916-928). It is the only way an account with no password ever acquires one.

Service.ChangePassword compares unconditionally (service.go:276), and verifyPassword("", "") is a bcrypt error, so delegating the passwordless case to it reports a password the account does not have as incorrect. The passwordless half is therefore handled here rather than by widening Service.ChangePassword: a library consumer calling the service directly may deliberately not want a caller setting a password without presenting one, and this PR's contract is the HTTP surface. The service-level divergence is filed upstream.

func (*Auth) CleanupExpiredSessions added in v0.2.0

func (a *Auth) CleanupExpiredSessions(ctx context.Context) (int, error)

CleanupExpiredSessions delegates to Service.CleanupExpiredSessions.

func (*Auth) ConfirmEmailChange added in v0.2.0

func (a *Auth) ConfirmEmailChange(ctx context.Context, in ConfirmEmailChangeInput) error

ConfirmEmailChange delegates to Service.ConfirmEmailChange.

func (*Auth) DeleteAccount

func (a *Auth) DeleteAccount(ctx context.Context, in DeleteAccountInput) error

DeleteAccount delegates to Service.DeleteAccount.

func (*Auth) DisableTOTP added in v0.2.0

func (a *Auth) DisableTOTP(ctx context.Context, userID, tenantID string) error

DisableTOTP delegates to Service.DisableTOTP.

func (*Auth) FindUser added in v0.2.0

func (a *Auth) FindUser(ctx context.Context, userID, email, tenantID string) (User, error)

FindUser delegates to Service.FindUser.

func (*Auth) ForgotPassword added in v0.2.0

func (a *Auth) ForgotPassword(ctx context.Context, in ForgotPasswordInput) (string, error)

ForgotPassword delegates to Service.ForgotPassword. The returned reset token is the credential the host application has to deliver: this port has no mail sender wired into Config, so no adapter route emails it and no adapter route puts it in a response body.

func (*Auth) IssueTempToken added in v0.2.0

func (a *Auth) IssueTempToken(ctx context.Context, user User) (string, error)

IssueTempToken delegates to Service.IssueTempToken.

func (*Auth) IssueTokensForUser added in v0.2.0

func (a *Auth) IssueTokensForUser(ctx context.Context, userID, tenantID string) (User, AuthTokens, error)

IssueTokensForUser delegates to Service.IssueTokensForUser.

func (*Auth) LinkRequest added in v0.2.0

func (a *Auth) LinkRequest(ctx context.Context, in LinkRequestInput) (LinkRequestResult, error)

LinkRequest issues a single-use account-link token and hands the verification link to the configured delivery hook.

func (*Auth) LinkVerify added in v0.2.0

func (a *Auth) LinkVerify(ctx context.Context, in LinkVerifyInput) (LinkVerifyResult, error)

LinkVerify consumes an account-link token and writes the association.

func (*Auth) ListLinkedAccounts added in v0.2.0

func (a *Auth) ListLinkedAccounts(ctx context.Context, userID string) ([]PublicLinkedAccount, error)

ListLinkedAccounts returns a user's provider associations, always as a non-nil slice so the JSON is [] rather than null.

func (*Auth) ListSessions added in v0.2.0

func (a *Auth) ListSessions(ctx context.Context, userID, tenantID string) ([]Session, error)

ListSessions returns a user's *active* sessions, which is what the reference's GET /sessions serves: ISessionStore.getSessionsForUser is documented "Return all active sessions for the given user" (session-store.interface.ts:58-61), and because revokeSession is documented to "Invalidate (delete) a single session" (:70-76) — which the family's own store does literally, `async revokeSession(h) { this.sessions.delete(h); }` (ng-awesome-node-auth/src/server/in-memory-session-store.ts:29-31) — a revoked session is simply gone from the next list.

This port's MemorySessionStore tombstones instead of deleting, and correctly so: /refresh needs the tombstone to answer 401 SESSION_REVOKED rather than "not found". The filter therefore has to live above the store, and this is the accessor the routes go through.

Expired-but-not-revoked sessions are deliberately NOT filtered. The reference's store does not filter them out of getSessionsForUser either — only deleteExpiredSessions removes them — so a device-manager UI sees them there too.

func (*Auth) Login

func (a *Auth) Login(ctx context.Context, in LoginInput) (User, AuthTokens, error)

Login delegates to Service.Login.

func (*Auth) Logout

func (a *Auth) Logout(ctx context.Context, refreshToken string) error

Logout delegates to Service.Logout.

func (*Auth) LogoutAccessToken added in v0.2.0

func (a *Auth) LogoutAccessToken(ctx context.Context, accessToken string) error

LogoutAccessToken revokes the session an access token belongs to. It is the revocation path a cookie client's logout takes, since the refresh cookie is out of scope for <prefix>/logout whenever it is path-scoped.

func (*Auth) LogoutRequest added in v0.2.0

func (a *Auth) LogoutRequest(ctx context.Context, r *http.Request)

LogoutRequest ends the session the caller presents, best effort, and is the single logout revocation path all four adapters call.

The order matters. A refresh token in the body or the refresh cookie revokes the whole session, so it is tried first. But the refresh cookie is scoped to <prefix>/refresh in every configuration whose name does not resolve to __Host- — a Domain, or a non-root cookie Path — and a browser therefore sends nothing at all to <prefix>/logout. That is why the reference logout reads the *access* token cookie (auth.router.ts:592) and revokes payload.sid: the access cookie is scoped to "/" and is always in scope. Without the fallback, logout answers 200 {"success":true}, clears the cookies, and leaves the refresh token and the session live server-side.

Every failure is swallowed: an absent or already-unusable credential must still leave the caller logged out rather than stranded with live cookies.

func (*Auth) Me

func (a *Auth) Me(ctx context.Context, accessToken string) (User, error)

Me delegates to Service.Me.

func (*Auth) OAuthBegin added in v0.2.0

func (a *Auth) OAuthBegin(ctx context.Context, in OAuthBeginInput) (OAuthBeginResult, error)

OAuthBegin mints a signed state, derives the PKCE challenge and builds the provider authorization URL.

func (*Auth) OAuthComplete added in v0.2.0

func (a *Auth) OAuthComplete(ctx context.Context, in OAuthCompleteInput) (OAuthCompleteResult, error)

OAuthComplete verifies the state, exchanges the code with the PKCE verifier and resolves the user.

func (*Auth) OAuthConfigured added in v0.2.0

func (a *Auth) OAuthConfigured() bool

OAuthConfigured reports whether the OAuth routes have anything to serve. The adapters mount the routes unconditionally, so they need this to answer the reference's "not configured" stub.

func (*Auth) Refresh

func (a *Auth) Refresh(ctx context.Context, refreshToken string) (AuthTokens, error)

Refresh delegates to Service.Refresh.

func (*Auth) Register

func (a *Auth) Register(ctx context.Context, in RegisterInput) (User, AuthTokens, error)

Register delegates to Service.Register.

func (*Auth) RequestEmailChange added in v0.2.0

func (a *Auth) RequestEmailChange(ctx context.Context, in ChangeEmailRequestInput) (string, error)

RequestEmailChange delegates to Service.RequestEmailChange.

func (*Auth) ResetPassword added in v0.2.0

func (a *Auth) ResetPassword(ctx context.Context, in ResetPasswordInput) error

ResetPassword delegates to Service.ResetPassword.

func (*Auth) ResolveHTTPConfig added in v0.2.0

func (a *Auth) ResolveHTTPConfig(cfg HTTPConfig) HTTPConfig

ResolveHTTPConfig fills cfg's derived defaults from this instance's token lifetimes. Adapters call it once at mount time.

func (*Auth) RevokeUserSession added in v0.2.0

func (a *Auth) RevokeUserSession(ctx context.Context, userID, tenantID, sessionID string) error

RevokeUserSession delegates to Service.RevokeUserSession.

func (a *Auth) SendMagicLink(ctx context.Context, in MagicLinkSendInput) (string, error)

SendMagicLink delegates to Service.SendMagicLink.

func (*Auth) SendSMSCode added in v0.2.0

func (a *Auth) SendSMSCode(ctx context.Context, in SMSCodeSendInput) (string, error)

SendSMSCode delegates to Service.SendSMSCode.

func (*Auth) SendVerificationEmailToken added in v0.2.0

func (a *Auth) SendVerificationEmailToken(ctx context.Context, in EmailVerificationInput) (string, error)

SendVerificationEmailToken delegates to Service.SendVerificationEmailToken. An empty token with no error means the address is already verified.

func (*Auth) Service

func (a *Auth) Service() *Service

Service exposes the configured core service.

func (*Auth) StartTOTPEnrolment added in v0.2.0

func (a *Auth) StartTOTPEnrolment(ctx context.Context, userID, tenantID string) (TOTPSetup, error)

StartTOTPEnrolment delegates to Service.StartTOTPEnrolment.

func (*Auth) StepUpSubject added in v0.2.0

func (a *Auth) StepUpSubject(tempToken string, missing, invalid HTTPError) (TempTokenSubject, HTTPError, bool)

StepUpSubject resolves the tempToken a 2FA-mode request carries, reporting the envelope to write when it cannot.

The two error arguments are what keeps the reference's inconsistency reproducible without spreading it through the handlers: the magic-link and SMS routes pass HTTPErrTempTokenRequired and HTTPErrInvalidTempToken, while /2fa/verify passes HTTPErrInvalidStepUpToken for both — it has no missing-token branch at all, so an absent tempToken simply fails verification there.

func (*Auth) TwoFactorPolicy added in v0.2.0

func (a *Auth) TwoFactorPolicy() bool

TwoFactorPolicy reports whether the deployment requires a second factor of every user. It is the port's equivalent of the reference's settingsStore require2FA flag, which is what /2fa/disable refuses on.

func (*Auth) UnlinkAccount added in v0.2.0

func (a *Auth) UnlinkAccount(ctx context.Context, userID, provider, providerAccountID string) error

UnlinkAccount removes one provider association.

The reference performs no existence check and answers success unconditionally (wire-contract.md §4, DELETE /linked-accounts). This keeps that, and adds the ownership check the reference gets for free by passing the user id into its store: a link that belongs to somebody else is left alone.

func (*Auth) UpdatePhoneNumber added in v0.2.0

func (a *Auth) UpdatePhoneNumber(ctx context.Context, in AddPhoneInput) (User, error)

UpdatePhoneNumber delegates to Service.UpdatePhoneNumber.

func (*Auth) UpdateProfile

func (a *Auth) UpdateProfile(ctx context.Context, in UpdateProfileInput) (User, error)

UpdateProfile delegates to Service.UpdateProfile.

func (*Auth) VerifyEmail added in v0.2.0

func (a *Auth) VerifyEmail(ctx context.Context, in VerifyEmailInput) error

VerifyEmail delegates to Service.VerifyEmail.

func (a *Auth) VerifyMagicLink(ctx context.Context, in MagicLinkVerifyInput) (User, AuthTokens, error)

VerifyMagicLink delegates to Service.VerifyMagicLink.

func (*Auth) VerifyMagicLinkForUser added in v0.2.0

func (a *Auth) VerifyMagicLinkForUser(ctx context.Context, in MagicLinkVerifyInput, userID string) (User, AuthTokens, error)

VerifyMagicLinkForUser delegates to Service.VerifyMagicLinkForUser. The step-up branch of POST /magic-link/verify uses it so that a link belonging to another user is refused before a session exists.

func (*Auth) VerifySMSCode added in v0.2.0

func (a *Auth) VerifySMSCode(ctx context.Context, in SMSCodeVerifyInput) (User, AuthTokens, error)

VerifySMSCode delegates to Service.VerifySMSCode.

func (*Auth) VerifyTOTP added in v0.2.0

func (a *Auth) VerifyTOTP(ctx context.Context, userID, tenantID, code string) (User, AuthTokens, error)

VerifyTOTP delegates to Service.VerifyTOTP.

func (*Auth) VerifyTOTPSetup added in v0.2.0

func (a *Auth) VerifyTOTPSetup(ctx context.Context, userID, tenantID, secret, code string) error

VerifyTOTPSetup delegates to Service.VerifyTOTPSetup.

type AuthTokens

type AuthTokens struct {
	AccessToken  string
	RefreshToken string
	ExpiresIn    time.Duration
}

AuthTokens are the issued access/refresh pair.

type CSRFConfig

type CSRFConfig struct {
	// Enabled gates both enforcement and cookie distribution, mirroring the
	// reference's config.csrf.enabled.
	Enabled    bool
	CookieName string
	HeaderName string
}

CSRFConfig configures the cookie+header double-submit CSRF protection. The cookie attributes come from HTTPConfig.Cookies so that the CSRF cookie is serialised by the same matrix as the token cookies.

func DefaultCSRFConfig

func DefaultCSRFConfig() CSRFConfig

DefaultCSRFConfig returns defaults compatible with browser cookie auth flows.

type ChangeEmailRequestInput

type ChangeEmailRequestInput struct {
	UserID   string
	TenantID string
	NewEmail string
}

type ChangePasswordInput

type ChangePasswordInput struct {
	UserID          string
	TenantID        string
	CurrentPassword string
	NewPassword     string
}

type Config

type Config struct {
	Secret          string
	Issuer          string
	AccessTokenTTL  time.Duration
	RefreshTokenTTL time.Duration
	SessionCheckOn  string
	ClockSkew       time.Duration
	MinPasswordLen  int
	// BcryptCost is the bcrypt cost used for every password hash the library
	// writes. It is this port's spelling of the reference's
	// AuthConfig.bcryptSaltRounds; "cost" is what x/crypto/bcrypt calls the
	// parameter, and what bcrypt.DefaultCost, MinCost and MaxCost name.
	//
	// Zero means unset, and unset means bcrypt.DefaultCost — never
	// bcrypt.MinCost. A Config that was never told a cost must not silently get
	// the weakest one. Any other value must lie in
	// [bcrypt.MinCost, bcrypt.MaxCost]; validate rejects the rest.
	BcryptCost            int
	ResetTokenTTL         time.Duration
	MagicLinkTTL          time.Duration
	SMSCodeTTL            time.Duration
	EmailVerificationTTL  time.Duration
	EmailVerificationMode string
	EmailChangeTTL        time.Duration
	TempTokenTTL          time.Duration
	Require2FA            bool
	BuildTokenClaims      func(ctx context.Context, user User) (map[string]any, error)
	Logger                func(format string, args ...any)
}

Config contains security and token settings for the auth service.

func DefaultConfig

func DefaultConfig(secret string) Config

DefaultConfig returns secure defaults for development and production bootstrap.

type ConfirmEmailChangeInput

type ConfirmEmailChangeInput struct {
	Token string
}

type CookieOptions added in v0.2.0

type CookieOptions struct {
	Secure   bool
	SameSite http.SameSite
	Path     string
	Domain   string
	// RefreshTokenPath scopes the refresh cookie. Empty resolves to
	// <APIPrefix>/refresh, matching the reference's router-time default.
	RefreshTokenPath   string
	AccessTokenMaxAge  time.Duration
	RefreshTokenMaxAge time.Duration
	CSRFTokenMaxAge    time.Duration
}

CookieOptions is the cookie serialisation matrix. Every adapter writes cookies through it, so the attributes cannot diverge between frameworks.

func DefaultCookieOptions added in v0.2.0

func DefaultCookieOptions() CookieOptions

DefaultCookieOptions returns the port's defaults. Secure defaults to true — stricter than the reference's false — because every cookie here is either a credential or a CSRF token.

func (CookieOptions) ClearAuthCookies added in v0.2.0

func (o CookieOptions) ClearAuthCookies(w http.ResponseWriter, includeCSRF bool)

ClearAuthCookies expires every name variant of the auth cookies. Expiring a cookie requires an exact name+path+domain match, so a cookie written under a different prefix — by an older release, or before Secure was switched on — would otherwise survive logout.

func (CookieOptions) CookieName added in v0.2.0

func (o CookieOptions) CookieName(base string) string

CookieName resolves the prefix a cookie must carry: an insecure cookie stays bare, a secure cookie that is host-scoped to "/" earns __Host-, and any other secure cookie earns __Secure-. The refresh cookie is resolved from Path, not RefreshTokenPath, exactly as the reference does.

func (CookieOptions) SetAccessTokenCookie added in v0.2.0

func (o CookieOptions) SetAccessTokenCookie(w http.ResponseWriter, value string)

SetAccessTokenCookie writes the access-token cookie.

func (CookieOptions) SetCSRFTokenCookie added in v0.2.0

func (o CookieOptions) SetCSRFTokenCookie(w http.ResponseWriter, value string)

SetCSRFTokenCookie writes the CSRF cookie. It is deliberately not HttpOnly: double-submit requires the page to read it and mirror it into the header.

func (CookieOptions) SetRefreshTokenCookie added in v0.2.0

func (o CookieOptions) SetRefreshTokenCookie(w http.ResponseWriter, value string)

SetRefreshTokenCookie writes the refresh-token cookie.

type DeleteAccountInput

type DeleteAccountInput struct {
	UserID   string
	TenantID string
}

type EmailChangeStore

type EmailChangeStore interface {
	UpdateEmailChangeToken(ctx context.Context, userID, tenantID, pendingEmail, tokenHash string, expiry time.Time) error
	GetUserByEmailChangeTokenHash(ctx context.Context, tokenHash string) (User, error)
	ApplyEmailChange(ctx context.Context, userID, tenantID string) error
	ClearEmailChangeToken(ctx context.Context, userID, tenantID string) error
}

EmailChangeStore persists pending email change flow.

type EmailVerificationInput

type EmailVerificationInput struct {
	UserID   string
	TenantID string
}

type EmailVerificationStore

type EmailVerificationStore interface {
	UpdateEmailVerificationToken(ctx context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
	GetUserByEmailVerificationTokenHash(ctx context.Context, tokenHash string) (User, error)
	MarkEmailVerified(ctx context.Context, userID, tenantID string, verified bool) error
	ClearEmailVerificationToken(ctx context.Context, userID, tenantID string) error
}

EmailVerificationStore persists verification flow.

type Event

type Event struct {
	Name      string
	UserID    string
	TenantID  string
	Timestamp time.Time
	Data      map[string]any
}

Event is a generic auth domain event payload.

type EventBus

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

EventBus is a tiny in-process pub/sub for auth events.

func NewEventBus

func NewEventBus() *EventBus

func (*EventBus) Publish

func (b *EventBus) Publish(ev Event)

func (*EventBus) Subscribe

func (b *EventBus) Subscribe(event string, handler func(Event))

type ForgotPasswordInput

type ForgotPasswordInput struct {
	Email    string
	TenantID string
}

type HTTPConfig added in v0.2.0

type HTTPConfig struct {
	APIPrefix string
	Cookies   CookieOptions
	CSRF      CSRFConfig
}

HTTPConfig is the wire configuration an adapter mounts with. The four adapters take the same value, so a change here reaches all of them.

func DefaultHTTPConfig added in v0.2.0

func DefaultHTTPConfig() HTTPConfig

DefaultHTTPConfig returns the conventions an adapter uses when the host app does not configure them.

func (HTTPConfig) Prefix added in v0.2.0

func (c HTTPConfig) Prefix() string

Prefix returns the normalised mount prefix.

func (HTTPConfig) WriteTokens added in v0.2.0

func (c HTTPConfig) WriteTokens(w http.ResponseWriter, r *http.Request, status int, tokens AuthTokens, extra map[string]any)

WriteTokens delivers an issued token pair. A bearer caller gets the tokens in the body and no cookies; a cookie caller gets the cookies and a body that never contains a token. extra carries any route-specific top-level fields, such as register's userId.

type HTTPError added in v0.2.0

type HTTPError struct {
	Status  int    `json:"-"`
	Message string `json:"error"`
	Code    string `json:"code,omitempty"`
}

HTTPError is the family error envelope together with its status code.

The "code" field is omitted when the reference emits no code for that failure; clients are documented not to pattern-match on an absent code.

func AccessHTTPError added in v0.2.0

func AccessHTTPError(err error) HTTPError

AccessHTTPError maps an access-token verification failure the way the reference auth middleware does.

func ChangeEmailConfirmHTTPError added in v0.2.0

func ChangeEmailConfirmHTTPError(err error) HTTPError

ChangeEmailConfirmHTTPError maps a Service.ConfirmEmailChange failure.

func ChangeEmailInlineError added in v0.2.0

func ChangeEmailInlineError(user User) (HTTPError, bool)

ChangeEmailInlineError reproduces the reference's PASSWORD_REQUIRED guard on /change-email/request: an account whose only credential is the address itself may not move that address (auth.router.ts:1015-1021).

The reference runs this check after the 409 and 404 branches; here it runs before them, because the service persists the pending change in the same call that would report those two failures and there is no point at which the guard could still run without leaving a token behind. The status and code are therefore exact, and only the precedence between two simultaneous failures differs.

func ChangeEmailRequestHTTPError added in v0.2.0

func ChangeEmailRequestHTTPError(err error) HTTPError

ChangeEmailRequestHTTPError maps a Service.RequestEmailChange failure.

As on /send-verification-email, the 404 branch is a store-race path only: this port's auth gate answers 403 where the reference's handler would answer 404.

func ChangePasswordHTTPError added in v0.2.0

func ChangePasswordHTTPError(err error) HTTPError

ChangePasswordHTTPError maps a Service.ChangePassword failure.

ErrInvalidCredentials becomes 401 "Current password is incorrect" rather than the reference's 404 "User not found", because the service returns the same sentinel for both and the auth middleware resolved this user from the store moments earlier: a wrong password is the reachable case, a row that vanished in between is not.

func ChangePasswordInlineError added in v0.2.0

func ChangePasswordInlineError(user User, currentPassword, newPassword string) (HTTPError, bool)

ChangePasswordInlineError reproduces the reference's inline validation on /change-password: an account with no stored password that submits neither field gets 400 "New password is required" (auth.router.ts:922-925). An account that does have a password never reaches it — a missing current password fails the comparison instead.

func ForgotPasswordHTTPError added in v0.2.0

func ForgotPasswordHTTPError(error) HTTPError

ForgotPasswordHTTPError maps a Service.ForgotPassword failure.

Every failure is a bare 500: the reference has no error branch on this route at all, so a throwing store reaches handleError and becomes {"error":"Internal server error"} (auth.router.ts:796-798). ErrFeatureNotSupported is included on purpose — a store that cannot persist a reset token is a deployment fault, not a feature the caller can be told about, and answering 501 would tell an anonymous caller something the 200-always rule is there to hide.

func HTTPErrorFor added in v0.2.0

func HTTPErrorFor(err error) HTTPError

HTTPErrorFor maps a service sentinel onto the envelope. Routes whose failure means something more specific than the sentinel says — /refresh and the auth middleware both narrow "invalid token" — use their own mapper below.

Anything unmapped becomes a 500, so a sentinel that reaches a route before it reaches this switch turns a client-visible failure into an internal error. The sentinels deliberately left out are the ones whose reference wire string is route-specific and must not be guessed here: ErrInvalidCode is "Invalid or expired SMS code" on /sms/verify and "Invalid TOTP code" on /2fa/verify (both 401, both code-less), and ErrTenantNotFound / ErrRoleNotFound belong to admin routes. A route that can return one of those has to write it itself. TestUnmappedSentinelsAreDeliberate keeps that list honest.

func MagicLinkVerifyHTTPError added in v0.2.0

func MagicLinkVerifyHTTPError(err error) HTTPError

MagicLinkVerifyHTTPError maps a Service.VerifyMagicLink or Service.VerifyMagicLinkForUser failure. See HTTPErrInvalidMagicLink for why the expiry code does not appear.

func OAuthHTTPError added in v0.2.0

func OAuthHTTPError(err error) HTTPError

OAuthHTTPError maps a flow failure from this file onto the wire envelope. Routes call it instead of WriteServiceError because every literal here is route-specific: HTTPErrorFor would turn each of them into a 500.

func RefreshHTTPError added in v0.2.0

func RefreshHTTPError(err error) HTTPError

RefreshHTTPError maps a Service.Refresh failure. A revoked session stays distinguishable from an unusable token, which is the whole point of the code.

func ResetPasswordHTTPError added in v0.2.0

func ResetPasswordHTTPError(err error) HTTPError

ResetPasswordHTTPError maps a Service.ResetPassword failure.

func SMSVerifyHTTPError added in v0.2.0

func SMSVerifyHTTPError(err error) HTTPError

SMSVerifyHTTPError maps a Service.VerifySMSCode failure.

ErrInvalidCredentials means the userId or tempToken named a user the store does not have, and that is 401 "Invalid or expired SMS code", not 404. The reference looks the user up inside the strategy, where a miss is indistinguishable from a bad code: sms.strategy.ts:24-25 returns false for an unknown id, and the router turns false into 401 {"error":"Invalid or expired SMS code"} (auth.router.ts:1275-1279). Its 404 (:1281-1284) sits *after* that check and only fires if the user vanished between the verify and the follow-up findById — a race this port cannot reproduce, because Service.VerifySMSCode does both in one call. Answering 404 here would both change the status a client sees for a stale userId and turn an unauthenticated route into a user-existence oracle the reference does not have.

func SendVerificationEmailHTTPError added in v0.2.0

func SendVerificationEmailHTTPError(err error) HTTPError

SendVerificationEmailHTTPError maps a Service.SendVerificationEmailToken failure.

The 404 "User not found" branch is not reachable over HTTP in this port and is kept only for a store race: the reference's auth gate trusts the JWT and never reads the store (auth.middleware.ts:60), so its handler's own lookup can miss and answer 404, whereas this port's gate resolves the user through Me() and answers 403 "Invalid or expired access token" for a row that no longer exists. The divergence belongs to the shared middleware, not to this route.

func TOTPSetupHTTPError added in v0.2.0

func TOTPSetupHTTPError(err error) HTTPError

TOTPSetupHTTPError maps a Service.VerifyTOTPSetup failure (POST /2fa/verify-setup), where a bad code is a 400 rather than the 401 the step-up route answers.

func TOTPVerifyHTTPError added in v0.2.0

func TOTPVerifyHTTPError(err error) HTTPError

TOTPVerifyHTTPError maps a Service.VerifyTOTP failure (POST /2fa/verify).

func VerifyEmailHTTPError added in v0.2.0

func VerifyEmailHTTPError(err error) HTTPError

VerifyEmailHTTPError maps a Service.VerifyEmail failure.

func (HTTPError) Error added in v0.2.0

func (e HTTPError) Error() string

type HTTPMailerTransport

type HTTPMailerTransport struct {
	EndpointURL string
	Secret      string
	// contains filtered or unexported fields
}

HTTPMailerTransport sends mail via any HTTP endpoint (no SMTP needed).

func NewHTTPMailerTransport

func NewHTTPMailerTransport(endpointURL, secret string) *HTTPMailerTransport

NewHTTPMailerTransport creates a transport that POSTs to a webhook-style HTTP endpoint.

func (*HTTPMailerTransport) Send

type IDP

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

IDP is an OIDC Identity Provider.

func NewIDP

func NewIDP(cfg IDPConfig, authSvc *Service, clients ...IDPClient) (*IDP, error)

NewIDP creates a new OIDC IDP backed by the given auth service.

func (*IDP) RegisterHandlers

func (idp *IDP) RegisterHandlers(mux *http.ServeMux, basePath string)

RegisterHandlers mounts OIDC endpoints on the given mux.

type IDPClient

type IDPClient struct {
	ClientID     string
	ClientSecret string
	RedirectURIs []string
	Name         string
}

IDPClient represents a registered OIDC client application.

type IDPConfig

type IDPConfig struct {
	Issuer         string
	AccessTokenTTL time.Duration
	IDTokenTTL     time.Duration
}

IDPConfig configures the OIDC Identity Provider.

type LinkRequestInput added in v0.2.0

type LinkRequestInput struct {
	Email    string
	Provider string
	// EmailLang is the reference's third body field. It selects the language of
	// the verification mail and is forwarded verbatim to DeliverLinkToken, the way
	// the reference passes it as sendVerificationEmail's fourth argument
	// (auth.router.ts:1530-1535).
	EmailLang   string
	AccessToken string
	// Origin and Referer resolve which origin the verification link points at, as
	// the reference's resolveSiteUrl(req, config, allowedOrigins) does
	// (auth.router.ts:1528). A multi-origin deployment must email a link back to
	// the origin the request came from, not always to the default one.
	Origin  string
	Referer string
	// APIPrefix is the mount prefix, needed to build the verification link.
	APIPrefix string
}

LinkRequestInput is POST <prefix>/link-request.

type LinkRequestResult added in v0.2.0

type LinkRequestResult struct {
	Token string
	URL   string
}

LinkRequestResult reports what was issued. The token never reaches the HTTP response; it is returned so an embedder driving the service directly can send it themselves.

type LinkTokenDelivery added in v0.2.0

type LinkTokenDelivery struct {
	Email    string
	Provider string
	Token    string
	URL      string
	// EmailLang is the /link-request body's emailLang, passed through untouched so
	// the host's mail transport can localise. Empty when the caller omitted it.
	EmailLang string
}

LinkTokenDelivery is the payload handed to OAuthWiring.DeliverLinkToken. The four members are the reference's sendVerificationEmail(email, token, link, emailLang) arguments, plus the provider the link records.

type LinkVerifyInput added in v0.2.0

type LinkVerifyInput struct {
	Token             string
	LoginAfterLinking bool
}

LinkVerifyInput is POST <prefix>/link-verify.

type LinkVerifyResult added in v0.2.0

type LinkVerifyResult struct {
	User     User
	Tokens   AuthTokens
	LoggedIn bool
}

LinkVerifyResult carries the tokens when loginAfterLinking was asked for.

type LinkedAccountStore

type LinkedAccountStore interface {
	Save(ctx context.Context, link OAuthLinkedAccount) error
	FindByProvider(ctx context.Context, provider, providerID string) (OAuthLinkedAccount, error)
	ListForUser(ctx context.Context, userID string) ([]OAuthLinkedAccount, error)
	Delete(ctx context.Context, id string) error
}

LinkedAccountStore persists OAuth provider associations per user.

type LoginInput

type LoginInput struct {
	Email    string
	Password string
	TenantID string
}

LoginInput is the payload used for user authentication.

type MCPServer

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

MCPServer exposes auth configuration as MCP tools for AI editors. It speaks JSON-RPC 2.0 over HTTP and implements the Model Context Protocol tools/list and tools/call methods.

func NewMCPServer

func NewMCPServer(authSvc *Service) *MCPServer

NewMCPServer creates an MCP server wrapping an auth service.

func (*MCPServer) ServeHTTP

func (m *MCPServer) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles MCP JSON-RPC 2.0 requests (tools/list, tools/call).

type MagicLinkSendInput

type MagicLinkSendInput struct {
	Email    string
	TenantID string
}

type MagicLinkStore

type MagicLinkStore interface {
	UpdateMagicLinkToken(ctx context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
	GetUserByMagicLinkTokenHash(ctx context.Context, tokenHash string) (User, error)
	ClearMagicLinkToken(ctx context.Context, userID, tenantID string) error
}

MagicLinkStore persists magic link state.

type MagicLinkVerifyInput

type MagicLinkVerifyInput struct {
	Token string
}

type MailMessage

type MailMessage struct {
	To      string
	Subject string
	Body    string
	IsHTML  bool
}

MailMessage is a single email to be sent.

type MailTemplateData

type MailTemplateData struct {
	AppName  string
	UserName string
	Token    string
	URL      string
	Code     string
	Extra    map[string]any
}

MailTemplateData holds the common variables injected into mail templates.

type MailTemplater

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

MailTemplater renders localized mail templates.

func NewMailTemplater

func NewMailTemplater(appName string) *MailTemplater

NewMailTemplater creates a templater with built-in en and it templates.

func (*MailTemplater) Render

func (m *MailTemplater) Render(locale, name string, data MailTemplateData) (subject, body string, err error)

Render produces the subject and HTML body for the given locale and template name.

type MailerTransport

type MailerTransport interface {
	Send(ctx context.Context, msg MailMessage) error
}

MailerTransport delivers email messages.

type MemoryLinkedAccounts

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

MemoryLinkedAccounts is an in-memory implementation of LinkedAccountStore, suitable for development, testing, and embedded deployments.

func NewMemoryLinkedAccounts

func NewMemoryLinkedAccounts() *MemoryLinkedAccounts

NewMemoryLinkedAccounts creates an in-memory LinkedAccountStore.

func (*MemoryLinkedAccounts) Delete

func (m *MemoryLinkedAccounts) Delete(_ context.Context, id string) error

func (*MemoryLinkedAccounts) FindByProvider

func (m *MemoryLinkedAccounts) FindByProvider(_ context.Context, provider, providerID string) (OAuthLinkedAccount, error)

func (*MemoryLinkedAccounts) ListForUser

func (m *MemoryLinkedAccounts) ListForUser(_ context.Context, userID string) ([]OAuthLinkedAccount, error)

func (*MemoryLinkedAccounts) Save

type MemoryMetadataStore

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

MemoryMetadataStore is an in-memory implementation of UserMetadataStore.

func NewMemoryMetadataStore

func NewMemoryMetadataStore() *MemoryMetadataStore

func (*MemoryMetadataStore) ClearMetadata

func (s *MemoryMetadataStore) ClearMetadata(_ context.Context, userID string) error

func (*MemoryMetadataStore) GetMetadata

func (s *MemoryMetadataStore) GetMetadata(_ context.Context, userID string) (map[string]any, error)

func (*MemoryMetadataStore) UpdateMetadata

func (s *MemoryMetadataStore) UpdateMetadata(_ context.Context, userID string, metadata map[string]any) error
type MemoryPendingLinks struct {
	// contains filtered or unexported fields
}

MemoryPendingLinks is an in-memory PendingLinkStore, the counterpart of MemoryLinkedAccounts. It enforces the ttl, which is what makes an OAuth state nonce single-use and an account-link token expire.

func NewMemoryPendingLinks() *MemoryPendingLinks

NewMemoryPendingLinks creates an in-memory PendingLinkStore.

func (*MemoryPendingLinks) Delete added in v0.2.0

func (m *MemoryPendingLinks) Delete(_ context.Context, key string) error

func (*MemoryPendingLinks) Get added in v0.2.0

func (*MemoryPendingLinks) Save added in v0.2.0

type MemoryRolesPermissionsStore

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

MemoryRolesPermissionsStore is an in-memory RBAC store.

func NewMemoryRolesPermissionsStore

func NewMemoryRolesPermissionsStore() *MemoryRolesPermissionsStore

func (*MemoryRolesPermissionsStore) AddPermissionToRole

func (s *MemoryRolesPermissionsStore) AddPermissionToRole(_ context.Context, role, permission string) error

func (*MemoryRolesPermissionsStore) AddRoleToUser

func (s *MemoryRolesPermissionsStore) AddRoleToUser(_ context.Context, userID, role string, tenantID string) error

func (*MemoryRolesPermissionsStore) CreateRole

func (s *MemoryRolesPermissionsStore) CreateRole(_ context.Context, role string, permissions []string) error

func (*MemoryRolesPermissionsStore) DeleteRole

func (s *MemoryRolesPermissionsStore) DeleteRole(_ context.Context, role string) error

func (*MemoryRolesPermissionsStore) GetPermissionsForRole

func (s *MemoryRolesPermissionsStore) GetPermissionsForRole(_ context.Context, role string) ([]string, error)

func (*MemoryRolesPermissionsStore) GetPermissionsForUser

func (s *MemoryRolesPermissionsStore) GetPermissionsForUser(ctx context.Context, userID, tenantID string) ([]string, error)

func (*MemoryRolesPermissionsStore) GetRolesForUser

func (s *MemoryRolesPermissionsStore) GetRolesForUser(_ context.Context, userID, tenantID string) ([]string, error)

func (*MemoryRolesPermissionsStore) RemovePermissionFromRole

func (s *MemoryRolesPermissionsStore) RemovePermissionFromRole(_ context.Context, role, permission string) error

func (*MemoryRolesPermissionsStore) RemoveRoleFromUser

func (s *MemoryRolesPermissionsStore) RemoveRoleFromUser(_ context.Context, userID, role string, tenantID string) error

func (*MemoryRolesPermissionsStore) UserHasPermission

func (s *MemoryRolesPermissionsStore) UserHasPermission(ctx context.Context, userID, permission, tenantID string) (bool, error)

type MemorySessionStore

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

MemorySessionStore is a thread-safe in-memory implementation of SessionStore.

func NewMemorySessionStore

func NewMemorySessionStore() *MemorySessionStore

func (*MemorySessionStore) CreateSession

func (s *MemorySessionStore) CreateSession(_ context.Context, session Session) (Session, error)

func (*MemorySessionStore) DeleteExpiredSessions

func (s *MemorySessionStore) DeleteExpiredSessions(_ context.Context, now time.Time) (int, error)

func (*MemorySessionStore) GetSessionByID

func (s *MemorySessionStore) GetSessionByID(_ context.Context, sessionID string) (Session, error)

func (*MemorySessionStore) GetSessionByRefreshTokenHash

func (s *MemorySessionStore) GetSessionByRefreshTokenHash(_ context.Context, tokenHash string) (Session, error)

func (*MemorySessionStore) ListSessionsForUser

func (s *MemorySessionStore) ListSessionsForUser(_ context.Context, userID, tenantID string) ([]Session, error)

func (*MemorySessionStore) RevokeSessionByID

func (s *MemorySessionStore) RevokeSessionByID(_ context.Context, sessionID string) error

func (*MemorySessionStore) UpdateSession

func (s *MemorySessionStore) UpdateSession(_ context.Context, session Session) error

type MemoryTelemetryStore

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

MemoryTelemetryStore is an in-memory implementation of TelemetryStore.

func NewMemoryTelemetryStore

func NewMemoryTelemetryStore() *MemoryTelemetryStore

NewMemoryTelemetryStore creates an in-memory telemetry store.

func (*MemoryTelemetryStore) Query

func (*MemoryTelemetryStore) Record

type MemoryTenantStore

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

MemoryTenantStore is an in-memory tenant/membership store.

func NewMemoryTenantStore

func NewMemoryTenantStore() *MemoryTenantStore

func (*MemoryTenantStore) AssociateUserWithTenant

func (s *MemoryTenantStore) AssociateUserWithTenant(_ context.Context, userID, tenantID string) error

func (*MemoryTenantStore) CreateTenant

func (s *MemoryTenantStore) CreateTenant(_ context.Context, tenant Tenant) (Tenant, error)

func (*MemoryTenantStore) DeleteTenant

func (s *MemoryTenantStore) DeleteTenant(_ context.Context, id string) error

func (*MemoryTenantStore) DisassociateUserFromTenant

func (s *MemoryTenantStore) DisassociateUserFromTenant(_ context.Context, userID, tenantID string) error

func (*MemoryTenantStore) GetAllTenants

func (s *MemoryTenantStore) GetAllTenants(_ context.Context) ([]Tenant, error)

func (*MemoryTenantStore) GetTenantByID

func (s *MemoryTenantStore) GetTenantByID(_ context.Context, id string) (Tenant, error)

func (*MemoryTenantStore) GetTenantsForUser

func (s *MemoryTenantStore) GetTenantsForUser(_ context.Context, userID string) ([]Tenant, error)

func (*MemoryTenantStore) GetUsersForTenant

func (s *MemoryTenantStore) GetUsersForTenant(_ context.Context, tenantID string) ([]string, error)

func (*MemoryTenantStore) UpdateTenant

func (s *MemoryTenantStore) UpdateTenant(_ context.Context, id string, update Tenant) error

type MemoryUserStore

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

MemoryUserStore is a thread-safe in-memory implementation of UserStore.

func NewMemoryUserStore

func NewMemoryUserStore() *MemoryUserStore

func (*MemoryUserStore) ApplyEmailChange

func (s *MemoryUserStore) ApplyEmailChange(_ context.Context, userID, tenantID string) error

func (*MemoryUserStore) ClearEmailChangeToken

func (s *MemoryUserStore) ClearEmailChangeToken(_ context.Context, userID, tenantID string) error

func (*MemoryUserStore) ClearEmailVerificationToken

func (s *MemoryUserStore) ClearEmailVerificationToken(_ context.Context, userID, tenantID string) error

func (*MemoryUserStore) ClearMagicLinkToken

func (s *MemoryUserStore) ClearMagicLinkToken(_ context.Context, userID, tenantID string) error

func (*MemoryUserStore) ClearResetToken

func (s *MemoryUserStore) ClearResetToken(_ context.Context, userID, tenantID string) error

func (*MemoryUserStore) ClearSMSCode

func (s *MemoryUserStore) ClearSMSCode(_ context.Context, userID, tenantID string) error

func (*MemoryUserStore) CreateUser

func (s *MemoryUserStore) CreateUser(_ context.Context, user User) (User, error)

func (*MemoryUserStore) DeleteUser

func (s *MemoryUserStore) DeleteUser(_ context.Context, userID, tenantID string) error

func (*MemoryUserStore) GetUserByEmail

func (s *MemoryUserStore) GetUserByEmail(_ context.Context, email, tenantID string) (User, error)

func (*MemoryUserStore) GetUserByEmailChangeTokenHash

func (s *MemoryUserStore) GetUserByEmailChangeTokenHash(_ context.Context, tokenHash string) (User, error)

func (*MemoryUserStore) GetUserByEmailVerificationTokenHash

func (s *MemoryUserStore) GetUserByEmailVerificationTokenHash(_ context.Context, tokenHash string) (User, error)

func (*MemoryUserStore) GetUserByID

func (s *MemoryUserStore) GetUserByID(_ context.Context, id, tenantID string) (User, error)

func (*MemoryUserStore) GetUserByMagicLinkTokenHash

func (s *MemoryUserStore) GetUserByMagicLinkTokenHash(_ context.Context, tokenHash string) (User, error)

func (*MemoryUserStore) GetUserByResetTokenHash

func (s *MemoryUserStore) GetUserByResetTokenHash(_ context.Context, tokenHash string) (User, error)

func (*MemoryUserStore) GetUserBySMSCodeHash

func (s *MemoryUserStore) GetUserBySMSCodeHash(_ context.Context, userID, tenantID, codeHash string) (User, error)

func (*MemoryUserStore) MarkEmailVerified

func (s *MemoryUserStore) MarkEmailVerified(_ context.Context, userID, tenantID string, verified bool) error

func (*MemoryUserStore) UpdateEmailChangeToken

func (s *MemoryUserStore) UpdateEmailChangeToken(_ context.Context, userID, tenantID, pendingEmail, tokenHash string, expiry time.Time) error

func (*MemoryUserStore) UpdateEmailVerificationToken

func (s *MemoryUserStore) UpdateEmailVerificationToken(_ context.Context, userID, tenantID, tokenHash string, expiry time.Time) error

func (*MemoryUserStore) UpdateMagicLinkToken

func (s *MemoryUserStore) UpdateMagicLinkToken(_ context.Context, userID, tenantID, tokenHash string, expiry time.Time) error

func (*MemoryUserStore) UpdatePassword

func (s *MemoryUserStore) UpdatePassword(_ context.Context, userID, tenantID, passwordHash string) error

func (*MemoryUserStore) UpdatePhoneNumber added in v0.2.0

func (s *MemoryUserStore) UpdatePhoneNumber(_ context.Context, userID, tenantID, phoneNumber string) (User, error)

UpdatePhoneNumber makes MemoryUserStore satisfy UserPhoneStore. It lives beside the interface rather than in memory_store.go so that the capability and its in-memory implementation stay in one place.

func (*MemoryUserStore) UpdateProfile

func (s *MemoryUserStore) UpdateProfile(_ context.Context, userID, tenantID, firstName, lastName string) (User, error)

func (*MemoryUserStore) UpdateResetToken

func (s *MemoryUserStore) UpdateResetToken(_ context.Context, userID, tenantID, tokenHash string, expiry time.Time) error

func (*MemoryUserStore) UpdateSMSCode

func (s *MemoryUserStore) UpdateSMSCode(_ context.Context, userID, tenantID, codeHash string, expiry time.Time) error

func (*MemoryUserStore) UpdateTOTPSecret

func (s *MemoryUserStore) UpdateTOTPSecret(_ context.Context, userID, tenantID, secret string, enabled bool) error

type OAuthBeginInput added in v0.2.0

type OAuthBeginInput struct {
	Provider   string
	ReturnPath string
	Origin     string
	Referer    string
}

OAuthBeginInput is GET <prefix>/oauth/{provider}.

There is deliberately no "link to the caller's account" field. The reference's /oauth/{provider} has no auth gate and reads no credential (wire-contract.md §4: "Auth gate: none"); the intent is not carried in the state either, and the callback resolves the identity purely through the provider account. Reading the initiating request's access token and linking the provider identity to it is an account-takeover shape: whoever happened to be signed in when the flow started would get the session, and the (provider, providerAccountId) binding would be re-pointed at them. Linking an existing account to a provider goes through /link-request and /link-verify, where an emailed token proves the address.

type OAuthBeginResult added in v0.2.0

type OAuthBeginResult struct {
	AuthorizationURL string
	State            string
}

OAuthBeginResult is what the route redirects to.

type OAuthCompleteInput added in v0.2.0

type OAuthCompleteInput struct {
	Provider string
	Code     string
	State    string
}

OAuthCompleteInput is GET <prefix>/oauth/{provider}/callback.

type OAuthCompleteResult added in v0.2.0

type OAuthCompleteResult struct {
	User       User
	Tokens     AuthTokens
	RedirectTo string
}

OAuthCompleteResult carries the issued session and where to send the browser.

type OAuthLinkedAccount

type OAuthLinkedAccount struct {
	ID         string
	UserID     string
	Provider   string
	ProviderID string
	Email      string
	Name       string
	Picture    string
	CreatedAt  time.Time
}

OAuthLinkedAccount stores a linked provider<->user association.

Email, Name and Picture are the reference's optional LinkedAccount profile columns (linked-accounts-store.interface.ts:9-14). GET /linked-accounts projects all three, and clients render them: the family admin UI falls back through `a.name || a.email || a.providerAccountId`. The library fills Email at every link it writes, as the reference's linkAccount calls do; Name and Picture are left for a host store that wants to keep them, again as in the reference.

type OAuthPendingMeta

type OAuthPendingMeta struct {
	Provider    string
	RedirectURL string
	TenantID    string
	UserID      string // non-empty when linking to existing account
	// Email and ProviderAccountID carry the account-linking stash: the address
	// a /link-request token was issued for, and the provider account a callback
	// conflict parked for it.
	Email             string
	ProviderAccountID string
	// ExpiresAt lets a consumer enforce the deadline itself rather than trusting
	// the store to have honoured the ttl argument.
	ExpiresAt time.Time
}

OAuthPendingMeta is the metadata stored during OAuth flow.

type OAuthProvider

type OAuthProvider struct {
	Name         string
	ClientID     string
	ClientSecret string
	RedirectURL  string
	AuthURL      string
	TokenURL     string
	UserInfoURL  string
	Scopes       []string
}

OAuthProvider holds configuration for a single OAuth 2.0 provider.

func GitHubProvider

func GitHubProvider(clientID, clientSecret, redirectURL string) OAuthProvider

GitHubProvider builds a pre-configured GitHub OAuth2 provider.

func GoogleProvider

func GoogleProvider(clientID, clientSecret, redirectURL string) OAuthProvider

GoogleProvider builds a pre-configured Google OAuth2 provider.

type OAuthService

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

OAuthService handles OAuth 2.0 authorization code flow.

func NewOAuthService

func NewOAuthService(providers ...OAuthProvider) *OAuthService

NewOAuthService creates an OAuthService for the given providers.

func (*OAuthService) AuthorizeURL

func (s *OAuthService) AuthorizeURL(providerName, state string) (string, error)

AuthorizeURL builds the redirect URL for the given provider and state token.

func (*OAuthService) AuthorizeURLPKCE added in v0.2.0

func (s *OAuthService) AuthorizeURLPKCE(providerName, state, codeChallenge string) (string, error)

AuthorizeURLPKCE builds the redirect URL and, when codeChallenge is non-empty, binds the exchange to it with the S256 method (RFC 7636). The HTTP routes always pass a challenge; the plain AuthorizeURL stays for embedders driving the service themselves.

func (*OAuthService) ExchangeCode

func (s *OAuthService) ExchangeCode(ctx context.Context, providerName, code string) (OAuthUserInfo, error)

ExchangeCode exchanges an authorization code for user info.

func (*OAuthService) ExchangeCodePKCE added in v0.2.0

func (s *OAuthService) ExchangeCodePKCE(ctx context.Context, providerName, code, codeVerifier string) (OAuthUserInfo, error)

ExchangeCodePKCE exchanges an authorization code, sending the PKCE verifier when one was used to build the authorization URL.

func (*OAuthService) HandleCallback

func (s *OAuthService) HandleCallback(
	ctx context.Context,
	authSvc *Service,
	linkedAccounts LinkedAccountStore,
	info OAuthUserInfo,
	tenantID string,
	linkToUserID string,
) (User, AuthTokens, error)

HandleCallback resolves/creates a user after OAuth callback. If linkToUserID is non-empty the provider is linked to that existing account.

func (*OAuthService) Has added in v0.2.0

func (s *OAuthService) Has(name string) bool

Has reports whether a provider is registered.

type OAuthUserInfo

type OAuthUserInfo struct {
	ProviderID string
	Provider   string
	Email      string
	Name       string
	AvatarURL  string
	Raw        map[string]any
}

OAuthUserInfo is the normalized profile returned after token exchange.

type OAuthWiring added in v0.2.0

type OAuthWiring struct {
	// Service resolves providers, builds authorization URLs and exchanges codes.
	Service *OAuthService
	// LinkedAccounts persists the provider ↔ user associations the
	// /linked-accounts routes read and write.
	LinkedAccounts LinkedAccountStore
	// PendingLinks is the stash. It carries two kinds of entry, both keyed by an
	// opaque string and both single-use: the in-flight OAuth state (so a nonce
	// cannot be replayed) and the account-link token issued by /link-request.
	PendingLinks PendingLinkStore
	// AllowedOrigins is the redirect allowlist. An empty allowlist accepts the
	// origin embedded in the state, matching the reference — safe here only
	// because the state is signed.
	AllowedOrigins []string
	// SiteURL is the fallback redirect target, the reference's first
	// config.email.siteUrl entry.
	SiteURL string
	// TenantID scopes users created or resolved by the callback.
	TenantID string
	// StateTTL bounds how long a signed state stays acceptable. Default 10m.
	StateTTL time.Duration
	// LinkTokenTTL bounds the /link-request token. Default 1h, as the reference.
	LinkTokenTTL time.Duration
	// DeliverLinkToken hands the /link-request verification link to the host's
	// mail transport. The route answers success either way, as the reference
	// does when no transport is configured.
	DeliverLinkToken func(ctx context.Context, delivery LinkTokenDelivery) error
}

OAuthWiring is everything the OAuth and account-linking routes need beyond the core service. The reference gates each route on the presence of the corresponding option; the equivalent here is that an absent member turns its routes into NOT_IMPLEMENTED rather than leaving them unmounted, because a single dynamic /oauth/{provider} route cannot be registered per provider.

type OpenAPIInfo

type OpenAPIInfo struct {
	Title       string
	Description string
	Version     string
	ServerURL   string
}

OpenAPIInfo holds metadata for the generated OpenAPI spec.

type Option

type Option func(*authBuilder) error

Option configures Auth initialization.

func WithBcryptCost added in v0.2.0

func WithBcryptCost(cost int) Option

WithBcryptCost sets the bcrypt cost used for every password hash the library writes. See Config.BcryptCost.

Unlike the zero value of Config.BcryptCost, which means "unset" and resolves to bcrypt.DefaultCost, calling this Option with 0 is an error: an explicit setter reached with an unset value is a caller mistake, not a request for the default. Omit the Option to get the default.

func WithIssuer

func WithIssuer(issuer string) Option

WithIssuer configures token issuer.

func WithLogger

func WithLogger(fn func(format string, args ...any)) Option

WithLogger provides optional library logging callback.

func WithMetadataProvider

func WithMetadataProvider(store UserMetadataStore) Option

WithMetadataProvider wires optional metadata support.

func WithOAuth added in v0.2.0

func WithOAuth(wiring OAuthWiring) Option

WithOAuth wires the OAuth service and the linking stores.

func WithRBACProvider

func WithRBACProvider(store RolesPermissionsStore) Option

WithRBACProvider wires optional role/permission support.

func WithRequire2FA

func WithRequire2FA(enabled bool) Option

WithRequire2FA enables mandatory second-factor login checks.

func WithSecret

func WithSecret(secret string) Option

WithSecret configures the JWT signing secret.

func WithSessionCheckOn added in v0.2.0

func WithSessionCheckOn(mode string) Option

WithSessionCheckOn selects when a session's revocation state is re-checked: allcalls, refresh (the default) or none. Without allcalls the auth middleware cannot report a revoked session, so a client that relies on the SESSION_REVOKED signal has to opt in here.

func WithSessionStore

func WithSessionStore(store SessionStore) Option

WithSessionStore sets a custom SessionStore implementation.

func WithTenantProvider

func WithTenantProvider(store TenantStore) Option

WithTenantProvider wires optional tenant support.

func WithTokenClaimsBuilder

func WithTokenClaimsBuilder(fn func(ctx context.Context, user User) (map[string]any, error)) Option

WithTokenClaimsBuilder customizes token claims creation.

func WithTokenTTLs

func WithTokenTTLs(access, refresh time.Duration) Option

WithTokenTTLs configures access and refresh TTL.

func WithUserStore

func WithUserStore(store UserStore) Option

WithUserStore sets a custom UserStore implementation.

type PendingLinkStore

type PendingLinkStore interface {
	Save(ctx context.Context, state string, meta OAuthPendingMeta, ttl time.Duration) error
	Get(ctx context.Context, state string) (OAuthPendingMeta, error)
	Delete(ctx context.Context, state string) error
}

PendingLinkStore holds temporary OAuth link state.

type PublicLinkedAccount added in v0.2.0

type PublicLinkedAccount struct {
	Provider          string    `json:"provider"`
	ProviderAccountID string    `json:"providerAccountId"`
	Email             string    `json:"email,omitempty"`
	Name              string    `json:"name,omitempty"`
	Picture           string    `json:"picture,omitempty"`
	LinkedAt          time.Time `json:"linkedAt"`
}

PublicLinkedAccount is the wire projection of a linked account. The field names and their optionality are the reference's LinkedAccount interface (linked-accounts-store.interface.ts:4-17), which is what the Angular client, the served auth.js and the family admin UI read.

Email is populated by every link the library writes, because the reference's linkAccount calls always pass `email: user.email` from the callback profile (auth.router.ts:1336-1343, :1578-1583). Name and Picture are optional in the reference's interface and its own linkAccount calls never set them, so they only ever appear when a host-supplied LinkedAccountStore fills them in; the admin UI's `a.name || a.email || a.providerAccountId` therefore falls through to the email, as it does against the reference.

type PublicSession added in v0.2.0

type PublicSession struct {
	SessionHandle string    `json:"sessionHandle"`
	UserID        string    `json:"userId"`
	TenantID      string    `json:"tenantId,omitempty"`
	CreatedAt     time.Time `json:"createdAt"`
	ExpiresAt     time.Time `json:"expiresAt"`
}

PublicSession is the response-safe projection of Session, and the only session shape an adapter may serialise: Session carries the refresh-token hash, which is credential material and must never reach a response body.

The field names and the key set are the reference's SessionInfo (session.model.ts:8-27, wire-contract §3.9): sessionHandle, userId, createdAt, expiresAt and an optional tenantId. Both browser clients read sessionHandle off each entry and post it straight back to DELETE /sessions/{handle}.

There is deliberately no revokedAt key. SessionInfo has no such field, so no shipped client knows it exists or could filter on it, and the reference never needs one: revokeSession deletes the record, so a revoked session is absent from the next list rather than flagged in it. Emitting the tombstone as an extra key would be an unrequested addition to the contract and, worse, the only way a client could tell a live session from a dead one — see Auth.ListSessions, which drops them instead.

func NewPublicSession added in v0.2.0

func NewPublicSession(session Session) PublicSession

NewPublicSession projects a Session onto its response-safe representation.

func NewPublicSessions added in v0.2.0

func NewPublicSessions(sessions []Session) []PublicSession

NewPublicSessions projects a session list. The result is never nil: a store that returns no sessions must serialise as [] and not as null, because a client iterating response.sessions breaks on null.

type PublicTenant

type PublicTenant struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	IsActive  bool      `json:"isActive"`
	CreatedAt time.Time `json:"createdAt"`
}

PublicTenant is the response-safe projection of Tenant. The tenant Config blob is server-side configuration and is never serialised.

type PublicUser

type PublicUser struct {
	ID              string         `json:"id"`
	Email           string         `json:"email"`
	TenantID        string         `json:"tenantId,omitempty"`
	FirstName       string         `json:"firstName,omitempty"`
	LastName        string         `json:"lastName,omitempty"`
	PhoneNumber     string         `json:"phoneNumber,omitempty"`
	IsEmailVerified bool           `json:"isEmailVerified"`
	IsTOTPEnabled   bool           `json:"isTotpEnabled"`
	Roles           []string       `json:"roles,omitempty"`
	Permissions     []string       `json:"permissions,omitempty"`
	Tenants         []PublicTenant `json:"tenants,omitempty"`
	Metadata        map[string]any `json:"metadata,omitempty"`
	CustomClaims    map[string]any `json:"customClaims,omitempty"`
	CreatedAt       time.Time      `json:"createdAt"`
}

PublicUser is the response-safe projection of User. It is the only user shape adapters may serialise: credential material (password hash, TOTP secret, one-time token hashes and the pending email change) is deliberately absent.

func NewPublicUser

func NewPublicUser(user User) PublicUser

NewPublicUser projects a User onto its response-safe representation.

type RegisterInput

type RegisterInput struct {
	Email    string
	Password string
	TenantID string
}

RegisterInput is the payload to create a user and initial session.

type ResetPasswordInput

type ResetPasswordInput struct {
	Token       string
	NewPassword string
}

type RolesPermissionsStore

type RolesPermissionsStore interface {
	AddRoleToUser(ctx context.Context, userID, role string, tenantID string) error
	RemoveRoleFromUser(ctx context.Context, userID, role string, tenantID string) error
	GetRolesForUser(ctx context.Context, userID, tenantID string) ([]string, error)
	CreateRole(ctx context.Context, role string, permissions []string) error
	DeleteRole(ctx context.Context, role string) error
	AddPermissionToRole(ctx context.Context, role, permission string) error
	RemovePermissionFromRole(ctx context.Context, role, permission string) error
	GetPermissionsForRole(ctx context.Context, role string) ([]string, error)
	GetPermissionsForUser(ctx context.Context, userID, tenantID string) ([]string, error)
	UserHasPermission(ctx context.Context, userID, permission, tenantID string) (bool, error)
}

RolesPermissionsStore manages RBAC assignments and permissions.

type SMSCodeSendInput

type SMSCodeSendInput struct {
	UserID   string
	Email    string
	TenantID string
}

type SMSCodeVerifyInput

type SMSCodeVerifyInput struct {
	UserID   string
	Email    string
	TenantID string
	Code     string
}

type SMSStore

type SMSStore interface {
	UpdateSMSCode(ctx context.Context, userID, tenantID, codeHash string, expiry time.Time) error
	GetUserBySMSCodeHash(ctx context.Context, userID, tenantID, codeHash string) (User, error)
	ClearSMSCode(ctx context.Context, userID, tenantID string) error
}

SMSStore persists sms code state.

type Service

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

Service is the main entry point for authentication operations.

func NewService

func NewService(cfg Config, users UserStore, sessions SessionStore, opts ...ServiceOption) (*Service, error)

NewService builds an auth service with validated configuration.

func (*Service) AddUserToTenant

func (s *Service) AddUserToTenant(ctx context.Context, userID, tenantID string) error

func (*Service) AssignRole

func (s *Service) AssignRole(ctx context.Context, userID, role, tenantID string) error

func (*Service) ChangePassword

func (s *Service) ChangePassword(ctx context.Context, in ChangePasswordInput) error

func (*Service) CleanupExpiredSessions

func (s *Service) CleanupExpiredSessions(ctx context.Context) (int, error)

func (*Service) ConfirmEmailChange

func (s *Service) ConfirmEmailChange(ctx context.Context, in ConfirmEmailChangeInput) error

func (*Service) CreateRole

func (s *Service) CreateRole(ctx context.Context, role string, permissions []string) error

func (*Service) CreateTenant

func (s *Service) CreateTenant(ctx context.Context, name string, config map[string]any) (Tenant, error)

func (*Service) DeleteAccount

func (s *Service) DeleteAccount(ctx context.Context, in DeleteAccountInput) error

DeleteAccount runs the reference's account cleanup in the reference's order (auth.router.ts:1597-1636): sessions, then role assignments, then tenant memberships, then metadata, and the user record last. The order is not cosmetic — deleting the identity first and failing anywhere afterwards leaves sessions that still authenticate a user who no longer exists.

func (*Service) DisableTOTP

func (s *Service) DisableTOTP(ctx context.Context, userID, tenantID string) error

func (*Service) FindUser added in v0.2.0

func (s *Service) FindUser(ctx context.Context, userID, email, tenantID string) (User, error)

FindUser resolves a user by id, or by email when no id is given. The passwordless routes need it because they have to tell "no such user" apart from "that user has no phone number" — two different statuses in the contract — and the send calls answer both with silence.

func (*Service) ForgotPassword

func (s *Service) ForgotPassword(ctx context.Context, in ForgotPasswordInput) (string, error)

func (*Service) GetMetadata

func (s *Service) GetMetadata(ctx context.Context, userID string) (map[string]any, error)

func (*Service) IssueTempToken added in v0.2.0

func (s *Service) IssueTempToken(ctx context.Context, user User) (string, error)

IssueTempToken mints the step-up token a 2FA challenge hands the client. Its lifetime is Config.TempTokenTTL, 5 minutes by default, matching the reference's hardcoded '5m'.

func (*Service) IssueTokensForUser added in v0.2.0

func (s *Service) IssueTokensForUser(ctx context.Context, userID, tenantID string) (User, AuthTokens, error)

IssueTokensForUser starts a session for an identity that has already been proven by something other than a password — here, an emailed account-link token. It is the one service method this route group needed and the core did not have: every other issuance path (Register, Login, VerifyMagicLink, VerifySMSCode, VerifyTOTP) reaches newSessionTokens through a credential check that does not apply.

func (*Service) ListSessions

func (s *Service) ListSessions(ctx context.Context, userID, tenantID string) ([]Session, error)

func (*Service) Login

func (s *Service) Login(ctx context.Context, in LoginInput) (User, AuthTokens, error)

func (*Service) Logout

func (s *Service) Logout(ctx context.Context, refreshToken string) error

func (*Service) LogoutAccessToken added in v0.2.0

func (s *Service) LogoutAccessToken(ctx context.Context, accessToken string) error

LogoutAccessToken revokes the session named by an access token's sid claim.

func (*Service) Me

func (s *Service) Me(ctx context.Context, accessToken string) (User, error)

func (*Service) Refresh

func (s *Service) Refresh(ctx context.Context, refreshToken string) (AuthTokens, error)

func (*Service) Register

func (s *Service) Register(ctx context.Context, in RegisterInput) (User, AuthTokens, error)

func (*Service) RequestEmailChange

func (s *Service) RequestEmailChange(ctx context.Context, in ChangeEmailRequestInput) (string, error)

func (*Service) ResetPassword

func (s *Service) ResetPassword(ctx context.Context, in ResetPasswordInput) error

func (*Service) RevokeSessionByID

func (s *Service) RevokeSessionByID(ctx context.Context, sessionID string) error

func (*Service) RevokeUserSession added in v0.2.0

func (s *Service) RevokeUserSession(ctx context.Context, userID, tenantID, sessionID string) error

RevokeUserSession revokes one of a user's own sessions. Ownership is part of the lookup rather than a check bolted on afterwards: a handle belonging to another user is indistinguishable from one that never existed.

A handle that resolves to a session which is already revoked, or which has expired, is also indistinguishable from one that never existed. The reference resolves the handle with getSession, documented to return null "when the session does not exist or has expired" (session-store.interface.ts:51-54), and its revokeSession deletes the record — so both cases reach the same `if (!session || …) → 404` as an unknown handle (auth.router.ts:762-767). A second DELETE of the same handle is therefore *not* idempotently successful, and neither is a DELETE of an expired one.

func (s *Service) SendMagicLink(ctx context.Context, in MagicLinkSendInput) (string, error)

func (*Service) SendSMSCode

func (s *Service) SendSMSCode(ctx context.Context, in SMSCodeSendInput) (string, error)

func (*Service) SendVerificationEmailToken

func (s *Service) SendVerificationEmailToken(ctx context.Context, in EmailVerificationInput) (string, error)

func (*Service) SetupTOTP

func (s *Service) SetupTOTP(ctx context.Context, userID, tenantID string) (string, error)

func (*Service) StartTOTPEnrolment added in v0.2.0

func (s *Service) StartTOTPEnrolment(ctx context.Context, userID, tenantID string) (TOTPSetup, error)

StartTOTPEnrolment mints an enrolment secret and its provisioning URI.

func (*Service) TokenTTLs added in v0.2.0

func (s *Service) TokenTTLs() (access, refresh time.Duration)

TokenTTLs reports the configured access and refresh token lifetimes. The HTTP layer needs them to give the auth cookies the same lifetime as the tokens they carry.

func (*Service) UpdateMetadata

func (s *Service) UpdateMetadata(ctx context.Context, userID string, metadata map[string]any) error

func (*Service) UpdatePhoneNumber added in v0.2.0

func (s *Service) UpdatePhoneNumber(ctx context.Context, in AddPhoneInput) (User, error)

UpdatePhoneNumber sets or clears a user's phone number.

func (*Service) UpdateProfile

func (s *Service) UpdateProfile(ctx context.Context, in UpdateProfileInput) (User, error)

UpdateProfile applies a partial profile update: a nil field in the input is one the caller did not submit and must be left as it is stored (§3.5).

UserAccountStore.UpdateProfile takes both names by value, so a nil field is resolved against the current record before the write rather than by widening the store interface — a store written against this package keeps compiling and still gets partial semantics. The read-modify-write is not atomic; two concurrent partial patches of *different* fields can still lose one. Making it atomic needs a store-level patch primitive, which is a breaking interface change and its own item; silently erasing a field on every single partial call is the failure this closes.

func (*Service) UserHasPermission

func (s *Service) UserHasPermission(ctx context.Context, userID, permission, tenantID string) (bool, error)

func (*Service) VerifyEmail

func (s *Service) VerifyEmail(ctx context.Context, in VerifyEmailInput) error
func (s *Service) VerifyMagicLink(ctx context.Context, in MagicLinkVerifyInput) (User, AuthTokens, error)

func (*Service) VerifyMagicLinkForUser added in v0.2.0

func (s *Service) VerifyMagicLinkForUser(ctx context.Context, in MagicLinkVerifyInput, userID string) (User, AuthTokens, error)

VerifyMagicLinkForUser consumes a magic link that must belong to userID, the step-up case: the caller has already identified the user through a step-up token and the link is only the second factor.

It exists because the identity check has to happen before a session is created. Service.VerifyMagicLink issues one as part of verifying, so a caller that compared the ids afterwards would refuse the request on the wire and still leave a live session for the link's owner — a session that shows up in GET /sessions and that only the owner can revoke. A mismatch here returns ErrMagicLinkOwnerMismatch and issues nothing.

The link is consumed even on a mismatch, matching the reference, which burns it in the strategy before the router compares ids (magic-link.strategy.ts:50 versus auth.router.ts:1150-1152).

func (*Service) VerifySMSCode

func (s *Service) VerifySMSCode(ctx context.Context, in SMSCodeVerifyInput) (User, AuthTokens, error)

func (*Service) VerifyTOTP

func (s *Service) VerifyTOTP(ctx context.Context, userID, tenantID, code string) (User, AuthTokens, error)

func (*Service) VerifyTOTPSetup

func (s *Service) VerifyTOTPSetup(ctx context.Context, userID, tenantID, secret, code string) error

func (*Service) VerifyTempToken added in v0.2.0

func (s *Service) VerifyTempToken(token string) (TempTokenSubject, error)

VerifyTempToken resolves the identity a step-up token names, or ErrInvalidToken.

type ServiceOption

type ServiceOption func(*Service)

func WithMetadataStore

func WithMetadataStore(store UserMetadataStore) ServiceOption

func WithRolesPermissionsStore

func WithRolesPermissionsStore(store RolesPermissionsStore) ServiceOption

func WithTenantStore

func WithTenantStore(store TenantStore) ServiceOption

type Session

type Session struct {
	ID               string
	UserID           string
	TenantID         string
	RefreshTokenHash string
	CreatedAt        time.Time
	ExpiresAt        time.Time
	RevokedAt        *time.Time
}

Session stores refresh-token bound session metadata.

type SessionAdminStore

type SessionAdminStore interface {
	ListSessionsForUser(ctx context.Context, userID, tenantID string) ([]Session, error)
	RevokeSessionByID(ctx context.Context, sessionID string) error
	DeleteExpiredSessions(ctx context.Context, now time.Time) (int, error)
}

SessionAdminStore exposes session listing and cleanup primitives.

type SessionLookupStore

type SessionLookupStore interface {
	GetSessionByID(ctx context.Context, sessionID string) (Session, error)
}

SessionLookupStore resolves sessions by session ID.

type SessionStore

type SessionStore interface {
	CreateSession(ctx context.Context, session Session) (Session, error)
	GetSessionByRefreshTokenHash(ctx context.Context, tokenHash string) (Session, error)
	UpdateSession(ctx context.Context, session Session) error
}

SessionStore persists refresh-token sessions.

type SseDistributor

type SseDistributor interface {
	Publish(ctx context.Context, channel string, msg SseMessage) error
	Subscribe(ctx context.Context, channel string) (<-chan SseMessage, error)
}

SseDistributor broadcasts SSE messages to all connected clients. Implement this interface for Redis/Kafka-based multi-instance scaling.

type SseHub

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

SseHub manages in-process SSE subscriptions.

func NewSseHub

func NewSseHub() *SseHub

NewSseHub creates an in-process SSE hub.

func (*SseHub) Publish

func (h *SseHub) Publish(_ context.Context, channel string, msg SseMessage) error

Publish sends a message to all subscribers of channel.

func (*SseHub) Subscribe

func (h *SseHub) Subscribe(_ context.Context, channel string) (<-chan SseMessage, error)

Subscribe registers a new client channel for the given user/channel key.

func (*SseHub) Unsubscribe

func (h *SseHub) Unsubscribe(channel string, ch <-chan SseMessage)

Unsubscribe removes a client channel from the hub.

type SseMessage

type SseMessage struct {
	ID    string
	Event string
	Data  map[string]any
}

SseMessage is a single SSE payload.

type TOTPSetup added in v0.2.0

type TOTPSetup struct {
	Secret     string `json:"secret"`
	OTPAuthURL string `json:"otpauthUrl"`
}

TOTPSetup is the enrolment material POST /2fa/setup hands back. Enrolment is stateless in the reference and stateless here: nothing is persisted until the client returns the secret to /2fa/verify-setup together with a code that verifies against it.

The reference also returns a `qrCode` PNG data URL. It is absent here: the root package is stdlib plus golang.org/x/crypto and a QR encoder is neither. A client renders OTPAuthURL itself, which is what the Rust port of this family does too.

type TOTPStore

type TOTPStore interface {
	UpdateTOTPSecret(ctx context.Context, userID, tenantID, secret string, enabled bool) error
}

TOTPStore persists authenticator setup.

type TelemetryEvent

type TelemetryEvent struct {
	ID        string
	EventName string
	UserID    string
	TenantID  string
	IP        string
	UserAgent string
	Success   bool
	Error     string
	Timestamp time.Time
	Meta      map[string]any
}

TelemetryEvent represents a single persisted auth event for analytics.

type TelemetryFilter

type TelemetryFilter struct {
	UserID    string
	TenantID  string
	EventName string
	Since     time.Time
	Until     time.Time
	Limit     int
}

TelemetryFilter restricts query results.

type TelemetryStore

type TelemetryStore interface {
	Record(ctx context.Context, event TelemetryEvent) error
	Query(ctx context.Context, filter TelemetryFilter) ([]TelemetryEvent, error)
}

TelemetryStore persists auth telemetry events.

type TempTokenSubject added in v0.2.0

type TempTokenSubject struct {
	UserID   string
	TenantID string
}

TempTokenSubject is the identity a step-up token names. It is deliberately not a User: the routes that accept a tempToken re-read the user from the store, as the reference does, so a flag set since the token was minted is honoured.

type Tenant

type Tenant struct {
	ID        string
	Name      string
	IsActive  bool
	Config    map[string]any
	CreatedAt time.Time
}

Tenant represents an isolated workspace or organization.

type TenantStore

type TenantStore interface {
	CreateTenant(ctx context.Context, tenant Tenant) (Tenant, error)
	GetTenantByID(ctx context.Context, id string) (Tenant, error)
	GetAllTenants(ctx context.Context) ([]Tenant, error)
	UpdateTenant(ctx context.Context, id string, update Tenant) error
	DeleteTenant(ctx context.Context, id string) error
	AssociateUserWithTenant(ctx context.Context, userID, tenantID string) error
	DisassociateUserFromTenant(ctx context.Context, userID, tenantID string) error
	GetTenantsForUser(ctx context.Context, userID string) ([]Tenant, error)
	GetUsersForTenant(ctx context.Context, tenantID string) ([]string, error)
}

TenantStore manages tenants and user memberships.

type UpdateProfileInput

type UpdateProfileInput struct {
	UserID    string
	TenantID  string
	FirstName *string
	LastName  *string
}

UpdateProfileInput is the payload behind PATCH /profile. FirstName and LastName are pointers because the route is a *partial* update: the reference types the body { firstName?: string | null, lastName?: string | null } and passes it through verbatim, so a key the caller omitted arrives at the store as undefined and leaves the stored column alone (wire-contract §3.5, user-store.interface.ts:136).

nil therefore means "not submitted, leave as it is" and a pointer to "" means "clear it" — the distinction a plain string cannot carry, and without which sending only firstName silently erases the stored lastName.

type User

type User struct {
	ID                           string
	Email                        string
	PasswordHash                 string
	TenantID                     string
	PhoneNumber                  string
	FirstName                    string
	LastName                     string
	Role                         string
	IsEmailVerified              bool
	Require2FA                   bool
	IsTOTPEnabled                bool
	TOTPSecret                   string
	ResetTokenHash               string
	ResetTokenExpiresAt          *time.Time
	MagicLinkTokenHash           string
	MagicLinkTokenExpiresAt      *time.Time
	SMSCodeHash                  string
	SMSCodeExpiresAt             *time.Time
	EmailVerificationTokenHash   string
	EmailVerificationTokenExpiry *time.Time
	PendingEmail                 string
	EmailChangeTokenHash         string
	EmailChangeTokenExpiry       *time.Time
	Metadata                     map[string]any
	Roles                        []string
	Permissions                  []string
	Tenants                      []Tenant
	CustomClaims                 map[string]any
	CreatedAt                    time.Time
	UpdatedAt                    time.Time
}

User is the canonical identity model.

type UserAccountStore

type UserAccountStore interface {
	UpdateProfile(ctx context.Context, userID, tenantID, firstName, lastName string) (User, error)
	DeleteUser(ctx context.Context, userID, tenantID string) error
}

UserAccountStore adds account lifecycle operations.

type UserMetadataStore

type UserMetadataStore interface {
	GetMetadata(ctx context.Context, userID string) (map[string]any, error)
	UpdateMetadata(ctx context.Context, userID string, metadata map[string]any) error
	ClearMetadata(ctx context.Context, userID string) error
}

UserMetadataStore manages arbitrary key/value user metadata.

type UserPasswordStore

type UserPasswordStore interface {
	UpdatePassword(ctx context.Context, userID, tenantID, passwordHash string) error
	UpdateResetToken(ctx context.Context, userID, tenantID, tokenHash string, expiry time.Time) error
	GetUserByResetTokenHash(ctx context.Context, tokenHash string) (User, error)
	ClearResetToken(ctx context.Context, userID, tenantID string) error
}

UserPasswordStore adds password lifecycle methods.

type UserPhoneStore added in v0.2.0

type UserPhoneStore interface {
	UpdatePhoneNumber(ctx context.Context, userID, tenantID, phoneNumber string) (User, error)
}

UserPhoneStore adds the phone-number update behind POST /add-phone. It is a capability of its own rather than a method on UserAccountStore so that a store written against the existing interface keeps compiling; a store that does not implement it makes the route answer 501, which is the reference's own behaviour when userStore.updatePhoneNumber is absent (auth.router.ts:700-703).

type UserStore

type UserStore interface {
	CreateUser(ctx context.Context, user User) (User, error)
	GetUserByEmail(ctx context.Context, email, tenantID string) (User, error)
	GetUserByID(ctx context.Context, id, tenantID string) (User, error)
}

UserStore persists and resolves users.

type VerifyEmailInput

type VerifyEmailInput struct {
	Token string
}

type WebhookDispatcher

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

WebhookDispatcher delivers signed webhook payloads.

func NewWebhookDispatcher

func NewWebhookDispatcher(endpoints ...WebhookEndpoint) *WebhookDispatcher

NewWebhookDispatcher creates a dispatcher for the given endpoints.

func (*WebhookDispatcher) Dispatch

func (d *WebhookDispatcher) Dispatch(ctx context.Context, event Event)

Dispatch delivers the event to all matching endpoints.

type WebhookEndpoint

type WebhookEndpoint struct {
	ID     string
	URL    string
	Secret string
	Events []string // event names to forward; empty = all
}

WebhookEndpoint configures a single outbound webhook target.

type WebhookPayload

type WebhookPayload struct {
	ID        string         `json:"id"`
	Event     string         `json:"event"`
	Timestamp time.Time      `json:"timestamp"`
	Data      map[string]any `json:"data"`
}

WebhookPayload is the envelope sent to webhook targets.

Directories

Path Synopsis
adapter
chi
gin
internal/wiretest
Package wiretest is the shared wire-contract conformance suite for the HTTP adapters.
Package wiretest is the shared wire-contract conformance suite for the HTTP adapters.

Jump to

Keyboard shortcuts

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