Add API token auth and issues doc
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/igodwin/notifier/internal/logging"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// GRPCAuthMiddleware provides authentication for gRPC APIs
|
||||
type GRPCAuthMiddleware struct {
|
||||
store *APIKeyStore
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewGRPCAuthMiddleware creates a new gRPC auth middleware
|
||||
func NewGRPCAuthMiddleware(store *APIKeyStore, logger *logging.Logger) *GRPCAuthMiddleware {
|
||||
return &GRPCAuthMiddleware{
|
||||
store: store,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UnaryInterceptor returns a unary server interceptor for gRPC authentication
|
||||
func (m *GRPCAuthMiddleware) UnaryInterceptor() grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
// Extract API key from metadata
|
||||
apiKey := m.extractAPIKey(ctx)
|
||||
if apiKey == "" {
|
||||
m.logger.Warnf("gRPC: Missing API key in request for method=%s", info.FullMethod)
|
||||
return nil, status.Error(codes.Unauthenticated, "Missing or invalid Authorization header")
|
||||
}
|
||||
|
||||
// Validate API key
|
||||
key, err := m.store.ValidateKey(apiKey)
|
||||
if err != nil {
|
||||
m.logger.Warnf("gRPC: Invalid API key for method=%s - error=%v", info.FullMethod, err)
|
||||
return nil, status.Error(codes.Unauthenticated, "Invalid API key")
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
allowed, err := m.store.CheckRateLimit(apiKey)
|
||||
if err != nil || !allowed {
|
||||
m.logger.Warnf("gRPC: Rate limit exceeded for client=%s method=%s", key.ClientID, info.FullMethod)
|
||||
return nil, status.Error(codes.ResourceExhausted, "Rate limit exceeded")
|
||||
}
|
||||
|
||||
// Update last used timestamp
|
||||
if err := m.store.UpdateLastUsed(apiKey); err != nil {
|
||||
m.logger.Errorf("gRPC: Failed to update last used time for client=%s - error=%v", key.ClientID, err)
|
||||
}
|
||||
|
||||
// Create auth context and attach to request
|
||||
authCtx := &AuthContext{
|
||||
APIKey: key,
|
||||
ClientID: key.ClientID,
|
||||
Roles: key.Roles,
|
||||
}
|
||||
|
||||
// Add auth context to request context
|
||||
newCtx := ContextWithAuth(ctx, authCtx)
|
||||
m.logger.Debugf("gRPC: Authenticated request from client=%s method=%s with roles=%v", key.ClientID, info.FullMethod, key.Roles)
|
||||
|
||||
return handler(newCtx, req)
|
||||
}
|
||||
}
|
||||
|
||||
// StreamInterceptor returns a stream server interceptor for gRPC authentication
|
||||
func (m *GRPCAuthMiddleware) StreamInterceptor() grpc.StreamServerInterceptor {
|
||||
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
// Extract API key from metadata
|
||||
apiKey := m.extractAPIKey(ss.Context())
|
||||
if apiKey == "" {
|
||||
m.logger.Warnf("gRPC: Missing API key in stream for method=%s", info.FullMethod)
|
||||
return status.Error(codes.Unauthenticated, "Missing or invalid Authorization header")
|
||||
}
|
||||
|
||||
// Validate API key
|
||||
key, err := m.store.ValidateKey(apiKey)
|
||||
if err != nil {
|
||||
m.logger.Warnf("gRPC: Invalid API key for stream method=%s - error=%v", info.FullMethod, err)
|
||||
return status.Error(codes.Unauthenticated, "Invalid API key")
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
allowed, err := m.store.CheckRateLimit(apiKey)
|
||||
if err != nil || !allowed {
|
||||
m.logger.Warnf("gRPC: Rate limit exceeded for client=%s stream method=%s", key.ClientID, info.FullMethod)
|
||||
return status.Error(codes.ResourceExhausted, "Rate limit exceeded")
|
||||
}
|
||||
|
||||
// Update last used timestamp
|
||||
if err := m.store.UpdateLastUsed(apiKey); err != nil {
|
||||
m.logger.Errorf("gRPC: Failed to update last used time for client=%s - error=%v", key.ClientID, err)
|
||||
}
|
||||
|
||||
// Create auth context and attach to request
|
||||
authCtx := &AuthContext{
|
||||
APIKey: key,
|
||||
ClientID: key.ClientID,
|
||||
Roles: key.Roles,
|
||||
}
|
||||
|
||||
// Add auth context to request context
|
||||
newCtx := ContextWithAuth(ss.Context(), authCtx)
|
||||
m.logger.Debugf("gRPC: Authenticated stream from client=%s method=%s with roles=%v", key.ClientID, info.FullMethod, key.Roles)
|
||||
|
||||
// Create wrapped server stream with new context
|
||||
wrappedStream := &wrappedServerStream{ServerStream: ss, ctx: newCtx}
|
||||
return handler(srv, wrappedStream)
|
||||
}
|
||||
}
|
||||
|
||||
// wrappedServerStream wraps grpc.ServerStream to override context
|
||||
type wrappedServerStream struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (w *wrappedServerStream) Context() context.Context {
|
||||
return w.ctx
|
||||
}
|
||||
|
||||
// extractAPIKey extracts API key from gRPC metadata
|
||||
func (m *GRPCAuthMiddleware) extractAPIKey(ctx context.Context) string {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try authorization header first
|
||||
if authHeaders := md.Get("authorization"); len(authHeaders) > 0 {
|
||||
authHeader := authHeaders[0]
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Try x-api-key header
|
||||
if keyHeaders := md.Get("x-api-key"); len(keyHeaders) > 0 {
|
||||
return keyHeaders[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user