Authorization Code Flow
The Authorization Code flow is the most secure and recommended OAuth 2.0 flow for WeTrials integrations. This guide covers implementation with PKCE (Proof Key for Code Exchange) for enhanced security.
When to Use This Flow
Use the Authorization Code flow for:
- Server-side web applications
- Single-page applications (SPAs)
- Mobile applications (iOS, Android)
- Desktop applications
Flow Diagram
sequenceDiagram
participant User
participant Client as Your App
participant Auth as WeTrials Auth Server
participant API as WeTrials API
Client->>Client: Generate code_verifier & code_challenge
User->>Client: Click "Login with WeTrials"
Client->>Auth: Redirect to /v1/oauth/authorize
Note over Auth: with client_id, redirect_uri,<br/>scope, state, code_challenge
Auth->>User: Display login form
User->>Auth: Enter credentials & consent
Auth->>Client: Redirect to callback with code & state
Client->>Auth: POST /v1/oauth/token
Note over Auth: with code, code_verifier,<br/>client_id, client_secret
Auth->>Client: Return access_token & refresh_token
Client->>API: API request with Bearer token
API->>Client: Return protected resourceStep-by-Step Implementation
Step 1: Generate PKCE Parameters
For public clients (SPAs, mobile apps), generate PKCE parameters:
// Generate code verifier
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
// Generate code challenge from verifier
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(hash));
}
// Base64 URL encoding
function base64URLEncode(buffer) {
return btoa(String.fromCharCode.apply(null, buffer)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// Usage
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store codeVerifier securely for later use
sessionStorage.setItem('code_verifier', codeVerifier);Step 2: Redirect to Authorization Endpoint
Build the authorization URL and redirect the user:
const authorizationUrl = new URL('https://auth.wetrials.com/v1/oauth/authorize');
const params = {
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://yourapp.com/callback',
scope: 'read:profile read:studies',
state: generateRandomString(), // CSRF protection
code_challenge: codeChallenge,
code_challenge_method: 'S256',
};
// Add parameters to URL
Object.keys(params).forEach((key) => authorizationUrl.searchParams.append(key, params[key]));
// Redirect user
window.location.href = authorizationUrl.toString();Parameters:
response_type(required): Must becodeclient_id(required): Your application's client IDredirect_uri(required): Registered callback URLscope(optional): Space-separated list of requested permissionsstate(recommended): Random string for CSRF protectioncode_challenge(required for public clients): PKCE challengecode_challenge_method(required with PKCE): Must beS256
Step 3: Handle the Authorization Callback
After user authorization, WeTrials redirects to your callback URL:
https://yourapp.com/callback?code=AUTH_CODE&state=STATE_VALUE// Parse callback parameters
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
// Verify state matches what you sent
const savedState = sessionStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('Invalid state parameter');
}
// Retrieve stored code verifier
const codeVerifier = sessionStorage.getItem('code_verifier');Step 4: Exchange Code for Tokens
Exchange the authorization code for access and refresh tokens:
const tokenResponse = await fetch('https://auth.wetrials.com/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET', // Only for confidential clients
code_verifier: codeVerifier, // For PKCE
}),
});
const tokens = await tokenResponse.json();
// tokens.access_token - JWT access token (expires in 1 hour)
// tokens.refresh_token - Refresh token (expires in 30 days)
// tokens.expires_in - Token lifetime in seconds
// tokens.token_type - "Bearer"
// tokens.scope - Granted scopesRequest Parameters:
grant_type: Must beauthorization_codecode: The authorization code receivedredirect_uri: Must match the original redirect URIclient_id: Your application's client IDclient_secret: Required for confidential clientscode_verifier: Required when PKCE was used
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8xLvf2YqzT...",
"scope": "read:profile read:studies"
}Step 5: Use the Access Token
Include the access token in API requests:
const response = await fetch('https://api.wetrials.com/v1/user/profile', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const profile = await response.json();Step 6: Refresh Access Tokens
When the access token expires, use the refresh token to get a new one:
const refreshResponse = await fetch('https://auth.wetrials.com/v1/oauth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET', // For confidential clients
}),
});
const newTokens = await refreshResponse.json();
// Store the new refresh token - it's rotated on each useServer-Side Implementation Examples
Node.js with Express
const express = require('express');
const crypto = require('crypto');
const fetch = require('node-fetch');
const app = express();
// Step 1: Initiate OAuth flow
app.get('/auth/wetrials', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const params = new URLSearchParams({
response_type: 'code',
client_id: process.env.WETRIALS_CLIENT_ID,
redirect_uri: process.env.REDIRECT_URI,
scope: 'read:profile read:studies',
state: state,
});
res.redirect(`https://auth.wetrials.com/v1/oauth/authorize?${params}`);
});
// Step 2: Handle callback
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
// Exchange code for tokens
const tokenResponse = await fetch('https://auth.wetrials.com/v1/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: process.env.REDIRECT_URI,
client_id: process.env.WETRIALS_CLIENT_ID,
client_secret: process.env.WETRIALS_CLIENT_SECRET,
}),
});
const tokens = await tokenResponse.json();
// Store tokens securely
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
res.redirect('/dashboard');
});Python with Flask
import os
import secrets
from flask import Flask, redirect, request, session, url_for
import requests
from urllib.parse import urlencode
app = Flask(__name__)
app.secret_key = os.environ['SECRET_KEY']
@app.route('/auth/wetrials')
def auth_wetrials():
# Generate and store state
state = secrets.token_urlsafe(16)
session['oauth_state'] = state
# Build authorization URL
params = {
'response_type': 'code',
'client_id': os.environ['WETRIALS_CLIENT_ID'],
'redirect_uri': os.environ['REDIRECT_URI'],
'scope': 'read:profile read:studies',
'state': state
}
auth_url = f"https://auth.wetrials.com/v1/oauth/authorize?{urlencode(params)}"
return redirect(auth_url)
@app.route('/auth/callback')
def auth_callback():
code = request.args.get('code')
state = request.args.get('state')
# Verify state
if state != session.get('oauth_state'):
return 'Invalid state', 400
# Exchange code for tokens
token_data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': os.environ['REDIRECT_URI'],
'client_id': os.environ['WETRIALS_CLIENT_ID'],
'client_secret': os.environ['WETRIALS_CLIENT_SECRET']
}
response = requests.post(
'https://auth.wetrials.com/v1/oauth/token',
data=token_data
)
tokens = response.json()
# Store tokens
session['access_token'] = tokens['access_token']
session['refresh_token'] = tokens['refresh_token']
return redirect('/dashboard')Security Best Practices
Use PKCE for All Clients
Even confidential clients should implement PKCE for additional security.
Validate State Parameter
Always validate the state parameter to prevent CSRF attacks.
Secure Token Storage
- Access tokens: Store in secure session storage or memory
- Refresh tokens: Encrypt before storing in database
- Never expose tokens: Don't include in URLs or logs
Handle Token Expiration
Implement automatic token refresh before making API calls:
async function makeAuthenticatedRequest(url, options = {}) {
// Check if token is expired
if (isTokenExpired(accessToken)) {
await refreshAccessToken();
}
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${accessToken}`,
},
});
}Common Errors
Invalid Client
{
"error": "invalid_client",
"error_description": "Invalid client ID or secret"
}Solution: Verify your client credentials are correct.
Invalid Grant
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}Solution: Authorization codes expire in 10 minutes. Request a new code.
Invalid Request
{
"error": "invalid_request",
"error_description": "Missing required parameters"
}Solution: Ensure all required parameters are included.
Testing Your Integration
Test Authorization URL
curl -G "https://auth.wetrials.com/v1/oauth/authorize" \
--data-urlencode "response_type=code" \
--data-urlencode "client_id=YOUR_CLIENT_ID" \
--data-urlencode "redirect_uri=https://yourapp.com/callback" \
--data-urlencode "scope=read:profile" \
--data-urlencode "state=RANDOM_STATE"Test Token Exchange
curl -X POST "https://auth.wetrials.com/v1/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=https://yourapp.com/callback" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"