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
Token Exposure in URLs
- Access tokens are exposed in browser history
- Tokens visible in HTTP referrer headers
- Risk of token leakage through browser extensions
No Refresh Tokens
- Cannot obtain refresh tokens securely
- Requires frequent re-authentication
Token Interception
- Vulnerable to token theft via XSS attacks
- No mechanism like PKCE to prevent code interception
Limited Token Validation
- Cannot verify token integrity client-side
- No client authentication possible
Recommended Alternative: Authorization Code Flow with PKCE
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:
// 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
// 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
// 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
// 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
// ❌ 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)
<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
// 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
// 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:
Update Authorization Request
- Change
response_typefromtokentocode - Add PKCE parameters (
code_challenge,code_challenge_method)
- Change
Add Token Exchange Step
- Implement callback handler to exchange code for tokens
- Add token endpoint call with PKCE verifier
Implement Token Refresh
- Add refresh token handling
- Implement automatic token renewal
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
- Authorization Code Flow Guide - Implement secure OAuth
- Security Best Practices - Additional security measures
- Error Handling - Handle OAuth errors gracefully
- Scopes and Permissions - Understand available permissions
Support
For help with OAuth implementation:
- Email: developers@wetrials.com
- GitHub: WeTrials OAuth Examples
- Stack Overflow: Tag your questions with
wetrials-oauth