fix(11-06): send Cache-Control: no-cache, private on the jwt.auth 401
The recorded PHP 401 for a missing bearer (realtime token no-bearer case) carries Laravel's default Cache-Control header; the Go guard's 401 omitted it, so the replay failed on header.Cache-Control.
This commit is contained in:
@@ -13,7 +13,7 @@ bouncer decides who is making a request. Guards (`bouncer.Guard`, `bouncer.Crede
|
|||||||
- Token minting with `bouncer.Mint` (frontend audience) and `bouncer.MintAudience` (any audience), each returning the signed token and its random jti.
|
- Token minting with `bouncer.Mint` (frontend audience) and `bouncer.MintAudience` (any audience), each returning the signed token and its random jti.
|
||||||
- Verification with HS256 pinned and `exp` and `sub` required: `bouncer.Verify` and `bouncer.VerifyClaims` accept frontend tokens, including legacy tokens with no audience claim; `bouncer.VerifyClaimsAudience` requires an explicit audience, so a backend token cannot pass a frontend check and the other way round.
|
- Verification with HS256 pinned and `exp` and `sub` required: `bouncer.Verify` and `bouncer.VerifyClaims` accept frontend tokens, including legacy tokens with no audience claim; `bouncer.VerifyClaimsAudience` requires an explicit audience, so a backend token cannot pass a frontend check and the other way round.
|
||||||
- Refresh with `bouncer.Refresh`, `bouncer.RefreshAudience` and `bouncer.RefreshAudienceFor`: an expired token can be reissued while its `iat` is inside the refresh window; the old jti is blacklisted after a grace period. `bouncer.RefreshAudienceFor` also reloads the user and refuses deleted users and tokens issued before `bouncer.Principal.TokensValidAfter`, reporting `bouncer.ErrSubjectRejected`.
|
- Refresh with `bouncer.Refresh`, `bouncer.RefreshAudience` and `bouncer.RefreshAudienceFor`: an expired token can be reissued while its `iat` is inside the refresh window; the old jti is blacklisted after a grace period. `bouncer.RefreshAudienceFor` also reloads the user and refuses deleted users and tokens issued before `bouncer.Principal.TokensValidAfter`, reporting `bouncer.ErrSubjectRejected`.
|
||||||
- JWT guards: `bouncer.NewJWTGuard` (frontend) and `bouncer.NewBackendJWTGuard` (admin audience, optional custom 401 writer) read the bearer token first and then any configured cookies, load the user through a `bouncer.UserProvider`, check the blacklist and the `bouncer.Principal.TokensValidAfter` cutoff, and write a JSON 401 body (`{"error":true,"message":...}`) on failure.
|
- JWT guards: `bouncer.NewJWTGuard` (frontend) and `bouncer.NewBackendJWTGuard` (admin audience, optional custom 401 writer) read the bearer token first and then any configured cookies, load the user through a `bouncer.UserProvider`, check the blacklist and the `bouncer.Principal.TokensValidAfter` cutoff, and write a JSON 401 body (`{"error":true,"message":...}`, with `Cache-Control: no-cache, private`) on failure.
|
||||||
- Named guard registry: `bouncer.Registry.Register` accepts any `bouncer.Guard` or `bouncer.CredentialGuard`; `bouncer.Registry.Middleware` derives middleware that stores the principal (and credential, if any) on the context. Guards that do not implement `bouncer.UnauthorizedWriter` let unauthenticated requests through so later middleware can decide.
|
- Named guard registry: `bouncer.Registry.Register` accepts any `bouncer.Guard` or `bouncer.CredentialGuard`; `bouncer.Registry.Middleware` derives middleware that stores the principal (and credential, if any) on the context. Guards that do not implement `bouncer.UnauthorizedWriter` let unauthenticated requests through so later middleware can decide.
|
||||||
- Standalone bearer middleware: `bouncer.Middleware`.
|
- Standalone bearer middleware: `bouncer.Middleware`.
|
||||||
- Context helpers: `bouncer.WithUser` and `bouncer.User` for the principal, `bouncer.WithCredential` and `bouncer.Credential` for the credential behind it (for example an API token record).
|
- Context helpers: `bouncer.WithUser` and `bouncer.User` for the principal, `bouncer.WithCredential` and `bouncer.Credential` for the credential behind it (for example an API token record).
|
||||||
|
|||||||
@@ -366,6 +366,9 @@ func mapJWTError(err error) error {
|
|||||||
|
|
||||||
func write401(w http.ResponseWriter, message string) {
|
func write401(w http.ResponseWriter, message string) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
// The reference backend (Laravel) sends this on every response, and a
|
||||||
|
// replayed 401 compares it.
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, private")
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"error": true, "message": message})
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": true, "message": message})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ func TestMiddlewareStatusBodies(t *testing.T) {
|
|||||||
if rec.Header().Get("X-Hit") != "" {
|
if rec.Header().Get("X-Hit") != "" {
|
||||||
t.Fatal("handler ran")
|
t.Fatal("handler ran")
|
||||||
}
|
}
|
||||||
|
if got := rec.Header().Get("Cache-Control"); got != "no-cache, private" {
|
||||||
|
t.Fatalf("Cache-Control = %q", got)
|
||||||
|
}
|
||||||
var body map[string]any
|
var body map[string]any
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
Reference in New Issue
Block a user