refactor(auth): require bearer token only, drop legacy token header and cookie
Authenticate requests solely from the Authorization: Bearer header on the server; stop accepting the legacy `token` header and `usertoken` cookie. The frontend no longer sets auth cookies (only clears legacy ones), omits credentials on all requests, and drops the redundant `token` header. Project event SSE now streams over fetch so it can carry the Authorization header, which EventSource cannot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -86,7 +86,7 @@ Development response includes an email preview so the frontend can render the sa
|
||||
}
|
||||
```
|
||||
|
||||
Returns an authenticated session with `accessToken`, `refreshToken`, and the user profile. Project creation, generation, upload, save, and deletion require this login token.
|
||||
Returns an authenticated session with `accessToken`, `refreshToken`, and the user profile. Project creation, generation, upload, save, and deletion require `Authorization: Bearer <accessToken>`.
|
||||
|
||||
`POST /api/auth/global/google-login`
|
||||
|
||||
@@ -124,7 +124,7 @@ After binding succeeds, later QR logins use only `code` and `state`.
|
||||
|
||||
## Account
|
||||
|
||||
All account endpoints require the bearer token returned by login. Account profile state is owned by the auth module and persisted in PostgreSQL or the configured in-memory store.
|
||||
All account endpoints require `Authorization: Bearer <accessToken>`. Cookie auth and the legacy `token` header are not used. Account profile state is owned by the auth module and persisted in PostgreSQL or the configured in-memory store.
|
||||
|
||||
`GET /api/account/profile`
|
||||
|
||||
@@ -151,7 +151,7 @@ Removes non-current devices when a future server-side session registry exists. I
|
||||
|
||||
`POST /api/account/logout`
|
||||
|
||||
Records a logout request server-side and returns `{"removed": 0}`. The frontend then clears its local access token and cookies.
|
||||
Records a logout request server-side and returns `{"removed": 0}`. The frontend then clears its locally stored session.
|
||||
|
||||
## Projects
|
||||
|
||||
|
||||
@@ -34,20 +34,7 @@ func UserContextMiddleware(authenticator bearerAuthenticator) func(http.HandlerF
|
||||
}
|
||||
|
||||
func userIDFromRequestAuth(ctx context.Context, r *http.Request, authenticator bearerAuthenticator) (string, bool) {
|
||||
if tokenUserID, ok := authenticator.UserIDFromBearer(ctx, r.Header.Get("Authorization")); ok {
|
||||
return tokenUserID, true
|
||||
}
|
||||
if token := strings.TrimSpace(r.Header.Get("token")); token != "" {
|
||||
if tokenUserID, ok := authenticator.UserIDFromBearer(ctx, "Bearer "+token); ok {
|
||||
return tokenUserID, true
|
||||
}
|
||||
}
|
||||
if cookie, err := r.Cookie("usertoken"); err == nil && strings.TrimSpace(cookie.Value) != "" {
|
||||
if tokenUserID, ok := authenticator.UserIDFromBearer(ctx, "Bearer "+cookie.Value); ok {
|
||||
return tokenUserID, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
return authenticator.UserIDFromBearer(ctx, r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func requiresAuthenticatedUser(r *http.Request) bool {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
)
|
||||
|
||||
type fakeBearerAuthenticator struct{}
|
||||
|
||||
func (fakeBearerAuthenticator) UserIDFromBearer(_ context.Context, authorization string) (string, bool) {
|
||||
if authorization == "Bearer valid" {
|
||||
return "91cd197b-8255-4172-9981-77391fd38f33", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareUsesAuthorizationHeader(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/projects", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
called := false
|
||||
UserContextMiddleware(fakeBearerAuthenticator{})(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
if got := design.UserIDFromContext(r.Context()); got != "91cd197b-8255-4172-9981-77391fd38f33" {
|
||||
t.Fatalf("expected authenticated user in context, got %s", got)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})(rec, req)
|
||||
|
||||
if !called {
|
||||
t.Fatal("expected next handler to be called")
|
||||
}
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusNoContent, rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserContextMiddlewareIgnoresLegacyTokenHeaderAndCookie(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/projects", nil)
|
||||
req.Header.Set("token", "valid")
|
||||
req.AddCookie(&http.Cookie{Name: "usertoken", Value: "valid"})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
called := false
|
||||
UserContextMiddleware(fakeBearerAuthenticator{})(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
})(rec, req)
|
||||
|
||||
if called {
|
||||
t.Fatal("expected next handler not to be called")
|
||||
}
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user