Files
2026-04-17 23:26:01 +00:00

4.2 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Integrate OAuth2 Providers Correctly CRITICAL Secure third-party authentication with proper flow handling authentication, oauth2, google, github, social-login

Integrate OAuth2 Providers Correctly

OAuth2 integration should use the all-in-one method for simplicity and security. Manual code exchange should only be used when necessary (e.g., mobile apps with deep links).

Incorrect (manual implementation without SDK):

// Don't manually handle OAuth flow
async function loginWithGoogle() {
  // Redirect user to Google manually
  window.location.href = 'https://accounts.google.com/oauth/authorize?...';
}

// Manual callback handling
async function handleCallback(code) {
  // Exchange code manually - error prone!
  const response = await fetch('/api/auth/callback', {
    method: 'POST',
    body: JSON.stringify({ code })
  });
}

Correct (using SDK's all-in-one method):

import PocketBase from 'pocketbase';

const pb = new PocketBase('http://127.0.0.1:8090');

// All-in-one OAuth2 (recommended for web apps)
async function loginWithOAuth2(providerName) {
  try {
    const authData = await pb.collection('users').authWithOAuth2({
      provider: providerName,  // 'google', 'github', 'microsoft', etc.
      // Optional: create new user data if not exists
      createData: {
        emailVisibility: true,
        name: ''  // Will be populated from OAuth provider
      }
    });

    console.log('Logged in via', providerName);
    console.log('User:', authData.record.email);
    console.log('Is new user:', authData.meta?.isNew);

    return authData;
  } catch (error) {
    if (error.isAbort) {
      console.log('OAuth popup was closed');
      return null;
    }
    throw error;
  }
}

// Usage
document.getElementById('google-btn').onclick = () => loginWithOAuth2('google');
document.getElementById('github-btn').onclick = () => loginWithOAuth2('github');

Manual code exchange (for React Native / deep links):

// Only use when all-in-one isn't possible
async function loginWithOAuth2Manual() {
  // Get auth methods - PocketBase provides state and codeVerifier
  const authMethods = await pb.collection('users').listAuthMethods();
  const provider = authMethods.oauth2.providers.find(p => p.name === 'google');

  // Store the provider's state and codeVerifier for callback verification
  // PocketBase generates these for you - don't create your own
  sessionStorage.setItem('oauth_state', provider.state);
  sessionStorage.setItem('oauth_code_verifier', provider.codeVerifier);

  // Build the OAuth URL using provider.authURL + redirect
  const redirectUrl = window.location.origin + '/oauth-callback';
  const authUrl = provider.authURL + encodeURIComponent(redirectUrl);

  // Redirect to OAuth provider
  window.location.href = authUrl;
}

// In your callback handler (e.g., /oauth-callback page):
async function handleOAuth2Callback() {
  const params = new URLSearchParams(window.location.search);

  // CSRF protection: verify state matches
  if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
    throw new Error('State mismatch - potential CSRF attack');
  }

  const code = params.get('code');
  const codeVerifier = sessionStorage.getItem('oauth_code_verifier');
  const redirectUrl = window.location.origin + '/oauth-callback';

  // Exchange code for auth token
  const authData = await pb.collection('users').authWithOAuth2Code(
    'google',
    code,
    codeVerifier,
    redirectUrl,
    { emailVisibility: true }
  );

  // Clean up
  sessionStorage.removeItem('oauth_state');
  sessionStorage.removeItem('oauth_code_verifier');

  return authData;
}

Configure OAuth2 provider (Admin UI or API):

// Via API (superuser only) - usually done in Admin UI
// IMPORTANT: Never hardcode client secrets. Use environment variables.
await pb.collections.update('users', {
  oauth2: {
    enabled: true,
    providers: [{
      name: 'google',
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET
    }],
    mappedFields: {
      avatarURL: 'avatar'  // Map OAuth field to collection field
    }
  }
});

Reference: PocketBase OAuth2