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