package v1 // gRPC Authentication Interceptor // // This file implements the authentication interceptor for gRPC requests. // It extracts credentials from gRPC metadata and delegates to the shared Authenticator. // // Authentication flow: // 1. Extract session cookie or bearer token from metadata // 2. Validate credentials using Authenticator // 3. Check authorization (admin-only methods) // 4. Set user context and proceed with request // // For public methods (defined in acl_config.go), authentication is skipped. import ( "context" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) // GRPCAuthInterceptor is the authentication interceptor for gRPC server. // It validates incoming requests and sets user context for authenticated requests. type GRPCAuthInterceptor struct { authenticator *auth.Authenticator } // NewGRPCAuthInterceptor creates a new gRPC authentication interceptor. func NewGRPCAuthInterceptor(store *store.Store, secret string) *GRPCAuthInterceptor { return &GRPCAuthInterceptor{ authenticator: auth.NewAuthenticator(store, secret), } } // AuthenticationInterceptor is the unary interceptor for gRPC API. // // Authentication strategy (in priority order): // 1. Session Cookie: "user_session" cookie with format "{userID}-{sessionID}" // 2. Bearer Token: "Authorization: Bearer {jwt_token}" header // 3. Public Methods: Allow without auth if method is in public allowlist // 4. Reject: Return Unauthenticated error // // On successful authentication, context values are set: // - auth.UserIDContextKey: The authenticated user's ID // - auth.SessionIDContextKey: Session ID (cookie auth only) // - auth.AccessTokenContextKey: JWT token (bearer auth only). func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { // If metadata is missing, only allow public methods if IsPublicMethod(serverInfo.FullMethod) { return handler(ctx, request) } return nil, status.Errorf(codes.Unauthenticated, "failed to parse metadata from incoming context") } // Try session cookie authentication if sessionCookie := extractSessionCookieFromMetadata(md); sessionCookie != "" { user, err := in.authenticator.AuthenticateBySession(ctx, sessionCookie) if err == nil && user != nil { _, sessionID, err := auth.ParseSessionCookieValue(sessionCookie) if err != nil { // This should not happen since AuthenticateBySession already validated the cookie // but handle it gracefully anyway sessionID = "" } ctx, err = in.authenticator.AuthorizeAndSetContext(ctx, serverInfo.FullMethod, user, sessionID, "", IsAdminOnlyMethod) if err != nil { return nil, toGRPCError(err, codes.PermissionDenied) } return handler(ctx, request) } } // Try bearer token authentication if token := extractBearerTokenFromMetadata(md); token != "" { user, err := in.authenticator.AuthenticateByJWT(ctx, token) if err == nil && user != nil { ctx, err = in.authenticator.AuthorizeAndSetContext(ctx, serverInfo.FullMethod, user, "", token, IsAdminOnlyMethod) if err != nil { return nil, toGRPCError(err, codes.PermissionDenied) } return handler(ctx, request) } } // Allow public methods without authentication if IsPublicMethod(serverInfo.FullMethod) { return handler(ctx, request) } return nil, status.Errorf(codes.Unauthenticated, "authentication required") } // toGRPCError converts an error to a gRPC status error with the given code. // If the error is already a gRPC status error, it is returned as-is. func toGRPCError(err error, code codes.Code) error { if err == nil { return nil } if _, ok := status.FromError(err); ok { return err } return status.Errorf(code, "%v", err) } // extractSessionCookieFromMetadata extracts the session cookie value from gRPC metadata. // Checks both "grpcgateway-cookie" (from gRPC-Gateway) and "cookie" (native gRPC). // Returns empty string if no session cookie is found. func extractSessionCookieFromMetadata(md metadata.MD) string { // gRPC-Gateway puts cookies in "grpcgateway-cookie", native gRPC uses "cookie" for _, cookieHeader := range append(md.Get("grpcgateway-cookie"), md.Get("cookie")...) { if cookie := auth.ExtractSessionCookieFromHeader(cookieHeader); cookie != "" { return cookie } } return "" } // extractBearerTokenFromMetadata extracts JWT token from Authorization header in gRPC metadata. // Returns empty string if no valid bearer token is found. func extractBearerTokenFromMetadata(md metadata.MD) string { authHeaders := md.Get("Authorization") if len(authHeaders) == 0 { return "" } return auth.ExtractBearerToken(authHeaders[0]) }