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