auth.go 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. package grpcserver
  2. import (
  3. "context"
  4. "crypto/subtle"
  5. "fmt"
  6. "strings"
  7. "git3.techno-world.net/lrosales/broad-announce/internal/pipeline"
  8. "google.golang.org/grpc/codes"
  9. "google.golang.org/grpc/metadata"
  10. "google.golang.org/grpc/status"
  11. )
  12. // authenticate validates the API key from gRPC metadata and returns the
  13. // corresponding SourceConfig. It is the gRPC equivalent of the HTTP
  14. // X-BA-Signature HMAC check.
  15. //
  16. // Metadata key: "authorization" → "Bearer <company_id>:<source_id>:<secret>"
  17. //
  18. // Returns codes.Unauthenticated on any failure.
  19. func authenticate(ctx context.Context, sources map[string]pipeline.SourceConfig) (*pipeline.SourceConfig, error) {
  20. md, ok := metadata.FromIncomingContext(ctx)
  21. if !ok {
  22. return nil, status.Error(codes.Unauthenticated, "missing metadata")
  23. }
  24. // Support both canonical "authorization" and " Authorization".
  25. authVals := md.Get("authorization")
  26. if len(authVals) == 0 {
  27. authVals = md.Get("Authorization")
  28. }
  29. if len(authVals) == 0 || authVals[0] == "" {
  30. return nil, status.Error(codes.Unauthenticated, "missing authorization header")
  31. }
  32. raw := strings.TrimPrefix(authVals[0], "Bearer ")
  33. if raw == authVals[0] {
  34. return nil, status.Error(codes.Unauthenticated, "authorization must use Bearer scheme")
  35. }
  36. key := parseAPIKey(raw)
  37. if key == nil {
  38. return nil, status.Error(codes.Unauthenticated, "malformed API key")
  39. }
  40. src, ok := sources[key.SourceKey()]
  41. if !ok {
  42. return nil, status.Error(codes.Unauthenticated,
  43. fmt.Sprintf("no such source %s/%s", key.CompanyID, key.SourceID))
  44. }
  45. // Verify the secret. The sources map stores the full raw API key as the
  46. // secret for env-based auth. Constant-time compare to avoid timing leaks.
  47. want := key.CompanyID + ":" + key.SourceID + ":" + string(src.HMACSecret)
  48. if subtle.ConstantTimeCompare([]byte(raw), []byte(want)) != 1 {
  49. return nil, status.Error(codes.Unauthenticated, "invalid API key")
  50. }
  51. return &src, nil
  52. }
  53. // apiKey represents a parsed API key in "<company_id>:<source_id>:<secret>" form.
  54. type apiKey struct {
  55. CompanyID string
  56. SourceID string
  57. Secret string
  58. }
  59. func parseAPIKey(raw string) *apiKey {
  60. parts := strings.SplitN(raw, ":", 3)
  61. if len(parts) != 3 {
  62. return nil
  63. }
  64. return &apiKey{CompanyID: parts[0], SourceID: parts[1], Secret: parts[2]}
  65. }
  66. func (k *apiKey) SourceKey() string { return k.CompanyID + ":" + k.SourceID }