Documentation/api/AUTHENTICATION.md
Complete guide to authenticating with the OpenEMR API using OAuth 2.0 and OpenID Connect.
OpenEMR uses OAuth 2.0 with OpenID Connect (OIDC) for API authentication and authorization. This standards-based approach ensures secure access to patient data while maintaining interoperability with healthcare applications.
OAuth2 endpoints use the following base URL pattern:
https://{your-openemr-host}/oauth2/{site}/
Example for default site:
https://localhost:9300/oauth2/default/
Enable the appropriate API services in OpenEMR:
Administration → Config → Connectors
/api/ endpoints)/fhir/ endpoints)/portal/ endpoints) - EXPERIMENTALSSL/TLS is required for all OAuth2 operations.
Set your base URL: Administration → Config → Connectors → Site Address (required for OAuth2 and FHIR)
Example: https://your-openemr.example.com
Configure OAuth2 security settings:
Administration → Config → Connectors → OAuth2
Before using the API, you must register your application to obtain credentials.
Register a client application via API call:
curl -X POST -k -H 'Content-Type: application/json' \
https://localhost:9300/oauth2/default/registration \
--data '{
"application_type": "private",
"redirect_uris": ["https://client.example.org/callback"],
"post_logout_redirect_uris": ["https://client.example.org/logout/callback"],
"client_name": "My Healthcare App",
"token_endpoint_auth_method": "client_secret_post",
"contacts": ["[email protected]"],
"scope": "openid offline_access api:oemr api:fhir user/Patient.read user/Observation.read"
}'
| Parameter | Required | Description |
|---|---|---|
application_type | Yes | private (confidential) or public |
redirect_uris | Yes | Array of allowed redirect URLs |
post_logout_redirect_uris | No | Redirect URLs after logout |
client_name | Yes | Human-readable app name |
token_endpoint_auth_method | Yes | client_secret_post, client_secret_basic, or private_key_jwt |
contacts | No | Admin email addresses |
scope | Yes | Space-separated list of requested scopes |
jwks_uri | No | URL to public JWKS (for asymmetric auth) |
jwks | No | Inline JWKS (for asymmetric auth) |
{
"client_id": "LnjqojEEjFYe5j2Jp9m9UnmuxOnMg4VodEJj3yE8_OA",
"client_secret": "j21ecvLmFi9HPc_Hv0t7Ptmf1pVcZQLtHjIdU7U9tkS9WAjFJwVM...",
"registration_access_token": "uiDSXx2GNSvYy5n8eW50aGrJz0HjaGpUdrGf07Agv_Q",
"registration_client_uri": "https://localhost:9300/oauth2/default/client/6eUVG0-qK2dY...",
"client_id_issued_at": 1604767861,
"client_secret_expires_at": 0,
"contacts": ["[email protected]"],
"application_type": "private",
"client_name": "My Healthcare App",
"redirect_uris": ["https://client.example.org/callback"],
"token_endpoint_auth_method": "client_secret_post",
"scope": "openid offline_access api:oemr api:fhir user/Patient.read..."
}
Important: Save your client_id and client_secret securely. The secret cannot be retrieved later.
SMART apps can be registered through the web interface:
https://your-openemr.example.com/interface/smart/register-app.phpAfter registration:
See SMART on FHIR documentation for more details.
New in SMART v2.2.0: Confidential clients can use asymmetric authentication with JSON Web Keys (JWKS) instead of client secrets.
system/*.$export)Option 1: JWKS URI (recommended)
{
"application_type": "private",
"redirect_uris": ["https://client.example.org/callback"],
"client_name": "Secure Healthcare App",
"token_endpoint_auth_method": "private_key_jwt",
"jwks_uri": "https://client.example.org/.well-known/jwks.json",
"scope": "openid api:fhir system/Group.$export"
}
Option 2: Inline JWKS
{
"application_type": "private",
"redirect_uris": ["https://client.example.org/callback"],
"client_name": "Secure Healthcare App",
"token_endpoint_auth_method": "private_key_jwt",
"jwks": {
"keys": [
{
"kty": "RSA",
"kid": "my-key-1",
"use": "sig",
"alg": "RS384",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx...",
"e": "AQAB"
}
]
},
"scope": "openid api:fhir system/Group.$export"
}
See Using Asymmetric Authentication for implementation details.
Recommended for most applications. Secure, standards-based flow for web and mobile apps.
sequenceDiagram
participant Client as Your App
participant Browser as User's Browser
participant OpenEMR as OpenEMR Server
participant User as User
Client->>Browser: 1. Redirect to /authorize
Browser->>OpenEMR: 2. Authorization Request
OpenEMR->>User: 3. Login Prompt*
User->>OpenEMR: 4. Enter Credentials*
OpenEMR->>User: 5. Consent Screen
User->>OpenEMR: 6. Approve Scopes
OpenEMR->>Browser: 7. Redirect with Code
Browser->>Client: 8. Authorization Code
Client->>OpenEMR: 9. Exchange Code for Token
OpenEMR->>Client: 10. Access + Refresh Tokens
* Steps 3,4 are skipped in EHR Launch context if the OAuth2 EHR-Launch Authorization Flow Skip Enable App Setting is set to true
Redirect the user's browser to the authorization endpoint:
GET /oauth2/default/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https%3A%2F%2Fclient.example.org%2Fcallback&
scope=openid%20offline_access%20patient%2FPatient.read%20patient%2FObservation.read&
state=RANDOM_STATE_STRING&
aud=https://localhost:9300/apis/default/fhir
Parameters:
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code |
client_id | Yes | Your registered client ID |
redirect_uri | Yes | Must match registered redirect URI |
scope | Yes | Space-separated scopes (URL encoded) |
state | Yes | Random string for CSRF protection |
aud | Conditional | Required for FHIR: FHIR base URL |
code_challenge | Recommended | PKCE code challenge (required for public apps) |
code_challenge_method | Recommended | S256 (SHA-256) |
Example:
https://localhost:9300/oauth2/default/authorize?response_type=code&client_id=LnjqojEEjFYe5j2Jp9m9UnmuxOnMg4VodEJj3yE8_OA&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcallback&scope=openid%20offline_access%20patient%2FPatient.read&state=af0ifjsldkj&aud=https://localhost:9300/apis/default/fhir
OpenEMR will:
After approval, OpenEMR redirects back to your redirect_uri:
https://client.example.org/callback?
code=def50200a8f...(authorization_code)&
state=af0ifjsldkj
Verify that the state parameter matches your original request (CSRF protection).
Exchange the authorization code for tokens:
For Confidential Apps (using client_secret_post):
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'redirect_uri=https://client.example.org/callback' \
--data-urlencode 'code=def50200a8f...'
For Confidential Apps (using client_secret_basic):
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Authorization: Basic BASE64(client_id:client_secret)' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'redirect_uri=https://client.example.org/callback' \
--data-urlencode 'code=def50200a8f...'
For Public Apps (with PKCE):
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'redirect_uri=https://client.example.org/callback' \
--data-urlencode 'code=def50200a8f...' \
--data-urlencode 'code_verifier=ORIGINAL_CODE_VERIFIER'
{
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJrYn...",
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJrYnl1RkRp...",
"refresh_token": "def5020017b484b0add020bf3491a8a537fa04eda12...",
"scope": "openid offline_access patient/Patient.read patient/Observation.read"
}
Note: refresh_token is only provided if offline_access scope was requested.
New in SMART v2.2.0: Authorization requests can be submitted via POST for enhanced security.
Instead of GET with query parameters, submit a form POST:
<form method="POST" action="https://localhost:9300/oauth2/default/authorize">
<input type="hidden" name="response_type" value="code">
<input type="hidden" name="client_id" value="YOUR_CLIENT_ID">
<input type="hidden" name="redirect_uri" value="https://client.example.org/callback">
<input type="hidden" name="scope" value="openid offline_access patient/Patient.read">
<input type="hidden" name="state" value="RANDOM_STATE">
<input type="hidden" name="aud" value="https://localhost:9300/apis/default/fhir">
<input type="hidden" name="code_challenge" value="CODE_CHALLENGE">
<input type="hidden" name="code_challenge_method" value="S256">
<button type="submit">Authorize</button>
</form>
Using curl:
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/authorize \
--data-urlencode 'response_type=code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'redirect_uri=https://client.example.org/callback' \
--data-urlencode 'scope=openid offline_access patient/Patient.read' \
--data-urlencode 'state=RANDOM_STATE' \
--data-urlencode 'aud=https://localhost:9300/apis/default/fhir' \
--data-urlencode 'code_challenge=CODE_CHALLENGE' \
--data-urlencode 'code_challenge_method=S256'
Parameters: Same as GET-based authorization (see above).
Response: Same redirect behavior with authorization code.
EHR Launch allows apps to be launched with pre-established context (patient, encounter, etc.) without requiring the user to select these resources.
sequenceDiagram
participant EHR as OpenEMR
participant App as SMART App
participant User as User
User->>EHR: 1. Click "Launch App"
EHR->>App: 2. Redirect with launch token
App->>EHR: 3. Authorization request (with launch)
EHR->>User: 4. Login (if needed)*
EHR->>User: 5. Consent screen
User->>EHR: 6. Approve
EHR->>App: 7. Redirect with code
App->>EHR: 8. Exchange code for token
EHR->>App: 9. Access token + launch context
* Step 4 is skipped in EHR Launch context if the OAuth2 EHR-Launch Authorization Flow Skip Enable App Setting is set to true
OpenEMR redirects to your app's launch URL with a launch parameter:
https://client.example.org/launch?
iss=https://localhost:9300/apis/default/fhir&
launch=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Parameters:
iss: FHIR base URL (issuer)launch: Opaque launch token (use in authorization request)Redirect user to authorization endpoint, including the launch parameter:
GET /oauth2/default/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://client.example.org/callback&
scope=openid launch patient/Patient.read patient/Observation.read&
state=RANDOM_STATE&
aud=https://localhost:9300/apis/default/fhir&
launch=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Required Scopes:
launch - Indicates EHR launch flowopenid - OpenID ConnectOptional Context Scopes:
launch/patient - Receive patient contextfhirUser - Receive practitioner contextAfter code exchange, the token response includes launch context:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid launch patient/Patient.read",
"patient": "123",
"encounter": "456",
"fhirUser": "Practitioner/789"
}
Context Fields:
patient: Patient resource ID (if launch/patient scope approved). If a patient has not been selected in the EHR, the authorization flow will force a patient to be selected.encounter: Encounter resource ID (if encounter context available) if an encounter has been selected within the EHR.fhirUser: Practitioner/RelatedPerson resource referenceYour app can immediately access the context resources:
const tokenResponse = await exchangeCodeForToken(code);
// Extract context
const patientId = tokenResponse.patient;
const encounterId = tokenResponse.encounter;
// Make FHIR requests
const patient = await fetch(
`https://localhost:9300/apis/default/fhir/Patient/${patientId}`,
{ headers: { 'Authorization': `Bearer ${tokenResponse.access_token}` }}
);
if (encounterId) {
const encounter = await fetch(
`https://localhost:9300/apis/default/fhir/Encounter/${encounterId}`,
{ headers: { 'Authorization': `Bearer ${tokenResponse.access_token}` }}
);
}
See SMART on FHIR documentation for complete details.
** Intended for SMART clients that can manage and sign assertions with asymmetric keys, commonly used for backend services and bulk data operations.**
*Only option allowed for system/ level scopes ** (e.g., bulk exports, backend services).
private_key_jwt authenticationsystem/*.$export)Create a signed JWT assertion:
JWT Header:
{
"alg": "RS384",
"kid": "my-key-1",
"typ": "JWT"
}
JWT Claims:
{
"iss": "YOUR_CLIENT_ID",
"sub": "YOUR_CLIENT_ID",
"aud": "https://localhost:9300/oauth2/default/token",
"exp": 1609459200,
"jti": "unique-jwt-id-12345",
"iat": 1609455600
}
Required Claims:
iss: Your client_id (issuer)sub: Your client_id (subject)aud: Token endpoint URLexp: Expiration time (Unix timestamp, max 5 minutes from iat)jti: Unique JWT identifier (prevent replay attacks)iat: Issued at time (Unix timestamp)Sign with your private key (RS384 algorithm).
Token Request:
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
--data-urlencode 'client_assertion=eyJhbGciOiJSUzM4NCIsImtpZCI6Im15LWtleS0xIiwidHlwIjoiSldUIn0...' \
--data-urlencode 'scope=system/Patient.read system/Group.$export'
Response:
{
"token_type": "Bearer",
"expires_in": 60,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"scope": "system/Patient.read system/Patient.$export"
}
⚠️ Token Lifetime: Client credentials tokens are short-lived (60 seconds). Request new tokens as needed. There is no refresh token with this grant.
# 1. Get access token
TOKEN=$(curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
--data-urlencode 'client_assertion=YOUR_SIGNED_JWT' \
--data-urlencode 'scope=system/Patient.$export system/*.$bulkdata-status system/Binary.read' \
| jq -r '.access_token')
# 2. Initiate bulk export
curl -X GET -k \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/fhir+json" \
-H "Prefer: respond-async" \
https://localhost:9300/apis/default/fhir/Patient/\$export
See Bulk FHIR Exports for complete workflow.
⚠️ NOT RECOMMENDED for production. Less secure than authorization code flow.
Administration → Config → Connectors → Enable OAuth2 Password Grant (Not considered secure)
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'scope=openid offline_access api:oemr user/Patient.read' \
--data-urlencode 'user_role=users' \
--data-urlencode 'username=admin' \
--data-urlencode 'password=pass'
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'scope=openid api:port patient/Patient.read' \
--data-urlencode 'user_role=patient' \
--data-urlencode 'username=patient123' \
--data-urlencode 'password=patientpass' \
--data-urlencode '[email protected]'
Parameters:
grant_type: Must be passworduser_role: users or patientusername: OpenEMR usernamepassword: User's passwordemail: Required for patient roleCLI Testing Tip: The examples above use single-quoted
--data-urlencode 'password=...'arguments, which prevent bash from interpreting special characters like!,$, and\. If you modify these examples (e.g., switching to double quotes or using-dinstead of--data-urlencode), you may encounter authentication failures due to shell interpretation.Solutions for modified commands:
- Disable bash history expansion (
!):set +Hbefore running curl- Use single quotes around arguments containing special characters
- For passwords containing both
!and', use a heredoc:bashread -r password <<'EOF' my!complex'password EOF curl ... --data-urlencode "password=$password"This is a shell behavior issue, not an OpenEMR bug. Production apps using HTTP libraries will not experience this problem.
Obtain new access tokens without re-authentication.
offline_access scopecurl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'refresh_token=def5020017b484b0add020bf3491a8a537fa04eda12...'
For confidential clients, include client authentication:
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Authorization: Basic BASE64(client_id:client_secret)' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=def5020017b484b0add020bf3491a8a537fa04eda12...'
{
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"refresh_token": "def5020089a766d16..."
}
Note: A new refresh token is always issued as part of this flow. Store it and discard the old one.
POST /oauth2/{site}/introspect
curl -X POST -k \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Authorization: Basic BASE64(client_id:client_secret)' \
https://localhost:9300/oauth2/default/introspect \
--data-urlencode 'token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...'
Authentication: Requires client authentication (Basic Auth or client_assertion).
Parameters:
token: The access token or refresh token to introspecttoken_type_hint: Optional. access_token or refresh_token{
"active": true,
"scope": "openid patient/Patient.read patient/Observation.read",
"client_id": "LnjqojEEjFYe5j2Jp9m9UnmuxOnMg4VodEJj3yE8_OA",
"username": "admin",
"token_type": "Bearer",
"exp": 1640000000,
"iat": 1639996400,
"sub": "user-uuid-12345",
"aud": "https://localhost:9300/apis/default/fhir",
"iss": "https://localhost:9300/oauth2/default"
}
{
"active": false
}
Reasons for inactive tokens:
Validating incoming requests:
async function validateToken(token) {
const response = await fetch('https://localhost:9300/oauth2/default/introspect', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(clientId + ':' + clientSecret)}`
},
body: `token=${token}`
});
const result = await response.json();
if (!result.active) {
throw new Error('Token is not active');
}
return result;
}
Checking before API calls:
// Before making API request, verify token is still active
const tokenInfo = await introspectToken(accessToken);
if (!tokenInfo.active) {
// Token expired or revoked - refresh it
const newTokens = await refreshAccessToken(refreshToken);
accessToken = newTokens.access_token;
}
// Proceed with API call
const patients = await fetchPatients(accessToken);
See AUTHORIZATION.md - Revoking Access for details on:
| Token Type | Default Lifetime | Refresh? |
|---|---|---|
| Access Token (Authorization Code) | 1 hour | Yes |
| Access Token (Client Credentials) | 1 minute | No |
| Refresh Token (Authorization Code) | 3 months | No, but new refresh token issued on use |
| Refresh Token (Native App) | 3 months | No, but new refresh token issued on use |
| ID Token | 1 hour | N/A |
Revoke a user's session and tokens.
GET /oauth2/{site}/logout?id_token_hint={id_token}
id_token_hint (required): The ID token from the original authorizationpost_logout_redirect_uri (optional): Where to redirect after logoutstate (optional): Opaque value for client# Simple logout
curl -X GET \
'https://localhost:9300/oauth2/default/logout?id_token_hint=eyJ0eXAiOiJKV1QiLCJhbGci...'
# Logout with redirect
curl -X GET \
'https://localhost:9300/oauth2/default/logout?id_token_hint=eyJ0eXAiOiJKV1QiLCJhbGci...&post_logout_redirect_uri=https://client.example.org/goodbye&state=xyz123'
Note: post_logout_redirect_uri must be registered during client registration.
function logout(idToken, redirectUri) {
const params = new URLSearchParams({
id_token_hint: idToken,
post_logout_redirect_uri: redirectUri,
state: generateRandomState()
});
window.location.href =
`https://localhost:9300/oauth2/default/logout?${params}`;
}
OpenEMR provides OIDC discovery at:
GET /oauth2/{site}/.well-known/openid-configuration
Example:
curl https://localhost:9300/oauth2/default/.well-known/openid-configuration
Response:
{
"issuer": "https://localhost:9300/oauth2/default",
"authorization_endpoint": "https://localhost:9300/oauth2/default/authorize",
"token_endpoint": "https://localhost:9300/oauth2/default/token",
"jwks_uri": "https://localhost:9300/oauth2/default/jwk",
"userinfo_endpoint": "https://localhost:9300/oauth2/default/userinfo",
"registration_endpoint": "https://localhost:9300/oauth2/default/registration",
"end_session_endpoint": "https://localhost:9300/oauth2/default/logout",
"introspection_endpoint": "https://localhost:9300/oauth2/default/introspect",
"scopes_supported": [
"openid",
"fhirUser",
"online_access",
"offline_access",
"launch",
"launch/patient",
"api:oemr",
"api:fhir",
"api:port"
],
"response_types_supported": ["code"],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials",
"password"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic",
"private_key_jwt"
],
"code_challenge_methods_supported": ["S256"]
}
Retrieve information about the authenticated user:
curl -X GET \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
https://localhost:9300/oauth2/default/userinfo
Response:
{
"sub": "user-uuid-12345",
"name": "Dr. John Smith",
"email": "[email protected]",
"fhirUser": "Practitioner/789"
}
Use HTTPS/TLS for all API communication
Implement PKCE for public apps
Validate state parameter
Secure token storage
Rotate refresh tokens
Limit token lifetime
Use asymmetric authentication when possible
❌ Don't use password grant in production
❌ Don't expose client secrets in public apps
❌ Don't transmit tokens in URLs (query parameters)
❌ Don't skip TLS certificate validation
❌ Don't hardcode credentials in source code
❌ Don't use wildcard redirect URIs
Required for public apps, recommended for all apps.
// 1. Generate code verifier (random string)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
// 2. Generate code challenge from verifier
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(hash));
}
// 3. Base64 URL encoding
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Usage
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store codeVerifier securely
sessionStorage.setItem('code_verifier', codeVerifier);
GET /oauth2/default/authorize?
...
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
curl -X POST ... \
--data-urlencode 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
# Generate private key (4096-bit RSA)
openssl genrsa -out private_key.pem 4096
# Extract public key
openssl rsa -in private_key.pem -pubout -out public_key.pem
# Convert to JWK format (use online tool or library)
{
"keys": [
{
"kty": "RSA",
"kid": "my-key-2024",
"use": "sig",
"alg": "RS384",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx...",
"e": "AQAB"
}
]
}
GET https://client.example.org/.well-known/jwks.json
Register with jwks_uri:
{
"jwks_uri": "https://client.example.org/.well-known/jwks.json"
}
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('private_key.pem');
const payload = {
iss: 'YOUR_CLIENT_ID',
sub: 'YOUR_CLIENT_ID',
aud: 'https://localhost:9300/oauth2/default/token',
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
jti: crypto.randomUUID(),
iat: Math.floor(Date.now() / 1000)
};
const token = jwt.sign(payload, privateKey, {
algorithm: 'RS384',
keyid: 'my-key-2024'
});
curl -X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
https://localhost:9300/oauth2/default/token \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=AUTHORIZATION_CODE' \
--data-urlencode 'redirect_uri=https://client.example.org/callback' \
--data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
--data-urlencode "client_assertion=$token"
Next Steps:
This documentation represents the collective knowledge and contributions of the OpenEMR open-source community. The content is based on:
The organization, structure, and presentation of this documentation was enhanced using Claude AI (Anthropic) to:
All technical accuracy is maintained from the original community-authored documentation.
OpenEMR is an open-source project. To contribute to this documentation:
Last Updated: November 2025 License: GPL v3
For complete documentation, see Documentation/api/