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 GraphGenerate 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
| Field | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Must be "rimo" |
returnUrl | string | No | URL to redirect after Rimo session |
context | object | No | Additional 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
| Feature | Details |
|---|---|
| Algorithm | HS256 |
| TTL | 5 minutes |
| Single-use | Session ID-based replay prevention |
| Rate limit | 5 per minute per user |
| Secret | RIMO_SSO_SECRET shared between platforms |
| Encryption | Patient health data encrypted in transit |
Token Validation (Rimo Health side)
- Verify JWT signature using shared
RIMO_SSO_SECRET - Check
issis"loop-health"andaudis"rimo-health" - Verify
exphas not passed (5-minute window) - Check session ID has not been used before
- Extract patient context and health data
- Create authenticated Rimo session
- 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.healthBoth 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);
}
};