648 lines
18 KiB
Markdown
648 lines
18 KiB
Markdown
### OAuth2 purpose
|
|
|
|
A way for the `user` to tell `google` to give an access to `xorismesiti.gr` app
|
|
|
|
<br>
|
|
|
|
### OAuth2 Frontend/Backend 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>
|
|
|
|
|
|
### OAuth2 Frontend/Backend Flow Details:
|
|
|
|
1. Get Code
|
|
|
|
1. Front **GET** to Google `https://accounts.google.com/o/oauth2` with callback url
|
|
3. 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 `https://xorismesiti.gr/api/auth/profile` using the `cookie`
|
|
2. Back **GET** profile data from Google `https://www.googleapis.com/oauth2/v3/userinfo` using the `access_token` from Front `cookie`
|
|
3. Google **response** to Back with profile data
|
|
4. Back **response** to Front with profile data
|
|
|
|
<br><br>
|
|
|
|
|
|
|
|
# 1. Get Code
|
|
|
|
### 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://xorismesiti.gr/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.
|
|
```
|
|
|
|
### Google **302** to Front
|
|
|
|
```bash
|
|
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
|
|
```
|
|
|
|
*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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<details>
|
|
<summary><h3>Frontend Code</h3></summary>
|
|
|
|
```js
|
|
// 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
|
|
};
|
|
```
|
|
|
|
</details>
|
|
|
|
<br><br><br>
|
|
|
|
|
|
|
|
|
|
|
|
# 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`
|
|
|
|
<details>
|
|
<summary><h3>Frontend HTTP POST Request to Backend</h3></summary>
|
|
|
|
```bash
|
|
POST https://xorismesiti.gr/api/auth/exchange-token
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"code": "AAAABCX4XfWgyVyziyLg0QHHHHH"
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
#### Step 3 takes place here, Backend exchanges the Code with the Tokens
|
|
|
|
<details>
|
|
<summary><h3>Backend HTTP Response to Frontend</h3></summary>
|
|
|
|
```json
|
|
{
|
|
"access_token": "ya29.a0AfH6SMC8Op6zXZkHi2XITkDoOVzYXt3hTY6sny54UlWlxrnKlX5Xv78is7BEHekVX-VoA",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
"refresh_token": "1//04d5XHqmn6Hdy3wTf5OYDP1SyBa74zEFURjddQ2A1cFw78PY13pQyWhlD2A6XhDQtKlrjAqU4kS3vGdMvckw",
|
|
"scope": "email profile"
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>Frontend Code</h3></summary>
|
|
|
|
```js
|
|
// 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;
|
|
```
|
|
|
|
</details>
|
|
|
|
<br><br><br>
|
|
|
|
|
|
|
|
|
|
|
|
# 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.*
|
|
|
|
<details>
|
|
<summary><h3>Backend HTTP POST Request to Google</h3></summary>
|
|
|
|
```sh
|
|
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).
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>Google HTTP Response Backend</h3></summary>
|
|
|
|
```json
|
|
{
|
|
"access_token": "ya29.a0AfH6SMC8Op6zXZkHi2XITkDoOVzYXt3hTY6sny54UlWlxrnKlX5Xv78is7BEHekVX-VoA",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
"refresh_token": "1//04d5XHqmn6Hdy3wTf5OYDP1SyBa74zEFURjddQ2A1cFw78PY13pQyWhlD2A6XhDQtKlrjAqU4kS3vGdMvckw",
|
|
"scope": "email profile"
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>Backend Code:</h3></summary>
|
|
|
|
```js
|
|
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');
|
|
});
|
|
```
|
|
|
|
</details>
|
|
|
|
<br><br><br>
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
<details>
|
|
<summary><h3>Frontend HTTP GET Request to Backend</h3></summary>
|
|
|
|
```bash
|
|
GET https://xorismesiti.gr/api/user-profile
|
|
Authorization: Bearer access-token-from-backend
|
|
```
|
|
|
|
</details>
|
|
|
|
**↴** Step 5 takes place here: The Backend request the user data from Google using the Tokens
|
|
|
|
<details>
|
|
<summary><h3>Backedn HTTP Response to Frontend</h3></summary>
|
|
|
|
```json
|
|
{
|
|
"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"
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>Frontend Code</h3></summary>
|
|
|
|
```js
|
|
// 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();
|
|
```
|
|
|
|
</details>
|
|
|
|
<br><br><br>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
<details>
|
|
<summary><h3>HTTP GET Request</h3></summary>
|
|
|
|
```sh
|
|
GET https://www.googleapis.com/oauth2/v3/userinfo
|
|
Authorization: Bearer ya29.a0AfH6SMC8Op6zXZkHi2XITkDoOVzYXt3hTY6sny54UlWlxrnKlX5Xv78is7BEHekVX-VoA
|
|
```
|
|
|
|
- URL: https://www.googleapis.com/oauth2/v3/userinfo
|
|
- HTTP Method: GET
|
|
- Headers:
|
|
- Authorization: Bearer {access_token}: The access token obtained in step 5.
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>HTTP Response</h3></summary>
|
|
|
|
```json
|
|
{
|
|
"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"
|
|
}
|
|
```
|
|
|
|
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>Backend GET Code</h3></summary>
|
|
|
|
```js
|
|
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');
|
|
});
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>Front GET Code</h3></summary>
|
|
|
|
```js
|
|
// 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);
|
|
});
|
|
```
|
|
</details>
|
|
|
|
<br><br><br>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
|
|
<details>
|
|
<summary><h3>HTTP POST Request</h3></summary>
|
|
|
|
```bash
|
|
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.
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>HTTP Response</h3></summary>
|
|
|
|
```json
|
|
{
|
|
"access_token": "ya29.a0AfH6SMC8Op6zXZkHi2XITkDoOVzYXt3hTY6sny54UlWlxrnKlX5Xv78is7BEHekVX-VoA",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><h3>Example Backend Code:</h3></summary>
|
|
|
|
```js
|
|
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' });
|
|
}
|
|
});
|
|
```
|
|
|
|
</details>
|
|
|
|
|
|
|