dev/OAuth2-Front-Approach.md

13 KiB
Raw Blame History

OAuth2 purpose

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




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. Get Authorization code

Frontend ⇢ Google ⇢ Frontend


  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.


HTTP GET Request from Frontend 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.

HTTP GET Response from Google 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. Exchange Authorization Code with Tokens

Frontend ⇢ Backend ⇢ Google ⇢ Backend ⇢ Frontend


2.1 Frontend

Now that the frontend has the Authorization code on the callback url, it can send it to the backend

HTTP POST Request from Frontend to Backend

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

{
  "code": "AAAABCX4XfWgyVyziyLg0QHHHHH"
}



2.2 Backend

  1. The backend receives the authorization code form the 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 response to frontends POST with the the tokens

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

HTTP POST Request from Backend 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).

HTTP POST Response from Google to Backend

{
    "access_token": "ACCESSTOKEN.6zXZkHi2XITkDoOV.ACCESSTOKEN",
    "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');
});

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;



2.3 Front

The frontend gets the tokens from the backend response, and saves them somewhere secure. Now its ready to use the tokens to get the user data from Google

HTTP Response from Backend to Frontend

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




4. Use the Token

Frontend ⇢ Google ⇢ Frontend


  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

HTTP GET Request from Frontend to Backend

GET https://www.googleapis.com/oauth2/v3/userinfo
Authorization: Bearer ACCESSTOKEN6zXZkHi2XITkDoOVACCESSTOKEN

HTTP GET Response from Backend 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

const fetchUserData = async (accessToken) => {
  try {
    const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
      },
    });

    if (!response.ok) {
      throw new Error('Failed to fetch user data from Google');
    }

    // Parse the response to get the user data
    const userData = await response.json();

    console.log('User Data:', userData);
    // Example user data:
    // {
    //   "sub": "1234567890123456789012345678901234567890",
    //   "name": "John Doe",
    //   "given_name": "John",
    //   "family_name": "Doe",
    //   "email": "john.doe@example.com",
    //   "picture": "https://example.com/profile.jpg"
    // }
  } catch (error) {
    console.error('Error fetching user data:', error);
  }
};




5. Refresh the Token

Frontend ⇢ Backend ⇢ Google ⇢ Backend ⇢ Frontend


If the access token is expired, the frontend will receive an error response from Google when attempting to fetch user data

HTTP GET Request from Frontend to Google

GET https://www.googleapis.com/oauth2/v3/userinfo
Authorization: Bearer ACCESSTOKEN6zXZkHi2XITkDoOVACCESSTOKEN

HTTP GET Response from Google to Frontend

{
  "error": "invalid_token",
  "error_description": "The access token expired"
}

HTTP POST Refresh token from Frontend to Backend


Frontend Code

const refreshAccessToken = async (refreshToken) => {
  try {
    const response = await fetch('/api/refresh-token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ refresh_token: refreshToken }),
    });

    if (!response.ok) {
      throw new Error('Failed to refresh access token');
    }

    const data = await response.json();
    return data.access_token;  // New access token
  } catch (error) {
    console.error('Error refreshing access token:', error);
  }
};

Frontend Code

const response = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({
    client_id: 'YOUR_GOOGLE_CLIENT_ID',
    client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
    refresh_token: refreshToken,
    grant_type: 'refresh_token',
  }),
});

const data = await response.json();
const newAccessToken = data.access_token;  // New access token