Implementing JWT (JSON Web Token) Authentication using Flask
A Step-by-Step Guide to Adding JWT Authentication in Flask
Concept behind Implementation?
After logging in, you need to return both the access token and refresh token in the response.
There are two ways in which you can return these tokens.
a. in response headers
b. in HTTP-only cookies
If you are retuning tokens in response headers (
Authorization
,Refresh-Token
), you need to manually extract tokens from response headers, store them in localStorage or sessionStorage and include them in request headers for the protected routes.Instead of manually extracting tokens from response headers, a more secure approach is to send them in HTTP-only cookies. This way:
The browser automatically includes the token in each request.
You don’t need to manually set headers for every request.
JavaScript cannot access the tokens (better security).
Final Conclusion: If security is important, use HTTP-only cookies. If you prefer manual control, extract tokens from response headers and store them in localStorage or sessionStorage.
In this article, I will show you both approaches: returning the access token and refresh token in response headers and in HTTP-only cookies.
Now, let’s create a simple web app. Our app will look like:-
Flow of the app will be:-
When you visit
/
or/home
, you will land at the home page./about
and/contact
are protected endpoints. When you visit/about
or/contact
If you are not signed-in, you will be redirected to login page. After successful login, you will get access token and refresh token, and you will return to the page from where you have come from.
If you are a signed-in user, you will be redirected to the respective page.
When you visit
/products
, you will arrive at the products page. No sign-in is required to access this page.When you click on the
Sign In
button, a sign-in form will open. After successful login, you will get access token and refresh token, and you will be redirected to the Home page.Clicking the
Log Out
button expires the tokens and logs the user out of the web app.If you stay inactive on the app for more than 5 minutes, you will be redirected to the User Inactive page and you will need to generate access token again using refresh token.
If you are actively using the app (meaning no user inactivity), the access token will expire automatically after 10 minutes, and you will need to generate it again to access any of the protected routes.
UI will be created using React JS and backend using Flask.
Now open VS Code and set up the folders and files as shown below:-
The backend
folder contains a Python file named app.py
, which includes the Flask code.
app.py (returning tokens in response headers)
from flask import Flask, request, jsonify
from flask_jwt_extended import (
JWTManager, create_access_token, create_refresh_token,
jwt_required, get_jwt_identity
)
from flask_cors import CORS
app = Flask(__name__)
# Enable CORS
CORS(app, resources={r"/*": {"origins": "http://localhost:3000"}}) # Allow frontend origin
# Secret key for signing JWTs
app.config["JWT_SECRET_KEY"] = "supersecretkey"
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = 600 # Access token expires in 10 minutes
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = 86400 # Refresh token expires in 1 day
jwt = JWTManager(app)
@app.route("/")
def index():
return "Hello World"
# Login route to generate tokens
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
username = data.get("username")
password = data.get("password")
# Mock user data (in real apps, fetch from a database)
users = {"testuser": "password123"}
if (users.get(username) == None) or (users.get(username) != password):
return jsonify({"msg": "Invalid credentials"}), 401
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
response = jsonify({"msg": "Login successful"})
# send access token and refresh token in response header
response.headers["Authorization"] = f"Bearer {access_token}"
response.headers["Refresh-Token"] = f"Bearer {refresh_token}"
# make above headers available to javascript
response.headers.add("Access-Control-Expose-Headers","Authorization")
response.headers.add("Access-Control-Expose-Headers","Refresh-Token")
return response, 200
# Protected route (requires access token)
@app.route("/protected", methods=["GET"])
@jwt_required()
def protected():
current_user = get_jwt_identity()
return jsonify({"logged_in_as": current_user}), 200
# Refresh token route (To Get New Access Token)
@app.route("/refresh", methods=["GET"])
@jwt_required(refresh=True)
def refresh():
current_user = get_jwt_identity()
new_access_token = create_access_token(identity=current_user)
response = jsonify({"msg": "New Acccess Token Generated Successfully"})
# send new access token
response.headers["Authorization"] = f"Bearer {new_access_token}"
# make above header available to javascript
response.headers.add("Access-Control-Expose-Headers","Authorization")
return response, 200
if __name__ == "__main__":
app.run(debug=True)
app.py (returning tokens in HTTP-only cookies)
from flask import Flask, request, jsonify
from flask_jwt_extended import (
JWTManager, create_access_token, create_refresh_token,
jwt_required, get_jwt_identity
)
from flask_cors import CORS
app = Flask(__name__)
# Enable CORS with credentials support
CORS(app, supports_credentials=True, resources={r"/*": {"origins": "http://localhost:3000"}}) # Allow frontend origin
# Secret key for signing JWTs
app.config["JWT_SECRET_KEY"] = "supersecretkey"
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = 600 # Access token expires in 10 minutes
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = 86400 # Refresh token expires in 1 day
app.config["JWT_TOKEN_LOCATION"] = ["cookies"] # Allow JWTs from cookies
jwt = JWTManager(app)
@app.route("/")
def index():
return "Hello World"
# Login route to generate tokens
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
username = data.get("username")
password = data.get("password")
# Mock user data (in real apps, fetch from a database)
users = {"testuser": "password123"}
if (users.get(username) == None) or (users.get(username) != password):
return jsonify({"msg": "Invalid credentials"}), 401
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
response = jsonify({"msg": "Login successful"})
# send access token and refresh token in HTTP-only cookie
response.set_cookie("access_token_cookie", access_token, httponly=True, samesite="None", secure=True) # Secure cookie
response.set_cookie("refresh_token_cookie", refresh_token, httponly=True, samesite="None", secure=True) # Secure cookie
return response, 200
# Protected route (requires access token)
@app.route("/protected", methods=["GET"])
@jwt_required() # Automatically reads JWT from cookies
def protected():
current_user = get_jwt_identity()
return jsonify({"logged_in_as": current_user}), 200
# Refresh token route (To Get New Access Token)
@app.route("/refresh", methods=["GET"])
@jwt_required(refresh=True)
def refresh():
current_user = get_jwt_identity()
new_access_token = create_access_token(identity=current_user)
response = jsonify({"msg": "New Acccess Token Generated Successfully"})
# send new access token in HTTP-only cookie
response.set_cookie("access_token_cookie", new_access_token, httponly=True, samesite="None", secure=True) # Secure cookie
return response, 200
@app.route('/logout')
def logout():
# Read the HTTP-only cookie
if (request.cookies.get("access_token_cookie") and request.cookies.get("refresh_token_cookie")):
response = jsonify({"msg": "Tokens removed from cookies"})
response.set_cookie('access_token_cookie', '', expires=0, max_age=0, httponly=True, samesite="None", secure=True)
response.set_cookie('refresh_token_cookie', '', expires=0, max_age=0, httponly=True, samesite="None", secure=True)
else:
response = jsonify({"msg": "No Tokens are present in cookies"})
return response
if __name__ == "__main__":
app.run(debug=True)
The myapp
folder has been created using create-react-app
command. Below is the directory structure after deleting unnecessary files.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
</head>
<title>Web App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
App.js
import { BrowserRouter, Routes, Route } from "react-router-dom";
import './App.css';
import Navbar from './components/Navbar';
import Home from './components/Home';
import About from './components/About';
import Contact from './components/Contact';
import Products from './components/Products';
import LoginForm from "./components/LoginForm";
import SessionTimeout from "./components/SessionTimeout";
import { useEffect, useState } from "react";
function App() {
const [isSessionTimedOut, setIsSessionTimedOut] = useState(false);
function implementSessionTimeOut() {
let idleTime = 0;
function resetTimer() {
idleTime = 0;
}
function checkIdleTime() {
idleTime++;
if (idleTime > 5 * 60) { // 5 minutes (300 seconds)
setIsSessionTimedOut(true)
}
}
// Reset timer on user activity
window.onload = resetTimer;
document.onmousemove = resetTimer;
document.onkeydown = resetTimer;
document.onkeyup = resetTimer;
document.onscroll = resetTimer;
document.onclick = resetTimer;
// Check idle time every second
setInterval(checkIdleTime, 1000);
}
useEffect(implementSessionTimeOut, []);
return (
isSessionTimedOut?<SessionTimeout textToDisplay='User Inactive' />:
<BrowserRouter>
<Navbar />
<hr />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/products" element={<Products />} />
<Route path="/login" element={<LoginForm />} />
<Route path="/session-timeout" element={<SessionTimeout textToDisplay='Session Timed Out' />} />
</Routes>
</BrowserRouter>
);
}
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
App.css
.navbar {
display: flex;
align-items: baseline;
column-gap: 15px;
background-color: antiquewhite;
}
.app-name {
margin: 0;
}
.nav-link {
text-decoration: none;
}
.active {
text-decoration: underline;
}
About.js (returning tokens in response headers)
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom';
export default function About() {
const [jsx, setJsx] = useState(null);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const accessToken = localStorage.getItem("accessToken");
if (!accessToken) {
localStorage.setItem("redirectAfterLogin", location.pathname); // Store original path
navigate("/login");
return;
}
fetch("http://127.0.0.1:5000/protected", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${accessToken}`, // Send token in request header
},
})
.then((response) => response.json())
.then((data) => {
if (data.msg === "Token has expired") {
localStorage.setItem("redirectAfterLogin", location.pathname);
navigate("/session-timeout");
}
else {
setJsx(
<div>
<h2>You are at About Page!!</h2>
</div>
)
}
})
.catch((error) => console.error("Error:", error));
}, [navigate, location.pathname])
return (
jsx
)
}
About.js (returning tokens in HTTP-only cookies)
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom';
export default function About() {
const [jsx, setJsx] = useState(null);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
fetch("http://127.0.0.1:5000/protected", {
method: "GET",
credentials: "include", // Ensures cookies are sent
headers: {
"Content-Type": "application/json"
},
})
.then((response) => response.json())
.then((data) => {
if (data.msg === "Token has expired") {
localStorage.setItem("redirectAfterLogin", location.pathname);
navigate("/session-timeout");
}
else if (data.msg === `Missing cookie "access_token_cookie"`) {
localStorage.setItem("redirectAfterLogin", location.pathname);
navigate("/login");
}
else {
setJsx(
<div>
<h2>You are at About Page!!</h2>
</div>
)
}
})
.catch((error) => console.error("Error:", error));
}, [navigate, location.pathname])
return (
jsx
)
}
Contact.js (returning tokens in response headers)
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom';
export default function About() {
const [jsx, setJsx] = useState(null);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const accessToken = localStorage.getItem("accessToken");
if (!accessToken) {
localStorage.setItem("redirectAfterLogin", location.pathname); // Store original path
navigate("/login");
return;
}
fetch("http://127.0.0.1:5000/protected", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${accessToken}`, // Send token in request header
},
})
.then((response) => response.json())
.then((data) => {
if (data.msg === "Token has expired") {
localStorage.setItem("redirectAfterLogin", location.pathname);
navigate("/session-timeout");
}
else {
setJsx(
<div>
<h2>You are at Contact Page!!</h2>
</div>
)
}
})
.catch((error) => console.error("Error:", error));
}, [navigate, location.pathname])
return (
jsx
)
}
Contact.js (returning tokens in HTTP-only cookies)
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom';
export default function Contact() {
const [jsx, setJsx] = useState(null);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
fetch("http://127.0.0.1:5000/protected", {
method: "GET",
credentials: "include", // Ensures cookies are sent
headers: {
"Content-Type": "application/json"
},
})
.then((response) => response.json())
.then((data) => {
if (data.msg === "Token has expired") {
localStorage.setItem("redirectAfterLogin", location.pathname);
navigate("/session-timeout");
}
else if (data.msg === `Missing cookie "access_token_cookie"`) {
localStorage.setItem("redirectAfterLogin", location.pathname);
navigate("/login");
}
else {
setJsx(
<div>
<h2>You are at Contact Page!!</h2>
</div>
)
}
})
.catch((error) => console.error("Error:", error));
}, [navigate, location.pathname])
return (
jsx
)
}
Home.js
import React from 'react'
export default function Home() {
return (
localStorage.removeItem("redirectAfterLogin");
<div>
<h2>You are at the Home Page!!</h2>
</div>
)
}
LoginForm.js (returning tokens in response headers)
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom';
export default function LoginForm() {
const navigate = useNavigate();
const [formData, setFormData] = useState({
username: "",
password: "",
});
// Handle input changes
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
// Handle form submission
const handleLogin = (e) => {
e.preventDefault();
fetch("http://127.0.0.1:5000/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
})
.then((response) => {
const accessToken = response.headers.get("Authorization"); // Get access token from response header
const refreshToken = response.headers.get("Refresh-Token"); // Get refresh token from response header
if (accessToken) {
localStorage.setItem("accessToken", accessToken.split(' ')[1]); // Store access token in localStorage
}
if (refreshToken) {
localStorage.setItem("refreshToken", refreshToken.split(' ')[1]); // Store refresh token in localStorage
}
return response.json();
})
.then((data) => {
if (data.msg === "Login successful") {
// Retrieve the stored path or default to home
const redirectPath = localStorage.getItem("redirectAfterLogin") || "/";
// Clear the stored path (to prevent future unnecessary redirects)
localStorage.removeItem("redirectAfterLogin");
// Redirect to the stored path
navigate(redirectPath);
}
else if (data.msg === "Invalid credentials") {
alert(data.msg);
}
})
.catch((error) => console.error("Login Error:", error));
}
return (
<div>
<h3>Please fill the below form to sign-in!!</h3>
<form onSubmit={handleLogin}>
<div>
<label htmlFor="username">Username</label>
<input type="text" id="username" name="username" onChange={handleChange} />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" onChange={handleChange} />
</div>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
LoginForm.js (returning tokens in HTTP-only cookies)
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom';
export default function LoginForm() {
const navigate = useNavigate();
const [formData, setFormData] = useState({
username: "",
password: "",
});
// Handle input changes
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
// Handle form submission
const handleLogin = (e) => {
e.preventDefault();
fetch("http://127.0.0.1:5000/login", {
method: "POST",
credentials: "include", // Ensures cookies are received
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
})
.then((response) => response.json())
.then((data) => {
if (data.msg === "Login successful") {
// Retrieve the stored path or default to home
const redirectPath = localStorage.getItem("redirectAfterLogin") || "/";
// Clear the stored path (to prevent future unnecessary redirects)
localStorage.removeItem("redirectAfterLogin");
// Redirect to the stored path
navigate(redirectPath);
}
else if (data.msg === "Invalid credentials") {
alert(data.msg);
}
})
.catch((error) => console.error("Login Error:", error));
}
return (
<div>
<h3>Please fill the below form to sign-in!!</h3>
<form onSubmit={handleLogin}>
<div>
<label htmlFor="username">Username</label>
<input type="text" id="username" name="username" onChange={handleChange} />
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" onChange={handleChange} />
</div>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
)
}
Navbar.js (returning tokens in response headers)
import React from 'react'
import { NavLink, useNavigate } from 'react-router-dom'
export default function Navbar() {
const navigate = useNavigate();
const handleLogOut = () => {
if (localStorage.getItem("accessToken") && localStorage.getItem("refreshToken")) {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
navigate("/");
}
else {
//do nothing
}
}
return (
<nav className="navbar">
<h2 className="app-name">My App</h2>
<NavLink to="/" className="nav-link">Home</NavLink>
<NavLink to="/about" className="nav-link">About</NavLink>
<NavLink to="/contact" className="nav-link">Contact</NavLink>
<NavLink to="/products" className="nav-link">Products</NavLink>
<NavLink to="/login" className="nav-link"><button>Sign In</button></NavLink>
<button onClick={handleLogOut}>Log Out</button>
</nav>
)
}
Navbar.js (returning tokens in HTTP-only cookies)
import React from 'react'
import { NavLink, useNavigate } from 'react-router-dom'
export default function Navbar() {
const navigate = useNavigate();
const handleLogOut = () => {
fetch("http://127.0.0.1:5000/logout", {
method: "GET",
credentials: "include", // Ensures cookies are sent
headers: {
"Content-Type": "application/json"
},
})
.then((response) => response.json())
.then((data) => {
if (data.msg === "Tokens removed from cookies") {
navigate("/");
}
else if (data.msg === "No Tokens are present in cookies") {
// do nothing
}
})
.catch((error) => console.error("Error:", error));
}
return (
<nav className="navbar">
<h2 className="app-name">My App</h2>
<NavLink to="/" className="nav-link">Home</NavLink>
<NavLink to="/about" className="nav-link">About</NavLink>
<NavLink to="/contact" className="nav-link">Contact</NavLink>
<NavLink to="/products" className="nav-link">Products</NavLink>
<NavLink to="/login" className="nav-link"><button>Sign In</button></NavLink>
<button onClick={handleLogOut}>Log Out</button>
</nav>
)
}
Products.js
import React from 'react'
export default function Products() {
localStorage.removeItem("redirectAfterLogin");
return (
<div>
<h3>Below are the products that we offer.</h3>
<ul>
<li>Sofas</li>
<li>Chairs</li>
<li>Tables</li>
<li>Swings</li>
</ul>
</div>
)
}
SessionTimeout.js (returning tokens in response headers)
import React from 'react'
export default function SessionTimeout({ textToDisplay }) {
const handleBtnClick = () => {
const refreshToken = localStorage.getItem("refreshToken");
if (!refreshToken) {
window.location.href = "/";
return
}
fetch("http://127.0.0.1:5000/refresh", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${refreshToken}`, // Send refresh token in request header
},
})
.then((response) => {
const accessToken = response.headers.get("Authorization"); // Get access token from response header
localStorage.setItem("accessToken", accessToken.split(' ')[1]); // Store access token in localStorage
return response.json();
})
.then((data) => {
if (data.msg === "New Acccess Token Generated Successfully") {
// Retrieve the stored path or default to home
const redirectPath = localStorage.getItem("redirectAfterLogin") || "/";
// Clear the stored path (to prevent future unnecessary redirects)
localStorage.removeItem("redirectAfterLogin");
// Redirect to the stored path
window.location.href = redirectPath;
}
})
.catch((error) => console.error("Error:", error));
}
return (
<div>
<h3>{ textToDisplay }!!</h3>
<button onClick={handleBtnClick}>Click to Continue</button>
</div>
)
}
SessionTimeout.js (returning tokens in HTTP-only cookies)
import React from 'react'
export default function SessionTimeout({ textToDisplay }) {
const handleBtnClick = () => {
fetch("http://127.0.0.1:5000/refresh", {
method: "GET",
credentials: "include", // Ensures cookies are sent
headers: {
"Content-Type": "application/json"
},
})
.then((response) => {
return response.json();
})
.then((data) => {
if (data.msg === "New Acccess Token Generated Successfully") {
// Retrieve the stored path or default to home
const redirectPath = localStorage.getItem("redirectAfterLogin") || "/";
// Clear the stored path (to prevent future unnecessary redirects)
localStorage.removeItem("redirectAfterLogin");
// Redirect to the stored path
window.location.href = redirectPath;
}
else if (data.msg === `Missing cookie "refresh_token_cookie"`) {
window.location.href = '/';
}
})
.catch((error) => console.error("Error:", error));
}
return (
<div>
<h3>{textToDisplay}!!</h3>
<button onClick={handleBtnClick}>Click to Continue</button>
</div>
)
}
Run flask server and run React app. Make sure Flask is running at http://127.0.0.1:5000/
. Ensure your React app runs on a different port (e.g., http://localhost:3000/
).
When you log in (username = testuser; password = password123), you will receive an access token and a refresh token. Automatically, the access token expires in 10 minutes, and the refresh token expires after 1 day. You can easily access protected routes. After 10 minutes, you won't be able to access protected routes because your access token will have expired. To access protected routes again, you need to generate a new access token (by clicking on Click to Continue
button in session time out page) using the refresh token.
Thank you for your time! 😊
Connect with me on LinkedIn