Documentation
¶
Overview ¶
Package authn validates JWT access tokens and exposes the caller as a single principal. See README.md.
This is a temporary, unmodified copy of github.com/OpenNSW/core/authn from https://github.com/mushrafmim/nsw-core, branch refactor/291-single-principal, commit 1e6f75d. OpenNSW still depends on the older authn, so this version is not released upstream yet. Do not add OpenNDX-specific changes here; chain application middleware after authn instead. Once OpenNSW/core releases this version, delete this package and import the upstream module.
Index ¶
- func Middleware(tokenExtractor *TokenExtractor) func(http.Handler) http.Handler
- func RequireAuth(tokenExtractor *TokenExtractor) func(http.Handler) http.Handler
- type AuthContext
- func (a *AuthContext) ClientID() string
- func (a *AuthContext) Email() string
- func (a *AuthContext) ExtraClaims() ExtraClaims
- func (a *AuthContext) OUHandle() string
- func (a *AuthContext) OUID() string
- func (a *AuthContext) Roles() []string
- func (a *AuthContext) Scopes() []string
- func (a *AuthContext) Subject() string
- type ClaimConfig
- type ClaimsConfig
- type Config
- type ContextKey
- type ExtraClaims
- type Manager
- type Option
- type Principal
- type TokenExtractor
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Middleware ¶
func Middleware(tokenExtractor *TokenExtractor) func(http.Handler) http.Handler
Middleware creates an HTTP middleware that extracts and injects authentication context. This middleware: 1. Extracts the Authorization header 2. Parses the token into a Principal 3. Injects the auth context into the request
Behavior summary: - Missing Authorization header: request proceeds without auth context. - Invalid token: request is rejected with 401. - Auth dependencies unavailable: request is rejected with 500.
This design allows: - Public endpoints (no auth required) - Protected endpoints (check for context) - Optional auth endpoints (use context if available)
Anything an application wants to do with the validated principal (e.g. provisioning a user record) belongs in its own middleware chained after this one; see the package README.
func RequireAuth ¶
func RequireAuth(tokenExtractor *TokenExtractor) func(http.Handler) http.Handler
RequireAuth returns a middleware that requires authentication. If no auth context is found, returns 401 Unauthorized. This middleware should be applied to protected endpoints.
Usage:
mux.Handle("POST /api/protected", auth.RequireAuth(tokenExtractor)(handler))
TODO_JWT_FUTURE: Consider adding: - Different auth levels (basic, standard, admin) - Claim validation beyond token signature - Rate limiting per user
Types ¶
type AuthContext ¶
type AuthContext struct {
Principal Principal
}
AuthContext is the transient authentication context injected into each request by the auth middleware. It carries the Principal resolved from the request's token.
func GetAuthContext ¶
func GetAuthContext(ctx context.Context) *AuthContext
GetAuthContext extracts the AuthContext from a request context. Returns nil if no auth context is available (for example: public route, missing auth header, or middleware not applied).
Usage in handlers:
authCtx := auth.GetAuthContext(r.Context())
if authCtx == nil {
// Handle unauthorized request
}
subject := authCtx.Subject()
func (*AuthContext) ClientID ¶
func (a *AuthContext) ClientID() string
ClientID returns the OAuth2 client the principal's token was issued to, or "" when unauthenticated.
func (*AuthContext) Email ¶
func (a *AuthContext) Email() string
Email returns the principal's "email" claim, or "" when absent or unauthenticated.
func (*AuthContext) ExtraClaims ¶
func (a *AuthContext) ExtraClaims() ExtraClaims
ExtraClaims returns the principal's claims beyond the fixed schema, or nil. Nil-safe, and ExtraClaims' own methods are nil-safe too, so authCtx.ExtraClaims().String("email") never panics.
Like Roles and Scopes, this returns the live value rather than a copy — do not mutate it.
func (*AuthContext) OUHandle ¶
func (a *AuthContext) OUHandle() string
OUHandle returns the principal's "ouHandle" claim, or "" when absent or unauthenticated.
func (*AuthContext) OUID ¶
func (a *AuthContext) OUID() string
OUID returns the principal's "ouId" claim, or "" when absent or unauthenticated.
func (*AuthContext) Roles ¶
func (a *AuthContext) Roles() []string
Roles returns the granted roles for the principal, or nil.
func (*AuthContext) Scopes ¶
func (a *AuthContext) Scopes() []string
Scopes returns the granted OAuth2 scopes for the principal, or nil.
func (*AuthContext) Subject ¶
func (a *AuthContext) Subject() string
Subject returns the principal's JWT "sub" claim, or "" when unauthenticated.
type ClaimConfig ¶
type ClaimConfig struct {
// Required rejects a token whose claim is absent or blank. The
// requirement applies to every token, including client-credentials
// tokens issued to systems, which typically carry no email or
// organization unit. Default false: the Principal field is "" instead.
Required bool `yaml:"required"`
}
ClaimConfig configures one standard identity claim.
type ClaimsConfig ¶
type ClaimsConfig struct {
Email ClaimConfig `yaml:"email"`
OUID ClaimConfig `yaml:"ouId"`
OUHandle ClaimConfig `yaml:"ouHandle"`
}
ClaimsConfig configures each standard identity claim — "email", "ouId" and "ouHandle" — which populate the Principal fields of the same name.
ClaimsConfig carries yaml struct tags, so it can be populated generically (e.g. via yaml.Unmarshal) as part of Config.
type Config ¶
type Config struct {
JWKSURL string `yaml:"jwksURL"`
Issuer string `yaml:"issuer"`
Audience string `yaml:"audience"`
InsecureSkipTLSVerify bool `yaml:"insecureSkipTLSVerify"`
// Claims configures the standard identity claims. See WithClaims. Zero
// value = every claim optional.
Claims ClaimsConfig `yaml:"claims"`
}
Config carries yaml struct tags, so it can be embedded in a larger application config struct and populated generically (e.g. via yaml.Unmarshal).
type ContextKey ¶
type ContextKey string
ContextKey is a custom type for context keys to avoid collisions.
const AuthContextKey ContextKey = "authContext"
type ExtraClaims ¶
ExtraClaims holds every claim in a validated token outside authn's fixed schema (sub, client_id, scope, the roles claim, and the JWT registered claims), which already have dedicated Principal fields or are consumed by validation. Nothing has to be declared: whatever the IdP puts in the token is here. Deciding which of these a request needs is the application's job.
Values are the claim's JSON-decoded Go representation (string, []any, float64, bool, map[string]any), with one normalization: every JSON string a value directly carries is whitespace-trimmed — the value itself when it is a string, and the elements of a top-level array of strings. Nested objects are never rewritten, so what you index out of a map-valued claim is byte-for-byte what the IdP sent. Note the fixed-schema claims (sub, client_id, and the registered claims) are decoded by jwt.ParseWithClaims and are NOT trimmed.
Use String/Strings for the common shapes; index the map directly to read anything else. A nil or empty ExtraClaims is safe to read from and safe to call methods on, but is not writable — copy it before mutating.
func (ExtraClaims) String ¶
func (c ExtraClaims) String(name string) string
String returns the claim's value as a string, or "" if the claim is absent or not a JSON string (e.g. "email", "ouId", "ouHandle", "given_name").
func (ExtraClaims) Strings ¶
func (c ExtraClaims) Strings(name string) []string
Strings returns the claim's value as a string slice. It accepts a JSON array of strings, or a single space-delimited string — mirroring how the OAuth2 "scope" claim is parsed by spaceDelimitedScope in token_parser.go. An empty array, or an empty/whitespace-only string, returns an empty (non-nil) slice; an absent claim or any other shape (including an array with a non-string element) returns nil.
The space-splitting makes this the wrong accessor for a free-text claim: Strings("department") on "Customs Division" returns two elements. Use String for anything that is not list-shaped.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager handles all authentication-related operations and middleware setup. It encapsulates the token extraction and middleware creation, providing a clean interface for the HTTP server to use.
The manager only authenticates: it validates the token and injects the resulting Principal. Anything an application does with that principal (e.g. provisioning a user record) belongs in its own middleware chained after it.
func NewManager ¶
NewManager creates and initializes a new auth manager. This is the single entry point for all auth initialization in the application.
Usage:
authManager, err := auth.NewManager(cfg.Auth)
func (*Manager) Close ¶
Close performs any cleanup needed for the auth manager. Currently a no-op, but provided for future extensibility (e.g., closing JWT validators, flushing caches, logging stats).
Usage:
authManager := auth.NewManager(cfg.Auth) defer authManager.Close()
func (*Manager) Health ¶
Health checks if the auth system is functioning properly. It verifies that the auth system components are initialized correctly.
Usage in server startup:
authManager := auth.NewManager(cfg.Auth)
if err := authManager.Health(); err != nil {
log.Fatalf("auth system health check failed: %v", err)
}
Returns an error if auth system components are misconfigured.
func (*Manager) Middleware ¶
Middleware returns the auth middleware function. This middleware extracts and injects authentication context into requests.
Usage:
handler := authManager.Middleware()(mux)
The middleware: 1. Extracts Authorization header 2. Parses token into a Principal 3. Injects context into request
func (*Manager) OptionalAuthMiddleware ¶
OptionalAuthMiddleware returns a middleware for endpoints that work with or without auth. This is the same as the basic Middleware() - included for semantic clarity.
Usage:
mux.Handle("GET /api/products",
authManager.OptionalAuthMiddleware()(handler),
)
The handler can check if auth context is available and personalize response.
func (*Manager) RequireAuthMiddleware ¶
RequireAuthMiddleware returns a middleware that requires authentication. If no auth context is found, returns 401 Unauthorized. Use this for protected endpoints.
Usage:
mux.Handle("POST /api/protected",
authManager.RequireAuthMiddleware()(handler),
)
type Option ¶
type Option func(*TokenExtractor)
Option configures optional behavior on TokenExtractor construction.
Options never fail on their own: they record intent verbatim and every check runs afterwards in validateConfig, which NewTokenExtractor reports as a construction error.
func WithClaims ¶
func WithClaims(claims ClaimsConfig) Option
WithClaims configures the standard identity claims (see ClaimsConfig). A later call replaces an earlier one.
type Principal ¶
type Principal struct {
// Subject is the JWT "sub" claim: the identity provider's ID for the
// caller. Always non-empty: a token without "sub" is rejected.
Subject string `json:"subject"`
// ClientID is the JWT "client_id" claim: the OAuth2 client the token was
// issued to. Always non-empty: a token without "client_id" is rejected.
ClientID string `json:"clientId"`
// Roles is the JWT "roles" claim, nil when the token does not carry it. A
// "roles" claim that is not an array of strings rejects the token.
Roles []string `json:"roles"`
// Scopes is the space-delimited JWT "scope" claim, nil when the token does
// not carry it.
Scopes []string `json:"scopes"`
// Email, OUID and OUHandle are the standard identity claims "email",
// "ouId" and "ouHandle", whitespace-trimmed. Each is "" when the token
// does not carry it, unless configured as required (see ClaimsConfig),
// in which case such a token is rejected.
Email string `json:"email,omitempty"`
OUID string `json:"ouId,omitempty"`
OUHandle string `json:"ouHandle,omitempty"`
ExtraClaims ExtraClaims `json:"extraClaims,omitempty"`
}
Principal is the authenticated caller a validated token describes. There is one shape for every token, however it was obtained: a person signing in through a client and a system using client credentials are distinguished only by what they carry (roles, scopes, claims), never by kind.
type TokenExtractor ¶
type TokenExtractor struct {
// contains filtered or unexported fields
}
TokenExtractor handles token extraction and parsing from HTTP headers. It validates JWT signatures using JWKS and resolves the token's Principal.
func NewTokenExtractor ¶
func NewTokenExtractor(jwksURL, issuer, audience string, opts ...Option) (*TokenExtractor, error)
func (*TokenExtractor) ExtractPrincipalFromHeader ¶
func (te *TokenExtractor) ExtractPrincipalFromHeader(authHeader string) (*Principal, error)
ExtractPrincipalFromHeader extracts the principal from Authorization header. Expected header format: "Bearer <jwt_token>". JWT signature is validated against configured JWKS endpoint, then claims are mapped into a Principal.