v0.2.3
One identity endpoint instead of two, a session and its user resolved in a single query, correct client IPs behind a proxy, and the cookie settings a frontend on another host actually needs.
Sessions
-
GET /auth/mereturns the caller's ownsession. Every token hash ondomain.Sessionisjson:"-", so nothing secret travels with it. It is what lets a client readactiveOrgIdandactiveOrgRole, which no other endpoint exposes:PUTandDELETE /auth/orgs/activeonly answer with a bare{"message"}.This is the shape
POST /auth/login,/auth/register,/auth/invite/register,/auth/2fa/verifyand/auth/verify-emailalready returned./auth/mewas the last identity endpoint that did not. -
GET /auth/checkis removed. It answered200with{"user": ...}or{"user": null}, and it was the only handler that resolved a session by callingValidateSessiondirectly instead of going throughAuthMiddleware. That meant it never attempted the transparent refresh: a session that was expired but refreshable, oneAuthMiddlewarewould have silently renewed, read as logged out.GET /auth/mecovers the same ground. It costs the same two round trips, and a401from it is the "not logged in" signal:try { setUser(await authApi.me()) } catch { setUser(null) }The
Auth.CheckSession(ctx, token) boolhelper is unchanged. It is for consumers routing their own handlers, and it goes through the same validation the middleware does. -
POST /auth/refreshreturns the session verbatim. It was the one endpoint that hand built a four field subset, which renamedlastActiveAttolastActiveand droppedactiveOrgIdandactiveOrgRole. A client that refreshed therefore received a different shape than the one/auth/mehad just handed it. It now returnsdomain.Sessionlike every other endpoint that returns a session. -
GET /auth/meis now rate limited like every other authenticated read. It was the only authenticated route without a limit, and a junk cookie shaped like a session still costs a session lookup.
Deployment: cookies and CSRF across hosts
Three changes that together make a frontend on a different host workable, and a new Deployment guide explaining which one you need and why.
-
CSRFTokenConfig.CookieDomainscopes the_csrfcookie, defaulting fromCookieConfig.Domainso the two stay in step. Previously the token cookie was always host only, with no way to change it, so a frontend on a sibling subdomain could never read the token and every state changing request answered403while reads kept working.Setting only this leaves the session cookie host only, which is narrower than widening
CookieConfig.Domain: the session cookie is sent to the API's own host either way, so it does not need widening for a sibling subdomain frontend to authenticate. -
CSRFTokenConfig.ExposeCSRFTokenInBodymakesGET /auth/csrf-tokenanswer200 {"token": "..."}withCache-Control: no-storeinstead of a bare204. Off by default.It is for a frontend on a different registrable domain, where no cookie scope can make
_csrfreadable, so the response body is the only channel left. The cookie is still set and still compared server side, and reading that body requires the caller's origin to be inAllowedOrigins, the same gateOriginCheckapplies to every mutation.It does not by itself enable cross site use.
CookieSameSite=Noneis what lets the session cookie be sent at all, and the two are the same decision seen from two sides.goauth.Newnow logs a startup warning when it sees one without the other, since either half alone produces a deployment that half works: a read only frontend, or one that never authenticates. -
SameSite=Nonewithout a secure cookie is now a configuration error. Browsers reject that pairing outright, so it does not yield a weaker session, it yields no session at all from the first request with nothing in the logs to say why.NewConfignames it instead.
Performance
-
A session and its user resolve in one query.
AuthMiddlewareruns on every authenticated request and needs both, and it was doing a session lookup followed by a user lookup whoseWHEREclause the first result had already determined.SessionRepository.GetByTokenHashWithUserjoins the two, andSessionService.ValidateWithUserwraps it with the same liveness rulesValidateapplies. Both now share onecheckSession, so they cannot disagree about what counts as a usable session.Nothing observable changed: same status codes, same bodies, same cookies. The refresh branch still does its own user lookup, since
RefreshSessionreturns only a session. The join is inner, so a session whose user row is gone now reads as "no session" rather than "no user"; both were already the same401, only the log line differs.
Correctness
-
The client IP recorded on sessions and audit events is now proxy aware.
middleware.ClientIPhonors the forwarding header named byIPAddressHeader, and only when the immediate peer is inTrustedIPs, taking the rightmost hop that is not itself a trusted proxy. It is the same trust posture the rate limiter and the CSRF origin check already used, now shared by the handler path.Registration, login, admin login, invite acceptance, two factor verification, email verification and the OAuth callback all used
r.RemoteAddrdirectly. Behind any reverse proxy that recorded the proxy's address on every session and every audit row, which is exactly what the session list and the audit log exist to show an operator.Unlike the rate limit key, IPv6 is not masked to a subnet here. That masking exists so a routed prefix cannot rotate for a fresh counter; an audit record wants the exact address.
TrustedIPsandIPAddressHeaderstill live on the rate limit config, and are now read by three subsystems. Rate limiting does not need to be enabled for the other two to use them.
Removed
-
middleware.RemoteAuth. It authenticated a request by forwarding the caller's cookies to a remote go-auth server'sGET /auth/me, for a second Go service fronting an API it does not own. In practice the services that need this, an admin console or a gateway, proxy the API rather than re-authenticating in front of it, and a proxy needs no auth middleware at all: the upstream server runs the real check on every forwarded request.Removed with it: the
auth_unavailable503error code, the Remote Auth guide, andNewRemoteAuth,RemoteAuth.RequireAuth,RequireRole,RequireAdmin,GetUser,ErrRemoteNoSession,ErrRemoteUnauthorized,ErrRemoteUnavailableand theWithRemote*options.
Documentation
- A new Deployment guide covering the three topologies a frontend can sit in relative to the API, what each needs, and why. It separates the two things a browser must do with a cookie, send it and read it, since almost every confusing failure in this area is one of those two working without the other. It also covers running two frontends against one API, which is what an admin panel is, including the shared cookie jar on localhost and why a valid session is not an admin session.
- The Sessions guide's "Checking who's logged in" section now covers one endpoint instead of two, and says why
/auth/meanswering401is worth more than a soft200. - The
/auth/meandGET /auth/sessionspayload samples now list the session fields that actually ship, includingipAddress,userAgent,parsedUA,isRevokedandrefreshExpiresAt, with a table of when each appears. Both previously showed a subset no client would ever receive. Configurationdocuments the new fields, the new error, and the two startup warnings.
v0.2.4
Admin two-factor auth, account lockout, peer comparison, OAuth PKCE, configurable hasher, and password pepper rotation.
v0.2.2
RemoteAuth — authenticate a second Go service against a go-auth server it shares no database with — plus an admin view of any user's organizations, and role filters that reject a bad value instead of silently returning everything.