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
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)
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
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
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
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
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
Phase 2 — SMART backend service + refresh
Phase 3 — Runtime + docs
Phase 4 — Hardening (stretch)
Acceptance criteria (MVP)
Security requirements (MVP)
Related issues
References
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:
pkg/authpkg/smartpkg/httpPrincipalResolver+AuthCheckerhooks (Bearer validation only)pkg/clientGap: 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/oauthlayer that issues tokens and wires into existingpkg/smart+pkg/auth+pkg/httpso a single binary can be a self-contained SMART FHIR server.Goals
pkg/oauthis opt-inNon-goals (v1)
pkg/auth+ search)pkg/authpolicy engine or moving SMART scope logic intopkg/authGo 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 withpkg/clientoutbound flows)crypto/encoding/json— JWT signing (access tokens, client assertions)github.com/ory/fosite— mature OAuth2/OIDC frameworkgithub.com/go-oauth2/oauth2— lighter OAuth2 serverRecommendation: spike 1–2 libraries behind
pkg/oauthinterfaces; 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| SMARTpkg/oauthresponsibilitiesOAuth2 endpoints (HTTP handlers or
http.Handlermountable bypkg/runtime)GET/POST /authorize— authorization code + PKCE (S256)POST /token— authorization_code, client_credentials (backend service), refresh_token (basic)POST /revoke,GET /.well-known/openid-configuration(if OIDC subset added)SMART surface
GET {fhir-base}/.well-known/smart-configuration(delegate to or share withpkg/smartmetadata types)scope,patient,fhirUser,tenanthint as applicablepkg/smart.BackendServiceAuthvalidation rules) or issue tokens after assertion exchangeClient registry (v1: static)
ClientRegistration(extendpkg/smart.ClientRegistration)Integration wiring
oauth.WireHTTP(cfg)→ returns:RootHandleror route table for auth + well-knownhahttp.PrincipalResolverthat validates issued JWTs viapkg/smart.TokenValidatorsmart.ScopePolicyAuthCheckerpattern: scopes →pkg/authwith policy deny winningruntime.Builder.WithOAuth(...)convenience for managed serversPersistence (v1 minimal)
pkg/sqlitefor codes/refresh/replay (multi-instance later)pkg/smartreplay protection patterns for backendjtiUser / consent (v1 minimal)
Proposed package layout
Phased delivery
Phase 1 — MVP authorization server
S256only)/.well-known/smart-configurationon FHIR baseWireHTTP→PrincipalResolverforpkg/httpexamples/oauth-fhir-serverPhase 2 — SMART backend service + refresh
pkg/smart.BackendServiceAuth)Phase 3 — Runtime + docs
runtime.Builder.WithOAuthpkg/http/READMEandpkg/smart/README(replace “hosts own OAuth” with “hosts may use pkg/oauth or external IdP”)Phase 4 — Hardening (stretch)
openid,fhirUser, id_token)pkg/oauthreference deploymentAcceptance criteria (MVP)
GET /fhir/Patientwith the returned Bearer tokenpkg/httpauth middleware accepts tokens issued bypkg/oauthwithout custom host codepkg/authpolicy deny overrides granted scopes (reuse/extendauthztestscenarios)patientclaim in token andTenantContext.PatientScopeflows through to FHIR enforcementpkg/oauthand use Keycloak/Epic/etc.Security requirements (MVP)
plainstateparameter supported and documentedpkg/httpauth enabled without valid BearerRelated issues
pkg/testkit/authztest)pkg/oauthreference host when ready)References
pkg/client— outbound SMART client patterns to mirror server-side