Skip to main content

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:

  1. Authorization Request - Redirect users to TranscriMed
  2. User Consent - Users grant permissions to your app
  3. Authorization Code - Receive authorization code via callback
  4. Token Exchange - Exchange code for access tokens

Before You Start

Register Your Application

  1. Visit TranscriMed Developer Portal
  2. Create a developer account
  3. 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
Security

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

ParameterTypeDescription
response_typestringMust be code
client_idstringYour application's client ID
redirect_uristringMust match registered URI
scopestringSpace-separated list of scopes
statestringRandom value for CSRF protection

Available Scopes

Only request scopes you need. Available scopes:

ScopeDescription
medical_records:readRead medical records
medical_records:writeCreate and update medical records
medical_records:deleteDelete medical records
jobs:readRead job status and results
jobs:writeCreate, cancel and manage jobs
worklists:writeSend worklist items from RIS/PACS systems
worklists:manageUpdate worklist item status
worklists:readRead 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

ParameterTypeDescription
grant_typestringMust be authorization_code
codestringAuthorization code from callback
redirect_uristringSame URI used in authorization
client_idstringYour application's client ID
client_secretstringYour 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

ParameterTypeDescription
grant_typestringMust be refresh_token
refresh_tokenstringYour refresh token
client_idstringYour application's client ID
client_secretstringYour 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

ErrorDescriptionHandling
access_deniedUser denied accessShow friendly message, allow retry
invalid_clientInvalid client IDCheck configuration
invalid_scopeInvalid scope requestedReview required scopes
server_errorServer errorRetry after delay

Token Errors

ErrorDescriptionHandling
invalid_grantInvalid/expired codeRestart auth flow
invalid_clientInvalid credentialsCheck client secret
unsupported_grant_typeWrong grant typeCheck 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

  1. Import Collection: Use our Postman collection for testing
  2. Set Variables: Configure your client credentials
  3. 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:

  1. Explore API Reference - Learn about available endpoints
  2. Try Examples - See real-world integration patterns
  3. Use Tools - Simplify integration with our development tools
  4. Contact Support - Get integration help

Need help? Contact our developers for integration assistance.