Skip to content

pkg/oauth: built-in OAuth2/SMART authorization server for self-contained deployments #23

Description

@cursor

Summary

Add pkg/oauth — a basic, embeddable OAuth 2.0 / SMART-on-FHIR authorization server for HAIStack so edge, offline, and small deployments can run a complete FHIR stack without Keycloak, Auth0, or an EHR IdP.

Today HAIStack splits concerns cleanly but leaves token issuance to the host:

Package Role today
pkg/auth Authorization decisions (policy, roles, patient compartment)
pkg/smart Scope parsing, JWT validation, launch context → principal adapter
pkg/http PrincipalResolver + AuthChecker hooks (Bearer validation only)
pkg/client Outbound OAuth client (discover, PKCE, token exchange)

Gap: no in-tree server for /authorize, /token, /.well-known/smart-configuration, client registry, or refresh tokens. Hosts must wire an external IdP.

This issue adds an optional pkg/oauth layer that issues tokens and wires into existing pkg/smart + pkg/auth + pkg/http so a single binary can be a self-contained SMART FHIR server.

Goals

  • Self-contained deployments — clinic edge, demos, integration tests, air-gapped environments
  • SMART-compatible — enough for standalone launch + backend-service client credentials used by HAIStack today
  • Pluggable — Keycloak/enterprise IdP remains supported; pkg/oauth is opt-in
  • Basic v1 — correct, auditable, boring; not a Keycloak replacement

Non-goals (v1)

  • Full OIDC provider feature matrix (federation, complex consent UI, social login)
  • Dynamic Client Registration server (RFC 7591) — static/file-backed clients OK for v1
  • EHR launch UI orchestration (browser chrome inside Epic/Cerner)
  • SMART 2.2 granular scope enforcement at the OAuth layer (see SMART 2.2 granular scopes: parse, enforce, and test vectors #20; token may carry scopes, enforcement stays pkg/auth + search)
  • Replacing pkg/auth policy engine or moving SMART scope logic into pkg/auth

Go ecosystem note

Go’s standard library does not ship an OAuth2 authorization server. Relevant pieces:

  • golang.org/x/oauth2 — primarily an OAuth client (already aligned with pkg/client outbound flows)
  • crypto / encoding/json — JWT signing (access tokens, client assertions)
  • Candidate server libraries to evaluate (pick one for v1, don’t reinvent RFC 6749):

Recommendation: spike 1–2 libraries behind pkg/oauth interfaces; keep HAIStack-specific SMART wiring out of the forked RFC logic.

Proposed architecture

flowchart TB
  subgraph oauth [pkg/oauth NEW]
    AS[Authorization Server]
    CR[Client registry]
    TR[Token service JWT]
    WM[SMART metadata /.well-known]
  end
  subgraph existing
    SMART[pkg/smart scopes + AuthAdapter]
    AUTH[pkg/auth policy engine]
    HTTP[pkg/http FHIR + middleware]
  end
  App[SMART app / backend client] -->|authorize + token| AS
  AS --> CR
  AS --> TR
  WM --> App
  HTTP -->|PrincipalResolver validates JWT| TR
  HTTP --> SMART --> AUTH
  AS -->|granted scopes + launch context| SMART
Loading

pkg/oauth responsibilities

  1. OAuth2 endpoints (HTTP handlers or http.Handler mountable by pkg/runtime)

    • GET/POST /authorize — authorization code + PKCE (S256)
    • POST /token — authorization_code, client_credentials (backend service), refresh_token (basic)
    • Optional: POST /revoke, GET /.well-known/openid-configuration (if OIDC subset added)
  2. SMART surface

    • GET {fhir-base}/.well-known/smart-configuration (delegate to or share with pkg/smart metadata types)
    • Access tokens include SMART claims: scope, patient, fhirUser, tenant hint as applicable
    • Backend-service flow: validate client assertion (pkg/smart.BackendServiceAuth validation rules) or issue tokens after assertion exchange
  3. Client registry (v1: static)

    • File-backed or in-memory ClientRegistration (extend pkg/smart.ClientRegistration)
    • Public + confidential clients; redirect URI allow-list; allowed scopes per client
  4. Integration wiring

    • oauth.WireHTTP(cfg) → returns:
      • RootHandler or route table for auth + well-known
      • hahttp.PrincipalResolver that validates issued JWTs via pkg/smart.TokenValidator
      • Optional smart.ScopePolicyAuthChecker pattern: scopes → pkg/auth with policy deny winning
    • runtime.Builder.WithOAuth(...) convenience for managed servers
  5. Persistence (v1 minimal)

    • In-memory auth codes + refresh tokens (single-instance OK)
    • Optional SQLite tables via pkg/sqlite for codes/refresh/replay (multi-instance later)
    • Reuse pkg/smart replay protection patterns for backend jti
  6. User / consent (v1 minimal)

    • Static demo login or API-key style approval screen (HTML template or JSON API for headless edge)
    • No requirement for polished SMART launch UI in v1 — document extension point

Proposed package layout

pkg/oauth/
  doc.go
  server.go          # Server config, mount routes
  authorize.go       # authorization endpoint
  token.go           # token endpoint
  clients.go         # client registry interface + file store
  codes.go           # auth code + PKCE validation
  refresh.go         # refresh token rotation (basic)
  jwt.go             # access token issue/validate (issuer, aud, exp)
  smart_metadata.go  # well-known smart-configuration
  wire.go            # PrincipalResolver + http.Handler composition
  store/             # optional sqlite persistence
  oauth_test.go
examples/oauth-fhir-server/   # runnable: oauth + fhir + auth policy

Phased delivery

Phase 1 — MVP authorization server

  • Static client registry
  • Authorization code + PKCE (S256 only)
  • Token endpoint (authorization_code grant)
  • JWT access tokens signed with configurable key (HS256 or RS256)
  • /.well-known/smart-configuration on FHIR base
  • WireHTTPPrincipalResolver for pkg/http
  • Example examples/oauth-fhir-server
  • Unit tests for code/PKCE/expiry; integration test: token → FHIR read

Phase 2 — SMART backend service + refresh

  • Client credentials + JWT client assertion (align with pkg/smart.BackendServiceAuth)
  • Refresh token grant (single-use rotation optional in v1)
  • Revocation endpoint (basic)
  • SQLite-backed code/refresh store option

Phase 3 — Runtime + docs

  • runtime.Builder.WithOAuth
  • Host documentation: self-contained vs external IdP
  • Link from pkg/http/README and pkg/smart/README (replace “hosts own OAuth” with “hosts may use pkg/oauth or external IdP”)

Phase 4 — Hardening (stretch)

Acceptance criteria (MVP)

  • A SMART standalone app can complete auth-code + PKCE against HAIStack-issued endpoints and call GET /fhir/Patient with the returned Bearer token
  • pkg/http auth middleware accepts tokens issued by pkg/oauth without custom host code
  • pkg/auth policy deny overrides granted scopes (reuse/extend authztest scenarios)
  • Patient-scoped launch puts patient claim in token and TenantContext.PatientScope flows through to FHIR enforcement
  • Documented escape hatch: production users can disable pkg/oauth and use Keycloak/Epic/etc.

Security requirements (MVP)

  • PKCE required for public clients; reject plain
  • Redirect URI exact match
  • Short-lived auth codes; single use
  • state parameter supported and documented
  • No tokens in logs; key material from env/file/config
  • Deny-by-default when pkg/http auth enabled without valid Bearer

Related issues

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions