mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-14 09:16:28 +00:00
feat(security): send request info as login claims
Add RequestInfoMiddleware / WithRequestInfo, which put client details in the context. Login, LoginWithAPIKey, LoginWithPasskey and JWT login merge them into the claims: ip_address, client_port, is_private_ip, previous_session, device_id, client_id, user_agent, server_ip_address, server_port, host, origin, referer. RequestInfoOptions adds trusted-proxy handling for X-Forwarded-For/X-Real-IP.
This commit is contained in:
@@ -1476,6 +1476,83 @@ func myHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
|
||||
### Request Info for Login (RequestInfoMiddleware / WithRequestInfo)
|
||||
|
||||
`Login`, `LoginWithAPIKey` and `LoginWithPasskey` (and `JWTAuthenticator.Login`) only receive a
|
||||
`context.Context`, not the `*http.Request`. To send the client's IP address and the session the
|
||||
client already holds, put them in the context. The authenticators then add two claims to the login
|
||||
claims before calling the stored procedure / backend:
|
||||
|
||||
| Claim | Value |
|
||||
|-------|-------|
|
||||
| `ip_address` | Client IP: first `X-Forwarded-For` entry, else `X-Real-IP`, else `RemoteAddr` without the port |
|
||||
| `previous_session` | Session token the client sent: first token of the `Authorization` header (`Bearer `/`Token ` stripped), else the session cookie |
|
||||
| `user_agent` | `User-Agent` header |
|
||||
| `server_ip_address` | IP of the local address that accepted the connection (from `http.LocalAddrContextKey`; omitted if the server didn't record it) |
|
||||
| `host` | Requested `Host` |
|
||||
| `origin` | `Origin` header |
|
||||
| `referer` | `Referer` header |
|
||||
| `client_port` | Port of the connecting peer (the proxy's port when behind one) |
|
||||
| `server_port` | Port of the local address that accepted the connection |
|
||||
| `is_private_ip` | `true` when `ip_address` is private, loopback or link-local (bool; sent whenever the IP parses) |
|
||||
| `device_id` | `X-Device-ID` header, else the `device_id` cookie |
|
||||
| `client_id` | `X-Client-ID` header |
|
||||
|
||||
Claims you set yourself (e.g. your own `user_agent`) are never overwritten, and the caller's claims map is not mutated. A claim
|
||||
is omitted when its value is empty (e.g. no previous session on a first login).
|
||||
`resolvespec_login` already reads `claims.ip_address`; `previous_session` is only carried through to the
|
||||
login backend — nothing is stored for it, so use it in your own procedure/backend if you need it.
|
||||
|
||||
**Option 1 — middleware (recommended).** Add it on the root router, before auth, so it also covers
|
||||
routes that skip authentication such as login:
|
||||
|
||||
```go
|
||||
router.Use(security.RequestInfoMiddleware())
|
||||
```
|
||||
|
||||
Pass `RequestInfoOptions` to change how the request is read:
|
||||
|
||||
```go
|
||||
router.Use(security.RequestInfoMiddleware(security.RequestInfoOptions{
|
||||
Cookie: cookieOpts, // same options you give the authenticator
|
||||
TrustedProxies: []string{"10.0.0.0/8", "172.16.0.1"}, // CIDRs or IPs
|
||||
// IgnoreProxyHeaders: true, // always use the connecting peer
|
||||
// DeviceIDHeader: "X-Device-ID", DeviceIDCookie: "device_id", // defaults
|
||||
// ClientIDHeader: "X-Client-ID", // default
|
||||
}))
|
||||
```
|
||||
|
||||
With `TrustedProxies` set, `X-Forwarded-For` / `X-Real-IP` are only honoured when the connecting
|
||||
peer is in the list, and `X-Forwarded-For` is read right-to-left, skipping trusted proxies, so a
|
||||
client cannot spoof `ip_address` by sending its own header. With it empty, the headers are trusted
|
||||
from any peer.
|
||||
|
||||
The OAuth server's login and device-login handlers use `r.Context()`, so they pick the claims up
|
||||
from this middleware.
|
||||
|
||||
**Option 2 — per handler.** Use `WithRequestInfo` when the route is not behind the middleware:
|
||||
|
||||
```go
|
||||
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := security.WithRequestInfo(r.Context(), r) // optional: , security.RequestInfoOptions{...}
|
||||
resp, err := auth.Login(ctx, security.LoginRequest{
|
||||
Username: r.PostFormValue("username"),
|
||||
Password: r.PostFormValue("password"),
|
||||
Claims: map[string]any{"user_agent": r.UserAgent()},
|
||||
})
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Related helpers: `security.ClientIP(r)`, `security.RequestInfoFromContext(ctx)`, and the claim
|
||||
constants (`security.ClaimIPAddress`, `ClaimPreviousSession`, `ClaimUserAgent`,
|
||||
`ClaimServerIPAddress`, `ClaimHost`, `ClaimOrigin`, `ClaimReferer`, `ClaimClientPort`,
|
||||
`ClaimServerPort`, `ClaimIsPrivateIP`, `ClaimDeviceID`, `ClaimClientID`).
|
||||
|
||||
> Without `TrustedProxies`, `X-Forwarded-For` / `X-Real-IP` are taken at face value, so they are
|
||||
> client-controlled unless your reverse proxy overwrites them. Set `TrustedProxies` for anything
|
||||
> that relies on `ip_address`.
|
||||
|
||||
### Helper Functions
|
||||
|
||||
Extract user information from context:
|
||||
|
||||
Reference in New Issue
Block a user