Documentation
¶
Index ¶
- func Middleware(userProfileService UserProfileService, tokenExtractor *TokenExtractor) func(http.Handler) http.Handler
- func RequireAuth(userProfileService UserProfileService, tokenExtractor *TokenExtractor) func(http.Handler) http.Handler
- type AllowedGrantType
- type AuthContext
- type ClaimSpec
- type ClientContext
- type ClientPrincipal
- type Config
- type ContextKey
- type ExtraClaims
- type Manager
- type Option
- type Principal
- type PrincipalType
- type TokenExtractor
- type UserContext
- type UserPrincipal
- type UserProfileService
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Middleware ¶
func Middleware(userProfileService UserProfileService, 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 user principal or client principal 3. For user principals, resolves user profile idempotently if UserProfileService is provided 4. 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. - User principal on first login: resolves (get-or-create) user profile if service is provided.
This design allows: - Public endpoints (no auth required) - Protected endpoints (check for context) - Optional auth endpoints (use context if available) - Generic auth that works with or without a user profile service
func RequireAuth ¶
func RequireAuth(userProfileService UserProfileService, 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(userProfileService, tokenExtractor)(handler))
TODO_JWT_FUTURE: Consider adding: - Different auth levels (basic, standard, admin) - Claim validation beyond token signature - Rate limiting per user
Types ¶
type AllowedGrantType ¶
type AllowedGrantType string
const ( AuthorizationCodeGrant AllowedGrantType = "authorization_code" ClientCredentialsGrant AllowedGrantType = "client_credentials" )
type AuthContext ¶
type AuthContext struct {
User *UserContext
Client *ClientContext
}
AuthContext is the transient authentication context injected into each request by the auth middleware. For user principals, User contains identity fields and roles. For client principals (M2M), Client is set.
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
}
userID := authCtx.User.ID
func (*AuthContext) ExtraClaims ¶ added in v0.3.0
func (a *AuthContext) ExtraClaims() ExtraClaims
ExtraClaims returns the consumer-declared extra claims for the principal (user or client), 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) Roles ¶
func (a *AuthContext) Roles() []string
Roles returns the granted roles for the principal (user or client), or nil.
func (*AuthContext) Scopes ¶
func (a *AuthContext) Scopes() []string
Scopes returns the granted OAuth2 scopes for the principal (user or client), or nil.
func (*AuthContext) Subject ¶
func (a *AuthContext) Subject() string
Subject returns a stable identifier for the principal: the resolved user ID (falling back to the IdP user ID) for users, the client ID for clients, or "".
func (*AuthContext) Type ¶
func (a *AuthContext) Type() PrincipalType
Type reports the principal type of the context: UserPrincipalType, ClientPrincipalType, or "" when unauthenticated.
type ClaimSpec ¶ added in v0.3.0
ClaimSpec declares the extra JWT claims — those beyond authn's fixed schema (client_id, grant_type, scope, roles, and the JWT registered claims) — to extract for one principal type.
Optional claims are best-effort: a name that is absent, JSON-null, or an empty/whitespace-only string is silently skipped and never fails a token. Required claims must carry a usable value — a non-blank string, or a non-empty array of non-blank strings — or ExtractPrincipalFromHeader rejects the token. A name listed in both is required.
Names are matched exactly against the token payload: JWT claim names are case-sensitive (RFC 7519 §4). Nested lookups are not supported; a name is a top-level key, which is what makes namespaced names like "https://app.example.com/roles" work unchanged.
type ClientContext ¶
type ClientContext struct {
ClientID string `json:"clientId"`
Roles []string `json:"roles"`
Scopes []string `json:"scopes"`
ExtraClaims ExtraClaims `json:"extraClaims,omitempty"`
}
ClientContext represents a machine client's context.
type ClientPrincipal ¶
type ClientPrincipal struct {
ClientID string `json:"clientId"`
Roles []string `json:"roles"`
Scopes []string `json:"scopes"`
ExtraClaims ExtraClaims `json:"extraClaims,omitempty"`
}
type Config ¶
type Config struct {
JWKSURL string
Issuer string
Audience string
ClientIDs []string
InsecureSkipTLSVerify bool
// UserClaims declares extra JWT claims (beyond authn's fixed schema, e.g.
// "email", "phone_number", "ouId", "ouHandle", "given_name") to extract
// for user-principal (authorization_code grant) tokens. ClientClaims is
// the client-credential (M2M) analogue. See WithUserClaims /
// WithClientClaims. Zero value = no extra claims extracted.
UserClaims ClaimSpec
ClientClaims ClaimSpec
// RolesClaim overrides which claim carries the principal's roles, for
// IdPs that do not emit a top-level "roles" claim. See WithRolesClaim.
// Empty = "roles".
RolesClaim string
}
type ContextKey ¶
type ContextKey string
ContextKey is a custom type for context keys to avoid collisions.
const AuthContextKey ContextKey = "authContext"
type ExtraClaims ¶ added in v0.3.0
ExtraClaims holds JWT claims outside authn's fixed schema, declared explicitly by a consumer via WithUserClaims (user principals) or WithClientClaims (client principals).
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 ¶ added in v0.3.0
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 ¶ added in v0.3.0
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.
This manager pattern keeps auth logic self-contained and optionally delegates user persistence to a UserProfileService (if provided). The manager can work without a user profile service, making it suitable for any authentication use case.
func NewManager ¶
func NewManager(userProfileService UserProfileService, authConfig Config) (*Manager, error)
NewManager creates and initializes a new auth manager. This is the single entry point for all auth initialization in the application.
userProfileService is OPTIONAL. If not provided (nil), user creation on first login is disabled. tokenExtractor is REQUIRED and is always initialized by this constructor. This allows the auth package to be used in systems that don't track user profiles while still guaranteeing auth token parsing is available.
Usage examples:
// With user profile service (NSW example)
userProfileService := user.NewService(db)
authManager := auth.NewManager(userProfileService, cfg.Auth)
// Without user profile service (generic auth only)
authManager := auth.NewManager(nil, cfg.Auth)
// With custom user profile service
customService := &MyCustomUserService{}
authManager := auth.NewManager(customService, 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(userService, cfg.Auth) defer authManager.Close()
func (*Manager) Health ¶
Health checks if the auth system is functioning properly. Since the UserProfileService is optional, this only verifies that the auth system components are initialized correctly.
Usage in server startup:
authManager := auth.NewManager(userProfileService, 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 user or client principal 3. For user principals, creates user record if it's their first login 4. 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 ¶ added in v0.3.0
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, so NewTokenExtractor reports a bad declaration as a construction error regardless of the order options were passed in.
func WithClientClaims ¶ added in v0.3.0
WithClientClaims is the WithUserClaims analogue for client-credential (M2M) tokens, populating ClientPrincipal.ExtraClaims / ClientContext.ExtraClaims.
The declaration is separate because the two token types carry different claim sets: a client token may carry e.g. "department" or "cost_center", while "email"/"ouHandle"/"given_name" only appear on user tokens. A name declared for one principal type is never extracted from the other.
func WithRolesClaim ¶ added in v0.3.0
WithRolesClaim points authn at a differently-named claim for the roles that populate Principal.Roles and AuthContext.Roles(), for identity providers that do not emit a top-level "roles" claim (Okta commonly uses "groups"). Defaults to "roles". The name applies to both user and client principals.
The claim must be a top-level JSON array of strings — exactly the shape the default "roles" claim must have. An absent claim yields no roles; a present claim of any other shape rejects the token, so a mistyped name fails loudly on the first request instead of silently disabling every role check.
Dotted paths into nested objects (Keycloak's realm_access.roles) are NOT supported: names are matched exactly, which is what keeps namespaced claim names such as "https://app.example.com/roles" working. Flatten a nested roles claim with an IdP-side protocol mapper.
The configured name is reserved: it cannot also be declared as an extra claim via WithUserClaims/WithClientClaims, since it already has a dedicated field.
func WithUserClaims ¶ added in v0.3.0
WithUserClaims declares extra claims to extract from user-principal (authorization_code grant) tokens. Extracted values surface on UserPrincipal.ExtraClaims / UserContext.ExtraClaims.
Repeated calls accumulate rather than replace, so a name declared required by any call stays required.
type Principal ¶
type Principal struct {
Type PrincipalType `json:"type"`
UserPrincipal *UserPrincipal `json:"userPrincipal,omitempty"`
ClientPrincipal *ClientPrincipal `json:"clientPrincipal,omitempty"`
}
type PrincipalType ¶
type PrincipalType string
const ( UserPrincipalType PrincipalType = "user" ClientPrincipalType PrincipalType = "client" )
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 a user principal or client principal based on grant type.
func NewTokenExtractor ¶
func NewTokenExtractor(jwksURL, issuer, audience string, expectedClientIDs []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 either UserPrincipal or ClientPrincipal.
type UserContext ¶
type UserContext struct {
ID string `json:"id"`
IDPUserID string `json:"idpUserId"`
Roles []string `json:"roles"`
Scopes []string `json:"scopes"`
ExtraClaims ExtraClaims `json:"extraClaims,omitempty"`
}
UserContext represents a user principal's runtime context injected into each request. It includes identity fields and principal-derived roles. Note: Per-request NSWData is not persisted here; services requiring user metadata should call the user profile service on-demand.
type UserPrincipal ¶
type UserPrincipal struct {
// Subject is the JWT "sub" claim: the identity provider's ID for the user.
// It is NOT the internally persisted user ID a UserProfileService returns
// — see UserContext, which carries both as ID and IDPUserID.
Subject string `json:"subject"`
Roles []string `json:"roles"`
Scopes []string `json:"scopes"`
ExtraClaims ExtraClaims `json:"extraClaims,omitempty"`
}
type UserProfileService ¶
type UserProfileService interface {
// GetOrCreateUser creates or retrieves a user profile.
//
// principal is never nil, and MUST NOT be mutated: the middleware has
// already built the request's AuthContext from it and shares the same
// ExtraClaims map, so writing to it would alter the live request context.
// (ExtraClaims is also nil unless claims were declared — nil maps read
// fine but panic on assignment.)
//
// - principal.Subject is the identity provider's user ID (the JWT "sub"
// claim), NOT the persisted ID this method returns.
// - principal.ExtraClaims holds the claims declared via WithUserClaims /
// Config.UserClaims; nil if none were declared.
//
// Implementation notes:
// - Should be idempotent: calling multiple times with the same subject should be safe
// - Called during first login after token validation
// - Errors are logged but don't block authentication
// - Should not return error if user already exists
// Returns user ID of the created or existing user, or an error if the operation fails.
GetOrCreateUser(ctx context.Context, principal *UserPrincipal) (string, error)
}
UserProfileService defines the contract for managing user profiles. Implementations are responsible for persisting and managing user records in their system.
This interface is OPTIONAL when using the auth package. If not provided (nil), user creation on first login will be skipped. This allows:
1. Systems that don't track user profiles - just use auth for token validation 2. Systems that manage user profiles separately - implement this interface 3. Systems that handle user creation elsewhere - pass nil
The whole authenticated principal is passed rather than a list of named identity values, because everything except the JWT "sub" claim (email, phone number, organization/tenant identifiers, ...) is IdP/consumer-specific. Naming those as parameters is what forced this interface to change once already; passing the principal means adding a claim never changes the signature again.
Example implementation:
type MyUserService struct {
db *sql.DB
}
func (s *MyUserService) GetOrCreateUser(ctx context.Context, principal *authn.UserPrincipal) (string, error) {
// Your implementation to create or fetch the user idempotently
email := principal.ExtraClaims.String("email")
persistedID := "generated-id"
if err := s.db.Exec("INSERT INTO users ...", principal.Subject, email).Error; err != nil {
return "", err
}
return persistedID, nil
}
authManager := auth.NewManager(myUserService, cfg.Auth) // myUserService can be nil