go-auth
Guides

Sessions

Checking who's logged in, listing active sessions (paginated and not), and revoking them — one, several, or all. curl, Go, and a browser client.

Sessions

The self-service surface on top of the two-token session model: checking whether a request is authenticated, listing a user's own active sessions, and revoking them. For the token/cookie mechanics themselves (rotation, hashing, the grace window) see Security concepts and the sessions table on Schemas. For an admin viewing or revoking another user's sessions, see Admin — that's a different, role-gated surface and isn't covered here.

Configuration

Two options govern everything on this page: SessionConfig sets the lifetimes that decide when the endpoints below actually have something to show, and CookieConfig sets the cookie names every curl example on this page reads and writes.

goauth.WithSession(goauth.SessionConfig{
    TTL:             30 * 24 * time.Hour, // optional, default 30d
    IdleTTL:         7 * 24 * time.Hour,  // optional, default 7d
    RefreshTokenTTL: 30 * 24 * time.Hour, // optional, default 30d
    MaxLifetime:     0,                   // optional, default 0 (no limit)
    GraceWindow:     10 * time.Second,    // optional, default 5s — or goauth.Disabled
    TouchDebounce:   goauth.Disabled,     // optional, default 5m — or goauth.Disabled
})
FieldTypeRequiredDefaultNotes
TTLtime.DurationOptional30dAbsolute hard expiry — the expires_at on every session in the list responses above.
IdleTTLtime.DurationOptional7dTimeout since last activity. Must not exceed TTL.
RefreshTokenTTLtime.DurationOptional30dMust not be less than TTL. Governs how long the transparent refresh described in the curl note above keeps working after the session itself expires.
MaxLifetimetime.DurationOptional0 — no limitIf set, must be >= TTL.
GraceWindowtime.DurationOptional5sWindow a just-rotated refresh token is still accepted, so two requests racing to refresh don't log the user out. goauth.Disabled turns it off — a plain 0 means "use the default," not "off."
TouchDebouncetime.DurationOptional5mMinimum interval between last_active_at writes — what makes last_active_at in the list responses a few minutes stale rather than exact. goauth.Disabled writes on every authenticated request.

Expected errors

  • session_ttl must be positive
  • session_idle_ttl must be positive
  • session_idle_ttl must not exceed session_ttl
  • refresh_token_ttl must be positive
  • refresh_token_ttl must not be less than session_ttl
  • session grace_window must not be negative (use goauth.Disabled to turn it off) — hint: you passed a negative duration that isn't the Disabled sentinel.
  • session touch_debounce must not be negative (use goauth.Disabled to turn it off) — same hint.
  • session max_lifetime must not be negative (0 = no limit)
  • session max_lifetime must not be less than session_ttl — hint: only checked when MaxLifetime is set above zero.
goauth.WithCookie(goauth.CookieConfig{
    Name:        "goauth_session",     // optional, default "goauth_session"
    RefreshName: "goauth_refresh",     // optional, default "goauth_refresh"
    Domain:      "",                    // optional, default "" (host-only cookie)
    Path:        "/",                   // optional, default "/"
    SameSite:    http.SameSiteLaxMode,  // optional, default Lax
    Secure:      goauth.SecureAlways(), // optional, default derived from Environment/BaseURL
})
FieldTypeRequiredDefaultNotes
NamestringOptionalgoauth_sessionThe cookie name every curl example on this page passes via -b/-c. Change it here and the examples change with it.
RefreshNamestringOptionalgoauth_refreshThe cookie AuthMiddleware reads for the transparent refresh.
DomainstringOptional"" — host-only cookie
PathstringOptional/
SameSitehttp.SameSiteOptionalLax
Secure*boolOptionalderived — true unless Environment is dev and BaseURL is http://Tri-state: nil derives it correctly for dev and prod. Override with goauth.SecureAlways() or goauth.SecureNever() (local http:// only).

No field on CookieConfig reaches validation as an error — every field is defaulted before the config is checked.

Full reference for both, including how they interact with everything else: Configuration → WithSession and Configuration → WithCookie.

Frontend client setup

The Client examples below call the same apiRequest helper defined on Client → Setup — see that page (or Authentication) if you haven't already.

A note on curl and sessions — read this if something isn't working

Every endpoint on this page needs the actual session cookie, and curl doesn't manage cookies automatically the way a browser does. Two things cause almost all of the confusion:

1. -b and -c need to point at the same file, on every request — not just login.

# Log in and create the jar
curl -X POST https://api.myapp.com/auth/login \
  -H "Content-Type: application/json" \
  -H "Origin: https://myapp.com" \
  -c cookies.txt \
  -d '{"email":"ada@example.com","password":"..."}'

# Every later request: read AND write the same file
curl https://api.myapp.com/auth/sessions -b cookies.txt -c cookies.txt

AuthMiddleware transparently rotates your session when the session cookie has expired but the refresh cookie is still valid — it issues new Set-Cookie headers on that same response rather than erroring. If you only ever read from the jar (-b) and never let curl write back to it (-c), you keep replaying the old token, and once the refresh window closes too you'll get a 401 session_expired that looks like it came from nowhere. Always pass both flags pointing at the same file.

2. The revoke endpoints also need the Origin header. DELETE /auth/sessions/{id}, POST /auth/sessions/revoke, and DELETE /auth/sessions are state-changing, so they run through the same OriginCheck as register/login — see Authentication → Origin checking. The two list endpoints and GET /auth/check are GET requests and don't need it; OriginCheck only inspects POST/PUT/PATCH/DELETE.

Is this secure?

Yes — curl isn't bypassing anything here, it's just making manual what a browser normally hides. The session cookie is the actual credential: it's a random token, stored server-side only as a SHA-256 hash (see Schemas → sessions), never guessable from the session ID. Every revoke query is scoped with WHERE user_id = ... AND id = ..., so knowing or guessing another session's ID never lets you touch it — RevokeSession returns the same 404 session_not_found whether the ID doesn't exist or just isn't yours. curl can do everything a browser can because it has the same cookie, not because any check was skipped.

Checking who's logged in

Two endpoints, for two different situations: one for a page that requires auth and should redirect if there isn't any, one for a page that renders differently either way without treating "not logged in" as an error.

GET /auth/me — Auth required

Returns the authenticated user plus whether they have a password set (relevant for OAuth-only accounts, which can be null-password).

{
  "id": "b3f1...",
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "role": "user",
  "isVerified": true,
  "isBanned": false,
  "orgOwnerCount": 0,
  "createdAt": "2026-08-09T12:00:00Z",
  "updatedAt": "2026-08-09T12:00:00Z",
  "hasPassword": true
}
CodeStatusCause
session_expired401No session cookie, or one that expired with no usable refresh cookie
unauthorized401Session cookie doesn't validate (bad signature, not found, user deleted)
user_banned403Account is banned — surfaces here even mid-session, during the transparent-refresh path

curl

curl https://api.myapp.com/auth/me -b cookies.txt -c cookies.txt

Programmatic (Go)

There's no dedicated "get me" service method — the HTTP layer reads the user already attached to the request context by AuthMiddleware. Calling this directly in Go means you already have the user from wherever you got it (e.g. middleware.GetUserFromContext(ctx) inside your own handler), so there's nothing extra to call.


Client

const me = await apiRequest(API_BASE, "GET", "/auth/me");

GET /auth/check — public, soft check

Never errors — always 200. Returns { "user": null } instead of a 401 when there's no valid session, which is what makes it suitable for a layout component that renders a "log in" vs. "account" button without treating the anonymous case as an error.

{ "user": { "...": "same shape as /auth/me, minus hasPassword" } }

or, with no valid session:

{ "user": null }

curl

curl https://api.myapp.com/auth/check -b cookies.txt -c cookies.txt

Programmatic (Go)

user, _, err := auth.Services.Auth.ValidateSession(ctx, sessionToken)
if err != nil {
    // treat as user == nil rather than propagating the error
}

Client

const { user } = await apiRequest(API_BASE, "GET", "/auth/check");
// user is null when there's no valid session — never throws for that case

Listing sessions

Both endpoints return the caller's own sessions only, plus current_session_id so the UI can highlight the device in use and disable its own revoke button.

Paginated — GET /auth/sessions

Query params

ParamTypeDefaultNotes
offsetint0
limitint20Clamped to a max of 100 — a larger value is silently capped, not rejected
{
  "sessions": [
    {
      "id": "9e2c...",
      "user_id": "b3f1...",
      "ip_address": "203.0.113.4",
      "user_agent": "Mozilla/5.0 ...",
      "parsed_ua": { "browser": "Chrome", "os": "Windows", "device_type": "desktop" },
      "is_revoked": false,
      "expires_at": "2026-09-08T12:00:00Z",
      "last_active_at": "2026-08-09T11:55:00Z",
      "created_at": "2026-08-01T09:00:00Z"
    }
  ],
  "total": 3,
  "limit": 20,
  "offset": 0,
  "current_session_id": "9e2c..."
}
CodeStatusCause
session_expired / unauthorized401Same as Checking who's logged in above
internal_error500Database failure

curl

curl "https://api.myapp.com/auth/sessions?offset=0&limit=20" -b cookies.txt -c cookies.txt

Programmatic (Go)

sessions, total, err := auth.Services.Session.List(ctx, userID, 0, 20)

Client

const { sessions, total, current_session_id } = await apiRequest(
  API_BASE, "GET", "/auth/sessions?offset=0&limit=20"
);

All at once — GET /auth/sessions/all

No query params, no pagination — every active session in one response. Same per-session shape as above, just without total/limit/offset.

{
  "sessions": [ "...": "unpaginated array, same shape as above" ],
  "current_session_id": "9e2c..."
}

Errors are the same as the paginated endpoint.


curl

curl https://api.myapp.com/auth/sessions/all -b cookies.txt -c cookies.txt

Programmatic (Go)

sessions, err := auth.Services.Session.ListAll(ctx, userID)

Client

const { sessions, current_session_id } = await apiRequest(API_BASE, "GET", "/auth/sessions/all");

Revoking sessions

All three endpoints are user-scoped — every query includes WHERE user_id = ..., so there's no way to revoke a session that isn't yours, even with a valid-looking ID.

Revoke one — DELETE /auth/sessions/{id}

id is the session's own ID (the id field from the list responses above), not the raw session token.

{ "message": "Session revoked" }
CodeStatusCause
session_not_found404ID doesn't exist, or exists but isn't yours — the two cases are indistinguishable on purpose
internal_error500Database failure

curl

curl -X DELETE https://api.myapp.com/auth/sessions/9e2c... \
  -H "Origin: https://myapp.com" \
  -b cookies.txt -c cookies.txt

Programmatic (Go)

revoked, err := auth.Services.Session.RevokeByIDForUser(ctx, sessionID, userID)
if err == nil && !revoked {
    // treat as session_not_found — it existed but wasn't this user's, or didn't exist at all
}

Client

await apiRequest(API_BASE, "DELETE", `/auth/sessions/${sessionId}`);

Revoke several — POST /auth/sessions/revoke

Body: { "session_ids": ["...", "..."] } — 1 to 100 IDs. IDs that don't exist or don't belong to the caller are silently skipped rather than erroring individually.

{ "revoked": 2 }

revoked is the count actually deleted, which can be less than the number of IDs you sent — that's not an error, it just means some IDs didn't match.

CodeStatusCause
invalid_input400session_ids empty, or more than 100 entries
invalid_json400Malformed body
internal_error500Database failure

curl

curl -X POST https://api.myapp.com/auth/sessions/revoke \
  -H "Content-Type: application/json" \
  -H "Origin: https://myapp.com" \
  -b cookies.txt -c cookies.txt \
  -d '{"session_ids":["9e2c...","a71f..."]}'

Programmatic (Go)

revoked, err := auth.Services.Session.RevokeManyForUser(ctx, []string{"9e2c...", "a71f..."}, userID)

Client

const { revoked } = await apiRequest(API_BASE, "POST", "/auth/sessions/revoke", {
  session_ids: [id1, id2],
});

Revoke all but current — DELETE /auth/sessions

No body. Revokes every one of the caller's sessions except the one making the request — the classic "log out everywhere else" button. If the current session can't be resolved for some reason, it falls back to revoking all of them.

{ "message": "Sessions revoked" }

There is no error response beyond the standard auth/internal ones — it always succeeds once you're authenticated at all.


curl

curl -X DELETE https://api.myapp.com/auth/sessions \
  -H "Origin: https://myapp.com" \
  -b cookies.txt -c cookies.txt

Programmatic (Go)

err := auth.Services.Session.RevokeAllExcept(ctx, userID, currentSessionID)
// or, if you don't have a "current" session to preserve:
err := auth.Services.Session.RevokeAll(ctx, userID)

Client

await apiRequest(API_BASE, "DELETE", "/auth/sessions");

Next

  • Authentication — register/login/logout, and the Origin-checking rules these endpoints share
  • Routes — the full path/param reference, including admin session management
  • Security — session hashing, refresh rotation, and the reuse-detection grace window
  • Schemas — the sessions table this page reads and writes

On this page