pkg/gateway/auth/authenticator.go
Source
- Package:
auth - File:
pkg/gateway/auth/authenticator.go - GitHub: https://github.com/theroutercompany/api_router/blob/main/pkg/gateway/auth/authenticator.go
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
import (
"errors"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
gatewayconfig "github.com/theroutercompany/api_router/pkg/gateway/config"
)
Variables
var block 1
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
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
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
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
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.
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
returnstatement (possibly returning values).
- L34:
- 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
returnstatement (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.
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
returnstatement (possibly returning values).
- L55:
- 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
returnstatement (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.
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
returnstatement (possibly returning values).
- L69:
- 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
returnstatement (possibly returning values).
- L74:
- 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
returnstatement (possibly returning values).
- L79:
- 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
returnstatement (possibly returning values).
- L86:
- 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
returnstatement (possibly returning values).
- L84:
- 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
returnstatement (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.
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.
- L98:
- 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.
- L101:
- 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
funcliteral 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
returnstatement (possibly returning values).
- L108:
- L107:
- 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
returnstatement (possibly returning values).
- L111:
- 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
returnstatement (possibly returning values).
- L115:
- 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
returnstatement (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.
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 ... rangeloop. - 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 ... rangeloop. - 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
returnstatement (possibly returning values).
- L129:
- L128:
- L127:
- 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
returnstatement (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.
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
returnstatement (possibly returning values).
- L144:
- 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
returnstatement (possibly returning values).
- L147:
- 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
returnstatement (possibly returning values).