Skip to Content

Rimo SSO API

Single sign-on token generation for seamless telehealth integration between Loop Health and Rimo Health. Enables secure, authenticated access to Rimo’s telehealth platform without requiring separate login credentials.

Overview

The Rimo SSO integration provides seamless access to telehealth consultations and prescription management through Rimo Health’s platform. When a Loop Health member needs telehealth services, an SSO token is generated that carries their identity and health context.

Integration Architecture

SSO Flow

1. User clicks "Book Consultation" on Loop Health 2. Frontend calls POST /api/telehealth/sso 3. Server calls createRimoSSO() function 4. JWT generated with user context and health data 5. User redirected to Rimo platform with SSO token 6. Rimo validates JWT and creates authenticated session 7. User accesses telehealth services seamlessly 8. Rimo sends webhooks back to Patient Graph

Generate SSO Token

Endpoint

curl -X POST "https://my.loop.health/api/telehealth/sso" \ -H "Authorization: Bearer $CLERK_JWT" \ -H "Content-Type: application/json" \ -d '{ "service": "rimo", "returnUrl": "https://my.loop.health/telehealth/callback" }'

Authentication: Clerk session required.

Rate Limit: 5 requests per minute per user.

Request Body

FieldTypeRequiredDescription
servicestringYesMust be "rimo"
returnUrlstringNoURL to redirect after Rimo session
contextobjectNoAdditional context for the session

Response

{ "success": true, "data": { "ssoToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "rimoUrl": "https://rimo.health/sso?token=eyJ...", "expiresAt": "2024-06-15T12:05:00Z", "sessionId": "sess_abc123" } }

createRimoSSO Function

The core SSO token generation is handled by the createRimoSSO function:

import { createRimoSSO } from '@loop/rimo'; const generateSSOToken = async (userId: string, context?: RimoContext) => { try { const result = await createRimoSSO({ userId, expiresIn: '5m', // 5 minutes context: { source: 'loop-health', returnUrl: context?.returnUrl, patientId: userId, ...context, }, }); if (result.success) { return { ssoToken: result.data.token, rimoUrl: result.data.url, expiresAt: result.data.expiresAt, sessionId: result.data.sessionId, }; } else { throw new Error(result.error.message); } } catch (error) { console.error('Failed to create Rimo SSO token:', error); throw error; } };

JWT Payload

{ "sub": "user_clerk_123", "iss": "loop-health", "aud": "rimo-health", "exp": 1718451900, "iat": 1718451600, "jti": "sess_abc123", "context": { "patientId": "prof_abc123", "source": "loop-health", "returnUrl": "https://my.loop.health/telehealth/callback", "healthData": { "hasActiveProtocols": true, "lastLabDate": "2024-06-01", "primaryGoals": ["hormone-optimization", "recovery"] } } }

Health Context Integration

The SSO token includes relevant health context to enhance the Rimo consultation:

interface RimoContext { patientId: string; source: 'loop-health'; returnUrl?: string; healthData?: { hasActiveProtocols: boolean; lastLabDate?: string; primaryGoals: string[]; currentMedications?: string[]; allergies?: string[]; medicalHistory?: string[]; }; } const buildHealthContext = async (userId: string): Promise<RimoContext['healthData']> => { const [protocols, labs, profile] = await Promise.all([ getActiveProtocols(userId), getRecentLabs(userId, 90), // Last 90 days getPatientProfile(userId), ]); return { hasActiveProtocols: protocols.length > 0, lastLabDate: labs[0]?.labDate, primaryGoals: profile.goals || [], currentMedications: protocols.map(p => p.compound), allergies: profile.allergies || [], medicalHistory: profile.medicalHistory || [], }; };

Security

FeatureDetails
AlgorithmHS256
TTL5 minutes
Single-useSession ID-based replay prevention
Rate limit5 per minute per user
SecretRIMO_SSO_SECRET shared between platforms
EncryptionPatient health data encrypted in transit

Token Validation (Rimo Health side)

  1. Verify JWT signature using shared RIMO_SSO_SECRET
  2. Check iss is "loop-health" and aud is "rimo-health"
  3. Verify exp has not passed (5-minute window)
  4. Check session ID has not been used before
  5. Extract patient context and health data
  6. Create authenticated Rimo session
  7. Pre-populate consultation forms with health context

Security Considerations

  • Short TTL: 5-minute expiration prevents token replay attacks
  • Single-use tokens: Session IDs tracked to prevent reuse
  • Health data encryption: Sensitive medical information encrypted
  • Audit logging: All SSO token generation and usage logged
  • Rate limiting: Prevents abuse and ensures system stability
const validateRimoToken = async (token: string): Promise<RimoSession> => { try { // Verify JWT signature and claims const payload = jwt.verify(token, process.env.RIMO_SSO_SECRET!) as RimoJWTPayload; // Check token hasn't been used const sessionExists = await checkSessionExists(payload.jti); if (sessionExists) { throw new Error('Token already used'); } // Create Rimo session const session = await createRimoSession({ patientId: payload.context.patientId, source: payload.context.source, healthContext: payload.context.healthData, }); // Mark token as used await markTokenUsed(payload.jti); return session; } catch (error) { console.error('Token validation failed:', error); throw new Error('Invalid SSO token'); } };

Environment Variables

# Rimo SSO Integration RIMO_SSO_SECRET=your-shared-secret-with-rimo-health RIMO_BASE_URL=https://rimo.health RIMO_WEBHOOK_SECRET=your-rimo-webhook-secret # Optional: Custom Rimo configuration RIMO_SESSION_TIMEOUT=3600 # 1 hour in seconds RIMO_RETURN_URL_WHITELIST=https://my.loop.health,https://app.loop.health

Both Loop Health and Rimo Health platforms must have the same RIMO_SSO_SECRET for token validation.

Error Handling

Common Error Responses

Invalid Token:

{ "success": false, "error": { "code": "INVALID_TOKEN", "message": "SSO token is invalid or expired" } }

Rate Limit Exceeded:

{ "success": false, "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Too many SSO requests. Please try again later." } }

Missing Patient Data:

{ "success": false, "error": { "code": "PATIENT_NOT_FOUND", "message": "Patient profile not found or incomplete" } }

Error Handling in Frontend

const handleRimoSSO = async () => { try { setLoading(true); const response = await fetch('/api/telehealth/sso', { method: 'POST', headers: { 'Authorization': `Bearer ${clerkToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ service: 'rimo', returnUrl: window.location.origin + '/telehealth/callback', }), }); const result = await response.json(); if (result.success) { // Redirect to Rimo platform window.location.href = result.data.rimoUrl; } else { throw new Error(result.error.message); } } catch (error) { console.error('SSO failed:', error); // Show user-friendly error message if (error.message.includes('RATE_LIMIT')) { showToast('Too many requests. Please wait a moment and try again.'); } else if (error.message.includes('PATIENT_NOT_FOUND')) { showToast('Please complete your profile before accessing telehealth services.'); } else { showToast('Unable to connect to telehealth platform. Please try again.'); } } finally { setLoading(false); } };