Skip to content

Commit 09afa57

Browse files
committed
chore: implement session sliding expiration and JWT authentication
- Added UpdateSessionLastAccessed method to update session access time. - Enhanced Authenticate method to support both session cookie and JWT token authentication. - Introduced AuthResult struct to encapsulate authentication results. - Added SetUserInContext function to simplify context management for authenticated users. refactor(auth): streamline gRPC and HTTP authentication - Removed gRPC authentication interceptor and replaced it with a unified approach using GatewayAuthMiddleware for HTTP requests. - Updated Connect interceptors to utilize the new authentication logic. - Consolidated public and admin-only method checks into service layer for better maintainability. chore(api): clean up unused code and improve documentation - Removed deprecated logger interceptor and unused gRPC server code. - Updated ACL configuration documentation for clarity on public and admin-only methods. - Enhanced metadata handling in Connect RPC to ensure consistent header access. fix(server): simplify server startup and shutdown process - Eliminated cmux dependency for handling HTTP and gRPC traffic. - Streamlined server initialization and shutdown logic for better performance and readability.
1 parent 65a19df commit 09afa57

File tree

11 files changed

+309
-525
lines changed

11 files changed

+309
-525
lines changed

go.mod

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ require (
1313
github.com/google/cel-go v0.26.1
1414
github.com/google/uuid v1.6.0
1515
github.com/gorilla/feeds v1.2.0
16-
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
1716
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2
1817
github.com/joho/godotenv v1.5.1
1918
github.com/labstack/echo/v4 v4.13.4
@@ -85,7 +84,6 @@ require (
8584
github.com/mattn/go-colorable v0.1.14 // indirect
8685
github.com/mattn/go-isatty v0.0.20 // indirect
8786
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
88-
github.com/soheilhy/cmux v0.1.5
8987
github.com/valyala/bytebufferpool v1.0.0 // indirect
9088
github.com/valyala/fasttemplate v1.2.2 // indirect
9189
golang.org/x/sys v0.36.0 // indirect

go.sum

Lines changed: 0 additions & 105 deletions
Large diffs are not rendered by default.

server/auth/authenticator.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,46 @@ func validateAccessToken(token string, tokens []*storepb.AccessTokensUserSetting
195195
}
196196
return false
197197
}
198+
199+
// UpdateSessionLastAccessed updates the last accessed time for a session.
200+
// This implements sliding expiration - sessions remain valid as long as they're used.
201+
// Should be called after successful session-based authentication.
202+
func (a *Authenticator) UpdateSessionLastAccessed(ctx context.Context, userID int32, sessionID string) {
203+
// Fire-and-forget update; failures are logged but don't block the request
204+
_ = a.store.UpdateUserSessionLastAccessed(ctx, userID, sessionID, timestamppb.Now())
205+
}
206+
207+
// AuthResult contains the result of an authentication attempt.
208+
type AuthResult struct {
209+
User *store.User
210+
SessionID string // Non-empty if authenticated via session cookie
211+
AccessToken string // Non-empty if authenticated via JWT
212+
}
213+
214+
// Authenticate tries to authenticate using the provided credentials.
215+
// It tries session cookie first, then JWT token.
216+
// Returns nil if no valid credentials are provided.
217+
// On successful session auth, it also updates the session sliding expiration.
218+
func (a *Authenticator) Authenticate(ctx context.Context, sessionCookie, authHeader string) *AuthResult {
219+
// Try session cookie authentication first
220+
if sessionCookie != "" {
221+
user, err := a.AuthenticateBySession(ctx, sessionCookie)
222+
if err == nil && user != nil {
223+
_, sessionID, parseErr := ParseSessionCookieValue(sessionCookie)
224+
if parseErr == nil && sessionID != "" {
225+
a.UpdateSessionLastAccessed(ctx, user.ID, sessionID)
226+
}
227+
return &AuthResult{User: user, SessionID: sessionID}
228+
}
229+
}
230+
231+
// Try JWT token authentication
232+
if token := ExtractBearerToken(authHeader); token != "" {
233+
user, err := a.AuthenticateByJWT(ctx, token)
234+
if err == nil && user != nil {
235+
return &AuthResult{User: user, AccessToken: token}
236+
}
237+
}
238+
239+
return nil
240+
}

server/auth/context.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package auth
22

3-
import "context"
3+
import (
4+
"context"
5+
6+
"github.com/usememos/memos/store"
7+
)
48

59
// ContextKey is the key type for context values.
610
// Using a custom type prevents collisions with other packages.
@@ -47,3 +51,22 @@ func GetAccessToken(ctx context.Context) string {
4751
}
4852
return ""
4953
}
54+
55+
// SetUserInContext sets the authenticated user's information in the context.
56+
// This is a simpler alternative to AuthorizeAndSetContext for cases where
57+
// authorization is handled separately (e.g., HTTP middleware).
58+
//
59+
// Parameters:
60+
// - user: The authenticated user
61+
// - sessionID: Set if authenticated via session cookie (empty string otherwise)
62+
// - accessToken: Set if authenticated via JWT token (empty string otherwise)
63+
func SetUserInContext(ctx context.Context, user *store.User, sessionID, accessToken string) context.Context {
64+
ctx = context.WithValue(ctx, UserIDContextKey, user.ID)
65+
if sessionID != "" {
66+
ctx = context.WithValue(ctx, SessionIDContextKey, sessionID)
67+
}
68+
if accessToken != "" {
69+
ctx = context.WithValue(ctx, AccessTokenContextKey, accessToken)
70+
}
71+
return ctx
72+
}

server/router/api/v1/acl.go

Lines changed: 0 additions & 134 deletions
This file was deleted.

server/router/api/v1/acl_config.go

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,40 @@
11
package v1
22

3-
// Access Control List (ACL) Configuration
3+
// PublicMethods defines API endpoints that don't require authentication.
4+
// All other endpoints require a valid session or access token.
45
//
5-
// This file defines which API methods require authentication and which require admin privileges.
6-
// Used by both gRPC and Connect interceptors to enforce access control.
6+
// This is the SINGLE SOURCE OF TRUTH for public endpoints.
7+
// Both Connect interceptor and gRPC-Gateway interceptor use this map.
78
//
8-
// Method names follow the gRPC full method format: "/{package}.{service}/{method}"
9-
// Example: "/memos.api.v1.MemoService/CreateMemo"
10-
11-
// publicMethods lists methods that can be called without authentication.
12-
// These are typically read-only endpoints for public content or login-related endpoints.
13-
var publicMethods = map[string]bool{
14-
// Instance info - needed before login
15-
"/memos.api.v1.InstanceService/GetInstanceProfile": true,
16-
"/memos.api.v1.InstanceService/GetInstanceSetting": true,
17-
18-
// Auth - login/session endpoints
19-
"/memos.api.v1.AuthService/CreateSession": true,
20-
"/memos.api.v1.AuthService/GetCurrentSession": true,
21-
22-
// User - public user info and registration
23-
"/memos.api.v1.UserService/CreateUser": true, // Registration (also admin-only when not first user)
24-
"/memos.api.v1.UserService/GetUser": true,
25-
"/memos.api.v1.UserService/GetUserAvatar": true,
26-
"/memos.api.v1.UserService/GetUserStats": true,
27-
"/memos.api.v1.UserService/ListAllUserStats": true,
28-
"/memos.api.v1.UserService/SearchUsers": true,
29-
30-
// Identity providers - needed for SSO login
31-
"/memos.api.v1.IdentityProviderService/ListIdentityProviders": true,
32-
33-
// Memo - public memo access
34-
"/memos.api.v1.MemoService/GetMemo": true,
35-
"/memos.api.v1.MemoService/ListMemos": true,
36-
37-
// Attachment - public attachment access
38-
"/memos.api.v1.AttachmentService/GetAttachmentBinary": true,
39-
}
40-
41-
// adminOnlyMethods lists methods that require admin (Host or Admin role) privileges.
42-
// Regular users cannot call these methods even if authenticated.
43-
var adminOnlyMethods = map[string]bool{
44-
"/memos.api.v1.UserService/CreateUser": true, // Admin creates users (except first user registration)
45-
"/memos.api.v1.InstanceService/UpdateInstanceSetting": true,
46-
}
47-
48-
// IsPublicMethod returns true if the method can be called without authentication.
49-
func IsPublicMethod(fullMethodName string) bool {
50-
return publicMethods[fullMethodName]
9+
// Format: Full gRPC procedure path as returned by req.Spec().Procedure (Connect)
10+
// or info.FullMethod (gRPC interceptor).
11+
var PublicMethods = map[string]struct{}{
12+
// Auth Service - login flow must be accessible without auth
13+
"/memos.api.v1.AuthService/CreateSession": {},
14+
"/memos.api.v1.AuthService/GetCurrentSession": {},
15+
16+
// Instance Service - needed before login to show instance info
17+
"/memos.api.v1.InstanceService/GetInstanceProfile": {},
18+
"/memos.api.v1.InstanceService/GetInstanceSetting": {},
19+
20+
// User Service - public user profiles and stats
21+
"/memos.api.v1.UserService/GetUser": {},
22+
"/memos.api.v1.UserService/GetUserAvatar": {},
23+
"/memos.api.v1.UserService/GetUserStats": {},
24+
"/memos.api.v1.UserService/ListAllUserStats": {},
25+
"/memos.api.v1.UserService/SearchUsers": {},
26+
27+
// Identity Provider Service - SSO buttons on login page
28+
"/memos.api.v1.IdentityProviderService/ListIdentityProviders": {},
29+
30+
// Memo Service - public memos (visibility filtering done in service layer)
31+
"/memos.api.v1.MemoService/GetMemo": {},
32+
"/memos.api.v1.MemoService/ListMemos": {},
5133
}
5234

53-
// IsAdminOnlyMethod returns true if the method requires admin privileges.
54-
func IsAdminOnlyMethod(fullMethodName string) bool {
55-
return adminOnlyMethods[fullMethodName]
35+
// IsPublicMethod checks if a procedure path is public (no authentication required).
36+
// Returns true for public methods, false for protected methods.
37+
func IsPublicMethod(procedure string) bool {
38+
_, ok := PublicMethods[procedure]
39+
return ok
5640
}

0 commit comments

Comments
 (0)