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.
This commit is contained in:
Steven 2025-12-19 00:09:08 +08:00
parent b0aeb06f85
commit a6c32908a0
7 changed files with 29 additions and 86 deletions

View File

@ -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)

View File

@ -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)
}

View File

@ -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.

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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.