authn

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

authn

JWT-based authentication. Validates tokens against a JWKS endpoint, extracts identity claims, and provides HTTP middleware that injects an AuthContext into each request's context.

Configuration

import "github.com/OpenNSW/core/authn"

manager, err := authn.NewManager(userProfileSvc, authn.Config{
    JWKSURL:   "https://idp.example.com/.well-known/jwks.json",
    Issuer:    "https://idp.example.com",
    Audience:  "my-api",
    ClientIDs: []string{"my-m2m-client"},
})

UserProfileService is optional. When provided, it is called on the first appearance of a user token to create or retrieve a persisted user record (e.g. to assign an internal user ID). Pass nil to skip user persistence.

Middleware

// 401 if no valid token
mux.Handle("/api/v1/tasks", manager.RequireAuthMiddleware()(handler))

// Proceeds with or without a token; handler checks presence itself
mux.Handle("/api/v1/public", manager.OptionalAuthMiddleware()(handler))

Reading identity in handlers

authCtx := authn.GetAuthContext(r.Context())
if authCtx == nil {
    // no token present (only possible with OptionalAuthMiddleware)
}

// Human user token
if authCtx.Type() == authn.UserPrincipalType {
    fmt.Println(authCtx.User.Email)
    fmt.Println(authCtx.User.ID)       // internal persisted ID (if UserProfileService set)
    fmt.Println(authCtx.User.OUID)     // organisation unit ID
}

// Machine-to-machine token
if authCtx.Type() == authn.ClientPrincipalType {
    fmt.Println(authCtx.Client.ClientID)
}

Principal fields

UserContext

Field Description
ID Internal persisted user ID (set by UserProfileService)
IDPUserID Subject claim from the token
Email Email claim
PhoneNumber Phone number claim
OUID Organisation unit ID
OUHandle Organisation unit handle
Roles Role claims
Scopes Scope claims

ClientContext

Field Description
ClientID Client ID claim
Roles Role claims
Scopes Scope claims

Health check

if err := manager.Health(ctx); err != nil {
    // JWKS endpoint unreachable
}

Authorization

authn handles identity only. For scope enforcement use the authz package — *AuthContext satisfies authz.Principal directly.

Documentation

Index

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) 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 ClientContext

type ClientContext struct {
	ClientID string   `json:"clientId"`
	Roles    []string `json:"roles"`
	Scopes   []string `json:"scopes"`
}

ClientContext represents a machine client's context.

type ClientPrincipal

type ClientPrincipal struct {
	ClientID string   `json:"clientId"`
	Roles    []string `json:"roles"`
	Scopes   []string `json:"scopes"`
}

type Config

type Config struct {
	JWKSURL               string
	Issuer                string
	Audience              string
	ClientIDs             []string
	InsecureSkipTLSVerify bool
}

func (Config) Validate

func (c Config) Validate() error

type ContextKey

type ContextKey string

ContextKey is a custom type for context keys to avoid collisions.

const AuthContextKey ContextKey = "authContext"

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

func (m *Manager) Close() error

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

func (m *Manager) Health() error

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

func (m *Manager) Middleware() func(http.Handler) http.Handler

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

func (m *Manager) OptionalAuthMiddleware() func(http.Handler) http.Handler

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

func (m *Manager) RequireAuthMiddleware() func(http.Handler) http.Handler

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 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) (*TokenExtractor, error)

func NewTokenExtractorWithClient

func NewTokenExtractorWithClient(jwksURL, issuer, audience string, expectedClientIDs []string, httpClient *http.Client) (*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"`
	Email       string   `json:"email"`
	PhoneNumber string   `json:"phoneNumber"`
	OUID        string   `json:"ouId"`
	OUHandle    string   `json:"ouHandle"`
	Roles       []string `json:"roles"`
	Scopes      []string `json:"scopes"`
}

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 {
	UserID      string   `json:"userId"`
	Email       string   `json:"email"`
	PhoneNumber *string  `json:"phone_number,omitempty"`
	OUID        string   `json:"ouId"`
	OUHandle    string   `json:"ouHandle"`
	Roles       []string `json:"roles"`
	Scopes      []string `json:"scopes"`
}

type UserProfileService

type UserProfileService interface {
	// GetOrCreateUser creates or retrieves a user profile.
	// Parameters:
	//   - idpUserID: the unique user ID from the identity provider (required)
	//   - email: user's email address (required)
	//   - phone: user's phone number (can be empty)
	//   - organizationID: organization/tenant identifier (required)
	//   - ouHandle: organization unit handle from the identity provider (required)
	//
	// Implementation notes:
	//   - Should be idempotent: calling multiple times with same idpUserID 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, idpUserID, email, phone, orgID, ouHandle string) (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

Example implementation:

type MyUserService struct {
    db *sql.DB
}

func (s *MyUserService) GetOrCreateUser(ctx context.Context, idpUserID, email, phone, orgID, ouHandle string) (string, error) {
    // Your implementation to create or fetch the user idempotently
    persistedID := "generated-id"
    if err := s.db.Exec("INSERT INTO users ...", idpUserID, email, phone, orgID).Error; err != nil {
        return "", err
    }
    return persistedID, nil
}

authManager := auth.NewManager(myUserService, cfg.Auth)  // myUserService can be nil

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL