Skip to content

JavaScript Implicit Flow

Important Security Notice

The Implicit Flow is deprecated and not supported by WeTrials OAuth 2.0 implementation due to security concerns. This page explains why and provides secure alternatives for JavaScript applications.

Why Implicit Flow is Not Supported

The OAuth 2.0 Security Best Current Practice (RFC 8252) and OAuth 2.1 draft specifications recommend against using the Implicit Flow due to several security vulnerabilities:

Security Vulnerabilities

  1. Token Exposure in URLs

    • Access tokens are exposed in browser history
    • Tokens visible in HTTP referrer headers
    • Risk of token leakage through browser extensions
  2. No Refresh Tokens

    • Cannot obtain refresh tokens securely
    • Requires frequent re-authentication
  3. Token Interception

    • Vulnerable to token theft via XSS attacks
    • No mechanism like PKCE to prevent code interception
  4. Limited Token Validation

    • Cannot verify token integrity client-side
    • No client authentication possible

For JavaScript applications, including Single Page Applications (SPAs), use the Authorization Code Flow with PKCE instead. This provides better security while maintaining the same user experience.

Why PKCE is Better

  • No client secret required: Perfect for public clients
  • Protection against code interception: Cryptographic proof required
  • Refresh token support: Long-lived sessions without re-authentication
  • Industry standard: Recommended by OAuth 2.1 and security experts

Implementing Secure JavaScript Authentication

For Single Page Applications (SPAs)

Here's how to implement secure OAuth in your JavaScript application:

javascript
// oauth-client.js - Secure OAuth implementation for SPAs

class WeTrialsOAuth {
  constructor(config) {
    this.clientId = config.clientId;
    this.redirectUri = config.redirectUri;
    this.authEndpoint = 'https://auth.wetrials.com/v1/oauth/authorize';
    this.tokenEndpoint = 'https://auth.wetrials.com/v1/oauth/token';
  }

  // Generate cryptographically secure random string
  generateRandomString(length = 43) {
    const array = new Uint8Array(length);
    crypto.getRandomValues(array);
    return this.base64URLEncode(array);
  }

  // Base64 URL encoding without padding
  base64URLEncode(buffer) {
    return btoa(String.fromCharCode.apply(null, buffer)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  }

  // Generate PKCE challenge
  async generatePKCEChallenge(verifier) {
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const hash = await crypto.subtle.digest('SHA-256', data);
    return this.base64URLEncode(new Uint8Array(hash));
  }

  // Initiate OAuth flow
  async authorize(scopes = ['read:profile']) {
    // Generate PKCE parameters
    const codeVerifier = this.generateRandomString();
    const codeChallenge = await this.generatePKCEChallenge(codeVerifier);
    const state = this.generateRandomString(16);

    // Store in sessionStorage (not localStorage for security)
    sessionStorage.setItem('pkce_code_verifier', codeVerifier);
    sessionStorage.setItem('oauth_state', state);

    // Build authorization URL
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: this.clientId,
      redirect_uri: this.redirectUri,
      scope: scopes.join(' '),
      state: state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256',
    });

    // Redirect to authorization endpoint
    window.location.href = `${this.authEndpoint}?${params}`;
  }

  // Handle OAuth 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');

    // Check for errors
    if (error) {
      throw new Error(`OAuth error: ${error} - ${urlParams.get('error_description')}`);
    }

    // Validate state
    const savedState = sessionStorage.getItem('oauth_state');
    if (state !== savedState) {
      throw new Error('Invalid state parameter - possible CSRF attack');
    }

    // Get code verifier
    const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
    if (!codeVerifier) {
      throw new Error('No PKCE code verifier found');
    }

    // Exchange code for tokens
    const response = await fetch(this.tokenEndpoint, {
      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,
        code_verifier: codeVerifier,
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Token exchange failed: ${error.error_description}`);
    }

    const tokens = await response.json();

    // Clean up session storage
    sessionStorage.removeItem('pkce_code_verifier');
    sessionStorage.removeItem('oauth_state');

    return tokens;
  }

  // Refresh access token
  async refreshToken(refreshToken) {
    const response = await fetch(this.tokenEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: this.clientId,
      }),
    });

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    return response.json();
  }
}

// Usage example
const oauth = new WeTrialsOAuth({
  clientId: 'your-client-id',
  redirectUri: 'https://yourapp.com/callback',
});

// Login button handler
document.getElementById('login-btn').addEventListener('click', () => {
  oauth.authorize(['read:profile', 'read:studies']);
});

// Callback page handler
if (window.location.pathname === '/callback') {
  oauth
    .handleCallback()
    .then((tokens) => {
      // Store tokens securely
      // Use memory storage or secure session storage
      // Never use localStorage for sensitive tokens
      window.tokenManager.setTokens(tokens);
      window.location.href = '/dashboard';
    })
    .catch((error) => {
      console.error('OAuth error:', error);
      window.location.href = '/login?error=' + encodeURIComponent(error.message);
    });
}

Token Management Best Practices

javascript
// token-manager.js - Secure token management

class TokenManager {
  constructor() {
    // Store tokens in memory, not localStorage
    this.accessToken = null;
    this.refreshToken = null;
    this.expiresAt = null;
  }

  setTokens(tokens) {
    this.accessToken = tokens.access_token;
    this.refreshToken = tokens.refresh_token;
    // Calculate expiration time
    this.expiresAt = Date.now() + tokens.expires_in * 1000;
  }

  getAccessToken() {
    // Check if token exists and is not expired
    if (!this.accessToken || Date.now() >= this.expiresAt) {
      return null;
    }
    return this.accessToken;
  }

  async getValidAccessToken() {
    // Return current token if valid
    if (this.getAccessToken()) {
      return this.accessToken;
    }

    // Refresh if we have a refresh token
    if (this.refreshToken) {
      const tokens = await oauth.refreshToken(this.refreshToken);
      this.setTokens(tokens);
      return this.accessToken;
    }

    // No valid token - user needs to re-authenticate
    throw new Error('No valid authentication');
  }

  clearTokens() {
    this.accessToken = null;
    this.refreshToken = null;
    this.expiresAt = null;
  }
}

const tokenManager = new TokenManager();

Making Authenticated API Requests

javascript
// api-client.js - Authenticated API requests

class WeTrialsAPI {
  constructor(tokenManager) {
    this.tokenManager = tokenManager;
    this.baseURL = 'https://api.wetrials.com/v1';
  }

  async request(endpoint, options = {}) {
    try {
      // Get valid access token
      const accessToken = await this.tokenManager.getValidAccessToken();

      // Make API request
      const response = await fetch(`${this.baseURL}${endpoint}`, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${accessToken}`,
          'Content-Type': 'application/json',
        },
      });

      // Handle 401 Unauthorized
      if (response.status === 401) {
        // Try to refresh token
        this.tokenManager.clearTokens();
        const newToken = await this.tokenManager.getValidAccessToken();

        // Retry request with new token
        return fetch(`${this.baseURL}${endpoint}`, {
          ...options,
          headers: {
            ...options.headers,
            Authorization: `Bearer ${newToken}`,
            'Content-Type': 'application/json',
          },
        });
      }

      return response;
    } catch (error) {
      // Handle authentication errors
      if (error.message === 'No valid authentication') {
        // Redirect to login
        window.location.href = '/login';
      }
      throw error;
    }
  }

  // Example API methods
  async getUserProfile() {
    const response = await this.request('/user/profile');
    return response.json();
  }

  async getStudies() {
    const response = await this.request('/studies');
    return response.json();
  }
}

// Usage
const api = new WeTrialsAPI(tokenManager);

// Fetch user profile
api
  .getUserProfile()
  .then((profile) => {
    console.log('User profile:', profile);
  })
  .catch((error) => {
    console.error('API error:', error);
  });

React Implementation Example

jsx
// WeTrialsAuth.jsx - React OAuth component

import React, { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

export function useAuth() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  const oauth = new WeTrialsOAuth({
    clientId: process.env.REACT_APP_WETRIALS_CLIENT_ID,
    redirectUri: process.env.REACT_APP_REDIRECT_URI,
  });

  useEffect(() => {
    // Check for OAuth callback
    if (window.location.pathname === '/callback') {
      handleCallback();
    } else {
      // Check for existing session
      checkSession();
    }
  }, []);

  const handleCallback = async () => {
    try {
      const tokens = await oauth.handleCallback();
      tokenManager.setTokens(tokens);

      // Fetch user profile
      const profile = await api.getUserProfile();
      setUser(profile);

      // Redirect to dashboard
      window.location.href = '/dashboard';
    } catch (error) {
      console.error('OAuth callback error:', error);
      window.location.href = '/login?error=auth_failed';
    }
  };

  const checkSession = async () => {
    try {
      const token = tokenManager.getAccessToken();
      if (token) {
        const profile = await api.getUserProfile();
        setUser(profile);
      }
    } catch (error) {
      console.error('Session check error:', error);
    } finally {
      setLoading(false);
    }
  };

  const login = (scopes = ['read:profile']) => {
    oauth.authorize(scopes);
  };

  const logout = () => {
    tokenManager.clearTokens();
    setUser(null);
    window.location.href = '/';
  };

  const value = {
    user,
    loading,
    login,
    logout,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Usage in components
function LoginButton() {
  const { user, login, logout } = useAuth();

  if (user) {
    return (
      <div>
        <span>Welcome, {user.name}!</span>
        <button onClick={logout}>Logout</button>
      </div>
    );
  }

  return <button onClick={() => login(['read:profile', 'read:studies'])}>Login with WeTrials</button>;
}

Security Considerations for JavaScript Apps

1. Never Store Sensitive Data in localStorage

javascript
// ❌ WRONG - Vulnerable to XSS attacks
localStorage.setItem('access_token', token);

// ✅ CORRECT - Use memory or sessionStorage
sessionStorage.setItem('temp_state', state);
// Or better - keep in memory
this.accessToken = token;

2. Implement Content Security Policy (CSP)

html
<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; 
               script-src 'self' https://auth.wetrials.com; 
               connect-src 'self' https://api.wetrials.com;"
/>

3. Use Secure Session Management

javascript
// Implement automatic logout on inactivity
let inactivityTimer;

function resetInactivityTimer() {
  clearTimeout(inactivityTimer);
  inactivityTimer = setTimeout(() => {
    // Auto logout after 30 minutes of inactivity
    logout();
  }, 30 * 60 * 1000);
}

// Reset timer on user activity
document.addEventListener('click', resetInactivityTimer);
document.addEventListener('keypress', resetInactivityTimer);

4. Validate Tokens

javascript
// Validate JWT token structure and claims
function validateToken(token) {
  try {
    const [header, payload, signature] = token.split('.');
    const claims = JSON.parse(atob(payload));

    // Check expiration
    if (claims.exp * 1000 < Date.now()) {
      throw new Error('Token expired');
    }

    // Check issuer
    if (claims.iss !== 'https://auth.wetrials.com') {
      throw new Error('Invalid issuer');
    }

    // Check audience
    if (claims.aud !== yourClientId) {
      throw new Error('Invalid audience');
    }

    return true;
  } catch (error) {
    console.error('Token validation failed:', error);
    return false;
  }
}

Migration from Implicit Flow

If you're migrating from an Implicit Flow implementation:

  1. Update Authorization Request

    • Change response_type from token to code
    • Add PKCE parameters (code_challenge, code_challenge_method)
  2. Add Token Exchange Step

    • Implement callback handler to exchange code for tokens
    • Add token endpoint call with PKCE verifier
  3. Implement Token Refresh

    • Add refresh token handling
    • Implement automatic token renewal
  4. Update Token Storage

    • Move from localStorage to memory or sessionStorage
    • Implement secure token management

Common Questions

Q: Why can't I use Implicit Flow like with other providers?

A: WeTrials prioritizes security. The Implicit Flow has known vulnerabilities and is being deprecated across the industry. Authorization Code Flow with PKCE provides better security without compromising user experience.

Q: How do I handle token refresh in an SPA?

A: Use the refresh token to automatically obtain new access tokens before they expire. Implement a token manager that handles this transparently.

Q: What about popup-based authentication?

A: While possible, redirect-based flow is more reliable across browsers and doesn't face popup blocker issues. If you need popup support, ensure proper error handling.

Q: How do I secure tokens in the browser?

A: Keep tokens in memory when possible, use sessionStorage for temporary storage, implement CSP headers, and validate tokens before use.

Next Steps

Support

For help with OAuth implementation: