smart-doctor v0.2.0 GitHub

Open source · runs on your machine

Why does this SMART on FHIR login fail?

EHRs answer almost every client problem with the same invalid_client. smart-doctor checks everything you can check from your side, runs the real login once, and ranks the likely causes in plain English.

npx smart-doctor init
Look up an error
  • Keys and secrets stay local
  • No account, no telemetry
  • Backend services and user sign-in
AlarmBackend services · private_key_jwtlaunch.smarthealthit.org
  1. Key passed
  2. Discovery passed
  3. Clock passed
  4. JWKS skipped
  5. Assertion passed
  6. Token failed
  7. FHIR not reached
  • Discovery596ms · SMART config
  • Clock−1s skew
  • Assertion240s until exp · ES384
  • Token401invalid_client
Most likely cause · very likelyYour kid is not in the JWKS the server knows aboutPublish the key with that kid, or sign with a kid that is published.

A real run against the public SMART launcher, with a mistyped kid.

Complaint

One error, dozens of causes

Servers say as little as possible on purpose, so an attacker learns nothing. You learn nothing either. smart-doctor works out the rest from your side of the connection.

What smart-doctor tells you

  1. Your kid is not in the JWKS the server knows aboutvery likely

    The server looks up the kid from your header in your registered JWKS and does not find it.

    server said “None of the keys found in the JWKS have kid equal to sandbox-typo”

    Publish the key with that kid, or sign with a kid that is published.

Ruled out on the way

  • ES384 key loads, kid set
  • aud = token endpoint
  • iss = sub = client_id
  • exp 240 s, jti unique
  • clock −1 s

How it works

Your side of the login, examined

smart-doctor sends the server exactly what your app would send, and keeps everything else on your machine.

YOUR MACHINE THE EHR network Private key or secretread in-process never leaves this machine You, or a ticketmasked report smart-doctor Gatherdiscovery · clock · JWKS · assertion Judgelocal checks, the server’s own words Reportranked causes, evidence, next step signs locally plain English SMART configuration.well-known · metadata Token endpointwhat your app would send Sign-in pageyou sign in, in your browser GET discovery POST assertion sign-in, code
Only what your app would send anyway crosses to the EHR: discovery requests, the token request and your own sign-in. The key or secret is used in-process, and everything shown back to you is masked.

Examination

Both kinds of login

Backend services

client_credentials · private_key_jwt or client secret

  • Your key or secret loads, and its algorithm fits
  • SMART configuration and the token endpoint
  • Your clock against the server’s
  • JWKS reachable, has your kid, and the published key is yours
  • No private key material in the public JWKS
  • The assertion: aud, exp ≤ 5 min, jti, kid, iss = sub
  • The real token request and the scopes granted
  • Optional FHIR call, status code only

User sign-in

SMART App Launch · authorization_code + PKCE

  • Authorize endpoint, PKCE S256 and capabilities
  • Your redirect URI, exactly as registered today
  • Sign-in, then state and iss on the way back
  • Code exchange as a public client, with a secret or with a key
  • Patient context
  • id_token signature, aud and fhirUser
  • Refresh token
  • Optional FHIR call, status code only

No registration changes.With a http://localhost redirect URI, smart-doctor catches the redirect itself. For anything else you sign in, land on your company’s page, and paste its address back.

Diagnosis

Ranked causes, with the evidence

Every cause comes with why it happens, the evidence smart-doctor found, and the exact next step.

  1. Rule out what can be checked

    Keys, JWKS, the assertion, your clock and the redirect URI, all verified locally first.

  2. Read the server’s own words

    Including the non-standard spellings vendors really send, like Invalid Grant and invalid_code.

  3. Add vendor knowledge

    Hints for Epic, Oracle Health (Cerner), OpenEMR, athenahealth, each from a public source.

  4. Name what it can’t see

    When everything local passes: the server-side causes left, and how to confirm them.

causes explained
57
server messages recognised
18
practice failure modes
18
breakages checked on a public server
16
Browse all 57 causes in the error catalogue →

Treatment

Run it in two minutes

  1. Practise first

    A web view with a practice EHR that fails on purpose. No account needed.

    npx smart-doctor ui --demo
  2. Point it at your server

    A few questions, the rest discovered, saved as a profile that never holds a secret.

    npx smart-doctor init
  3. Share the result safely

    The report masks the client ID, tokens and secrets: ready for a ticket, a vendor or an AI.

    npx smart-doctor check my-profile --report

No Node.js?

Standalone binaries for macOS, Linux and Windows on the releases page, with checksums.

Nightly monitoring

A GitHub Action catches expiring keys and registration drift before production does.

Docker

A small image for CI pipelines: mount the folder with your profile and run check.

AI agents

A Claude Code plugin whose skill passes keys by path only and shares --report output.

Precautions

Built to be trusted with keys

What leaves your machine
ItemLeaves your machineHow
Private keyneverSigns one test assertion in-process.
Client secrettoken endpoint onlyRead from an environment variable, never a flag.
Tokens and codeswhere your app sends themMasked in every trace and report.
Patient datanever readThe FHIR call records the status code only.
Usage datanoneNo account, no cloud, no telemetry.
  • Masked at the sourceEvery HTTP exchange is redacted inside the engine before anything can display it, and tests enforce it.
  • A local web view, locked downBinds to 127.0.0.1, needs a per-session token, strict Content-Security-Policy, no third-party requests.
  • Read every line that runsPlain JavaScript with no build step, MIT licensed. Security model.

Next time it says invalid_client, ask smart-doctor.

Free and open source. Run it once against the practice EHR, then against yours.

npx smart-doctor init
Star on GitHub