Skip to content

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 ​

mermaid
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 resource

Step-by-Step Implementation ​

Step 1: Generate PKCE Parameters ​

For public clients (SPAs, mobile apps), generate PKCE parameters:

javascript
// 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:

javascript
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 be code
  • client_id (required): Your application's client ID
  • redirect_uri (required): Registered callback URL
  • scope (optional): Space-separated list of requested permissions
  • state (recommended): Random string for CSRF protection
  • code_challenge (required for public clients): PKCE challenge
  • code_challenge_method (required with PKCE): Must be S256

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
javascript
// 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:

javascript
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 scopes

Request Parameters:

  • grant_type: Must be authorization_code
  • code: The authorization code received
  • redirect_uri: Must match the original redirect URI
  • client_id: Your application's client ID
  • client_secret: Required for confidential clients
  • code_verifier: Required when PKCE was used

Response:

json
{
  "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:

javascript
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:

javascript
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 use

Server-Side Implementation Examples ​

Node.js with Express ​

javascript
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 ​

python
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:

javascript
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 ​

json
{
  "error": "invalid_client",
  "error_description": "Invalid client ID or secret"
}

Solution: Verify your client credentials are correct.

Invalid Grant ​

json
{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired"
}

Solution: Authorization codes expire in 10 minutes. Request a new code.

Invalid Request ​

json
{
  "error": "invalid_request",
  "error_description": "Missing required parameters"
}

Solution: Ensure all required parameters are included.

Testing Your Integration ​

Test Authorization URL ​

bash
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 ​

bash
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"

Next Steps ​