Skip to content

Backend: Multi-issuer token validation #265

@danielbowne

Description

@danielbowne

Backend: Multi-issuer token validation

Epic: Multi-IdP Authentication (#150)
Blocked by: Per-IdP ALB listener rules (for E2E, but code can be developed first)

Summary

Update the Go backend to validate JWT tokens from multiple OIDC issuers (Okta and Entra ID). Currently, token.go fetches signing keys from a single TokenKeyUrl and middleware.go performs no issuer validation.

Current State

Config (backend/internal/config/config.go:35-39)

Auth struct {
    HS256_SECRET string `env:"AUTH_HS256_SECRET"`
    TokenKeyUrl  string `env:"AUTH_TOKEN_KEY_URL"`  // single key URL
    HeaderField  string `env:"AUTH_HEADER_FIELD"`   // single header
}

Token validation (backend/cmd/api/internal/auth/token.go:44-48)

// Fetches keys from single URL, no issuer check
url := cfg.Auth.TokenKeyUrl + kid

Middleware (backend/cmd/api/internal/auth/middleware.go:39)

// Email-only lookup, no issuer validation
user, err := model.FindUserByEmail(r.Context(), claims.Email)

Requirements

Config Changes

  • Replace flat Auth struct with multi-provider config
  • Support loading provider configs from env vars or Secrets Manager
  • Each provider needs: issuer, token_key_url, email_domain (optional)
  • Preserve HS256 local dev path unchanged

Example structure:

Auth struct {
    HS256_SECRET string   `env:"AUTH_HS256_SECRET"`
    HeaderField  string   `env:"AUTH_HEADER_FIELD"`
    Providers    []AuthProvider
}

type AuthProvider struct {
    Name        string // "okta", "entra"
    Issuer      string // expected iss claim
    TokenKeyUrl string // JWKS/key endpoint base URL
}

Token Validation (token.go)

  • Parse JWT header to get kid and peek at unverified iss claim
  • Look up iss against configured providers to find correct TokenKeyUrl
  • Fetch signing key from matched provider's key endpoint
  • Reject tokens with unknown issuers
  • Key caching should be per-provider (already keyed by kid, should be fine if kid values don't collide across providers — consider namespacing cache keys as issuer:kid)

Middleware (middleware.go)

  • After token validation, verify iss claim matches a configured provider
  • Log which provider authenticated the request (for debugging)
  • Continue with email-based user lookup (unchanged)

Unit Tests

  • Test token validation with mock Okta-issued token
  • Test token validation with mock Entra ID-issued token
  • Test rejection of token with unknown issuer
  • Test that HS256 local dev tokens still work
  • Test key caching behavior with multiple providers

Files to Modify

  • backend/internal/config/config.go — multi-provider auth config struct + loading
  • backend/cmd/api/internal/auth/token.go — issuer-aware key resolution
  • backend/cmd/api/internal/auth/middleware.go — issuer validation
  • backend/cmd/api/internal/auth/token_test.go — new/updated tests
  • backend/cmd/api/internal/auth/middleware_test.go — new/updated tests

Acceptance Criteria

  • Backend validates tokens from multiple configured OIDC issuers
  • Tokens with unknown iss claims are rejected with 401
  • HS256 local dev tokens continue to work
  • Key cache handles multiple providers without collision
  • make test-unit passes
  • No regression in Emberfall E2E tests

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions