Authentication Guide
TranscriMed uses OAuth2 authorization code flow for secure partner authentication. This guide covers everything you need to implement authentication in your application.
Overview
The OAuth2 flow consists of four main steps:
- Authorization Request - Redirect users to TranscriMed
- User Consent - Users grant permissions to your app
- Authorization Code - Receive authorization code via callback
- Token Exchange - Exchange code for access tokens
Before You Start
Register Your Application
- Visit TranscriMed Developer Portal
- Create a developer account
- Register your application with:
- Application Name: Display name for users
- Redirect URIs: Where users return after authorization
- Scopes: Permissions your app needs
- Application Type: Web, mobile, or server-to-server
Get Your Credentials
After registration, you'll receive:
- Client ID: Public identifier for your application
- Client Secret: Private key (keep secure!)
- Redirect URIs: Registered callback URLs
Never expose your client secret in client-side code or commit it to version control. Store it securely in environment variables or secure configuration.
Step 1: Authorization Request
Redirect users to the authorization endpoint to start the OAuth2 flow.
Authorization URL
GET https://api.transcrimed.com.br/api/oauth/authorize
Required Parameters
| Parameter | Type | Description |
|---|---|---|
response_type | string | Must be code |
client_id | string | Your application's client ID |
redirect_uri | string | Must match registered URI |
scope | string | Space-separated list of scopes |
state | string | Random value for CSRF protection |
Available Scopes
Only request scopes you need. Available scopes:
| Scope | Description |
|---|---|
medical_records:read | Read medical records |
medical_records:write | Create and update medical records |
medical_records:delete | Delete medical records |
jobs:read | Read job status and results |
jobs:write | Create, cancel and manage jobs |
worklists:write | Send worklist items from RIS/PACS systems |
worklists:manage | Update worklist item status |
worklists:read | Read worklist items (internal use) |
Example Implementation
JavaScript (Frontend)
function initiateOAuth2Flow() {
// Generate random state for CSRF protection
const state = generateRandomString(32);
// Store state in session/localStorage for verification
sessionStorage.setItem('oauth_state', state);
// Build authorization URL
const authUrl = new URL('https://api.transcrimed.com.br/api/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'your_client_id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/oauth/callback');
authUrl.searchParams.set('scope', 'medical_records:read medical_records:write jobs:read');
authUrl.searchParams.set('state', state);
// Open in popup for desktop apps, or redirect for web apps
if (isDesktopApplication()) {
openAuthPopup(authUrl.toString());
} else {
window.location.href = authUrl.toString();
}
}
function openAuthPopup(authUrl) {
const popup = window.open(
authUrl,
'oauth_popup',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
// Listen for authorization response
window.addEventListener('message', function(event) {
if (event.data.type === 'oauth2_callback') {
const { code, state } = event.data;
// Handle authorization code
handleAuthorizationCode(code, state);
popup.close();
}
});
}
function generateRandomString(length) {
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let text = '';
for (let i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
Python (Backend)
import secrets
import urllib.parse
from flask import Flask, redirect, session
app = Flask(__name__)
@app.route('/auth/login')
def login():
# Generate state for CSRF protection
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
# Build authorization URL
auth_url = 'https://api.transcrimed.com.br/api/oauth/authorize'
params = {
'response_type': 'code',
'client_id': 'your_client_id',
'redirect_uri': 'https://yourapp.com/oauth/callback',
'scope': 'medical_records:read medical_records:write jobs:read',
'state': state
}
full_url = f"{auth_url}?{urllib.parse.urlencode(params)}"
return redirect(full_url)
Node.js (Backend)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.get('/auth/login', (req, res) => {
// Generate state for CSRF protection
const state = crypto.randomBytes(32).toString('hex');
req.session.oauth_state = state;
// Build authorization URL
const authUrl = new URL('https://api.transcrimed.com.br/api/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', process.env.CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/oauth/callback');
authUrl.searchParams.set('scope', 'medical_records:read medical_records:write jobs:read');
authUrl.searchParams.set('state', state);
res.redirect(authUrl.toString());
});
Step 2: Handle Callback
After user consent, TranscriMed redirects to your registered URI with an authorization code.
Success Callback
https://yourapp.com/oauth/callback?code=AUTH_CODE&state=CSRF_STATE
Error Callback
https://yourapp.com/oauth/callback?error=access_denied&error_description=User+denied+access&state=CSRF_STATE
Example Callback Handler
JavaScript
function handleOAuthCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
// Check for errors
if (error) {
console.error('OAuth error:', urlParams.get('error_description'));
return;
}
// Verify state to prevent CSRF attacks
const storedState = sessionStorage.getItem('oauth_state');
if (state !== storedState) {
console.error('Invalid state parameter');
return;
}
// Exchange code for tokens
exchangeCodeForTokens(code);
}
Python
@app.route('/oauth/callback')
def oauth_callback():
code = request.args.get('code')
state = request.args.get('state')
error = request.args.get('error')
# Check for errors
if error:
return f"OAuth error: {request.args.get('error_description')}"
# Verify state
if state != session.get('oauth_state'):
return "Invalid state parameter"
# Exchange code for tokens
tokens = exchange_code_for_tokens(code)
# Store tokens securely
session['access_token'] = tokens['access_token']
session['refresh_token'] = tokens['refresh_token']
return redirect('/dashboard')
Step 3: Token Exchange
Exchange the authorization code for access and refresh tokens.
Token Endpoint
POST https://api.transcrimed.com.br/api/oauth/token
Request Parameters
| Parameter | Type | Description |
|---|---|---|
grant_type | string | Must be authorization_code |
code | string | Authorization code from callback |
redirect_uri | string | Same URI used in authorization |
client_id | string | Your application's client ID |
client_secret | string | Your application's client secret |
Example Implementation
JavaScript
async function exchangeCodeForTokens(authCode) {
try {
const response = await fetch('https://api.transcrimed.com.br/api/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
redirect_uri: 'https://yourapp.com/oauth/callback',
client_id: 'your_client_id',
client_secret: 'your_client_secret'
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
const tokens = await response.json();
// Store tokens securely
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
localStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
return tokens;
} catch (error) {
console.error('Token exchange error:', error);
throw error;
}
}
Python
import requests
def exchange_code_for_tokens(auth_code):
token_url = 'https://api.transcrimed.com.br/api/oauth/token'
data = {
'grant_type': 'authorization_code',
'code': auth_code,
'redirect_uri': 'https://yourapp.com/oauth/callback',
'client_id': 'your_client_id',
'client_secret': 'your_client_secret'
}
response = requests.post(token_url, data=data)
if response.status_code != 200:
raise Exception(f'Token exchange failed: {response.text}')
return response.json()
Token Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "refresh_token_string_here",
"scope": "medical_records:read medical_records:write jobs:read"
}
Step 4: Using Access Tokens
Include the access token in the Authorization header for all API requests:
Authorization: Bearer your_access_token_here
Example API Call
async function listMedicalRecords(accessToken) {
const response = await fetch('https://api.transcrimed.com.br/api/v1/medical-records', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`API call failed: ${response.status}`);
}
return response.json();
}
Token Refresh
Access tokens expire after 1 hour. Use refresh tokens to obtain new access tokens without requiring user re-authentication.
Refresh Token Request
POST https://api.transcrimed.com.br/api/oauth/token
Parameters
| Parameter | Type | Description |
|---|---|---|
grant_type | string | Must be refresh_token |
refresh_token | string | Your refresh token |
client_id | string | Your application's client ID |
client_secret | string | Your application's client secret |
Example Implementation
async function refreshAccessToken(refreshToken) {
try {
const response = await fetch('https://api.transcrimed.com.br/api/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'your_client_id',
client_secret: 'your_client_secret'
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokens = await response.json();
// Update stored tokens
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
// Refresh token may be rotated
if (tokens.refresh_token) {
localStorage.setItem('refresh_token', tokens.refresh_token);
}
return tokens.access_token;
} catch (error) {
console.error('Token refresh error:', error);
// Redirect to login if refresh fails
window.location.href = '/auth/login';
throw error;
}
}
Token Revocation
Token revocation endpoint will be documented when available. For now, rotate tokens by expiring/refreshing and clearing local credentials on logout.
Security Best Practices
State Parameter
Always use the state parameter to prevent CSRF attacks:
// Generate cryptographically secure random state
const state = window.crypto.getRandomValues(new Uint8Array(32))
.reduce((acc, byte) => acc + byte.toString(16).padStart(2, '0'), '');
Client Secret Security
- Never expose client secrets in frontend code
- Store secrets in environment variables
- Use secure backend proxy for token exchange
- Rotate secrets regularly
Token Storage
- Use secure, httpOnly cookies for web apps
- Use secure keychain/keystore for mobile apps
- Implement token encryption for local storage
- Set appropriate token expiration
PKCE (Proof Key for Code Exchange)
For enhanced security, especially in mobile/SPA applications, consider implementing PKCE:
// Generate code verifier and challenge
function generateCodeVerifier() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return base64UrlEncode(array);
}
function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
return window.crypto.subtle.digest('SHA-256', data)
.then(hash => base64UrlEncode(new Uint8Array(hash)));
}
function base64UrlEncode(array) {
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
Error Handling
Handle common OAuth2 errors gracefully:
Authorization Errors
| Error | Description | Handling |
|---|---|---|
access_denied | User denied access | Show friendly message, allow retry |
invalid_client | Invalid client ID | Check configuration |
invalid_scope | Invalid scope requested | Review required scopes |
server_error | Server error | Retry after delay |
Token Errors
| Error | Description | Handling |
|---|---|---|
invalid_grant | Invalid/expired code | Restart auth flow |
invalid_client | Invalid credentials | Check client secret |
unsupported_grant_type | Wrong grant type | Check request format |
Example Error Handler
function handleOAuthError(error, errorDescription) {
switch (error) {
case 'access_denied':
showMessage('Authorization was denied. Please try again.');
break;
case 'invalid_client':
console.error('Configuration error: Invalid client credentials');
break;
case 'invalid_scope':
console.error('Configuration error: Invalid scope requested');
break;
case 'server_error':
showMessage('Server error. Please try again in a few moments.');
setTimeout(() => window.location.reload(), 5000);
break;
default:
showMessage(`Authentication error: ${errorDescription}`);
}
}
Complete Example
Here's a complete OAuth2 implementation example:
class TranscriMedAuth {
constructor(clientId, redirectUri) {
this.clientId = clientId;
this.redirectUri = redirectUri;
this.baseUrl = 'https://api.transcrimed.com.br';
}
// Start OAuth2 flow
login(scopes = ['medical_records:read', 'medical_records:write', 'jobs:read']) {
const state = this.generateState();
sessionStorage.setItem('oauth_state', state);
const authUrl = new URL(`${this.baseUrl}/api/oauth/authorize`);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', this.clientId);
authUrl.searchParams.set('redirect_uri', this.redirectUri);
authUrl.searchParams.set('scope', scopes.join(' '));
authUrl.searchParams.set('state', state);
window.location.href = authUrl.toString();
}
// Handle callback
async handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
if (error) {
throw new Error(`OAuth error: ${urlParams.get('error_description')}`);
}
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state parameter');
}
return this.exchangeCodeForTokens(code);
}
// Exchange code for tokens
async exchangeCodeForTokens(code) {
const response = await fetch(`${this.baseUrl}/api/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
client_secret: this.clientSecret // Only use in backend
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
const tokens = await response.json();
this.storeTokens(tokens);
return tokens;
}
// Store tokens securely
storeTokens(tokens) {
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
localStorage.setItem('expires_at', Date.now() + (tokens.expires_in * 1000));
}
// Get current access token
async getAccessToken() {
const token = localStorage.getItem('access_token');
const expiresAt = localStorage.getItem('expires_at');
if (!token) {
throw new Error('No access token available');
}
if (Date.now() >= parseInt(expiresAt)) {
return this.refreshToken();
}
return token;
}
// Refresh access token
async refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch(`${this.baseUrl}/api/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.clientId,
client_secret: this.clientSecret // Only use in backend
})
});
if (!response.ok) {
// Clear tokens and redirect to login
this.logout();
throw new Error('Token refresh failed');
}
const tokens = await response.json();
this.storeTokens(tokens);
return tokens.access_token;
}
// Logout and revoke tokens
async logout() {
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
try {
await this.revokeToken(accessToken);
} catch (error) {
console.error('Token revocation failed:', error);
}
}
// Clear stored tokens
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('expires_at');
}
// Revoke token
async revokeToken(token) {
await fetch(`${this.baseUrl}/api/oauth/revoke`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
token: token,
token_type_hint: 'access_token',
client_id: this.clientId,
client_secret: this.clientSecret // Only use in backend
})
});
}
// Generate random state
generateState() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
}
// Usage
const auth = new TranscriMedAuth('your_client_id', 'https://yourapp.com/oauth/callback');
// Start login
document.getElementById('login-btn').addEventListener('click', () => {
auth.login(['medical_records:read', 'medical_records:write', 'jobs:read']);
});
// Handle callback (on callback page)
if (window.location.pathname === '/oauth/callback') {
auth.handleCallback()
.then(() => {
window.location.href = '/dashboard';
})
.catch(error => {
console.error('Authentication failed:', error);
alert('Authentication failed. Please try again.');
window.location.href = '/';
});
}
Testing Authentication
Using Postman
- Import Collection: Use our Postman collection for testing
- Set Variables: Configure your client credentials
- Test Flow: Run the OAuth2 flow step by step
Using cURL
# Step 1: Get authorization code (manual browser step)
open "https://api.transcrimed.com.br/api/oauth/authorize?response_type=code&client_id=your_client_id&redirect_uri=https://yourapp.com/callback&scope=medical_records:read&state=random_state"
# Step 2: Exchange code for tokens
curl -X POST https://api.transcrimed.com.br/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=received_code&redirect_uri=https://yourapp.com/callback&client_id=your_client_id&client_secret=your_client_secret"
# Step 3: Use access token
curl -X GET https://api.transcrimed.com.br/api/v1/medical-records \
-H "Authorization: Bearer your_access_token"
Next Steps
Once authentication is working:
- Explore API Reference - Learn about available endpoints
- Try Examples - See real-world integration patterns
- Use Tools - Simplify integration with our development tools
- Contact Support - Get integration help
Need help? Contact our developers for integration assistance.