324 lines
8.2 KiB
Markdown
324 lines
8.2 KiB
Markdown
### OAuth2 purpose
|
|
|
|
A way for the `user` to tell `google` to give an access to `myapp` app
|
|
|
|
<br>
|
|
|
|
### 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 `|
|
|
|
|
<br><br>
|
|
|
|
|
|
### 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
|
|
2. Google **Response** to Backend with an `access_token` and a `refresh token`
|
|
3. Backend **Redirect** to Frontend with the `access_token` in a `cookie`
|
|
|
|
3. Use Token
|
|
|
|
1. Frontend **GET** profile data from Backend using the `cookie`
|
|
2. Backend **GET** profile data from Google using the `access_token`
|
|
3. Google **Response** to Backend with profile data
|
|
4. Backend **Response** to Frontend with profile data
|
|
|
|
<br><br><br>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 1. Get Code
|
|
|
|
1. Frontend **Navigate** to Google `https://accounts.google.com/o/oauth2` with a callback url
|
|
2. Google **Redirect** to Backend callback url `https://myapp/api/auth/callback` with authorization code
|
|
|
|
### 1. Front **GET** to Google
|
|
|
|
```sh
|
|
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
|
|
|
|
```bash
|
|
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.*
|
|
|
|
<br><br><br>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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 with the `access_token` in a `cookie`
|
|
|
|
### 1. Backend **POST** the code to Google
|
|
|
|
```sh
|
|
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
|
|
|
|
```js
|
|
{
|
|
"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**
|
|
|
|
```bash
|
|
HTTP/1.1 302 Found
|
|
Location: http://localhost:3000/dashboard
|
|
Set-Cookie: access_token=ya29.a0AfH6SMC8Op6zkVX-VoA; HttpOnly; Secure; Max-Age=3600; Path=/;
|
|
```
|
|
|
|
<details>
|
|
<summary>
|
|
<h4>Backend code</h4>
|
|
<br><br>
|
|
Implements the endpoint /auth/google/callback
|
|
<br><br>
|
|
<p>1. Recieves authorization code from Google</p><br>
|
|
<p>2. POST send the authorization code to https://oauth2.googleapis.com/token</p><br>
|
|
<p>3. POST response the access & refresh tokens</p><br>
|
|
<p>4. Redirect to Fronend success page with a cookie contains the access token</p><br>
|
|
</summary>
|
|
|
|
<br><br>
|
|
|
|
```js
|
|
app.get('/callback', async (req, res) => {
|
|
try {
|
|
|
|
//
|
|
// 1. Get the authorization code from Google's redirect
|
|
//
|
|
const { code } = req.query;
|
|
|
|
//
|
|
// 2. POST the authorization code to Google
|
|
//
|
|
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',
|
|
}),
|
|
});
|
|
|
|
//
|
|
// 3. Get Response tokens from Google
|
|
//
|
|
const { access_token, refresh_token } = await tokenResponse.json();
|
|
|
|
//
|
|
// 4. Redirect to Fronend success page with a cookie contains 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
|
|
});
|
|
res.redirect('https://myapp/dashboard');
|
|
|
|
|
|
} catch (error) {
|
|
res.redirect('https://myapp/login');
|
|
}
|
|
});
|
|
```
|
|
|
|
The `redirect_uri` in the token exchange `POST` request needs to match EXACTLY the same `redirect_uri` that was used in the initial authorization request to Google. It's part of OAuth2 security verification.
|
|
|
|
</details>
|
|
|
|
<br><br><br>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 3. Use Token
|
|
|
|
### 1. Frontend **GET** profile data from Backend
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```js
|
|
// DashboardPage.jsx
|
|
import { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
function DashboardPage() {
|
|
const navigate = useNavigate();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [dashboardData, setDashboardData] = useState(null);
|
|
|
|
useEffect(() => {
|
|
async function initializeDashboard() {
|
|
try {
|
|
|
|
//
|
|
// First verify auth
|
|
//
|
|
const authResponse = await fetch('https://myapp/api/verify-auth', {
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!authResponse.ok) {
|
|
throw new Error('Authentication failed');
|
|
}
|
|
|
|
//
|
|
// Auth is valid, now fetch dashboard data
|
|
//
|
|
const dataResponse = await fetch('https://myapp/api/dashboard-data', {
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!dataResponse.ok) {
|
|
throw new Error('Failed to load dashboard data');
|
|
}
|
|
|
|
const data = await dataResponse.json();
|
|
setDashboardData(data);
|
|
setIsLoading(false);
|
|
} catch (err) {
|
|
//
|
|
// Redirect to login on auth failure
|
|
//
|
|
setError(err.message);
|
|
navigate('/login');
|
|
}
|
|
}
|
|
|
|
initializeDashboard();
|
|
}, [navigate]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-center">
|
|
<h2>Loading Dashboard...</h2>
|
|
<p>Please wait while we verify your session.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1>Welcome to Dashboard</h1>
|
|
</div>
|
|
);
|
|
}
|
|
```
|