mirror of https://github.com/usememos/memos.git
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:
parent
b0aeb06f85
commit
a6c32908a0
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue