dev/OAuth2-Backend-Approach.md

19 KiB
Raw Blame History

OAuth2 purpose

A way for the user to tell google to give an access to xorismesiti.gr app


OAuth2 Frontend/Backend Flow:

When What How
1 Get Code Front ⇢ Google ⇢ Front
2 Exchange Code with Token Front ⇢ Back ⇢ Google ⇢ Back ⇢ Front
4 Use Token Front ⇢ Back ⇢ Google ⇢ Back ⇢ Front

OAuth2 Frontend/Backend Flow Details:

  1. Get Code

    1. Front GET to Google https://accounts.google.com/o/oauth2 with callback url
    2. Google 302 to Front https://xorismesiti.gr/callback with authorization code
  2. Exchange Code with Token

    1. Front POST the code to the Back https://xorismesiti.gr/api/auth/exchange-token
    2. Back POST the code to Google https://oauth2.googleapis.com/token
    3. Google response to Back with an access_token and a refresh token
    4. Back response to Front with the access_token in a cookie
  3. Use Token

    1. Front GET profile data from Back using the cookie
    2. Back GET profile data from Google using the access_token from Front cookie
    3. Google response to Back with profile data
    4. Back response to Front with profile data



OAuth2 Standar Flow:

  1. User clicks button "Login with Google" on your platform xorismesiti.gr
  2. Authorization Request: Button redirects user to Google's authorization endpoint accounts.google.com/o/oauth2
  3. User Login and Consent: User login to Google and grants permissions.
  4. Authorization Code Response: Google redirects user back to your app xorismesiti.gr/callback with an authorization code.
  5. Access Token Request: App exchanges the authorization code for an access_token.
  6. Access Protected Resources: App uses the access_token to fetch the user's Google profile and email from googleapis.com/oauth2
  7. Token Refresh (Optional): If the access_token expires, app uses the refresh token to get a new access_token.




OAuth2 Frontend/Backend Flow:

Frontend

  1. Redirect the user to Google's OAuth authorization endpoint accounts.google.com/o/oauth2
  2. Get the authorization code after Google redirects back to the frontend xorismesiti.gr/callback
  3. Send the authorization code to the backend for token exchange.

Backend

  1. exchange the authorization code for an access_token and refresh token
  2. fetch user profile data from from googleapis.com/oauth2 using the access_token
  3. Store the tokens securely in session (front) or a database (back)
  4. Refresh the access_token if it expires




1. [Frontend] Request Authorization code

  1. A button "Login with Google" redirects the user to the Google's authorization endpoint accounts.google.com/o/oauth2/v2/auth
  2. After the redirection, the user will log in to Google and grant permissions (if they havent already).
  3. Google will redirect the user back to your redirect_uri https://xorismesiti.gr/callback with an authorization code ?code=

Security: the state string should be validated upon receiving the response from Google, as it ensures that the response corresponds to the request.

Frontend HTTP GET Request to Google

GET https://accounts.google.com/o/oauth2/v2/auth?
    response_type=code&
    client_id=ABC34JHS9D&
    redirect_uri=https://xorismesiti.gr/callback&
    scope=email%20profile&
    state=xyz123
  • response_type=code: This indicates you're using the "authorization code" flow.
  • client_id: Your Google API client ID. Created by the developers of the xorismesiti.gr on the google API console.
  • redirect_uri: The URI Google will redirect to after the user consents.
  • scope: The permissions you're requesting (e.g., email, profile).
  • state: A random string to protect against CSRF attacks.

Google HTTP Response to Frontend

HTTP/1.1 302 Found
Location: https://xorismesiti.gr/api/auth/callback?code=4/0AX4XfWgyVyz-uT8k7WiyLg0Q&state=xyz123
Content-Type: text/html; charset=UTF-8
Content-Length: 0

Frontend Code

// Redirect to Google's OAuth 2.0 endpoint when user clicks login
const loginWithGoogle = () => {
  const clientId = 'ABC34JHS9D';  // Replace with your actual Google Client ID
  const redirectUri = 'https://xorismesiti.gr/api/auth/callback'; // Backend URL where Google will send the user after login
  const scope = 'email profile';  // Scopes you're requesting (email, profile, etc.)
  const state = 'random-state-value';  // For CSRF protection
  
  const googleAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}`;
  
  window.location.href = googleAuthUrl; // Redirect user to Google
};




2. [Frontend] Receive Authorization Code

Now that the frontend has the Authorization code on th callback url https://xorismesiti.gr/api/auth/callback?code=AAAABCX4XfWgyVyziyLg0QHHHHH it can send it to the backend with POST to xorismesiti.gr/api/auth/exchange-token, in order the backend to exchange the code for an access_token and optionally an refresh_token

Frontend HTTP POST Request to Backend

POST https://xorismesiti.gr/api/auth/exchange-token
Content-Type: application/json

{
  "code": "AAAABCX4XfWgyVyziyLg0QHHHHH"
}

Step 3 takes place here, Backend exchanges the Code with the Tokens

Backend HTTP Response to Frontend

{
  "access_token": "ya29.a0AfH6SMC8Op6zXZkHi2XITkDoOVzYXt3hTY6sny54UlWlxrnKlX5Xv78is7BEHekVX-VoA",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "1//04d5XHqmn6Hdy3wTf5OYDP1SyBa74zEFURjddQ2A1cFw78PY13pQyWhlD2A6XhDQtKlrjAqU4kS3vGdMvckw",
  "scope": "email profile"
}

Frontend Code

// On the backend callback URL, the frontend receives the authorization code
import { useEffect } from 'react';
import { useRouter } from 'next/router';

const Callback = () => {
  const router = useRouter();
  
  useEffect(() => {
    const { code, state } = router.query;
    
    // Send the authorization code to the backend for token exchange
    fetch('/api/auth/exchange-token', {
      method: 'POST',
      body: JSON.stringify({ code }),
      headers: {
        'Content-Type': 'application/json',
      },
    })
    .then(response => response.json())
    .then(data => {
      // Handle success (store token, update UI, etc.)
      console.log(data);  // Typically, you'll store the access token here or manage the user session.
      router.push('/dashboard');  // Redirect the user to their dashboard or home page.
    })
    .catch(error => {
      console.error('Error exchanging token:', error);
    });
  }, []);
  
  return <div>Loading...</div>;
};

export default Callback;




3. [Backend] Exchange Code with Token

  1. The backend receives the authorization code form frontend (Frontend POST at xorismesiti.gr/api/auth/exchange-token)
  2. The backend POST Authorization code to Google API
  3. The Google API respond to backend POST with the tokens access_token and refresh_token
  4. The backend respond to frontend with the tokens (respond to frontend POST at xorismesiti.gr/api/auth/exchange-token)

Security: The backend never expose the client_secret to the frontend. This step should always be handled on the backend.

Backend HTTP POST Request to Google

POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AAAABCX4XfWgyVyziyLg0QHHHHH&
redirect_uri=https://xorismesiti.gr/callback&
client_id=ABC34JHS9D&
client_secret=PASS1234
  • HTTP Method: POST
  • URL: https://oauth2.googleapis.com/token
  • Headers
    • Content-Type: application/x-www-form-urlencoded
  • Body
    • grant_type=authorization_code: This specifies the grant type.
    • code: The authorization code you received in the previous step.
    • redirect_uri: The same redirect URI used in the authorization request.
    • client_id: Your Google API client ID.
    • client_secret: Your Google API client secret (which should be kept secure).

Google HTTP Response Backend

{
    "access_token": "ya29.a0AfH6SMC8Op6zXZkHi2XITkDoOVzYXt3hTY6sny54UlWlxrnKlX5Xv78is7BEHekVX-VoA",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "1//04d5XHqmn6Hdy3wTf5OYDP1SyBa74zEFURjddQ2A1cFw78PY13pQyWhlD2A6XhDQtKlrjAqU4kS3vGdMvckw",
    "scope": "email profile"
}

Backend Code:

const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.json()); // To parse JSON bodies

const clientId = 'YOUR_GOOGLE_CLIENT_ID'; // Google Client ID
const clientSecret = 'YOUR_GOOGLE_CLIENT_SECRET'; // Google Client Secret
const redirectUri = 'https://xorismesiti.gr/api/auth/callback'; // Must match the one used in frontend

// Handle token exchange
app.post('/api/auth/exchange-token', async (req, res) => {
  const { code } = req.body;

  try {
    const response = await axios.post('https://oauth2.googleapis.com/token', null, {
      params: {
        code,
        client_id: clientId,
        client_secret: clientSecret,
        redirect_uri: redirectUri,
        grant_type: 'authorization_code',
      },
    });
    
    const { access_token, refresh_token, expires_in } = response.data;

    // Optionally, store the tokens in a secure location (e.g., session, database)
    // For now, send them back to the frontend (not recommended for production)
    res.json({ access_token, refresh_token, expires_in });
  } catch (error) {
    console.error('Error exchanging authorization code for token:', error);
    res.status(500).json({ error: 'Failed to exchange authorization code for access token' });
  }
});

// Start the server
app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});




4. [Frontend] Use the Token

  1. The frontend receives the tokens from the Backend response,
  2. Store them in the localStorage of the browser
  3. Make authenticated requests directly to Google API

Frontend HTTP GET Request to Backend

GET https://xorismesiti.gr/api/user-profile
Authorization: Bearer access-token-from-backend

Step 5 takes place here: The Backend request the user data from Google using the Tokens

Backedn HTTP Response to Frontend

{
    "sub": "1234567890",
    "name": "John Doe",
    "given_name": "John",
    "family_name": "Doe",
    "profile": "https://plus.google.com/1234567890",
    "picture": "https://lh3.googleusercontent.com/a-/AOh14GgIXXl5JXzW0c1Szbl-e1Jch1vhl5rHhH65vlK6J5g5PqkGjj1O0p3t8bgVEOykQ6ykFSQ=s96",
    "email": "john.doe@example.com",
    "email_verified": true,
    "locale": "en"
}

Frontend Code

// Store tokens and expiration time after receiving them from the backend
const storeTokens = (access_token, refresh_token, expires_in) => {
  const expirationTime = Date.now() + expires_in * 1000; // expires_in is in seconds, so multiply by 1000 to get ms
  localStorage.setItem('access_token', access_token);
  localStorage.setItem('refresh_token', refresh_token);
  localStorage.setItem('token_expiration', expirationTime);
};

// Function to check if the access token has expired
const isTokenExpired = () => {
  const expirationTime = localStorage.getItem('token_expiration');
  return Date.now() > expirationTime;
};

// Function to refresh the access token using the refresh token
const refreshAccessToken = async () => {
  const refresh_token = localStorage.getItem('refresh_token');
  try {
    const response = await fetch('/api/auth/refresh-token', {
      method: 'POST',
      body: JSON.stringify({ refresh_token }),
      headers: {
        'Content-Type': 'application/json',
      },
    });

    const data = await response.json();
    if (data.access_token) {
      // Store the new access token and expiration time
      storeTokens(data.access_token, data.refresh_token, data.expires_in);
      return data.access_token;
    }
  } catch (error) {
    console.error('Error refreshing token:', error);
    return null; // Handle error appropriately (e.g., logout user or prompt to log in again)
  }
};

// Function to get the access token, either from localStorage or by refreshing it if expired
const getAccessToken = async () => {
  if (isTokenExpired()) {
    console.log('Access token expired. Refreshing...');
    const newAccessToken = await refreshAccessToken();
    return newAccessToken;
  } else {
    return localStorage.getItem('access_token');
  }
};

// Example usage: Get the access token and use it for an authenticated API request
const fetchUserProfile = async () => {
  const access_token = await getAccessToken();
  
  if (!access_token) {
    console.error('No valid access token found. User might need to log in.');
    return;
  }

  // Now you can use the access token to make an authenticated API request
  try {
    const response = await fetch('/api/user-profile', {
      headers: {
        'Authorization': `Bearer ${access_token}`,
      },
    });
    
    const userData = await response.json();
    console.log(userData);
  } catch (error) {
    console.error('Error fetching user profile:', error);
  }
};

// Example call to fetch user profile
fetchUserProfile();




5. [Backend] Fetch User Data

With the access token obtained in the previous step,

your platform can now use it to fetch the user's Google profile and email information.

The token is included in the Authorization header of the request.

HTTP GET Request

GET https://www.googleapis.com/oauth2/v3/userinfo
Authorization: Bearer ya29.a0AfH6SMC8Op6zXZkHi2XITkDoOVzYXt3hTY6sny54UlWlxrnKlX5Xv78is7BEHekVX-VoA

HTTP Response

{
    "sub": "1234567890",
    "name": "John Doe",
    "given_name": "John",
    "family_name": "Doe",
    "profile": "https://plus.google.com/1234567890",
    "picture": "https://lh3.googleusercontent.com/a-/AOh14GgIXXl5JXzW0c1Szbl-e1Jch1vhl5rHhH65vlK6J5g5PqkGjj1O0p3t8bgVEOykQ6ykFSQ=s96",
    "email": "john.doe@example.com",
    "email_verified": true,
    "locale": "en"
}

Backend GET Code

const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.json()); // To parse JSON bodies

const clientId = 'YOUR_GOOGLE_CLIENT_ID'; // Google Client ID
const clientSecret = 'YOUR_GOOGLE_CLIENT_SECRET'; // Google Client Secret
const redirectUri = 'https://xorismesiti.gr/api/auth/callback'; // Must match the one used in frontend

// Function to validate the access token by making a request to the Google user info endpoint
const validateAccessToken = async (accessToken) => {
  try {
    const response = await axios.get('https://www.googleapis.com/oauth2/v3/userinfo', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    // If this succeeds, the token is valid
    return response.data; // Return the user data
  } catch (error) {
    // If the request fails (token is invalid or expired), throw an error
    throw new Error('Invalid or expired access token');
  }
};

// Example route for fetching the user profile
app.get('/api/user-profile', async (req, res) => {
  const accessToken = req.headers['authorization']?.split(' ')[1]; // Extract token from Authorization header
  
  if (!accessToken) {
    return res.status(400).json({ error: 'No access token provided' });
  }

  try {
    // Validate the access token by fetching user info from Google
    const userData = await validateAccessToken(accessToken);

    // Send the user profile data to the frontend
    res.json(userData);
  } catch (error) {
    // Handle invalid or expired token
    console.error('Error validating access token:', error);
    res.status(401).json({ error: 'Invalid or expired access token' });
  }
});

// Start the server
app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Front GET Code

// After receiving the token, store it in the frontend (e.g., localStorage or context)
localStorage.setItem('access_token', response.access_token);

// Use it to make authenticated API requests to the backend
fetch('/api/user-profile', {
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
  },
})
  .then(response => {
    if (response.status === 401) {
      // Handle token expiration or invalid token
      console.error('Access token expired or invalid. Please log in again.');
      // Optionally, redirect to login page or refresh token
    } else {
      return response.json();
    }
  })
  .then(data => {
    // Handle user data
    console.log(data);
  })
  .catch(error => {
    console.error('Error fetching user profile:', error);
  });




6. [Backend] Token Expiry and Refresh (Optional)

If the access token expires,

your platform can use the refresh token (if provided) to obtain a new access token without requiring the user to log in again.

HTTP POST Request

POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token=refresh-token-from-backend&
client_id=YOUR_GOOGLE_CLIENT_ID&
client_secret=YOUR_GOOGLE_CLIENT_SECRET
  • URL: https://oauth2.googleapis.com/token
  • HTTP Method: POST
  • Headers:
  • Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
  • grant_type=refresh_token: This indicates the refresh token flow.
  • refresh_token: The refresh token obtained in step 5.
  • client_id: Your Google API client ID.
  • client_secret: Your Google API client secret.

HTTP Response

{
    "access_token": "ya29.a0AfH6SMC8Op6zXZkHi2XITkDoOVzYXt3hTY6sny54UlWlxrnKlX5Xv78is7BEHekVX-VoA",
    "token_type": "Bearer",
    "expires_in": 3600
}

Example Backend Code:

app.post('/api/auth/refresh-token', async (req, res) => {
  const { refresh_token } = req.body;

  try {
    const response = await axios.post('https://oauth2.googleapis.com/token', null, {
      params: {
        refresh_token,
        client_id: clientId,
        client_secret: clientSecret,
        grant_type: 'refresh_token',
      },
    });
    
    res.json(response.data); // Return new access token and refresh token
  } catch (error) {
    console.error('Error refreshing access token:', error);
    res.status(500).json({ error: 'Failed to refresh access token' });
  }
});