Overview

The OAuth integration system allows users to authenticate and link their accounts with third-party providers like Google, Telegram, and others. It supports both registration and login flows, as well as account linking for existing users.

Supported Providers

Currently supported OAuth providers:
  • Google: OAuth 2.0
  • Telegram: Telegram Bot authentication
  • More providers can be configured

OAuth Flow

Standard OAuth 2.0 Flow

Get Authorization URL

Get the OAuth authorization URL to redirect the user to the provider’s login page. Endpoint: GET /api/oauth/:provider/authorize Parameters:
  • provider: URL path parameter - provider name (e.g., google, telegram)
  • redirect_uri: Query parameter - Optional, custom redirect URI after authentication
  • state: Query parameter - Optional, state parameter for CSRF protection
Example Request:
GET /api/oauth/google/authorize?redirect_uri=https://app.rcoinx.com/callback&state=random_state_string
Response: 200 OK
{
  "code": 200,
  "message": "Authorization URL generated successfully",
  "data": {
    "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&scope=...&state=...",
    "state": "random_state_string",
    "session_id": "oauth_session_abc123"
  }
}
Example:
async function getOAuthAuthorizationURL(provider, redirectUri, state) {
  const params = new URLSearchParams();
  if (redirectUri) params.append('redirect_uri', redirectUri);
  if (state) params.append('state', state);
  
  const url = `https://api.rcoinx.com/api/oauth/${provider}/authorize?${params}`;
  
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    }
  });
  
  const data = await response.json();
  
  if (!response.ok) {
    throw new Error(data.message);
  }
  
  return data.data;
}

// Usage - Redirect user to OAuth provider
async function loginWithGoogle() {
  try {
    const state = generateRandomState(); // Generate CSRF token
    const result = await getOAuthAuthorizationURL(
      'google',
      'https://app.rcoinx.com/oauth/callback',
      state
    );
    
    // Store state for verification
    sessionStorage.setItem('oauth_state', state);
    sessionStorage.setItem('oauth_session_id', result.session_id);
    
    // Redirect user to OAuth provider
    window.location.href = result.authorization_url;
  } catch (error) {
    console.error('Failed to initiate OAuth:', error);
  }
}

function generateRandomState() {
  return Math.random().toString(36).substring(2, 15) +
         Math.random().toString(36).substring(2, 15);
}

OAuth Callback

Handle the OAuth callback after the user authorizes the application with the provider. Endpoint: GET /api/oauth/:provider/callback Query Parameters:
  • code: Authorization code from the provider
  • state: State parameter for CSRF verification
  • session_id: OAuth session ID (optional, from authorization step)
Example Request:
GET /api/oauth/google/callback?code=4/0AY0e-g7xQz...&state=random_state_string
Response: 200 OK
{
  "code": 200,
  "message": "OAuth authentication successful",
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "access_token_expires_at": "2025-10-13T12:30:00Z",
    "refresh_token_expires_at": "2025-10-20T12:00:00Z",
    "user": {
      "id": 123,
      "username": "johndoe",
      "email": {
        "email": "john@gmail.com",
        "verified": true
      }
    },
    "is_new_user": false,
    "provider": "google"
  }
}
If this is the user’s first time logging in with OAuth, a new account will be created automatically. The is_new_user field indicates whether this is a new registration.
Example:
// This is typically handled by your OAuth callback route
// Example: /oauth/callback route in your frontend

async function handleOAuthCallback() {
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  const state = urlParams.get('state');
  const provider = urlParams.get('provider') || 'google';
  
  // Verify state (CSRF protection)
  const savedState = sessionStorage.getItem('oauth_state');
  if (state !== savedState) {
    console.error('State mismatch - possible CSRF attack');
    window.location.href = '/login?error=security';
    return;
  }
  
  try {
    // The callback is typically handled server-side
    // If you need to call it from frontend:
    const response = await fetch(
      `https://api.rcoinx.com/api/oauth/${provider}/callback?code=${code}&state=${state}`,
      {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json'
        }
      }
    );
    
    const data = await response.json();
    
    if (!response.ok) {
      throw new Error(data.message);
    }
    
    // Store tokens
    localStorage.setItem('access_token', data.data.access_token);
    localStorage.setItem('refresh_token', data.data.refresh_token);
    localStorage.setItem('user', JSON.stringify(data.data.user));
    
    // Clean up
    sessionStorage.removeItem('oauth_state');
    sessionStorage.removeItem('oauth_session_id');
    
    // Show welcome message for new users
    if (data.data.is_new_user) {
      console.log('Welcome! Your account has been created.');
    }
    
    // Redirect to dashboard
    window.location.href = '/dashboard';
    
  } catch (error) {
    console.error('OAuth callback failed:', error);
    window.location.href = '/login?error=oauth_failed';
  }
}

Validate OAuth Session

Validate an OAuth session before completing the authentication. Endpoint: POST /api/oauth/:provider/validate-session Request:
{
  "session_id": "oauth_session_abc123"
}
Response: 200 OK
{
  "code": 200,
  "message": "Session is valid",
  "data": {
    "valid": true,
    "expires_at": "2025-10-13T12:35:00Z",
    "provider": "google"
  }
}
Example:
async function validateOAuthSession(provider, sessionId) {
  const response = await fetch(
    `https://api.rcoinx.com/api/oauth/${provider}/validate-session`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ session_id: sessionId })
    }
  );
  
  const data = await response.json();
  return data.data;
}

Get User OAuth Connections

Get all OAuth connections for the authenticated user. Endpoint: GET /api/oauth/connections Headers:
Authorization: Bearer <access_token>
Response: 200 OK
{
  "code": 200,
  "message": "OAuth connections retrieved successfully",
  "data": [
    {
      "id": 1,
      "provider": "google",
      "provider_user_id": "1234567890",
      "email": "john@gmail.com",
      "connected_at": "2025-10-01T10:00:00Z",
      "last_used_at": "2025-10-13T12:00:00Z"
    },
    {
      "id": 2,
      "provider": "telegram",
      "provider_user_id": "9876543210",
      "username": "johndoe_tg",
      "connected_at": "2025-10-05T15:00:00Z",
      "last_used_at": "2025-10-12T18:00:00Z"
    }
  ]
}
Example:
async function getOAuthConnections() {
  const accessToken = localStorage.getItem('access_token');
  
  const response = await fetch('https://api.rcoinx.com/api/oauth/connections', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    }
  });
  
  const data = await response.json();
  
  if (!response.ok) {
    throw new Error(data.message);
  }
  
  return data.data;
}

Disconnect OAuth Account

Disconnect an OAuth provider from the user’s account. Endpoint: POST /api/oauth/:provider/disconnect Headers:
Authorization: Bearer <access_token>
Request:
{
  "provider_user_id": "1234567890"
}
Response: 200 OK
{
  "code": 200,
  "message": "OAuth account disconnected successfully",
  "data": {
    "success": true,
    "provider": "google"
  }
}
Ensure the user has another authentication method (password, phone, or another OAuth provider) before disconnecting. Otherwise, they may lose access to their account.
Example:
async function disconnectOAuthAccount(provider, providerUserId) {
  const accessToken = localStorage.getItem('access_token');
  
  const confirmed = confirm(
    `Are you sure you want to disconnect your ${provider} account?`
  );
  
  if (!confirmed) return;
  
  const response = await fetch(
    `https://api.rcoinx.com/api/oauth/${provider}/disconnect`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ provider_user_id: providerUserId })
    }
  );
  
  const data = await response.json();
  
  if (!response.ok) {
    throw new Error(data.message);
  }
  
  return data.data;
}

Complete OAuth Integration Example

Here’s a complete example with UI:
import React, { useState, useEffect } from 'react';

function OAuthManager() {
  const [connections, setConnections] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    loadConnections();
  }, []);
  
  const loadConnections = async () => {
    try {
      const data = await getOAuthConnections();
      setConnections(data);
    } catch (error) {
      console.error('Failed to load connections:', error);
    } finally {
      setLoading(false);
    }
  };
  
  const handleConnect = async (provider) => {
    try {
      const state = generateRandomState();
      const redirectUri = `${window.location.origin}/oauth/callback`;
      
      const result = await getOAuthAuthorizationURL(provider, redirectUri, state);
      
      sessionStorage.setItem('oauth_state', state);
      sessionStorage.setItem('oauth_session_id', result.session_id);
      sessionStorage.setItem('oauth_action', 'connect'); // Indicate this is for linking
      
      window.location.href = result.authorization_url;
    } catch (error) {
      alert('Failed to connect: ' + error.message);
    }
  };
  
  const handleDisconnect = async (provider, providerUserId) => {
    // Check if user has other auth methods
    if (connections.length === 1 && !hasPassword()) {
      alert('You must have at least one authentication method. Please set a password first.');
      return;
    }
    
    try {
      await disconnectOAuthAccount(provider, providerUserId);
      await loadConnections();
      alert('Account disconnected successfully');
    } catch (error) {
      alert('Failed to disconnect: ' + error.message);
    }
  };
  
  const hasPassword = () => {
    // Check if user has password set (implement based on your API)
    return true; // Placeholder
  };
  
  const getProviderIcon = (provider) => {
    const icons = {
      google: '🔵',
      telegram: '✈️',
      facebook: '📘',
      twitter: '🐦'
    };
    return icons[provider] || '🔗';
  };
  
  if (loading) return <div>Loading...</div>;
  
  return (
    <div className="oauth-manager">
      <h2>Connected Accounts</h2>
      
      <div className="connected-accounts">
        {connections.length === 0 ? (
          <p>No accounts connected</p>
        ) : (
          connections.map(conn => (
            <div key={conn.id} className="connection-card">
              <div className="provider-info">
                <span className="icon">{getProviderIcon(conn.provider)}</span>
                <div>
                  <strong>{conn.provider}</strong>
                  <p>{conn.email || conn.username}</p>
                  <small>Connected: {new Date(conn.connected_at).toLocaleDateString()}</small>
                </div>
              </div>
              <button
                onClick={() => handleDisconnect(conn.provider, conn.provider_user_id)}
                className="disconnect-btn"
              >
                Disconnect
              </button>
            </div>
          ))
        )}
      </div>
      
      <div className="connect-new">
        <h3>Connect New Account</h3>
        <div className="provider-buttons">
          {['google', 'telegram'].map(provider => {
            const isConnected = connections.some(c => c.provider === provider);
            return (
              <button
                key={provider}
                onClick={() => handleConnect(provider)}
                disabled={isConnected}
                className="provider-btn"
              >
                {getProviderIcon(provider)} {provider}
                {isConnected && ' (Connected)'}
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

export default OAuthManager;

Telegram-Specific Flow

Telegram uses a slightly different OAuth flow. See the Telegram OAuth documentation for specific implementation details.

Security Best Practices

1. Verify State Parameter

Always verify the state parameter to prevent CSRF attacks:
function verifyState(receivedState) {
  const savedState = sessionStorage.getItem('oauth_state');
  
  if (!savedState || receivedState !== savedState) {
    throw new Error('Invalid state parameter - possible CSRF attack');
  }
  
  sessionStorage.removeItem('oauth_state');
  return true;
}

2. Use HTTPS

OAuth requires HTTPS in production. Ensure your redirect URIs use HTTPS:
function getRedirectUri() {
  if (process.env.NODE_ENV === 'production') {
    return 'https://app.rcoinx.com/oauth/callback';
  }
  return 'http://localhost:3000/oauth/callback';
}

3. Handle Popup Blockers

For popup-based OAuth flows:
function openOAuthPopup(url, provider) {
  const width = 600;
  const height = 700;
  const left = window.screen.width / 2 - width / 2;
  const top = window.screen.height / 2 - height / 2;
  
  const popup = window.open(
    url,
    `${provider}_oauth`,
    `width=${width},height=${height},left=${left},top=${top}`
  );
  
  if (!popup || popup.closed || typeof popup.closed === 'undefined') {
    alert('Please enable popups for this site to use OAuth login');
    return null;
  }
  
  return popup;
}

4. Store Minimal Data

Only store necessary OAuth data:
// Good - Store only what's needed
localStorage.setItem('access_token', token);

// Bad - Don't store OAuth provider tokens
// localStorage.setItem('google_access_token', googleToken);

Error Handling

Common Errors

ErrorDescriptionSolution
invalid_stateState parameter mismatchClear session and restart OAuth flow
invalid_codeAuthorization code is invalid or expiredRestart OAuth flow
provider_errorError from OAuth providerCheck provider status, retry later
account_existsEmail already registeredLink account or use different email

Error Handling Example

async function handleOAuthError(error) {
  const errorMessages = {
    invalid_state: 'Security verification failed. Please try again.',
    invalid_code: 'Authentication expired. Please try again.',
    provider_error: 'The authentication provider is unavailable. Please try again later.',
    account_exists: 'An account with this email already exists. Please login instead.'
  };
  
  const message = errorMessages[error.code] || error.message || 'An error occurred';
  alert(message);
  
  // Redirect to login
  window.location.href = '/login';
}