90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
// Package interceptors holds gRPC interceptors shared by both transports:
|
|||
|
|
// grpc-gateway forwards the incoming REST request's Authorization header as
|
||
|
|
// gRPC metadata, so the same unary/stream interceptor authenticates both
|
||
|
|
// REST and gRPC callers (design.md §8) — no separate REST auth middleware.
|
||
|
|
package interceptors
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||
|
|
"google.golang.org/grpc"
|
||
|
|
"google.golang.org/grpc/codes"
|
||
|
|
"google.golang.org/grpc/metadata"
|
||
|
|
"google.golang.org/grpc/status"
|
||
|
|
)
|
||
|
|
|
||
|
|
type claimsContextKey struct{}
|
||
|
|
|
||
|
|
// ClaimsFromContext returns the authenticated token's claims, set by the
|
||
|
|
// auth interceptor after a successful validation.
|
||
|
|
func ClaimsFromContext(ctx context.Context) (jwt.Token, bool) {
|
||
|
|
token, ok := ctx.Value(claimsContextKey{}).(jwt.Token)
|
||
|
|
return token, ok
|
||
|
|
}
|
||
|
|
|
||
|
|
// TokenValidator is the subset of auth.Validator the interceptor needs,
|
||
|
|
// abstracted so tests can inject a fake without a real JWKS endpoint.
|
||
|
|
type TokenValidator interface {
|
||
|
|
Validate(ctx context.Context, tokenString string) (jwt.Token, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func bearerToken(ctx context.Context) (string, error) {
|
||
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
||
|
|
if !ok {
|
||
|
|
return "", status.Error(codes.Unauthenticated, "missing metadata")
|
||
|
|
}
|
||
|
|
values := md.Get("authorization")
|
||
|
|
if len(values) == 0 {
|
||
|
|
return "", status.Error(codes.Unauthenticated, "missing authorization header")
|
||
|
|
}
|
||
|
|
const prefix = "Bearer "
|
||
|
|
header := values[0]
|
||
|
|
if !strings.HasPrefix(header, prefix) {
|
||
|
|
return "", status.Error(codes.Unauthenticated, "authorization header must be a bearer token")
|
||
|
|
}
|
||
|
|
return strings.TrimPrefix(header, prefix), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// UnaryServerInterceptor authenticates every unary RPC before it reaches
|
||
|
|
// handler logic.
|
||
|
|
func UnaryServerInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor {
|
||
|
|
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||
|
|
token, err := bearerToken(ctx)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
claims, err := validator.Validate(ctx, token)
|
||
|
|
if err != nil {
|
||
|
|
return nil, status.Error(codes.Unauthenticated, "invalid token")
|
||
|
|
}
|
||
|
|
return handler(context.WithValue(ctx, claimsContextKey{}, claims), req)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// StreamServerInterceptor authenticates every streaming RPC before it
|
||
|
|
// reaches handler logic.
|
||
|
|
func StreamServerInterceptor(validator TokenValidator) grpc.StreamServerInterceptor {
|
||
|
|
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||
|
|
token, err := bearerToken(ss.Context())
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
claims, err := validator.Validate(ss.Context(), token)
|
||
|
|
if err != nil {
|
||
|
|
return status.Error(codes.Unauthenticated, "invalid token")
|
||
|
|
}
|
||
|
|
return handler(srv, &authenticatedStream{ServerStream: ss, ctx: context.WithValue(ss.Context(), claimsContextKey{}, claims)})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type authenticatedStream struct {
|
||
|
|
grpc.ServerStream
|
||
|
|
ctx context.Context
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *authenticatedStream) Context() context.Context {
|
||
|
|
return s.ctx
|
||
|
|
}
|