shared/api/gRPC/authServerInterceptor.go
package gRPC
import (
"context"
"go.kicksware.com/api/services/users/core/model"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"go.kicksware.com/api/services/users/core/meta"
"go.kicksware.com/api/shared/api/jwt"
)
const (
AuthMetaKey = "authorization"
UserContextKey = "user_id"
)
type AuthServerInterceptor struct {
jwtManager *jwt.TokenManager
accessRoles map[string][]model.UserRole
}
func NewAuthServerInterceptor(jwt *jwt.TokenManager, accessRoles map[string][]model.UserRole) *AuthServerInterceptor {
return &AuthServerInterceptor{
jwtManager: jwt,
accessRoles: accessRoles,
}
}
// Unary returns a server interceptor function to authenticate and authorize unary RPC
func (i *AuthServerInterceptor) Unary() grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
if !i.zeroAccess(info.FullMethod) {
token, claims, err := i.authenticate(ctx); if err != nil {
return nil, err
}
err = i.authorize(claims, info.FullMethod); if err != nil {
return nil, err
}
ctx = metadata.AppendToOutgoingContext(ctx,
UserContextKey, claims.UniqueID,
AuthMetaKey, token,
)
}
return handler(ctx, req)
}
}
// Stream returns a server interceptor function to authenticate and authorize stream RPC
func (i *AuthServerInterceptor) Stream() grpc.StreamServerInterceptor {
return func(
srv interface{},
stream grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
if !i.zeroAccess(info.FullMethod) {
token, claims, err := i.authenticate(stream.Context()); if err != nil {
return err
}
err = i.authorize(claims, info.FullMethod); if err != nil {
return err
}
stream.SetHeader(metadata.New(map[string]string{
UserContextKey: claims.UniqueID,
AuthMetaKey: token,
}))
}
return handler(srv, stream)
}
}
func (i *AuthServerInterceptor) authenticate(ctx context.Context) (string, *meta.AuthClaims, error) {
meta, ok := metadata.FromIncomingContext(ctx); if !ok {
return "", nil, status.Errorf(codes.Unauthenticated, "metadata is not provided")
}
values := meta[AuthMetaKey]; if len(values) == 0 {
return "", nil, status.Errorf(codes.Unauthenticated, "authorization token is not provided")
}
accessToken := values[0]
claims, err := i.jwtManager.Verify(accessToken); if err != nil {
return accessToken, nil, status.Errorf(codes.Unauthenticated, "access token is invalid: %w", err)
}
return accessToken, claims, nil
}
func (i *AuthServerInterceptor) authorize(claims *meta.AuthClaims, method string) error {
accessibleRoles, ok := i.accessRoles[method]; if !ok {
return nil
}
for _, role := range accessibleRoles {
if string(role) == claims.Role {
return nil
}
}
return status.Error(codes.PermissionDenied, "no permission to access this RPC")
}
func (i *AuthServerInterceptor) zeroAccess(method string) bool {
if roles := i.accessRoles[method]; roles != nil && len(roles) == 0 {
return true
}
return false
}