Authentication
OAuth 2.0 authentication flow for Vim Connect applications
Vim Connect uses the OAuth 2.0 Authorization Code flow to authenticate applications. Your app is not opened by a click — once the provider is logged into Vim and on the EHR, Vim launches your app automatically: the Worker app (if you build one) starts immediately, and the UI app's iframe loads when the provider opens the sidepanel. From there the app runs the OAuth flow and initializes the SDK with an access token.
Flow Overview
1. Provider is logged into Vim and on the EHR — Vim launches your app automatically
(Worker app runs immediately; UI app iframe loads when the sidepanel is open)
│
▼
2. Vim opens your Launch URL
GET /launch?launch_id=abc123
│
▼
3. Your app redirects to Vim authorization
GET /app-auth/authorize?
response_type=code&
client_id=YOUR_ID&
launch=abc123&
scope=launch openid&
redirect_uri=https://your-app.com/app&
state=abc123:csrf_token
│
▼
4. Vim redirects back with authorization code
GET /app?code=AUTH_CODE&state=abc123:csrf_token
│
▼
5. Your server exchanges code for token ◄── with a token endpoint, this exchange runs through
POST /app-auth/token your hosted endpoint — you don't do it in your app (see Option 2)
{ grant_type, code, client_id, client_secret }
│
▼
6. Initialize SDK with access token ◄── with a token endpoint, call initVimSDK() with no token
initVimSDK({ accessToken: token })Ways to Give the SDK a Token
The SDK needs an access token to initialize. You choose one of three ways to provide it — the choice is the same for UI apps and Worker apps. Only Option 2 requires a Token Endpoint in your app configuration; it is optional for the other two.
Option 1 — You exchange the code yourself
Run the full flow above: after step 4, your server exchanges the authorization code for a token (step 5), then you pass that token to the SDK (step 6):
const sdk = await initVimSDK({ accessToken: token });This is the flow the steps below walk through in detail.
Option 2 — Configure a token endpoint and let the SDK fetch the token
Register a token endpoint in your app configuration — a backend endpoint you host that exchanges an authorization code for a token. You still initiate the OAuth /app-auth/authorize step yourself (step 3) exactly as in Option 1; the token endpoint only replaces the token-exchange step (step 5), so you don't wire that up in your app.
With a token endpoint configured, you don't exchange the code and you don't pass a token to init — call initVimSDK() (or initWorkerVimSDK()) with no accessToken. The SDK obtains the token through your endpoint and initializes:
// token endpoint configured — no accessToken passed
const sdk = await initVimSDK();Your token endpoint's contract
The endpoint is your own backend. It must:
- Accept a
POSTwith a JSON body{ code }(the authorization code). - Perform the server-side exchange with Vim's
/app-auth/tokenusingcode,client_id, andclient_secret. The secret stays on your server — this is the whole reason the exchange is not done in the browser. - Return JSON
{ access_token, id_token? }. The SDK requiresaccess_token(a missing or non-string value failsinitwithTOKEN_FETCH_ERROR);id_tokenis optional.
The exchange itself is the same call shown in Step 2 below — the token endpoint is that same server-side exchange, hosted at a URL you register so it runs automatically instead of you wiring it into your app.
If your endpoint returns an id_token, the SDK captures it and exposes it through the session context — use it to identify the Vim-logged-in user to your own backend:
const { idToken } = await sdk.sessionContext.getIdToken();If no id_token was captured, getIdToken() rejects with ID_TOKEN_UNAVAILABLE.
Option 3 — Your identity provider does the exchange
If you already run an identity provider — Auth0, Okta, Cognito, your own — you can point it at Vim's token endpoint instead of registering one with us. This is the right choice when your IdP already owns your users and you want a single place that handles sign-in.
Leave the Token Endpoint field blank in your app configuration. It is optional: the SDK resolves
an accessToken you pass to init() directly and never looks for a token endpoint. You only need
one if you want the SDK to perform the exchange for you (Option 2).
Worked example: Auth0 as the identity provider
Auth0 performs the OAuth exchange with Vim, stores the resulting Vim access token on the user's profile, and your backend hands it to your frontend.
1. Create a Vim social connection. In the Auth0 dashboard under Connections → Social, add a custom OAuth2 connection with the Client ID and Client Secret from your Vim app credentials, and these endpoints:
| Field | Value |
|---|---|
| Authorize URL | https://api.getvim.ai/app-auth/authorize |
| Token URL | https://api.getvim.ai/app-auth/token |
These are the production endpoints, which is what your Console credentials are issued against.
Only substitute https://api.stage.getvim.ai/... if Vim has explicitly given you staging access.
2. Forward launch_id. Vim puts launch_id on your launch URL, and the authorize call must
carry it. Map it onto login_hint for the connection, then pass it at login:
const launchId = new URL(window.location.href).searchParams.get('launch_id');
auth0.loginWithRedirect({
authorizationParams: {
login_hint: launchId,
connection: 'Vim',
audience: '<your-api-audience>',
redirect_uri: '<your-app-url>',
},
});3. Allow the callback. Add your app's callback URL to Auth0's Allowed Callback URLs, and add your Auth0 tenant domain to Allowed iframe URLs in your Vim app configuration — the sign-in redirect happens inside the Vim panel.
4. Read the Vim token back. After authentication the Vim access token sits on the user's Auth0 identity. Fetch it from your backend — never the browser, as this call needs a Management API token:
GET https://<your-tenant>/api/v2/users/<user-id>
Authorization: Bearer <MANAGEMENT_ACCESS_TOKEN>{
"identities": [
{ "provider": "oauth2", "connection": "Vim", "access_token": "<the Vim access token>" }
]
}5. Initialize the SDK with it.
const accessToken = await getVimTokenFromYourBackend();
const sdk = await initVimSDK({ accessToken });Identifying the user on this path
Your IdP knows who the user is in your system. For who they are in Vim — which account, which EHR, which session — use the SDK, not a Vim identity endpoint:
const sdk = await initVimSDK({ accessToken });
if (sdk.sessionContext) {
const { userId, account, ehrType, sessionId, deviceId } = sdk.sessionContext;
// account.id / account.name — the practice this launch belongs to
}sessionContext is seeded at handshake and needs no token of its own, so it is available whichever
option you chose. See Session Context for
the full field list.
Those fields are not proof — send the ID token for that
sessionContext values are plain JavaScript seeded into your frame, so anything running there can
change them. Never use userId or account.id as a tenant key on your own backend. For a claim a
server can trust, send the ID token and verify its signature, issuer, audience and expiry
server-side:
const { idToken } = await sdk.sessionContext.getIdToken();The ID token needs the SDK to be holding one. If your IdP performed the exchange it consumed
Vim's token response, so there is none and getIdToken() rejects with ID_TOKEN_UNAVAILABLE. Two
ways to have one on this path: have your backend run the Vim token exchange and pass it through —
initVimSDK({ accessToken, idToken }) — or keep using your own IdP's token as the identity your
backend verifies, and treat Vim's sessionContext as context rather than identity.
Let your app load inside Vim (CSP)
Your app runs inside the Vim extension. The one thing your Content-Security-Policy must allow is
loading the Vim SDK: add https://*.getvim.ai to script-src. The SDK runtime is served from
Vim's CDN and injected as a script element — without this it never loads.
Only if you restrict framing — with a CSP frame-ancestors directive or an X-Frame-Options
header — you must also let the Vim extension embed your app (it mounts your app in an iframe from a
chrome-extension:// page): add chrome-extension://hkgoafgiinlkilinanffdoehogbhckeo to
frame-ancestors. X-Frame-Options can't name an extension origin (its ALLOW-FROM value is
unsupported in modern browsers), so use frame-ancestors for this rather than X-Frame-Options.
Most apps set no framing headers and need nothing here. (Self-hosted or enterprise distributions
use a different extension ID — ask your Vim representative.)
You don't need connect-src for Vim — the SDK talks to the extension in-page, not over the network.
Step 1: Handle the Launch
When a provider clicks your app in the Vim Hub, the extension navigates to your launch URL with a launch_id query parameter. This is the entry point for every session.
// src/app/launch/page.tsx
'use client';
import { useEffect, useRef } from 'react';
import { useSearchParams } from 'next/navigation';
function LaunchContent() {
const searchParams = useSearchParams();
const redirectingRef = useRef(false);
useEffect(() => {
// Prevent double-redirect in React StrictMode
if (redirectingRef.current) return;
const launchId = searchParams.get('launch_id');
if (!launchId) return;
redirectingRef.current = true;
// Generate CSRF token and store keyed by launch_id
// (keying by launch_id isolates multiple tabs)
const csrfToken = crypto.randomUUID();
sessionStorage.setItem(`oauth_state_${launchId}`, csrfToken);
// Build authorization URL
const backendUrl = getVimBackendUrl(); // see Configuration section below
const authorizeUrl = new URL('/app-auth/authorize', backendUrl);
authorizeUrl.searchParams.set('response_type', 'code');
authorizeUrl.searchParams.set('client_id', process.env.NEXT_PUBLIC_CLIENT_ID!);
authorizeUrl.searchParams.set('launch', launchId);
authorizeUrl.searchParams.set('scope', 'launch openid');
authorizeUrl.searchParams.set('redirect_uri', `${window.location.origin}/app`);
authorizeUrl.searchParams.set('state', `${launchId}:${csrfToken}`);
window.location.href = authorizeUrl.toString();
}, [searchParams]);
return <div>Redirecting to authorization...</div>;
}Key Points
launch_id— Identifies this specific app launch session. Always pass it as thelaunchparameter in the authorization URL.- CSRF protection — Generate a random token and store it in
sessionStoragekeyed bylaunch_id. This prevents cross-site request forgery and isolates multiple tabs. stateparameter — Format:{launchId}:{csrfToken}. Vim passes it back unchanged in the redirect.- Redirect prevention — Use a
refto prevent React StrictMode from triggering a double redirect in development.
Step 2: Exchange the Authorization Code
After the provider authorizes, Vim redirects back to your redirect_uri with a code and state parameter. Exchange the code for an access token server-side to keep your client secret secure.
// src/app/api/auth/token/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { code } = await request.json();
if (!code) {
return NextResponse.json({ error: 'Missing authorization code' }, { status: 400 });
}
const backendUrl = getVimBackendUrl();
const response = await fetch(`${backendUrl}/app-auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id: process.env.NEXT_PUBLIC_CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return NextResponse.json(
{ error: errorData.error || 'token_exchange_failed' },
{ status: response.status },
);
}
const tokenData = await response.json();
return NextResponse.json({
access_token: tokenData.access_token,
// Forward id_token: the app passes it to initVimSDK() so getIdToken()
// resolves, which is how it identifies the provider to your backend.
// See Token Lifecycle — omit it only if your app has no backend session.
id_token: tokenData.id_token,
token_type: tokenData.token_type || 'Bearer',
expires_in: tokenData.expires_in,
scope: tokenData.scope,
});
} catch (error) {
console.error('Token exchange error:', error);
return NextResponse.json({ error: 'internal_server_error' }, { status: 500 });
}
}Security Considerations
CLIENT_SECRETmust never be exposed to the browser. Only use it in server-side code (API routes, server components).- Authorization codes are single-use. If the exchange fails, the provider must re-launch the app.
- Always validate the
stateparameter before exchanging the code to prevent CSRF attacks.
Step 3: Validate and Initialize
On your main app page, validate the OAuth callback, exchange the code, and initialize the SDK:
// src/app/app/page.tsx
async function initialize() {
// Validate OAuth parameters
const code = searchParams.get('code');
const stateParam = searchParams.get('state');
if (!code || !stateParam) throw new Error('Missing OAuth parameters');
// Verify CSRF token
const [launchId, csrfToken] = stateParam.split(':');
const storedToken = sessionStorage.getItem(`oauth_state_${launchId}`);
if (csrfToken !== storedToken) throw new Error('CSRF validation failed');
sessionStorage.removeItem(`oauth_state_${launchId}`); // clean up
// Exchange code for token (server-side)
const tokenRes = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (!tokenRes.ok) throw new Error('Token exchange failed');
const { access_token, id_token } = await tokenRes.json();
// Initialize SDK with the token
const sdk = await initVimSDK({
accessToken: access_token,
idToken: id_token, // so getIdToken() resolves — see Token Lifecycle
debug: true,
});
}Configuration
Create a helper to determine the correct Vim backend URL based on your environment:
// src/lib/sdk-config.ts
export function getVimBackendUrl(): string {
if (process.env.NEXT_PUBLIC_VIM_BACKEND_URL) {
return process.env.NEXT_PUBLIC_VIM_BACKEND_URL;
}
switch (process.env.NEXT_PUBLIC_ENV) {
case 'staging':
return 'https://api.stage.getvim.ai';
case 'production':
return 'https://api.getvim.ai';
default:
throw new Error('NEXT_PUBLIC_ENV must be "staging" or "production"');
}
}The Vim API is US-only
The Vim API is only available to USA server-based instances — IP allow-listing is not a substitute. Your application server must be hosted within the United States to reach Vim's EHR connectivity endpoints. If you develop from outside the US, use a VPN to connect during development, but your production app server must still be US-hosted.
Environment Variables
# .env (checked into source control)
NEXT_PUBLIC_ENV=staging
NEXT_PUBLIC_CLIENT_ID=your_client_id
# .env.local (NOT checked in — contains secrets)
CLIENT_SECRET=your_client_secret| Variable | Side | Description |
|---|---|---|
NEXT_PUBLIC_ENV | Client + Server | staging or production |
NEXT_PUBLIC_CLIENT_ID | Client + Server | OAuth client ID |
CLIENT_SECRET | Server only | OAuth client secret — never prefix with NEXT_PUBLIC_ |
NEXT_PUBLIC_VIM_BACKEND_URL | Client | Optional override for the Vim backend URL |
Token Lifecycle
- Access tokens have a limited lifetime (returned in
expires_in) - When a token expires, the provider will need to re-launch the app (the extension handles this transparently)
- There is no refresh token flow — each app launch generates a new session
Vim does not manage your session — you do
Keeping the provider signed in is your app's job
The token Vim issues establishes the initial session only — it cannot be extended. Your app needs its own session from the moment the SDK is ready, or the provider is signed out the moment Vim's token lapses.
The recommended pattern is to exchange the Vim ID token for your own application's tokens as soon as the SDK is ready, then run your own session lifecycle independently of Vim's.
getIdToken() resolves only when the SDK is holding an id_token, and there are two ways it comes
to hold one:
- Option 1 (you exchange the code). Vim's
/app-auth/tokenreturnsid_tokenalongsideaccess_token; forward it through your own endpoint (as Step 2 does) and hand both to the SDK:initVimSDK({ accessToken, idToken }). - Option 2 (token endpoint). The SDK calls your endpoint and captures
id_tokenfrom the response automatically. Nothing else to do.
Pass accessToken on its own and there is no id_token to return, so getIdToken() rejects with
ID_TOKEN_UNAVAILABLE.
import { initVimSDK } from '@vimconnect/app-sdk';
// Option 2: no accessToken — the SDK calls your token endpoint and captures the id_token
const sdk = await initVimSDK();
// sessionContext is null until the extension hands off the session seed
if (sdk.sessionContext) {
try {
const { idToken } = await sdk.sessionContext.getIdToken();
const res = await fetch('/api/auth/exchange', {
method: 'POST',
headers: { Authorization: `Bearer ${idToken}` },
});
if (!res.ok) throw new Error(`Exchange failed: ${res.status}`);
// Your backend verified the ID token and issued its own session.
// Prefer a Set-Cookie httpOnly refresh cookie over returning a refresh token
// to page JavaScript — this page runs third-party script from a CDN.
const { accessToken: appToken } = await res.json();
startSession(appToken);
} catch (err) {
// ID_TOKEN_UNAVAILABLE, or your exchange endpoint failed
showSignInError(err);
}
}Verifying the ID token
Verify server-side before you trust it. The id_token returned by /app-auth/token is signed by
the Vim backend, not by the Auth0 tenant that authenticates the Vim user — they are different
issuers with different keys:
| Claim | Value |
|---|---|
Issuer (iss) | https://connect.getvim.ai |
| JWKS | https://api.getvim.ai/app-auth/.well-known/jwks.json (staging: https://api.stage.getvim.ai/...) |
| Algorithm | RS256 — pin it; never accept the alg the token header asks for |
Check the signature against that JWKS, that iss matches, that aud matches your client ID, and
that exp is in the future. Treat the ID token as a launch-time credential, not a session one: exchange it once, then
authenticate later requests with your own session. Nothing stops a replay within the token's
lifetime, so if that matters to you, record the jti you have already accepted.
Why the split
- Separation of concerns — Vim authenticates the provider for the EHR session; your app owns its own session.
- Flexibility — you choose token lifetimes, storage, and refresh policy.
- Independence — your session survives Vim token expiry and app re-launches.
What not to do
- Don't try to refresh a Vim access token.
- Don't persist Vim access tokens for later use; they are short-lived and scoped to a single launch.
- Don't build a polling or refresh loop against Vim's token endpoint.
- Don't put your own refresh token in
localStorage— this page loads third-party script, so one XSS turns a short-lived launch into long-lived account access.