auth

package module
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 35 Imported by: 0

README ¶

ToGO

togo-framework/auth

marketplace pkg.go.dev MIT

Part of the togo framework.

Install

togo install togo-framework/auth

ToGO

auth

part of the togo-framework — the full-stack Go + React framework

The togo base auth provider: JWT token auth, bcrypt passwords, a self-contained users store (via the ORM), multi-guard, roles + permissions (RBAC), middleware, and /api/auth routes. Default driver for the framework; Supabase/Firebase/OAuth/WorkOS ship as driver plugins that depend on this package.

togo install togo-framework/auth

Admin user management

Mounted at /api/auth/admin/* for the built-in driver (AUTH_DRIVER=base). All routes require the admin role, re-checked against the database on every request (401 anonymous, 403 non-admin). Cookie-authenticated writes need X-CSRF-Token (from GET /api/auth/csrf); bearer requests are exempt.

Route Body Response
GET /users?q=&limit=&offset= [{id,email,roles[],permissions[],created_at}] (limit default 100, max 500)
POST /users {email, password?, roles?, permissions?} 201 {user}
GET /users/{id} user
PATCH /users/{id} {email?, roles?, permissions?} user
DELETE /users/{id} {deleted:true,id}
POST /users/{id}/impersonate {token, identity{..., impersonator}, expires_at}
POST /users/{id}/reset-password {password?} {reset:true} or {link, emailed:false, expires_at}
POST /users/{id}/magic-link {link, emailed:false, expires_at}

Also: GET /api/auth/magic?token= (redeems a magic link, 302 to AUTH_POST_LOGIN_URL), POST /api/auth/impersonation/stop (ends an impersonation), and GET /api/auth/me, which includes impersonator (the admin's id) while impersonating. Events: auth.user_created|updated|deleted|impersonated, auth.impersonation_ended, auth.magic_link_issued, auth.admin_reset_link_issued. Payloads name the actor and never carry a token or link. See SECURITY.md for the policy.

Rules for acting on another administrator: impersonate, reset-password, magic-link and changing their email, roles or permissions (PATCH) are all refused with 403 by default. AUTH_ADMIN_CROSS_CONTROL=true is the single, explicitly privileged opt-in that relaxes every one of these admin-target restrictions (it is the only such flag; it is configuration only and cannot be granted through the API). Each cross-admin operation done under it emits auth.admin_cross_control (actor_id, target_id, operation, policy). Always allowed: self-edits (including self-demotion, subject to the last-administrator rule), promoting a non-admin to admin, and deleting another admin (no session is created as them; the last administrator is always protected). These are audited: auth.user_updated carries actor_id, target_id and the changed field names (no values); auth.user_deleted carries actor_id and target_id. Reset and magic links an admin issued for an ordinary user stop working if that user has since become an admin (unless the opt-in is on); self-service forgot-password tokens are unaffected. Request bodies reject unknown fields. A magic link redeemed for a user starts a session that names the issuing admin (impersonator, act claim, auth.login event, plus auth.magic_link_redeemed) and lasts as long as an impersonation.

Config: AUTH_PUBLIC_URL/APP_URL (link base), AUTH_RESET_PATH (default /reset-password), AUTH_POST_LOGIN_URL, AUTH_IMPERSONATION_TTL_MINUTES (default 30, max 480), AUTH_ADMIN_CROSS_CONTROL (default off; the only switch for cross-admin control).

Token verification: Verify(token) now delegates to VerifyContext(ctx, token), which also enforces revocation and the actor/target checks for impersonation tokens (a database lookup). Prefer VerifyContext with the request context.

auth.password_reset_requested carries the raw reset token so a mailer can deliver it; it is for in-process hook subscribers only and must never be logged or forwarded to an external bus.

Sessions and the database (strict revalidation)

A token proves who the caller is. The database decides what that caller may do now. With the built-in driver (AUTH_DRIVER=base) every authenticated request loads the account by id (one primary-key read), and:

  • if the account is gone, the request is 401;
  • the identity's email, roles and permissions are replaced by the stored values.

This happens in VerifyContext and in the personal-access-token path of Authenticate/Middleware, so everything that consumes the identity is database-authoritative with no change on your side: RequireRole, RequirePermission, IdentityFrom, Authenticate, and any route that reads claims from the context. A demoted administrator gets 403 on the very next request, a deleted account 401, and a promoted user's existing session carries the admin role as soon as the database says so. An app that guards routes with svc.RequireRole("admin"), svc.Middleware or svc.Authenticate is covered. An app that parses the JWT itself (for example with its own jwt.Parse) is not: use VerifyContext.

Details:

  • Revoked impersonation and magic-link tokens (by jti) stay refused. An impersonation or magic-link session also needs its issuing administrator to still be an administrator, and is refused once its target is an administrator (unless AUTH_ADMIN_CROSS_CONTROL=true).
  • Personal access tokens revalidate only that the owner exists. They keep their own abilities and are never administrators.
  • There is no disabled or suspended state in this plugin, so "the account exists" is the whole test. revalidate in auth.go is the single place a future disabled or security-version check goes.
  • Applies to the base driver only. With AUTH_DRIVER=supabase (or any external driver) the provider owns identity, the users table is not authoritative, and tokens are verified as before.
  • No new configuration; no token claim or session format changes. Tokens issued before an upgrade keep working: their roles are simply read from the database.
  • Verify(token) now performs the database check (it calls VerifyContext with a background context). Prefer VerifyContext with the request context.

Provenance and the promotion gate

An administrator who sets an account's email or password, or who creates the account, is recorded as the writer of that field in the side table auth_account_state (created automatically; users is never altered). The password written by redeeming an administrator-issued reset link is attributed to the issuer. Provenance is sticky and cumulative: the holder changing their own password does not clear it, and a later administrator's write does not erase an earlier administrator's: each field keeps every administrator who ever wrote it.

Promoting an account to administrator (PATCH /users/{id} with roles containing admin) answers 409 Conflict if any field was written by someone other than the holder and the promoting administrator:

{"error":"identity_set_by_other_admin","message":"...","tainted_fields":["email"],
 "set_by":{"email":"<admin id>"},"set_at":{"email":"..."},"accept_field":"accept_identity_set_by_other"}

The condition is deliberate and must be surfaced, never retried silently: a UI should show who set which field and ask the promoter to confirm. Sending the same request with "accept_identity_set_by_other": true promotes, and the acceptance applies to that one request only. It is recorded (accepted_by, accepted_at) and audited in auth.admin_promoted (tainted, accepted); it does not clear the provenance for later promotions. A promoter's own earlier writes are exempt only when the promoter is the sole non-holder writer of that field: if another administrator ever wrote it too, promotion answers 409 (set_by lists the writers, comma-separated).

Redeeming an administrator-issued reset link and the admin set-password mode run in one transaction that writes the password only while the account's roles are still the value that was judged non-administrator. If the account was promoted in between, nothing is written: a redemption commits the burn of the token, answers the same generic 401 as any invalid link, and emits auth.credential_refused (type, issuer, target_id); set-password answers 409.

Promotion is judged on the provenance stored before the request. Changing the email in the same request as the promotion does not clear another administrator's email provenance (still 409); only a write the promoter made in an earlier request, and only as the field's sole writer, is exempt. A password set by redeeming a self-service reset token inherits the provenance of the email the token was sent to (an administrator-issued link: the issuer), so it counts as set by that administrator.

Reset tokens and magic links are bound to the account state they were issued against (auth_recovery_context; auth_priv_epoch counts promotions and demotions; both created automatically, users is never altered). Redemption refuses a token, burning it and answering the generic 401, if the email changed (self-service tokens), or the account was promoted or demoted since (all tokens). Tokens that predate the upgrade have no context and are refused. SetRoles bumps the counter too.

An administrator-issued reset link or magic link also stops working if its issuer is deleted or no longer an administrator when it is redeemed: it is refused and burned with the generic 401.

New events: auth.admin_promoted, auth.admin_demoted, auth.credential_refused.

Trusted-caller API

The exported Go methods (SetPassword, SetRoles, CreateUser, ...) are for trusted code in your own process. They apply the password policy but do not apply the admin-target rules above, write no provenance, and do not take the compare-and-set. Do not expose them to request input without your own authorization.

Startup logs a warning if the retired variable AUTH_IMPERSONATE_ADMINS is set: it has no effect; the flag is AUTH_ADMIN_CROSS_CONTROL.

Frontend

UI lives in the separate dashboard plugin (login/register/reset/2fa/lock/profile/dashboard), which depends on this package.


💎 Premium sponsors

togo is proudly sponsored by ID8 Media and One Studio.

ID8 Media        One Studio


Premium sponsors

ID8 Media  ·  One Studio

Support togo — become a sponsor.

Documentation ¶

Overview ¶

Package auth is togo's base authentication system: JWT token auth, bcrypt passwords, a self-contained users store (via the ORM), multi-guard, and roles/permissions (RBAC). It's the default auth driver; Supabase/Firebase/ OAuth/WorkOS ship as driver plugins that depend on this package.

Install: `togo install togo-framework/auth` (blank-import registers it).

Index ¶

Constants ¶

View Source
const (
	EventRegistered      = "auth.registered"
	EventLogin           = "auth.login"
	EventLogout          = "auth.logout"
	EventPasswordChanged = "auth.password_changed"
	EventLoginFailed     = "auth.login_failed"

	// EventPasswordResetRequested carries {user_id, email, token, expires_at}.
	// A mail/SMS/notifications listener delivers the reset link; the token is
	// never returned over HTTP.
	EventPasswordResetRequested = "auth.password_reset_requested"
	EventPasswordReset          = "auth.password_reset"
	// EventLoginChallenged fires when a password login is held for 2FA.
	EventLoginChallenged = "auth.login_challenged"

	// Admin user-management events. Every payload is a map[string]string that
	// names the acting administrator (actor_id) and never carries a secret: no
	// token, link or password.
	EventUserCreated = "auth.user_created" // actor_id, target_id, user_id, email
	EventUserUpdated = "auth.user_updated" // actor_id, target_id, user_id, fields (names only)
	EventUserDeleted = "auth.user_deleted" // actor_id, target_id, user_id, email
	// EventUserImpersonated: actor_id, target_id, at, expires_at.
	EventUserImpersonated = "auth.user_impersonated"
	// EventImpersonationEnded: actor_id, target_id, at.
	EventImpersonationEnded = "auth.impersonation_ended"
	// EventMagicLinkIssued: actor_id, user_id, expires_at. The link itself goes
	// only to the administrator who asked for it, in the HTTP response.
	EventMagicLinkIssued = "auth.magic_link_issued"
	// EventMagicLinkRedeemed: actor_id (the issuing admin), target_id, at.
	EventMagicLinkRedeemed = "auth.magic_link_redeemed"
	// EventAdminCrossControl is fired for every operation one administrator
	// performs on another under the AUTH_ADMIN_CROSS_CONTROL exception:
	// actor_id, target_id, operation, policy ("AUTH_ADMIN_CROSS_CONTROL").
	EventAdminCrossControl = "auth.admin_cross_control"
	// EventAdminResetLinkIssued: actor_id, user_id, expires_at (no link).
	EventAdminResetLinkIssued = "auth.admin_reset_link_issued"
	// EventAdminPromoted fires when an account becomes an administrator through
	// the admin API: actor_id, target_id, tainted (comma-joined fields whose
	// email/password another administrator set, empty if none), accepted
	// ("true" when the promoter explicitly accepted that, else "false").
	EventAdminPromoted = "auth.admin_promoted"
	// EventAdminDemoted fires when an administrator loses the role through the
	// admin API: actor_id, target_id.
	EventAdminDemoted = "auth.admin_demoted"
	// EventCredentialRefused fires when an admin-issued credential (a reset link
	// redemption or an admin-set password) is refused because the account is, or
	// became, an administrator or changed under the operation: type ("reset" or
	// "set-password"), issuer (the administrator), target_id. Never the token.
	EventCredentialRefused = "auth.credential_refused" //#nosec G101 -- an event name, not a credential
)

Auth lifecycle events fired on the kernel hook bus. Apps subscribe via k.Hooks.On(auth.EventLogin, 50, fn) to inject behavior — audit logging, welcome mail, post-login/redirect decisions, etc. Listeners run in priority order.

View Source
const EventOTPRequested = "auth.otp.requested"

EventOTPRequested carries {subject, code, purpose} for delivery plugins.

View Source
const LoginChallengeTTL = 5 * time.Minute

LoginChallengeTTL is how long a user has to enter their code.

View Source
const MagicLinkTTL = 15 * time.Minute

MagicLinkTTL is how long an admin-issued magic link stays valid.

View Source
const PasswordResetTTL = 30 * time.Minute

PasswordResetTTL is how long a reset link stays valid.

View Source
const SessionCookie = "togo_session"

SessionCookie is the name of the HttpOnly cookie holding the session token.

Variables ¶

View Source
var ErrInvalidCredentials = errors.New("invalid credentials")

ErrInvalidCredentials is returned when email/password don't match.

View Source
var ErrUserNotFound = errors.New("user not found")

ErrUserNotFound is returned when an account operation names no account.

Functions ¶

func RegisterLoginMethod ¶ added in v0.8.0

func RegisterLoginMethod(m LoginMethod)

RegisterLoginMethod advertises a sign-in method (call from a plugin init/provider).

func RegisterSecondFactor ¶ added in v0.9.0

func RegisterSecondFactor(f SecondFactor)

RegisterSecondFactor adds a check consulted at login (auth-mfa registers one).

func RegisterSessionStore ¶ added in v0.6.0

func RegisterSessionStore(name string, f func(k *togo.Kernel) (SessionStore, error))

RegisterSessionStore registers a server-side session driver (call from init()).

Types ¶

type Authenticator ¶

type Authenticator interface {
	Attempt(ctx context.Context, email, password string) (*Identity, error)
	ByID(ctx context.Context, id string) (*Identity, error)
}

Authenticator verifies credentials and loads identities. Drivers (supabase, oauth, …) implement this; the default is DB + bcrypt.

type Guard ¶

type Guard struct {
	Name string
	Auth Authenticator
}

Guard pairs a name with an Authenticator — enabling multi-guard setups.

type Identity ¶

type Identity struct {
	ID          string   `json:"id"`
	Email       string   `json:"email"`
	Roles       []string `json:"roles"`
	Permissions []string `json:"permissions"`
	Guard       string   `json:"guard"`
	// Impersonator is the id of the administrator acting as this identity. It is
	// empty for every real login, and set only on tokens issued through
	// POST /api/auth/admin/users/{id}/impersonate (the token's `act.sub` claim).
	Impersonator string `json:"impersonator,omitempty"`
	// TokenID is the token's jti. It is set only on impersonation tokens, which
	// are revocable.
	TokenID string `json:"-"`
}

Identity is the authenticated principal exposed to the app.

func IdentityFrom ¶

func IdentityFrom(ctx context.Context) (*Identity, bool)

IdentityFrom returns the authenticated identity from the request context.

func (Identity) Can ¶

func (i Identity) Can(perm string) bool

Can reports whether the identity has a permission.

func (Identity) HasRole ¶

func (i Identity) HasRole(role string) bool

HasRole reports whether the identity has a role.

type LoginMethod ¶ added in v0.8.0

type LoginMethod struct {
	Name  string `json:"name"`  // e.g. "google", "dev"
	Label string `json:"label"` // button text
	Type  string `json:"type"`  // "oauth" | "dev" | "sso"
	URL   string `json:"url"`   // endpoint to hit/redirect
}

LoginMethod is an alternative sign-in option (OAuth provider, dev login, SSO …) advertised to the frontend so it only renders methods that are actually configured. Plugins register theirs via RegisterLoginMethod.

type SecondFactor ¶ added in v0.9.0

type SecondFactor func(ctx context.Context, userID string) bool

SecondFactor reports whether a user must pass an extra factor at login.

type Service ¶

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

Service is the auth runtime stored on the kernel (k.Get("auth")).

func FromKernel ¶

func FromKernel(k *togo.Kernel) (*Service, bool)

FromKernel fetches the auth service from the kernel container.

func New ¶

func New(k *togo.Kernel) (*Service, error)

New builds the service, ensures the users table exists, and registers the default DB-backed guard. It fails closed in production when no strong secret is configured.

func (*Service) Authenticate ¶ added in v0.9.1

func (s *Service) Authenticate(r *http.Request) (*Identity, error)

Authenticate resolves the request's identity (bearer JWT, personal access token, or session cookie) without writing a response. Apps that layer their own policy on top of auth — public and protected routes on one router, or roles mapped to app actors — use it instead of Middleware, which answers 401.

func (*Service) CompleteLogin ¶ added in v0.9.0

func (s *Service) CompleteLogin(w http.ResponseWriter, ctx context.Context, userID string)

CompleteLogin issues the session for a user who has passed every factor. Exported so second-factor plugins finish logins the same way.

func (*Service) CreateUser ¶ added in v0.9.1

func (s *Service) CreateUser(ctx context.Context, email, password string, roles []string) (*Identity, error)

CreateUser adds an account with a password and roles, for seeders and bootstrap steps that must not go through public registration. It fires EventRegistered like registration does.

func (*Service) FindByEmail ¶ added in v0.9.0

func (s *Service) FindByEmail(ctx context.Context, email string) (*Identity, error)

FindByEmail returns the identity for an existing account, or nil if there is none. Unlike FindOrCreateByEmail it never creates an account, so it is the one to use for flows a stranger can trigger (magic links, OTP login).

func (*Service) FindOrCreateByEmail ¶ added in v0.5.0

func (s *Service) FindOrCreateByEmail(ctx context.Context, email string) (*Identity, error)

FindOrCreateByEmail returns the identity for email, creating a passwordless account if none exists (used by SSO/OAuth where there's no local password).

func (*Service) Guard ¶

func (s *Service) Guard(name string) *Guard

Guard returns a named guard (or the default).

func (*Service) IssueLoginChallenge ¶ added in v0.9.0

func (s *Service) IssueLoginChallenge(userID string) (string, error)

IssueLoginChallenge mints a signed, short-lived challenge for a user who has passed their password but still owes a second factor. It is not a session.

func (*Service) IssueSession ¶ added in v0.5.0

func (s *Service) IssueSession(w http.ResponseWriter, id Identity) (string, error)

IssueSession issues a JWT for id, sets the session cookie, and fires EventLogin. Returns the token (drivers may also redirect).

func (*Service) IssueToken ¶

func (s *Service) IssueToken(id Identity) (string, error)

IssueToken signs a JWT for an identity.

func (*Service) Middleware ¶

func (s *Service) Middleware(next http.Handler) http.Handler

Middleware authenticates the request from its bearer token and stores the Identity in context. 401 if the token is missing/invalid.

func (*Service) RegisterGuard ¶

func (s *Service) RegisterGuard(name string, a Authenticator)

RegisterGuard adds a named guard (multi-guard support).

func (*Service) RequirePermission ¶

func (s *Service) RequirePermission(perm string) func(http.Handler) http.Handler

RequirePermission guards a route by permission.

func (*Service) RequireRole ¶

func (s *Service) RequireRole(role string) func(http.Handler) http.Handler

RequireRole guards a route by role.

func (*Service) SecondFactorRequired ¶ added in v0.9.0

func (s *Service) SecondFactorRequired(ctx context.Context, userID string) bool

SecondFactorRequired reports whether the user must complete 2FA before a session is issued: core TOTP enabled, or any registered second factor.

func (*Service) SetPassword ¶ added in v0.9.3

func (s *Service) SetPassword(ctx context.Context, userID, password string) error

SetPassword replaces an account's password, for an admin resetting a user who cannot receive a reset link. The password policy applies, and EventPasswordChanged fires as it does for a self-service change.

func (*Service) SetRoles ¶ added in v0.9.1

func (s *Service) SetRoles(ctx context.Context, userID string, roles []string) error

SetRoles replaces an account's roles. Registration creates accounts with no roles, so this is how an app grants them (an admin screen, a seeder, or a bootstrap step).

func (*Service) UserByID ¶ added in v0.9.1

func (s *Service) UserByID(ctx context.Context, id string) (*Identity, error)

UserByID loads an account's current identity from the store. Roles and permissions in a JWT are a snapshot from sign-in; apps that must honour a role change or revocation immediately re-read them here.

func (*Service) Verify ¶

func (s *Service) Verify(token string) (*Identity, error)

Verify parses a token into an Identity. Enforces HS256, a required expiry, and the issuer — rejecting alg-confusion, unexpiring, and foreign tokens.

func (*Service) VerifyContext ¶ added in v0.10.0

func (s *Service) VerifyContext(ctx context.Context, token string) (*Identity, error)

VerifyContext parses and validates a token, then revalidates it against the database. It is the one authoritative verification path; Verify delegates to it.

Strict session revalidation (base driver): the token proves who the caller is and that it was issued by this service; the database decides what the caller may do now. The account is loaded by id on every call, a missing account is refused, and the identity's email, roles and permissions are replaced by the stored values. Every consumer of the identity (RequireRole, RequirePermission, IdentityFrom, requireAdmin, apps using Authenticate) is therefore database-authoritative without any change on their side. An impersonation or magic-link session additionally needs its jti unrevoked and its administrator still an administrator.

func (*Service) VerifyLoginChallenge ¶ added in v0.9.0

func (s *Service) VerifyLoginChallenge(token string) (string, error)

VerifyLoginChallenge validates a challenge and returns its user id.

type SessionStore ¶ added in v0.6.0

type SessionStore interface {
	Put(ctx context.Context, sid, token string, ttl time.Duration) error
	Get(ctx context.Context, sid string) (string, bool, error)
	Delete(ctx context.Context, sid string) error
}

SessionStore persists a token under a session id with a TTL.

type User ¶

type User struct {
	ID           string `db:"id" json:"id"`
	Email        string `db:"email" json:"email"`
	PasswordHash string `db:"password_hash" json:"-"`
	Roles        string `db:"roles" json:"roles"`
	Permissions  string `db:"permissions" json:"permissions"`
	CreatedAt    string `db:"created_at" json:"created_at"`
}

User is the stored account. All columns are TEXT for cross-driver portability.

Jump to

Keyboard shortcuts

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