From a6c32908a0d0079b3072076821cbbcf9868285c6 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 19 Dec 2025 00:09:08 +0800 Subject: [PATCH] refactor(auth): remove legacy session cookie authentication - Remove SessionCookieName and SessionSlidingDuration constants - Remove ExtractSessionCookieFromHeader() function - Remove SessionIDContextKey and GetSessionID() function - Remove sessionID parameter from SetUserInContext() - Remove SessionID field from AuthResult struct - Remove session cookie extraction from middleware - Update documentation to reflect JWT + PAT only auth Session cookies were never being set since migration to refresh token authentication. This change removes ~50 lines of dead code and clarifies that the system uses JWT access tokens, refresh tokens, and PATs only. --- server/auth/authenticator.go | 9 +++--- server/auth/context.go | 21 ++----------- server/auth/extract.go | 15 --------- server/auth/token.go | 23 ++------------ server/router/api/v1/connect_interceptors.go | 7 ++--- server/router/api/v1/v1.go | 8 ++--- server/router/fileserver/README.md | 32 +++++++++----------- 7 files changed, 29 insertions(+), 86 deletions(-) diff --git a/server/auth/authenticator.go b/server/auth/authenticator.go index 049cadbe6..3876406a0 100644 --- a/server/auth/authenticator.go +++ b/server/auth/authenticator.go @@ -19,8 +19,8 @@ import ( // consistent authentication behavior across all API endpoints. // // Authentication methods: -// - Session cookie: Browser-based authentication with sliding expiration -// - JWT token: API token authentication for programmatic access +// - JWT access tokens: Short-lived tokens (15 minutes) for API access +// - Personal Access Tokens (PAT): Long-lived tokens for programmatic access // // This struct is safe for concurrent use. type Authenticator struct { @@ -125,16 +125,15 @@ func (a *Authenticator) AuthenticateByPAT(ctx context.Context, token string) (*s // AuthResult contains the result of an authentication attempt. type AuthResult struct { - User *store.User // Set for PAT and legacy auth + User *store.User // Set for PAT authentication Claims *UserClaims // Set for Access Token V2 (stateless) - SessionID string // Non-empty if authenticated via session cookie AccessToken string // Non-empty if authenticated via JWT } // Authenticate tries to authenticate using the provided credentials. // Priority: 1. Access Token V2, 2. PAT // Returns nil if no valid credentials are provided. -func (a *Authenticator) Authenticate(ctx context.Context, _, authHeader string) *AuthResult { +func (a *Authenticator) Authenticate(ctx context.Context, authHeader string) *AuthResult { token := ExtractBearerToken(authHeader) // Try Access Token V2 (stateless) diff --git a/server/auth/context.go b/server/auth/context.go index d01ed9f04..cdeba0df5 100644 --- a/server/auth/context.go +++ b/server/auth/context.go @@ -12,14 +12,10 @@ type ContextKey int const ( // UserIDContextKey stores the authenticated user's ID. - // Set for both session-based and token-based authentication. + // Set for all authenticated requests. // Use GetUserID(ctx) to retrieve this value. UserIDContextKey ContextKey = iota - // SessionIDContextKey stores the session ID for session-based auth. - // Only set when authenticated via session cookie. - SessionIDContextKey - // AccessTokenContextKey stores the JWT token for token-based auth. // Only set when authenticated via Bearer token. AccessTokenContextKey @@ -40,15 +36,6 @@ func GetUserID(ctx context.Context) int32 { return 0 } -// GetSessionID retrieves the session ID from the context. -// Returns empty string if not authenticated via session cookie. -func GetSessionID(ctx context.Context) string { - if v, ok := ctx.Value(SessionIDContextKey).(string); ok { - return v - } - return "" -} - // GetAccessToken retrieves the JWT access token from the context. // Returns empty string if not authenticated via bearer token. func GetAccessToken(ctx context.Context) string { @@ -64,13 +51,9 @@ func GetAccessToken(ctx context.Context) string { // // Parameters: // - user: The authenticated user -// - sessionID: Set if authenticated via session cookie (empty string otherwise) // - accessToken: Set if authenticated via JWT token (empty string otherwise) -func SetUserInContext(ctx context.Context, user *store.User, sessionID, accessToken string) context.Context { +func SetUserInContext(ctx context.Context, user *store.User, accessToken string) context.Context { ctx = context.WithValue(ctx, UserIDContextKey, user.ID) - if sessionID != "" { - ctx = context.WithValue(ctx, SessionIDContextKey, sessionID) - } if accessToken != "" { ctx = context.WithValue(ctx, AccessTokenContextKey, accessToken) } diff --git a/server/auth/extract.go b/server/auth/extract.go index b48f688f3..4899a5051 100644 --- a/server/auth/extract.go +++ b/server/auth/extract.go @@ -5,21 +5,6 @@ import ( "strings" ) -// ExtractSessionCookieFromHeader extracts the session cookie value from an HTTP Cookie header. -// Returns empty string if the session cookie is not found. -func ExtractSessionCookieFromHeader(cookieHeader string) string { - if cookieHeader == "" { - return "" - } - // Use http.Request to parse cookies properly - req := &http.Request{Header: http.Header{"Cookie": []string{cookieHeader}}} - cookie, err := req.Cookie(SessionCookieName) - if err != nil { - return "" - } - return cookie.Value -} - // ExtractBearerToken extracts the JWT token from an Authorization header value. // Expected format: "Bearer {token}" // Returns empty string if no valid bearer token is found. diff --git a/server/auth/token.go b/server/auth/token.go index 0079dbc19..917064f7d 100644 --- a/server/auth/token.go +++ b/server/auth/token.go @@ -5,8 +5,9 @@ // - server/router/fileserver: HTTP file server authentication // // Authentication methods supported: -// - Session cookie: Browser-based authentication with sliding expiration -// - JWT token: API token authentication for programmatic access +// - JWT access tokens: Short-lived tokens (15 minutes) for API access +// - JWT refresh tokens: Long-lived tokens (30 days) for obtaining new access tokens +// - Personal Access Tokens (PAT): Long-lived tokens for programmatic access package auth import ( @@ -35,15 +36,6 @@ const ( // This ensures tokens are only used for API access, not other purposes. AccessTokenAudienceName = "user.access-token" - // SessionSlidingDuration is the sliding expiration duration for user sessions. - // Sessions remain valid if accessed within the last 14 days. - // Each API call extends the session by updating last_accessed_time. - SessionSlidingDuration = 14 * 24 * time.Hour - - // SessionCookieName is the HTTP cookie name used to store session information. - // Cookie value is the session ID (UUID). - SessionCookieName = "user_session" - // AccessTokenDuration is the lifetime of access tokens (15 minutes). AccessTokenDuration = 15 * time.Minute @@ -138,15 +130,6 @@ func generateToken(username string, userID int32, audience string, expirationTim return tokenString, nil } -// GenerateSessionID generates a unique session ID. -// -// Uses UUID v4 (random) for high entropy and uniqueness. -// Session IDs are stored in user settings and used to identify browser sessions. -// The session ID is stored directly in the cookie as the cookie value. -func GenerateSessionID() string { - return util.GenUUID() -} - // GenerateAccessTokenV2 generates a short-lived access token with user claims. func GenerateAccessTokenV2(userID int32, username, role, status string, secret []byte) (string, time.Time, error) { expiresAt := time.Now().Add(AccessTokenDuration) diff --git a/server/router/api/v1/connect_interceptors.go b/server/router/api/v1/connect_interceptors.go index 5af3d7b2b..03eb35de4 100644 --- a/server/router/api/v1/connect_interceptors.go +++ b/server/router/api/v1/connect_interceptors.go @@ -192,10 +192,9 @@ func NewAuthInterceptor(store *store.Store, secret string) *AuthInterceptor { func (in *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { header := req.Header() - sessionCookie := auth.ExtractSessionCookieFromHeader(header.Get("Cookie")) authHeader := header.Get("Authorization") - result := in.authenticator.Authenticate(ctx, sessionCookie, authHeader) + result := in.authenticator.Authenticate(ctx, authHeader) // Enforce authentication for non-public methods if result == nil && !IsPublicMethod(req.Spec().Procedure) { @@ -209,8 +208,8 @@ func (in *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { ctx = auth.SetUserClaimsInContext(ctx, result.Claims) ctx = context.WithValue(ctx, auth.UserIDContextKey, result.Claims.UserID) } else if result.User != nil { - // PAT or legacy auth - have full user - ctx = auth.SetUserInContext(ctx, result.User, result.SessionID, result.AccessToken) + // PAT - have full user + ctx = auth.SetUserInContext(ctx, result.User, result.AccessToken) } } diff --git a/server/router/api/v1/v1.go b/server/router/api/v1/v1.go index c03ea4130..74b342fa2 100644 --- a/server/router/api/v1/v1.go +++ b/server/router/api/v1/v1.go @@ -62,13 +62,9 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech rpcMethod, _ := runtime.RPCMethod(ctx) // Extract credentials from HTTP headers - var sessionCookie string - if cookie, err := r.Cookie("user_session"); err == nil { - sessionCookie = cookie.Value - } authHeader := r.Header.Get("Authorization") - result := authenticator.Authenticate(ctx, sessionCookie, authHeader) + result := authenticator.Authenticate(ctx, authHeader) // Enforce authentication for non-public methods if result == nil && !IsPublicMethod(rpcMethod) { @@ -84,7 +80,7 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech ctx = context.WithValue(ctx, auth.UserIDContextKey, result.Claims.UserID) } else if result.User != nil { // PAT - have full user - ctx = auth.SetUserInContext(ctx, result.User, result.SessionID, result.AccessToken) + ctx = auth.SetUserInContext(ctx, result.User, result.AccessToken) } r = r.WithContext(ctx) } diff --git a/server/router/fileserver/README.md b/server/router/fileserver/README.md index 22d98104c..6e8a4e3f0 100644 --- a/server/router/fileserver/README.md +++ b/server/router/fileserver/README.md @@ -9,7 +9,7 @@ The `fileserver` package handles all binary file serving for Memos using native - Serve attachment binary files (images, videos, audio, documents) - Serve user avatar images - Handle HTTP range requests for video/audio streaming -- Authenticate requests using session cookies or JWT tokens +- Authenticate requests using JWT tokens or Personal Access Tokens - Check permissions for private content - Generate and serve image thumbnails - Prevent XSS attacks on uploaded content @@ -82,18 +82,18 @@ GET /file/users/:identifier/avatar ### Supported Methods -The fileserver supports two authentication methods, checked in order: +The fileserver supports the following authentication methods: -1. **Session Cookie** (`user_session`) - - Cookie format: `{userID}-{sessionID}` - - Validates session exists and hasn't expired (14-day sliding window) - - Updates last accessed time on success - -2. **JWT Bearer Token** (`Authorization: Bearer {token}`) - - Validates JWT signature using server secret - - Checks token exists in user's access tokens (for revocation) +1. **JWT Access Token** (`Authorization: Bearer {token}`) + - Short-lived tokens (15 minutes) for API access + - Stateless validation using JWT signature - Extracts user ID from token claims +2. **Personal Access Token (PAT)** (`Authorization: Bearer {pat}`) + - Long-lived tokens for programmatic access + - Validates against database for revocation + - Prefixed with specific identifier + ### Authentication Flow ``` @@ -190,7 +190,7 @@ Parses data URI to extract MIME type and base64 data. - `golang.org/x/sync/semaphore` - Concurrency control for thumbnails ### Internal Packages -- `server/router/api/v1` - Auth constants (SessionCookieName, ClaimsMessage, etc.) +- `server/auth` - Authentication utilities - `store` - Database operations - `internal/profile` - Server configuration - `plugin/storage/s3` - S3 storage client @@ -199,11 +199,9 @@ Parses data URI to extract MIME type and base64 data. ### Constants -All auth-related constants are imported from `server/router/api/v1/auth.go`: -- `apiv1.SessionCookieName` - "user_session" -- `apiv1.SessionSlidingDuration` - 14 days -- `apiv1.KeyID` - "v1" (JWT key identifier) -- `apiv1.ClaimsMessage` - JWT claims struct +Auth-related constants are imported from `server/auth`: +- `auth.RefreshTokenCookieName` - "memos_refresh" +- `auth.PersonalAccessTokenPrefix` - PAT identifier prefix Package-specific constants: - `ThumbnailCacheFolder` - ".thumbnail_cache" @@ -245,7 +243,7 @@ if contentType == "image/svg+xml" || ``` ### 2. Authentication -Private content requires valid session or JWT token. +Private content requires valid JWT access token or Personal Access Token. ### 3. Authorization Memo visibility rules enforced before serving attachments.