Authentication

Open in Claude

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:

  1. Accept a POST with a JSON body { code } (the authorization code).
  2. Perform the server-side exchange with Vim's /app-auth/token using code, client_id, and client_secret. The secret stays on your server — this is the whole reason the exchange is not done in the browser.
  3. Return JSON { access_token, id_token? }. The SDK requires access_token (a missing or non-string value fails init with TOKEN_FETCH_ERROR); id_token is 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:

FieldValue
Authorize URLhttps://api.getvim.ai/app-auth/authorize
Token URLhttps://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 the launch parameter in the authorization URL.
  • CSRF protection — Generate a random token and store it in sessionStorage keyed by launch_id. This prevents cross-site request forgery and isolates multiple tabs.
  • state parameter — Format: {launchId}:{csrfToken}. Vim passes it back unchanged in the redirect.
  • Redirect prevention — Use a ref to 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_SECRET must 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 state parameter 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
VariableSideDescription
NEXT_PUBLIC_ENVClient + Serverstaging or production
NEXT_PUBLIC_CLIENT_IDClient + ServerOAuth client ID
CLIENT_SECRETServer onlyOAuth client secret — never prefix with NEXT_PUBLIC_
NEXT_PUBLIC_VIM_BACKEND_URLClientOptional 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/token returns id_token alongside access_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_token from 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:

ClaimValue
Issuer (iss)https://connect.getvim.ai
JWKShttps://api.getvim.ai/app-auth/.well-known/jwks.json (staging: https://api.stage.getvim.ai/...)
AlgorithmRS256 — 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.

On this page