dev/OAuth2-Backend-Approach.md

8.0 KiB

OAuth2 purpose

A way for the user to tell google to give an access to myapp app


Flow:

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



Details:

  1. Get Authorization Code

    1. Frontend Navigate to Google URL with a callback url
    2. Google Redirect to Backend's callback url with the authorization code
  2. Exchange Code with Token

    1. Backend POST the code to Google https://oauth2.googleapis.com/token
    2. Google response to Backend with an access_token and a refresh token
    3. Backend redirect to Frontend https://myapp/auth/success with the access_token in a cookie
  3. Use Token

    1. Frontend GET profile data from Backend https://myapp/api/auth/profile using the cookie
    2. Backend GET profile data from Google https://www.googleapis.com/oauth2/v3/userinfo using the access_token from Frontend cookie
    3. Google response to Backend with profile data
    4. Backend response to Frontend with profile data




1. Get Code

  1. Frontend GET to Google https://accounts.google.com/o/oauth2 with callback url
  2. Google 302 to Backend https://myapp/api/auth/callback with authorization code

1. Front GET to Google

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

2. Google 302 to Back

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

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




2. Exchange Code with Token

  1. Backend POST the code to Google https://oauth2.googleapis.com/token
  2. Google response to Backend with an access_token and a refresh token
  3. Backend response to Frontend with the access_token in a cookie

1. Backend POST the code to Google

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

grant_type=authorization_code&
code=AAAABCX4XfWgyVyziyLg0QHHHHH&
client_id=ABC34JHS9D&
client_secret=PASS1234&
redirect_uri=https://myapp/callback

2. Google response to Backend

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

3. Backend Redirect to Frontend success page

This redirect will place a cookie in the browser that contains the access token

HTTP/1.1 302 Found
Location: http://localhost:3000/dashboard
Set-Cookie: access_token=ya29.a0AfH6SMC8Op6zkVX-VoA; HttpOnly; Secure; Max-Age=3600; Path=/;

Backend code



Implements the endpoint /auth/google/callback

1. Recieves authorization code from Google


2. POST send the authorization code to https://oauth2.googleapis.com/token


3. POST response the access & refresh tokens


4. Respond the Fronend initial request with a cookie contains the access token




app.get('/callback', async (req, res) => {
  try {
    // 1. Get the authorization code from Google's redirect
    const { code } = req.query;
    
    // 2. Exchange the code for tokens
    const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        code,
        client_id: process.env.GOOGLE_CLIENT_ID,
        client_secret: process.env.GOOGLE_CLIENT_SECRET,
        redirect_uri: 'https://myapp/callback',
        grant_type: 'authorization_code',
      }),
    });
    
    const { access_token, refresh_token } = await tokenResponse.json();
    
    // 3. Set the HTTP-only cookie with the access token
    res.cookie('access', access_token, {
      httpOnly: true,           // Cannot be accessed by client-side JavaScript
      secure: true,             // Only sent over HTTPS
      sameSite: 'strict',       // CSRF protection
      maxAge: 24 * 60 * 60 * 1000, // 24 hours
    });
    
    // 4. Store refresh token securely in database (recommended)
    // await db.storeRefreshToken(user_id, refresh_token);
    
    // 5. Redirect back to frontend
    res.redirect('https://myapp/authentication-success');
  } catch (error) {
    res.redirect('https://myapp/authentication-error');
  }
});




3. Use Token

1. Frontend GET profile data from Backend

curl -X GET https://myapp/api/auth/profile \
  -H "Cookie: access_token=ya29.a0AfH6SMC8Op6zkVX-VoA" \
  -H "Accept: application/json"

2. Backend GET profile data from Google

curl -X GET "https://www.googleapis.com/oauth2/v3/userinfo" \
  -H "Authorization: Bearer {access_token}" \
  -H "Accept: application/json"

3. Google response to Back with profile data

{
  "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"
}

4. Backend response to Front with profile data

{
  "sub": "1234567890",
  "name": "John Doe",
  "picture": "https://lh3.googleusercontent.com/8bgVEOykQ6ykFSQ=s96",
  "email": "john.doe@example.com",
}

Frontend Code

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const App = () => {
  const [user, setUser] = useState(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  // Fetch user data from the backend's /profile endpoint
  const fetchUserData = async () => {
    try {
      const response = await axios.get('http://localhost:3000/profile', {
        withCredentials: true,  // Important: Ensure cookies are sent with the request
      });
      setUser(response.data);  // Set the user data from the response
      setIsAuthenticated(true);
    } catch (error) {
      console.error('Error fetching user profile:', error);
    }
  };

  useEffect(() => {
    // Check if the user is authenticated by checking the cookie
    if (document.cookie.includes('access_token')) {
      fetchUserData();  // Fetch user profile if the access token is available in cookies
    }
  }, []);

  return (
    <div className="App">
      <h1>Google OAuth2 with React</h1>

      {!isAuthenticated ? (
        <div>
          <p>Please log in with Google to access your profile:</p>
          <button onClick={() => window.location.href = 'http://localhost:3000/auth/google'}>
            Login with Google
          </button>
        </div>
      ) : (
        <div>
          <h2>Welcome, {user?.name}</h2>
          <p>Email: {user?.email}</p>
          <img src={user?.picture} alt="Profile" />
        </div>
      )}
    </div>
  );
};

export default App;