AnnouncementsIntroducing Arkoren

    Menu

    Session Login

    Check application-owned credentials, rotate the browser session, and revoke account sessions.

    What's on this page
    1. Load current accounts from typed session identifiers and reject deleted or disabled accounts.
    2. Check passwords without revealing account existence and rotate the session after verified login.
    3. End local logins, revoke older account sessions, and select the intended session context.
    On this page

    A browser login checks credentials once, then stores an account identifier in the session. On later requests, auth.SessionIdentity loads the current account from that identifier. You own the accounts, password checks, and account-status policy; Sessions owns cookie configuration, storage, and response-time persistence.

    The examples assume an existing router web, session store store, configured hash.Hasher, and application repository accounts. Import auth, session, and response from their arkoren.dev/core/http/ packages, csrf from arkoren.dev/core/http/middleware/csrf, and standard-library context, errors, and net/http where used.

    Load Accounts

    Keep the session small: store an account ID, not a snapshot of permissions. The loader can then reject a deleted or disabled account on the next request. This application separates the handler identity from its password record:

    type User struct {
    	ID   int
    	Name string
    }
    
    type Account struct {
    	User
    	PasswordHash   []byte
    	Disabled       bool
    	SessionVersion int
    }

    Here accounts.ByID(ctx, id) returns Account; the application sentinel ErrAccountNotFound means absence, while other errors retain storage failures. Bind the loader to the identifier type int:

    loadUser := func(ctx context.Context, id int) (User, error) {
    	account, err := accounts.ByID(ctx, id)
    	if errors.Is(err, ErrAccountNotFound) {
    		return User{}, auth.ErrNoCredentials
    	}
    	if err != nil {
    		return User{}, err
    	}
    	if account.Disabled {
    		return User{}, auth.ErrNoCredentials
    	}
    
    	return account.User, nil
    }

    The loader runs on every request with a usable stored identifier. Absence and disabled status become anonymous requests; a storage outage instead stops the request. Returning a zero user with nil error would authenticate it, so do not use that result for absence. If you cache accounts, your invalidation policy determines how quickly account changes take effect.

    Enable Login

    Construct one identity definition and share it between middleware and handlers:

    identity := auth.NewSessionIdentity(loadUser)
    web.Use(session.Middleware(store))
    web.Use(csrf.Middleware())
    web.Use(auth.Middleware(identity.Authenticator()))

    Session middleware must run first. Keep CSRF protection on login, logout, and other state-changing browser routes: browsers send session cookies automatically, including on requests another site can initiate. Add auth.Require[User]() only to routes requiring a signed-in account, not to the login route itself.

    Construction does not call the loader. Check identity.Err() during setup when configuration is assembled dynamically; a zero definition or nil loader returns auth.ErrInvalidSessionIdentity, not an anonymous result. Missing sessions, missing identity fields, and incompatible stored types are anonymous. Persisted JSON that remains compatible can still decode after an identifier type change; a type change is not a revocation mechanism.

    Check Passwords

    Start with validated request input, not an arbitrary email or account ID from a header. A login request has only the fields needed to check credentials:

    type LoginInput struct {
    	Email    string `json:"email"`
    	Password string `json:"password"`
    }

    Use the same rejection for an unknown email, a wrong password, and a disabled account. Otherwise login becomes an account-enumeration endpoint. The following fragment runs in your login handler with validated in LoginInput and a configured hasher *hash.Hasher. accounts.ByEmail returns the same application absence sentinel as ByID:

    account, err := accounts.ByEmail(r.Context(), in.Email)
    if err != nil && !errors.Is(err, ErrAccountNotFound) {
    	return err
    }
    
    found := err == nil
    storedHash := dummyHash
    if found {
    	storedHash = account.PasswordHash
    }

    dummyHash is a hash of a non-secret placeholder, computed once at startup with the same hasher and cost as real passwords. Check it on the missing-account path so that path does not skip the deliberately expensive password check:

    matches, err := hasher.Check(in.Password, storedHash)
    if err != nil {
    	return err
    }
    if !found || !matches || account.Disabled {
    	return auth.ErrUnauthenticated
    }

    The explicit !found check prevents a password matching the dummy hash from creating a login. This reduces a timing difference; it does not guarantee equal response times. Preserve storage and hash-check errors rather than classifying them as bad passwords, and keep passwords and hashes out of error messages. Apply login rate limits before credential checks to limit guessing and hashing work.

    Create password hashes at registration using the hashing guide. Keep the dummy hash aligned with cost changes. For old hashes, follow hash upgrades, including its conditional write that avoids overwriting a concurrent password reset.

    Start Sessions

    After the credential checks succeed, resolve the identity’s selected session and store the verified account ID. Do both before writing response headers:

    current, err := identity.Session(r.Context())
    if err != nil {
    	return err
    }
    if err := identity.Login(current, account.ID); err != nil {
    	return err
    }
    
    return response.Status(w, http.StatusNoContent)

    Login writes the identifier and then regenerates the session ID. Rotation protects against session fixation, where an attacker knows an ID planted before the victim signs in. If the field write fails, no rotation occurs and you must not report success. Login preserves other session values, creation time, and expiry; configure idle and absolute limits in session lifetimes.

    This call changes the request’s session, not persistent storage. On a non-5xx response, session middleware saves changes and retires the old ID before committing the response. Failed persistence or retirement can leave an old ID usable; configure and monitor the middleware’s error handling rather than treating Login success as proof of durable revocation. See Save Changes for the commit and concurrency boundaries.

    End Sessions

    Mount logout as a CSRF-protected POST, not a GET. Resolve the same identity’s session and remove its login before sending a response:

    current, err := identity.Session(r.Context())
    if err != nil {
    	return err
    }
    if err := identity.Logout(current); err != nil {
    	return err
    }
    
    return response.Status(w, http.StatusNoContent)

    Logout removes even a stale, wrongly typed identifier and rotates the ID after successful deletion. Other values, such as a language preference, remain. Use current.Destroy() instead when the whole session must end. Both paths still depend on response-time persistence; a failed retirement can leave the old ID valid.

    Revoke Account Sessions

    To end sessions across devices after a password change or compromise, store an account version with the ID. This is an alternative identity definition, not an additional identity entry:

    type SessionUser struct {
    	ID      int
    	Version int
    }

    Change your loader to func(context.Context, SessionUser) (User, error). After loading the account and handling absence or storage errors as above, reject a stale version:

    if account.Disabled || account.SessionVersion != saved.Version {
    	return User{}, auth.ErrNoCredentials
    }
    return account.User, nil

    Here saved is the loader’s SessionUser argument. Reconstruct the definition with that loader and supply both values after successful credential checks:

    if err := identity.Login(current, SessionUser{
    	ID:      account.ID,
    	Version: account.SessionVersion,
    }); err != nil {
    	return err
    }

    Increment the persisted SessionVersion to invalidate older versions on each device’s next account lookup. No session enumeration is required. Existing in-flight requests are not retroactively canceled, and account caching can delay the effect.

    Select Sessions

    If you configured a nondefault session context slot, share one handle between the middleware and the identity:

    handle := session.NewHandle()
    identity := auth.NewSessionIdentity(loadUser, func(binding *auth.SessionBuilder) {
    	binding.Handle(handle)
    })
    web.Use(session.Middleware(store, func(cookies *session.Builder) {
    	cookies.Handle(handle)
    }))

    Register CSRF and authentication after this session middleware as before. identity.Session(ctx) selects only this slot and returns session.ErrNoSession for an absent or nil session; it never falls back to the default slot. A zero handle selects the default. Explicit Login and Logout arguments are not checked against the handle, so use identity.Session to select them consistently.

    A handle is not cookie or storage isolation. Definitions writing to the same physical session share arkoren:auth:user: the last login replaces the identity, and logout removes it. Independent browser sessions also need distinct cookie and storage configuration in Sessions. For authentication tests, combine direct identity tests with a real cookie-based login/logout test that exercises persistence.

    For all functions and options, see the auth API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive