Skip to content

Commit

Permalink
chore: support refresh tokens in ssr apps
Browse files Browse the repository at this point in the history
Signed-off-by: Vaibhav Upreti <[email protected]>
  • Loading branch information
VaibhavUpreti committed Dec 31, 2024
1 parent 625b0ed commit 321913c
Show file tree
Hide file tree
Showing 17 changed files with 355 additions and 323 deletions.
18 changes: 6 additions & 12 deletions demo/react-spa-demo/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { useState, useEffect } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import { OAuthClient } from "authkeeper"; // Import OAuthClient class
import { OAuthClient } from "authkeeper";

// Define the OAuth configuration
const config = {
client_id: "PkaNwoOTJGY2rAtNaLr7iR7BznU7wM5o",
// Development
// redirect_uri: "http://localhost:5500", // Update this to your app's URL
// redirect_uri: "http://localhost:5500",
// Production
redirect_uri: "https://authkeeper-spa.vercel.app/",
authorization_url: "https://dev-k6ckdaso3ygmzm7u.us.auth0.com/authorize",
Expand All @@ -20,16 +20,15 @@ function App() {
const [count, setCount] = useState(0);
const [userInfo, setUserInfo] = useState(null);
const [authToken, setAuthToken] = useState(null);
const [refreshMessage, setRefreshMessage] = useState(""); // State to show refresh token message
const [expiresIn, setExpiresIn] = useState(null); // State to display the expires_in time
const [refreshMessage, setRefreshMessage] = useState("");
const [expiresIn, setExpiresIn] = useState(null);

const oauthClient = new OAuthClient(config);

const initiateAuthFlow = () => {
oauthClient.startAuthFlow();
};

// Handle the OAuth callback, exchange the code for a token, and fetch user information
const fetchUserData = async (code) => {
const token = await oauthClient.exchangeAuthCodeForToken(code);
if (token) {
Expand All @@ -45,12 +44,10 @@ function App() {
setAuthToken(refreshedToken);
setRefreshMessage("Token successfully refreshed!");

// Save the new token and its expiration time in localStorage
const expirationTime = new Date().getTime() + refreshedToken.expires_in * 1000; // Convert seconds to milliseconds
const expirationTime = new Date().getTime() + refreshedToken.expires_in * 1000;
localStorage.setItem("authToken", JSON.stringify(refreshedToken));
localStorage.setItem("tokenExpiration", expirationTime.toString());

// Update the expiresIn state
setExpiresIn(refreshedToken.expires_in);
} else {
setRefreshMessage("Failed to refresh token.");
Expand All @@ -67,23 +64,20 @@ function App() {
setUserInfo(JSON.parse(storedUser));
}

// Check for the stored token and its expiration time
const storedToken = localStorage.getItem("authToken");
const storedExpiration = localStorage.getItem("tokenExpiration");

if (storedToken && storedExpiration) {
const currentTime = new Date().getTime();
if (currentTime < storedExpiration) {
setAuthToken(JSON.parse(storedToken));
setExpiresIn((storedExpiration - currentTime) / 1000); // Convert ms to seconds
setExpiresIn((storedExpiration - currentTime) / 1000);
} else {
// Token expired, clear from localStorage
localStorage.removeItem("authToken");
localStorage.removeItem("tokenExpiration");
}
}

// Handle the OAuth callback after redirect with code in URL
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
if (code) {
Expand Down
1 change: 1 addition & 0 deletions demo/react-ssr-demo
Submodule react-ssr-demo added at f47e79
230 changes: 215 additions & 15 deletions demo/ssr-express-demo/index.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,242 @@
import express from 'express';
import * as authkeeper from 'authkeeper';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';

// Load environment variables from the .env file
dotenv.config();

const { OAuthClient } = authkeeper; // Destructure OAuthClient

const { OAuthClient } = authkeeper;
const app = express();
app.use(cookieParser());


// OAuth Configuration using process.env
const oauthConfig = {
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET_KEY,
redirect_uri: process.env.CLIENT_DOMAIN,
redirect_uri: process.env.REDIRECT_URI,
authorization_url: process.env.AUTHORIZATION_URL,
token_url: process.env.TOKEN_URL,
scope: process.env.SCOPE,
};

// Initialize OAuthClient with configuration
const oauthClient = new OAuthClient(oauthConfig);

app.get('/', (req, res) => {
res.send('Hello World!')
})
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OAuth Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
background-color: #f7f7f7;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
text-align: center;
background-color: white;
padding: 40px 60px;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
box-sizing: border-box;
}
h1 {
font-size: 36px;
font-weight: 600;
color: #20232a;
margin-bottom: 30px;
letter-spacing: 0.5px;
}
.description {
font-size: 18px;
color: #616161;
margin-bottom: 20px;
line-height: 1.5;
}
.button {
padding: 15px 30px;
font-size: 18px;
color: white;
background-color: #61dafb;
border: none;
border-radius: 30px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
display: inline-block;
text-decoration: none;
}
.button:hover {
background-color: #21a1f1;
transform: translateY(-2px);
}
.button:focus {
outline: none;
}
.button:active {
transform: translateY(2px);
}
.link {
text-decoration: none;
}
.footer {
margin-top: 20px;
color: #616161;
font-size: 14px;
}
.footer a {
color: #61dafb;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
@media (max-width: 480px) {
.container {
padding: 30px 20px;
}
h1 {
font-size: 30px;
}
.description {
font-size: 16px;
}
.button {
font-size: 16px;
padding: 12px 25px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to OAuth Demo!</h1>
<p class="description">
This is a simple demonstration of OAuth 2.0 authentication flow. Explore how it works with the buttons below.
</p>
<div>
<a href="/authorize" class="link">
<button class="button">Visit /authorize</button>
</a>
</div>
<div style="margin-top: 20px;">
<a href="/refresh-token" class="link">
<button class="button">Refresh Token</button>
</a>
</div>
<div class="footer">
<p>Powered by <a href="https://reactjs.org" target="_blank">React</a></p>
</div>
</div>
</body>
</html>
`);
});


app.get('/authorize', async (req, res) => {
try {
const authUrl = await oauthClient.startAuthFlow(oauthConfig);

res.redirect(authUrl);
} catch (error) {
console.error('Error starting auth flow:', error);
res.status(500).send('Error starting auth flow');
}
});


app.get('/callback', async (req, res) => {
const { code, state } = req.query;

// // Should verify that the state matches the one you sent during the auth flow (to prevent CSRF attacks)
// if (state !== 'someState') { // Replace 'someState' with the actual state you generated earlier
// return res.status(400).send('State mismatch, possible CSRF attack');
// }

if (code) {
try {
const tokenData = await oauthClient.exchangeAuthCodeForToken(code);
res.cookie('access_token', tokenData.access_token, { httpOnly: true });
res.cookie('id_token', tokenData.id_token, { httpOnly: true });
res.cookie('refresh_token', tokenData.refresh_token, { httpOnly: true });
res.cookie('expires_in', tokenData.expires_in, { httpOnly: true });

app.get('/authorize', (req, res) => {
// Assume startAuthFlow is a method in authkeeper to get the authorization URL
const authUrl = oauthClient.startAuthFlow(); // This should return the OAuth provider's authorization URL
res.redirect(authUrl); // Redirect to OAuth provider's authorization URL
res.redirect("/user")
} catch (error) {
console.error('Error exchanging authorization code for access token:', error);
res.status(500).send('Error obtaining access token');
}
} else {
res.status(400).send('Authorization code not found in the request.');
}
});

// Start the server

app.get('/refresh-token', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) {
return res.status(400).send('No refresh token found in cookies');
}
try {
if (refreshToken) {
const tokenData = await oauthClient.refreshAccessToken(refreshToken);
res.cookie('access_token', tokenData.access_token, { httpOnly: true });
res.cookie('id_token', tokenData.id_token, { httpOnly: true });
res.cookie('refresh_token', tokenData.refresh_token, { httpOnly: true });
res.cookie('expires_in', tokenData.expires_in, { httpOnly: true });
res.redirect('/user?success=true');
} else {
res.status(400).send('Failed to refresh the token');
}
} catch (error) {
console.error('Error refreshing token:', error);
res.status(500).send('Error refreshing token');
}
});


app.get("/user", async (req, res) => {
try {
const cookies = req.cookies;
res.status(200).json({
message: "Cookies successfully retrieved",
cookies: cookies,
});
} catch (error) {
console.error("Error fetching cookies:", error);
res.status(500).json({
message: "An error occurred while retrieving cookies",
error: error.message,
});
}
});


const port = 5500;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
});
29 changes: 25 additions & 4 deletions demo/ssr-express-demo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion demo/ssr-express-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"authkeeper": "^1.2.3",
"authkeeper": "^1.2.4",
"cookie-parser": "^1.4.7",
"crypto": "^1.0.1",
"dotenv": "^16.4.7",
"express": "^4.21.2",
Expand Down
Loading

0 comments on commit 321913c

Please sign in to comment.