Skip to content

Security Best Practices ​

This guide provides comprehensive security best practices for implementing OAuth 2.0 with WeTrials. Following these practices will help protect your application and users from common security vulnerabilities.

Core Security Principles ​

1. Always Use HTTPS ​

All OAuth communications must use HTTPS to prevent token interception:

javascript
// ✅ CORRECT - HTTPS everywhere
const AUTH_ENDPOINT = 'https://auth.wetrials.com/v1/oauth/authorize';
const TOKEN_ENDPOINT = 'https://auth.wetrials.com/v1/oauth/token';
const REDIRECT_URI = 'https://yourapp.com/callback';

// ❌ WRONG - Never use HTTP in production
const AUTH_ENDPOINT = 'http://auth.wetrials.com/v1/oauth/authorize';

// Exception: localhost for development only
const DEV_REDIRECT_URI = 'http://localhost:3000/callback';

2. Implement PKCE for All Clients ​

Even confidential clients should use PKCE for additional security:

javascript
class SecureOAuth {
  constructor() {
    this.pkceRequired = true; // Always use PKCE
  }

  async startAuthorization() {
    // Generate PKCE parameters for every authorization
    const codeVerifier = this.generateCodeVerifier();
    const codeChallenge = await this.generateCodeChallenge(codeVerifier);

    // Store verifier securely
    this.secureStorage.setItem('pkce_verifier', codeVerifier);

    // Include in authorization request
    const params = {
      response_type: 'code',
      client_id: this.clientId,
      redirect_uri: this.redirectUri,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256', // Always use S256, never plain
      state: this.generateState(),
      nonce: this.generateNonce(), // Additional replay protection
    };

    return this.buildAuthUrl(params);
  }

  generateCodeVerifier() {
    // Minimum 43 characters, maximum 128
    const length = 64;
    const array = new Uint8Array(length);
    crypto.getRandomValues(array);
    return this.base64URLEncode(array);
  }

  async generateCodeChallenge(verifier) {
    // Always use SHA-256, never plain text
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const hash = await crypto.subtle.digest('SHA-256', data);
    return this.base64URLEncode(new Uint8Array(hash));
  }
}

3. Validate State Parameter ​

Always use and validate the state parameter to prevent CSRF attacks:

javascript
class StateManager {
  generateState() {
    // Generate cryptographically secure random state
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    const state = this.base64URLEncode(array);

    // Store with expiration
    const stateData = {
      value: state,
      createdAt: Date.now(),
      expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
    };

    sessionStorage.setItem('oauth_state', JSON.stringify(stateData));
    return state;
  }

  validateState(receivedState) {
    const stored = sessionStorage.getItem('oauth_state');
    if (!stored) {
      throw new Error('No state found - possible CSRF attack');
    }

    const stateData = JSON.parse(stored);

    // Check expiration
    if (Date.now() > stateData.expiresAt) {
      sessionStorage.removeItem('oauth_state');
      throw new Error('State expired - please try again');
    }

    // Validate state matches
    if (receivedState !== stateData.value) {
      throw new Error('State mismatch - possible CSRF attack');
    }

    // Clear used state
    sessionStorage.removeItem('oauth_state');
    return true;
  }
}

Token Security ​

Secure Token Storage ​

Never Use localStorage for Sensitive Tokens ​

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

// ✅ BETTER - Use memory storage
class TokenStore {
  constructor() {
    this.accessToken = null;
    this.refreshToken = null;
    this.expiresAt = null;
  }

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

  getAccessToken() {
    if (Date.now() >= this.expiresAt) {
      return null; // Token expired
    }
    return this.accessToken;
  }

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

// ✅ BEST - Server-side session with httpOnly cookies
// Store tokens server-side, use secure session cookie

Server-Side Token Storage ​

javascript
// Node.js with encrypted session storage
const session = require('express-session');
const MongoStore = require('connect-mongo');

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    store: MongoStore.create({
      mongoUrl: process.env.MONGODB_URI,
      crypto: {
        secret: process.env.ENCRYPTION_KEY,
      },
    }),
    cookie: {
      secure: true, // HTTPS only
      httpOnly: true, // Not accessible via JavaScript
      sameSite: 'strict', // CSRF protection
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
    },
  })
);

// Store tokens in encrypted session
app.post('/auth/callback', async (req, res) => {
  const tokens = await exchangeCodeForTokens(req.body.code);

  // Encrypt tokens before storing
  req.session.tokens = {
    access: encrypt(tokens.access_token),
    refresh: encrypt(tokens.refresh_token),
    expiresAt: Date.now() + tokens.expires_in * 1000,
  };

  res.redirect('/dashboard');
});

Token Validation ​

Always validate tokens before use:

javascript
class TokenValidator {
  validateAccessToken(token) {
    try {
      // Decode JWT without verification (for client-side)
      const parts = token.split('.');
      if (parts.length !== 3) {
        throw new Error('Invalid token format');
      }

      const payload = JSON.parse(atob(parts[1]));

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

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

      // Check audience
      if (payload.aud !== this.clientId) {
        throw new Error('Invalid audience');
      }

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

  // Server-side validation with signature verification
  async validateTokenServerSide(token) {
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');

    const client = jwksClient({
      jwksUri: 'https://auth.wetrials.com/.well-known/jwks.json',
    });

    function getKey(header, callback) {
      client.getSigningKey(header.kid, (err, key) => {
        const signingKey = key.publicKey || key.rsaPublicKey;
        callback(null, signingKey);
      });
    }

    return new Promise((resolve, reject) => {
      jwt.verify(
        token,
        getKey,
        {
          algorithms: ['RS256'],
          issuer: 'https://auth.wetrials.com',
          audience: this.clientId,
        },
        (err, decoded) => {
          if (err) reject(err);
          else resolve(decoded);
        }
      );
    });
  }
}

Token Rotation ​

Implement automatic token rotation for enhanced security:

javascript
class TokenRotation {
  constructor() {
    this.rotationBuffer = 5 * 60 * 1000; // 5 minutes before expiry
  }

  async getValidToken() {
    const token = this.tokenStore.getAccessToken();
    const expiresAt = this.tokenStore.getExpiresAt();

    // Check if token needs refresh
    if (!token || Date.now() >= expiresAt - this.rotationBuffer) {
      return await this.refreshToken();
    }

    return token;
  }

  async refreshToken() {
    const refreshToken = this.tokenStore.getRefreshToken();

    if (!refreshToken) {
      throw new Error('No refresh token available');
    }

    try {
      const response = await fetch('/v1/oauth/refresh', {
        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');
      }

      const tokens = await response.json();

      // Update stored tokens
      this.tokenStore.setTokens(tokens);

      // Refresh tokens are rotated - update the stored refresh token
      return tokens.access_token;
    } catch (error) {
      // Refresh failed - need new authorization
      this.handleRefreshFailure();
      throw error;
    }
  }

  handleRefreshFailure() {
    // Clear all tokens
    this.tokenStore.clear();

    // Redirect to login
    window.location.href = '/login';
  }
}

Client Security ​

Protect Client Secrets ​

javascript
// ❌ WRONG - Never expose client secrets in client-side code
const CLIENT_SECRET = 'abc123_my_secret';

// ❌ WRONG - Don't commit secrets to version control
const config = {
  clientId: 'my_client_id',
  clientSecret: 'my_client_secret', // Never do this
};

// ✅ CORRECT - Server-side only, environment variables
// server.js
const config = {
  clientId: process.env.WETRIALS_CLIENT_ID,
  clientSecret: process.env.WETRIALS_CLIENT_SECRET,
};

// ✅ CORRECT - Use proxy endpoint for public clients
// Client-side makes request to your server
fetch('/api/oauth/token', {
  method: 'POST',
  body: JSON.stringify({ code, codeVerifier }),
});

// Server-side handles OAuth with secret
app.post('/api/oauth/token', async (req, res) => {
  const { code, codeVerifier } = req.body;

  const tokens = await exchangeCode(code, codeVerifier, CLIENT_SECRET);
  req.session.tokens = tokens;

  res.json({ success: true });
});

Implement Content Security Policy ​

Protect against XSS attacks with proper CSP headers:

javascript
// Express.js example
const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'", "https://auth.wetrials.com"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "https://api.wetrials.com", "https://auth.wetrials.com"],
    fontSrc: ["'self'"],
    objectSrc: ["'none'"],
    mediaSrc: ["'self'"],
    frameSrc: ["'none'"],
    sandbox: ['allow-forms', 'allow-scripts', 'allow-same-origin'],
    reportUri: '/api/csp-report',
    upgradeInsecureRequests: []
  }
}));

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

Network Security ​

Implement Request Signing ​

Add request signatures for additional security:

javascript
class SecureAPIClient {
  async makeSignedRequest(endpoint, method, body) {
    const timestamp = Date.now();
    const nonce = crypto.randomUUID();

    // Create signature
    const signatureBase = `${method}${endpoint}${timestamp}${nonce}${JSON.stringify(body)}`;
    const signature = await this.createHmacSignature(signatureBase);

    const response = await fetch(endpoint, {
      method,
      headers: {
        Authorization: `Bearer ${this.accessToken}`,
        'X-Request-Timestamp': timestamp,
        'X-Request-Nonce': nonce,
        'X-Request-Signature': signature,
      },
      body: JSON.stringify(body),
    });

    return response;
  }

  async createHmacSignature(data) {
    const encoder = new TextEncoder();
    const key = await crypto.subtle.importKey('raw', encoder.encode(this.clientSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);

    const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));

    return btoa(String.fromCharCode(...new Uint8Array(signature)));
  }
}

Certificate Pinning ​

For mobile applications, implement certificate pinning:

swift
// iOS Swift example
class PinnedSessionManager: NSObject, URLSessionDelegate {
    func urlSession(_ session: URLSession,
                   didReceive challenge: URLAuthenticationChallenge,
                   completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        guard let serverTrust = challenge.protectionSpace.serverTrust,
              let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        let serverCertData = SecCertificateCopyData(certificate) as Data
        let localCertPath = Bundle.main.path(forResource: "wetrials", ofType: "cer")!
        let localCertData = NSData(contentsOfFile: localCertPath)!

        if serverCertData == localCertData as Data {
            completionHandler(.useCredential, URLCredential(trust: serverTrust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

Session Management ​

Implement Secure Session Handling ​

javascript
class SessionManager {
  constructor() {
    this.sessionTimeout = 30 * 60 * 1000; // 30 minutes
    this.warningTime = 5 * 60 * 1000; // 5 minutes before timeout
    this.lastActivity = Date.now();
  }

  startSessionMonitoring() {
    // Monitor user activity
    ['click', 'keypress', 'scroll', 'mousemove'].forEach((event) => {
      document.addEventListener(event, () => this.updateActivity());
    });

    // Check session periodically
    setInterval(() => this.checkSession(), 60000); // Every minute
  }

  updateActivity() {
    this.lastActivity = Date.now();
  }

  checkSession() {
    const inactiveTime = Date.now() - this.lastActivity;

    if (inactiveTime >= this.sessionTimeout) {
      this.endSession('Session timeout due to inactivity');
    } else if (inactiveTime >= this.sessionTimeout - this.warningTime) {
      this.showWarning(this.sessionTimeout - inactiveTime);
    }
  }

  endSession(reason) {
    console.log('Ending session:', reason);

    // Clear tokens
    this.tokenStore.clear();

    // Revoke tokens on server
    this.revokeTokens();

    // Clear session storage
    sessionStorage.clear();

    // Redirect to login
    window.location.href = `/login?reason=${encodeURIComponent(reason)}`;
  }

  async revokeTokens() {
    try {
      await fetch('/v1/oauth/revoke', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          token: this.tokenStore.getRefreshToken(),
          token_type_hint: 'refresh_token',
        }),
      });
    } catch (error) {
      console.error('Token revocation failed:', error);
    }
  }
}

Concurrent Session Management ​

Prevent or manage concurrent sessions:

javascript
class ConcurrentSessionManager {
  async validateSession() {
    const sessionId = this.getSessionId();

    const response = await fetch('/api/validate-session', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.accessToken}`,
        'X-Session-ID': sessionId,
      },
    });

    if (!response.ok) {
      const error = await response.json();

      if (error.code === 'CONCURRENT_SESSION') {
        this.handleConcurrentSession();
      }
    }
  }

  handleConcurrentSession() {
    // Option 1: Force logout other sessions
    if (confirm('You are logged in elsewhere. Force logout other sessions?')) {
      this.forceLogoutOtherSessions();
    } else {
      // Option 2: Logout current session
      this.logout();
    }
  }
}

Audit and Monitoring ​

Implement Security Logging ​

javascript
class SecurityLogger {
  logSecurityEvent(event) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      event: event.type,
      userId: event.userId,
      clientId: event.clientId,
      ip: event.ip,
      userAgent: event.userAgent,
      success: event.success,
      details: event.details,
    };

    // Send to logging service
    this.sendToLoggingService(logEntry);

    // Check for suspicious patterns
    this.checkSuspiciousActivity(logEntry);
  }

  checkSuspiciousActivity(logEntry) {
    // Multiple failed login attempts
    if (logEntry.event === 'login_failed') {
      this.trackFailedAttempts(logEntry);
    }

    // Unusual location or device
    if (this.isUnusualAccess(logEntry)) {
      this.alertSecurityTeam(logEntry);
    }

    // Rapid token refreshes
    if (this.detectRapidRefreshes(logEntry)) {
      this.flagPotentialAbuse(logEntry);
    }
  }
}

Security Checklist ​

Development Phase ​

  • [ ] Use HTTPS for all OAuth endpoints
  • [ ] Implement PKCE for all authorization flows
  • [ ] Validate state parameter on every callback
  • [ ] Store tokens securely (never in localStorage)
  • [ ] Implement token validation before use
  • [ ] Set up CSP headers
  • [ ] Use secure random generators for state/nonce
  • [ ] Implement session timeout
  • [ ] Add security logging

Deployment Phase ​

  • [ ] Rotate client secrets
  • [ ] Review and minimize requested scopes
  • [ ] Set up monitoring and alerting
  • [ ] Implement rate limiting
  • [ ] Configure CORS properly
  • [ ] Enable security headers (HSTS, X-Frame-Options, etc.)
  • [ ] Implement request signing for sensitive operations
  • [ ] Set up audit logging
  • [ ] Document security procedures

Maintenance Phase ​

  • [ ] Regularly rotate client secrets
  • [ ] Monitor for suspicious activity
  • [ ] Review audit logs
  • [ ] Update dependencies for security patches
  • [ ] Conduct security audits
  • [ ] Test token revocation
  • [ ] Verify backup authentication methods
  • [ ] Update security documentation
  • [ ] Train team on security best practices

Common Security Vulnerabilities ​

1. Authorization Code Injection ​

Prevention: Always use PKCE and validate state parameter

2. Token Leakage ​

Prevention: Use secure storage, HTTPS only, proper CSP

3. CSRF Attacks ​

Prevention: State parameter, SameSite cookies, CSRF tokens

4. Open Redirect ​

Prevention: Validate redirect URIs against whitelist

5. Token Replay ​

Prevention: Use nonce, implement token binding, short expiration

Next Steps ​