Skip to main content

pkg/gateway/auth/authenticator.go

Source

Overview

What: Implements JWT-based authentication for the gateway using bearer tokens.

Why: Trade/task proxy routes require consistent authentication and scope enforcement that is reusable across the runtime and embedding scenarios.

How: Parses the Authorization header, validates an HS256 JWT using a shared secret, optionally enforces issuer/audience, and returns a Principal with extracted scopes.

Notes: Do not log raw tokens or secrets. This authenticator is designed around symmetric HS256 verification.

Contents

Imports

import block 1

pkg/gateway/auth/authenticator.go#L8
import (
"errors"
"net/http"
"strings"

"github.com/golang-jwt/jwt/v5"

gatewayconfig "github.com/theroutercompany/api_router/pkg/gateway/config"
)

Variables

var block 1

pkg/gateway/auth/authenticator.go#L39
var (
errMissingAuthorization = Error{Status: http.StatusUnauthorized, Title: "Authentication Required", Detail: "Missing authorization header"}
errMalformedHeader = Error{Status: http.StatusUnauthorized, Title: "Authentication Required", Detail: "Malformed authorization header"}
errTokenInvalid = Error{Status: http.StatusUnauthorized, Title: "Authentication Required", Detail: "Invalid or expired token"}
)

errMissingAuthorization

What: Auth error returned when the Authorization header is missing.

Why: Provides a consistent 401 error for unauthenticated requests.

How: Returned by Authenticate when the header is empty.

errMalformedHeader

What: Auth error returned when the Authorization header is not a valid Bearer token format.

Why: Separates "no header" from "bad header" to aid debugging and consistent messaging.

How: Returned by Authenticate when the header does not split into Bearer <token>.

errTokenInvalid

What: Auth error returned when the token cannot be validated or is not valid.

Why: Avoids leaking low-level JWT parse errors while still returning a consistent 401 response.

How: Returned by Authenticate for invalid tokens and by parseToken when token.Valid is false.

Types

type block 1

pkg/gateway/auth/authenticator.go#L19
type Principal struct {
Subject string
Scopes []string
Token string
}

Principal

What: Represents an authenticated caller (subject + scopes + token string).

Why: Downstream authorization decisions need a normalized view of identity and permissions.

How: Constructed from validated JWT claims and then used for scope checks and request metadata.

type block 2

pkg/gateway/auth/authenticator.go#L26
type Error struct {
Status int
Title string
Detail string
}

Error

What: Structured authentication error including HTTP status and error messaging.

Why: Server code needs to map auth failures to correct HTTP responses (401 vs 403, etc.) with stable titles/details.

How: Returned by auth helpers and handled specially by server error writers.

type block 3

pkg/gateway/auth/authenticator.go#L46
type Authenticator struct {
secret []byte
audiences []string
issuer string
}

Authenticator

What: Validates JWT bearer tokens and produces principals.

Why: Encapsulates auth policy (secret, audiences, issuer) and avoids spreading JWT verification across handlers.

How: Stores the secret bytes and optional issuer/audience constraints and exposes Authenticate.

type block 4

pkg/gateway/auth/authenticator.go#L136
type gatewayClaims struct {
Scope string `json:"scope"`
Scp []string `json:"scp"`
jwt.RegisteredClaims
}

gatewayClaims

What: JWT claims struct that supports both scope and scp scope encodings.

Why: Different issuers encode scopes differently; the gateway supports both formats for compatibility.

How: Embeds jwt.RegisteredClaims and adds fields for scope extraction.

Functions and Methods

(Error).Error

What: Implements the error interface for auth errors.

Why: Allows auth failures to carry HTTP status and user-facing titles/details while still behaving like errors.

How: Returns Detail when non-empty, otherwise returns Title.

pkg/gateway/auth/authenticator.go#L32
func (e Error) Error() string {
if e.Detail != "" {
return e.Detail
}
return e.Title
}

Walkthrough

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L33: if e.Detail != "" { return e.Detail }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L34: return e.Detail
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L36: return e.Title
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

New

What: Constructs an Authenticator from gatewayconfig.AuthConfig.

Why: Centralizes validation of required auth inputs and converts config into runtime-ready fields.

How: Requires a non-empty secret and stores secret bytes, audiences, and issuer on the authenticator.

pkg/gateway/auth/authenticator.go#L53
func New(cfg gatewayconfig.AuthConfig) (*Authenticator, error) {
if cfg.Secret == "" {
return nil, errors.New("jwt secret not configured")
}

return &Authenticator{
secret: []byte(cfg.Secret),
audiences: cfg.Audiences,
issuer: cfg.Issuer,
}, nil
}

Walkthrough

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L54: if cfg.Secret == "" { return nil, errors.New("jwt secret not configured") }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L55: return nil, errors.New("jwt secret not configured")
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L58: return &Authenticator{ secret: []byte(cfg.Secret), audiences: cfg.Audiences, issuer: cfg.Issuer, }, nil
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

(*Authenticator).Authenticate

What: Validates the request's bearer token and returns a Principal.

Why: Callers (server middleware/handlers) need a single function that handles header parsing plus token validation.

How: Reads the Authorization header, enforces the Bearer format, delegates JWT validation to parseToken, then attaches the raw token string to the returned principal.

pkg/gateway/auth/authenticator.go#L66
func (a *Authenticator) Authenticate(r *http.Request) (*Principal, error) {
header := r.Header.Get("Authorization")
if header == "" {
return nil, errMissingAuthorization
}

parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return nil, errMalformedHeader
}

tokenString := strings.TrimSpace(parts[1])
if tokenString == "" {
return nil, errMalformedHeader
}

principal, err := a.parseToken(tokenString)
if err != nil {
var authErr Error
if errors.As(err, &authErr) {
return nil, authErr
}
return nil, errTokenInvalid
}

principal.Token = tokenString
return principal, nil
}

Walkthrough

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L67: header := r.Header.Get("Authorization")
    • What: Defines header.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L68: if header == "" { return nil, errMissingAuthorization }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L69: return nil, errMissingAuthorization
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L72: parts := strings.SplitN(header, " ", 2)
    • What: Defines parts.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L73: if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { return nil, errMalformedHeader }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L74: return nil, errMalformedHeader
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L77: tokenString := strings.TrimSpace(parts[1])
    • What: Defines tokenString.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L78: if tokenString == "" { return nil, errMalformedHeader }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L79: return nil, errMalformedHeader
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L82: principal, err := a.parseToken(tokenString)
    • What: Defines principal, err.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L83: if err != nil { var authErr Error if errors.As(err, &authErr) { return nil, authErr } return nil, errTokenInvalid }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L84: var authErr Error
        • What: Declares local names.
        • Why: Introduces variables or types used later in the function.
        • How: Executes a Go declaration statement inside the function body.
      • L85: if errors.As(err, &authErr) { return nil, authErr }
        • What: Branches conditionally.
        • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
        • How: Evaluates the condition and executes the matching branch.
        • Nested steps:
          • L86: return nil, authErr
            • What: Returns from the current function.
            • Why: Ends the current execution path and hands control back to the caller.
            • How: Executes a return statement (possibly returning values).
      • L88: return nil, errTokenInvalid
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L91: principal.Token = tokenString
    • What: Assigns principal.Token.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L92: return principal, nil
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

(*Authenticator).parseToken

What: Parses and validates a JWT token string and extracts identity/scope information.

Why: Keeps JWT library usage isolated and allows Authenticate to focus on HTTP concerns.

How: Creates a JWT parser with HS256-only, optional audience/issuer enforcement, parses into gatewayClaims, checks validity, and returns a Principal with subject and scopes.

pkg/gateway/auth/authenticator.go#L95
func (a *Authenticator) parseToken(tokenString string) (*Principal, error) {
options := []jwt.ParserOption{jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})}
if len(a.audiences) > 0 {
options = append(options, jwt.WithAudience(a.audiences...))
}
if a.issuer != "" {
options = append(options, jwt.WithIssuer(a.issuer))
}

parser := jwt.NewParser(options...)
claims := &gatewayClaims{}

token, err := parser.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) {
return a.secret, nil
})
if err != nil {
return nil, Error{Status: http.StatusUnauthorized, Title: "Authentication Required", Detail: err.Error()}
}

if !token.Valid {
return nil, errTokenInvalid
}

principal := &Principal{
Subject: claims.Subject,
Scopes: claims.Scopes(),
}
return principal, nil
}

Walkthrough

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L96: options := []jwt.ParserOption{jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})}
    • What: Defines options.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L97: if len(a.audiences) > 0 { options = append(options, jwt.WithAudience(a.audiences...)) }
    • What: Branches conditionally.
    • Why: Handles different execution paths based on runtime state.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L98: options = append(options, jwt.WithAudience(a.audiences...))
        • What: Assigns options.
        • Why: Keeps intermediate state available for later steps in the function.
        • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L100: if a.issuer != "" { options = append(options, jwt.WithIssuer(a.issuer)) }
    • What: Branches conditionally.
    • Why: Handles different execution paths based on runtime state.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L101: options = append(options, jwt.WithIssuer(a.issuer))
        • What: Assigns options.
        • Why: Keeps intermediate state available for later steps in the function.
        • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L104: parser := jwt.NewParser(options...)
    • What: Defines parser.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L105: claims := &gatewayClaims{}
    • What: Defines claims.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L107: token, err := parser.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) { return a.secret, nil })
    • What: Defines token, err.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
    • Nested steps:
      • L107: func(_ *jwt.Token) (interface{}, error) { return a.secret, nil }
        • What: Defines an inline function (closure).
        • Why: Encapsulates callback logic and may capture variables from the surrounding scope.
        • How: Declares a func literal and uses it as a value (for example, as an HTTP handler or callback).
        • Nested steps:
          • L108: return a.secret, nil
            • What: Returns from the current function.
            • Why: Ends the current execution path and hands control back to the caller.
            • How: Executes a return statement (possibly returning values).
  • L110: if err != nil { return nil, Error{Status: http.StatusUnauthorized, Title: "Authentication Required", Detail: err.Error()} }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L111: return nil, Error{Status: http.StatusUnauthorized, Title: "Authentication Required", Detail: err.Error()}
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L114: if !token.Valid { return nil, errTokenInvalid }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L115: return nil, errTokenInvalid
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L118: principal := &Principal{ Subject: claims.Subject, Scopes: claims.Scopes(), }
    • What: Defines principal.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L122: return principal, nil
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

(*Principal).HasAnyScope

What: Checks whether the principal has at least one of the required scopes.

Why: Upstream products enforce scopes (e.g., read/write) and handlers need a simple predicate.

How: Performs a nested loop over required scopes and the principal's scopes.

pkg/gateway/auth/authenticator.go#L125
func (p *Principal) HasAnyScope(required []string) bool {
for _, scope := range required {
for _, owned := range p.Scopes {
if scope == owned {
return true
}
}
}
return false
}

Walkthrough

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L126: for _, scope := range required { for _, owned := range p.Scopes { if scope == owned { return true } } }
    • What: Iterates over a collection.
    • Why: Processes multiple elements with the same logic.
    • How: Executes a for ... range loop.
    • Nested steps:
      • L127: for _, owned := range p.Scopes { if scope == owned { return true } }
        • What: Iterates over a collection.
        • Why: Processes multiple elements with the same logic.
        • How: Executes a for ... range loop.
        • Nested steps:
          • L128: if scope == owned { return true }
            • What: Branches conditionally.
            • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
            • How: Evaluates the condition and executes the matching branch.
            • Nested steps:
              • L129: return true
                • What: Returns from the current function.
                • Why: Ends the current execution path and hands control back to the caller.
                • How: Executes a return statement (possibly returning values).
  • L133: return false
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

(*gatewayClaims).Scopes

What: Returns scopes extracted from gateway JWT claims.

Why: Tokens may encode scopes in different fields depending on issuer conventions.

How: Prefers scp (array) when present; otherwise splits the scope string on whitespace; returns nil when neither exists.

pkg/gateway/auth/authenticator.go#L142
func (c *gatewayClaims) Scopes() []string {
if len(c.Scp) > 0 {
return c.Scp
}
if c.Scope == "" {
return nil
}
parts := strings.Fields(c.Scope)
return parts
}

Walkthrough

The list below documents the statements inside the function body, including nested blocks and inline closures.

  • L143: if len(c.Scp) > 0 { return c.Scp }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L144: return c.Scp
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L146: if c.Scope == "" { return nil }
    • What: Branches conditionally.
    • Why: Short-circuits early when a precondition is not met or an error/edge case is detected.
    • How: Evaluates the condition and executes the matching branch.
    • Nested steps:
      • L147: return nil
        • What: Returns from the current function.
        • Why: Ends the current execution path and hands control back to the caller.
        • How: Executes a return statement (possibly returning values).
  • L149: parts := strings.Fields(c.Scope)
    • What: Defines parts.
    • Why: Keeps intermediate state available for later steps in the function.
    • How: Evaluates the right-hand side expressions and stores results in the left-hand variables.
  • L150: return parts
    • What: Returns from the current function.
    • Why: Ends the current execution path and hands control back to the caller.
    • How: Executes a return statement (possibly returning values).

Guides

Reference

Neighboring source