Skip to content

OSCAR OAuth 1.0a Flow

3-legged OAuth token exchange for OSCAR REST API access

Last Updated: 2026-03-09 (v4.3.0)


Overview

VitaraVox uses OAuth 1.0a to access OSCAR's REST API (Surface 3: /ws/services/*). This is required for:

  • Patient registration (createPatientPOST /ws/services/demographics)
  • All REST-mode operations when preferRest=true (Kai-hosted OSCAR behind Cloudflare WAF)
  • quickSearch patient lookup (GET /ws/services/demographics/quickSearch?query=)

The OAuth flow is a standard 3-legged exchange following RFC 5849, with tokens stored encrypted (AES-256-GCM) in the database.

Property Value
Signature Method HMAC-SHA1
Token Lifetime Set to 10 years (TTL=-1 in OSCAR = permanent)
Storage AES-256-GCM encrypted in ClinicConfig
Token Monitor Scheduled job at 6 AM + 6 PM daily
Source routes/oscar-oauth.ts, EmrAdapterFactory.ts:142-186

3-Legged OAuth Flow

Source: oscar-oauth.ts:45-269

  Admin Dashboard              VitaraVox Server           OSCAR EMR
       │                            │                        │
       │ POST /api/oscar-oauth/     │                        │
       │   initiate {clinicId}      │                        │
       │───────────────────────────►│                        │
       │                            │                        │
       │                            │ Load ClinicConfig      │
       │                            │ Decrypt consumer creds │
       │                            │                        │
       │                            │ POST /ws/oauth/initiate│
       │                            │ (OAuth-signed, includes│
       │                            │  oauth_callback URL)   │
       │                            │───────────────────────►│
       │                            │                        │
       │                            │ oauth_token +          │
       │                            │ oauth_token_secret     │
       │                            │◄───────────────────────│
       │                            │                        │
       │                            │ Store in pendingRequests
       │                            │ (Map, 10min TTL)       │
       │                            │                        │
       │ { authorizationUrl, state }│                        │
       │◄───────────────────────────│                        │
       │                            │                        │
       │ Redirect admin to          │                        │
       │ OSCAR authorization URL    │                        │
       │────────────────────────────────────────────────────►│
       │                            │                        │
       │                            │         OSCAR shows    │
       │                            │         authorization  │
       │                            │         page to admin  │
       │                            │                        │
       │                            │         Admin clicks   │
       │                            │         "Authorize"    │
       │                            │                        │
       │                            │ GET /api/oscar-oauth/  │
       │                            │   callback?oauth_token │
       │                            │   &oauth_verifier      │
       │                            │   &state               │
       │                            │◄───────────────────────│
       │                            │                        │
       │                            │ Verify CSRF state      │
       │                            │ Match pendingRequest   │
       │                            │                        │
       │                            │ POST /ws/oauth/token   │
       │                            │ (signed with request   │
       │                            │  token + verifier)     │
       │                            │───────────────────────►│
       │                            │                        │
       │                            │ access_token +         │
       │                            │ access_token_secret    │
       │                            │◄───────────────────────│
       │                            │                        │
       │                            │ Encrypt + store tokens │
       │                            │ Set expiry (10 years)  │
       │                            │ Invalidate adapter cache
       │                            │ Audit log              │
       │                            │                        │
       │                            │ HTML: "OAuth Complete" │
       │◄───────────────────────────│                        │

API Endpoints

POST /api/oscar-oauth/initiate

Starts the OAuth flow for a clinic. Source: oscar-oauth.ts:45-135.

Property Value
Auth JWT (admin)
Body { clinicId: string }
Prerequisites Clinic must have oscarUrl, oscarConsumerKey, oscarConsumerSecretEnc in DB
Response { authorizationUrl: string, state: string }

The authorizationUrl points to OSCAR's authorization page: {oscarUrl}/ws/oauth/authorize?oauth_token={token}&state={state}.

GET /api/oscar-oauth/callback

Receives the OAuth verifier after admin approves. Source: oscar-oauth.ts:142-269.

Property Value
Auth None (OSCAR redirect)
Query Params oauth_token, oauth_verifier, state
CSRF Protection state parameter must match the one from initiate
Response HTML page confirming success

On success: 1. Exchanges request token + verifier for access token 2. Encrypts access tokens with AES-256-GCM 3. Stores in ClinicConfig.oscarTokenKey + oscarTokenSecretEnc 4. Sets oscarOauthTokenExpiresAt to 10 years from now 5. Invalidates adapter cache for the clinic 6. Writes audit log entry (PHIPA s.10(1) compliance)


Credential Storage

All OAuth credentials are stored encrypted in the ClinicConfig table:

Field Purpose Encrypted?
oscarConsumerKey OAuth consumer key (from OSCAR admin) AES-256-GCM
oscarConsumerSecretEnc OAuth consumer secret AES-256-GCM
oscarTokenKey Access token (from exchange) AES-256-GCM
oscarTokenSecretEnc Access token secret AES-256-GCM
oscarOauthTokenExpiresAt Token expiry timestamp Plaintext (DateTime)

Encryption uses the ENCRYPTION_KEY env var (64-char hex = 32 bytes). Format: iv:authTag:ciphertext (base64). Source: lib/crypto.ts.


Scope Resolution (CRITICAL)

The resolveOAuthCreds() function (EmrAdapterFactory.ts:142-186) resolves credentials with strict scope isolation:

resolveOAuthCreds(clinicConfig, clinicId)
  ┌─────────────────────────────────────┐
  │ Clinic has oscarConsumerKey in DB?  │
  └─────┬──────────────────┬────────────┘
        │ YES              │ NO
        ▼                  ▼
  ┌──────────────┐  ┌──────────────────┐
  │ CLINIC SCOPE │  │ ENV SCOPE        │
  │              │  │                  │
  │ consumerKey  │  │ OSCAR_OAUTH_     │
  │   from DB    │  │  CONSUMER_KEY    │
  │ consumerSec  │  │ OSCAR_OAUTH_     │
  │   from DB    │  │  CONSUMER_SECRET │
  │ tokenKey     │  │ OSCAR_OAUTH_     │
  │   from DB    │  │  TOKEN_KEY       │
  │ tokenSecret  │  │ OSCAR_OAUTH_     │
  │   from DB    │  │  TOKEN_SECRET    │
  └──────────────┘  └──────────────────┘

NEVER Mix Scopes

If a clinic has its own oscarConsumerKey, ALL 4 credential fields must come from the clinic DB record. Mixing a clinic consumer key with env token values produces invalid OAuth signatures and routes traffic to the wrong EMR instance. This is enforced in code at EmrAdapterFactory.ts:153-172.


Token Lifetime & Monitoring

Token TTL

OSCAR tokens generated via the OAuth flow have TTL=-1 (permanent). VitaraVox sets a 10-year expiry as a safety ceiling (oscar-oauth.ts:219).

The oscarOauthTokenExpiresAt field tracks when tokens should be refreshed. This is checked:

  1. At adapter creation: EmrAdapterFactory.ts:107 passes tokenExpiresAt to adapter
  2. Before REST calls: Adapter fails fast if token has expired (OscarSoapAdapter.ts:527-530)
  3. By scheduled monitor: Runs at 6 AM + 6 PM daily (scheduler.ts:25-31)

Token Monitor Job

The OAuth token monitor runs twice daily and checks all clinics with preferRest=true for tokens approaching expiry. Source: jobs/scheduler.ts:25-31.


TLS Handling

For OSCAR instances with self-signed certificates:

  • ClinicConfig.oscarTlsVerify (boolean, default true)
  • When false: Creates HTTPS agent with rejectUnauthorized: false
  • Used in both initiate and callback requests

Prerequisites for Clinics

Before the OAuth flow can start, a clinic needs:

  1. OSCAR URL stored in ClinicConfig.oscarUrl
  2. Consumer credentials from OSCAR admin → stored encrypted in DB
  3. ModuleNames=REST enabled in OSCAR's oscar.properties (deploys /ws/oauth/* and /ws/services/*)
  4. Tomcat restart after enabling ModuleNames=REST

Kai-Hosted OSCAR

For Kai-hosted instances, enabling ModuleNames=REST requires a support request to WELL Health / Kai support. This is a one-time configuration change. OSCAR caches the REST module state at Tomcat startup — new OAuth keys also require a Tomcat restart.