In flask
, adding authentication has been made quite easy with the @login_required
decorator in the flask extension Flask-login
. I have an article on how to add basic authentication to your flask application that you can read up on here
However, since you will be working with API endpoints you can't use the approach above because the @login_required
decorator redirects to the application to an HTML page
when it discovers a user that is not authenticated trying to access a protected page. This defeats the idea of creating API endpoints as APIs are only designed to return data in json
format.
In this part of the series, you'll be learning how to add authentication to the connected React and Flask application you built in the previous part of the series. Authentication will be done with the flask extension: flask-jwt-extended
Prerequisites
1) Beginner-level understanding of the flask framework. If you are new to Flask
you can check out my article on how to set up your flask project and use it with the jinja template engine.
2) I strongly advise you to read the previous article. You can also get the files in the Github repo.
3) Familiarity with the basics of ReactJs
. You will be making use of the useState
hook, fetching data from API endpoints using axios
and also using react-router-dom
to handle routing of components.
Let's get started!!
Flask Backend
Installing the flask extension.
Navigate into the backend
directory and run:
pip install flask-jwt-extended
note: If you cloned the repo, you don't need to run the command above, just set up your flask application with the instructions in the README.md
file.
base.py
You'll be adding authentication to the /profile
API endpoint created in the previous tutorial. Navigate to the base.py
script you created in the backend directory of your application to create the token(login) and logout API endpoints.
token(login) API endpoint
import json
from flask import Flask, request, jsonify
from datetime import datetime, timedelta, timezone
from flask_jwt_extended import create_access_token,get_jwt,get_jwt_identity, \
unset_jwt_cookies, jwt_required, JWTManager
api = Flask(__name__)
api.config["JWT_SECRET_KEY"] = "please-remember-to-change-me"
jwt = JWTManager(api)
@api.route('/token', methods=["POST"])
def create_token():
email = request.json.get("email", None)
password = request.json.get("password", None)
if email != "test" or password != "test":
return {"msg": "Wrong email or password"}, 401
access_token = create_access_token(identity=email)
response = {"access_token":access_token}
return response
@api.route('/profile')
def my_profile():
response_body = {
"name": "Nagato",
"about" :"Hello! I'm a full stack developer that loves python and javascript"
}
return response_body
Let's go through the code above:
First, the required functions are imported from the installed flask_jwt_extended
extension.
from flask_jwt_extended import create_access_token,get_jwt,get_jwt_identity, \
unset_jwt_cookies, jwt_required, JWTManager
Next, the flask application instance is configured with the JWT
secret key then passed as an argument to the JWTManager
function and assigned to the jwt
variable.
api.config["JWT_SECRET_KEY"] = "please-remember-to-change-me"
jwt = JWTManager(api)
The token
API endpoint will have a POST
request method. Whenever the user submits a login request, the email and password are extracted and compared with the hardcoded email(test) and password(test). Please note that in an ideal scenario you are going to compare the extracted login details with data in your database.
If the login details are not correct, the error message Wrong email or password
with the status code 401
which means UNAUTHORIZED Error
is sent back to the user.
return {"msg": "Wrong email or password"}, 401
Else if the login details are confirmed to be correct, an access token is created for that particular email address by assigning the email
to the identity
variable. Finally, the token is returned to the user.
access_token = create_access_token(identity=email)
response = {"access_token":access_token}
return response
To test this, start your backend server with
npm run start-backend
Please note that the command above was specified in the package.json
file in the react frontend. This was done in the previous part of the series. If you have not checked it out yet, please head there so you can learn how to set it up. However if you have already cloned the repo, let's proceed.
Next, open up postman and send a POST
request to this API endpoint:
http://127.0.0.1:5000/token
You'll get a 500 internal server
error 👇
Check your terminal and you'll see the error as well 👇
AttributeError: 'NoneType' object has no attribute 'get'
the error occurred because you did not specify the login details when you made the POST
request to the API endpoint thus a None
value was passed as an argument to the request.json.get
function.
Return to POSTMAN
and pass the login details along with the POST
request.
Please ensure you adjust your settings as circled in the image above.
After making the request you should get your access token in the form:
"access_token":"your access token will be here"
You can try to pass in a wrong email or password to see the 401 UNAUTHORIZED error
Logout API endpoint
@api.route("/logout", methods=["POST"])
def logout():
response = jsonify({"msg": "logout successful"})
unset_jwt_cookies(response)
return response
When the logout
API endpoint is called, response
is passed to the unset_jwt_cookies
function which deletes the cookies containing the access token for the user and finally returns the success message to the user.
Head over to Postman
once again and make a POST request to the logout
API endpoint:
http://127.0.0.1:5000/logout
You should get the response below 👇
Refreshing tokens
The generated token always has a lifespan
after which it expires. To ensure that this does not happen while the user is logged in, you have to create a function that refreshes the token when it is close to the end of its lifespan.
First, specify the lifespan
for your generated tokens and add it as a new configuration for your application.
Note:You can change the time to suit your application.
api.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
Then, create the function below 👇, above the create_token
function:
@api.after_request
def refresh_expiring_jwts(response):
try:
exp_timestamp = get_jwt()["exp"]
now = datetime.now(timezone.utc)
target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
if target_timestamp > exp_timestamp:
access_token = create_access_token(identity=get_jwt_identity())
data = response.get_json()
if type(data) is dict:
data["access_token"] = access_token
response.data = json.dumps(data)
return response
except (RuntimeError, KeyError):
# Case where there is not a valid JWT. Just return the original respone
return response
The after_request
decorator ensures that the refresh_expiring_jwts
function runs after a request has been made to the protected API endpoint /profile
. The function takes as an argument, the response from the /profile
API call.
Then, the current expiry timestamp for the user's token is obtained and compared with the specified timestamp
for the token which is set at 30 minutes. You can change this as well.
If the expiry timestamp for the user's token happens to be 30minutes away from expiration, the token for that user is changed to a new one with the specified 1hr lifespan, and the new token is appended to the response returned to the user. But if the token is not close to expiration, the original response is sent to the user.
To conclude the backend setup, you need to add the @jwt_required()
decorator to the my_profile
function to prevent unauthenticated users from making requests to the API endpoint. But first, test the /profile
API endpoint by making a GET
request to the URL below using Postman
:
http://127.0.0.1:5000/profile
You should still get the json form of the dictionary created in the last article.
Next, add the @jwt_required()
decorator
@api.route('/profile')
@jwt_required() #new line
def my_profile():
response_body = {
"name": "Nagato",
"about" :"Hello! I'm a full stack developer that loves python and javascript"
}
return response_body
and try to make the API request to the /profile
endpoint using the URL above. You'll get a 401 UNAUTHORIZED error
because the token was absent when you made the request.
After the user logs in and gets the assigned token, the token needs to be sent with each call the user makes to the API endpoints in the backend as an Authorization Header
in this format:
Authorization: Bearer <access_token>
Before you head over to the frontend, you can also test this on Postman
by adding the user's token to the Authorization header before you call the protected \profile
API endpoint.
Make a POST
request to the endpoint below to get your token and copy it out.
http://127.0.0.1:5000/token
Next, add the authorization
header key with your token
as its value and then send the GET
request, you should get a json response containing the dictionary with your name and about_me info.
Congratulations you have successfully added authentication to your API endpoint. After the changes and additions, this should be the final look of the base.py
script.
import json
from flask import Flask, request, jsonify
from datetime import datetime, timedelta, timezone
from flask_jwt_extended import create_access_token,get_jwt,get_jwt_identity, \
unset_jwt_cookies, jwt_required, JWTManager
api = Flask(__name__)
api.config["JWT_SECRET_KEY"] = "please-remember-to-change-me"
api.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
jwt = JWTManager(api)
@api.after_request
def refresh_expiring_jwts(response):
try:
exp_timestamp = get_jwt()["exp"]
now = datetime.now(timezone.utc)
target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
if target_timestamp > exp_timestamp:
access_token = create_access_token(identity=get_jwt_identity())
data = response.get_json()
if type(data) is dict:
data["access_token"] = access_token
response.data = json.dumps(data)
return response
except (RuntimeError, KeyError):
# Case where there is not a valid JWT. Just return the original respone
return response
@api.route('/token', methods=["POST"])
def create_token():
email = request.json.get("email", None)
password = request.json.get("password", None)
if email != "test" or password != "test":
return {"msg": "Wrong email or password"}, 401
access_token = create_access_token(identity=email)
response = {"access_token":access_token}
return response
@api.route("/logout", methods=["POST"])
def logout():
response = jsonify({"msg": "logout successful"})
unset_jwt_cookies(response)
return response
@api.route('/profile')
@jwt_required()
def my_profile():
response_body = {
"name": "Nagato",
"about" :"Hello! I'm a full stack developer that loves python and javascript"
}
return response_body
Now you can head over to the react frontend where you'll be making the API endpoint calls.
React Frontend
In the last article, you only had to make a few changes to the App.js
file. But this time around major changes will be made and new components will also be created.
In the frontend, a Login
component that will hold the login page will be created. This component will be rendered anytime it notices that an unauthenticated user is trying to access a page that contains a protected API endpoint. This will ensure that any request made to the backend has a token appended to it.
To start with, create a new directory components
in the src
directory and in it, four new components Login.js
, useToken.js
, Header.js
and Profile.js
. Then navigate back to the base directory and install react-router-dom
before you go into the components:
npm install react-router-dom
Storage of token in the frontend
The token generated from the backend needs to be stored in your web browser after you log in. Presently, that is not the case. Whenever a user refreshes his browser page, the token gets deleted and the user would be prompted to log in once again.
To fix this, you'll need to make use of web storage objects: localStorage
or sessionStorage
. You can read more on that here.
i)sessionStorage: The user's token gets stored in the tab currently opened in the browser. If the user refreshes the page, the token is still retained. However, if the user opens a new tab to the same page in the web browser, the token won't reflect on that page as the new tab doesn't share the same storage with the previous one. Thus, the user would be prompted to log in again.
To see this in action, open any website of your choice and open up the Developer tools
menu with the Inspect Element
or Inspect
option by right-clicking on any page in your browser. You can also see the web storage under the Application
section.
Open up your console and store an object sample in the web storage using the sessionStorage function.
sessionStorage.setItem('test', 53)
Then to get the value 53
assigned to the key test
above run:
sessionStorage.getItem('test')
Refresh the page and run the getItem
function again, you'll still get the value from the storage.
Now, open the link to the same page you just worked with, in a new tab, and try to access the stored object value via the console:
sessionStorage.getItem('test')
You'll get a null
value because the current tab doesn't have access to the storage of the previous tab.
note: while carrying out all the tests above, keep an eye on the changes occurring in the web storage
section above your console
.
ii)localStorage: Here, the user's token get's stored in universal storage that can be accessed by all tabs and browser windows. The token is still retained even if the user refreshes or closes the page, creates a new tab or window, or restarts the browser entirely.
localStorage.setItem('test', 333)
Then to get the assigned value 333
:
localStorage.getItem('test')
Try to run the duplicate test done above, you'll notice that you can access the value from the duplicated page. You can also create a new browser window, open any page of the same website and try to access the value set above. You'll notice that you still have access to it. That is the beauty of using localStorage
, it ensures that the user only needs to log in once and they can easily navigate to any page on the website.
Whenever you are done, you can delete the object from the storage using:
localStorage.removeItem("token")
useToken.js
Now you need to replicate what was done above in your react code. Open the useToken
component.
import { useState } from 'react';
function useToken() {
function getToken() {
const userToken = localStorage.getItem('token');
return userToken && userToken
}
const [token, setToken] = useState(getToken());
function saveToken(userToken) {
localStorage.setItem('token', userToken);
setToken(userToken);
};
function removeToken() {
localStorage.removeItem("token");
setToken(null);
}
return {
setToken: saveToken,
token,
removeToken
}
}
export default useToken;
With the tests you carried out in the console, the functions created in the useToken
component should be easy to understand.
The getToken
function is used to retrieve the token
stored in the localStorage
and only returns a token if it exists hence the use of the &&
conditional operator.
The useState hook is used to handle the state of the token
variable which will contain the value of the token. This ensures that the react application always reloads when any of the functions are called. Such that when a user logs in and the token is stored or when the user logs out, the application also becomes aware that a change has occurred in the web storage of your browser and hence reacts accordingly by either redirecting to the page the user wants to access or returning to the login page once the user logs out.
The saveToken
function handles the storage of the token obtained when the user logs in and the setToken
function in it updates the state of the token
variable with the token
passed as an argument to the saveToken
function.
The removeToken
function deletes the token from the local storage and returns the token back to the null state whenever it gets called.
Finally, the saveToken
function assigned as a value to the setToken variable, the value of the token
itself and the removeToken
function are all returned as the result of calling the useToken
function.
App.js
I told you that you'll be making major changes right? 😜. Clean up App.js
; all the code that was added the last time will be moved into the Profile
component.
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Login from './components/Login'
import Profile from './components/Profile'
import Header from './components/Header'
import useToken from './components/useToken'
import './App.css'
function App() {
const { token, removeToken, setToken } = useToken();
return (
<BrowserRouter>
<div className="App">
<Header token={removeToken}/>
{!token && token!=="" &&token!== undefined?
<Login setToken={setToken} />
:(
<>
<Routes>
<Route exact path="/profile" element={<Profile token={token} setToken={setToken}/>}></Route>
</Routes>
</>
)}
</div>
</BrowserRouter>
);
}
export default App;
At the top of the file, the BrowserRouter
, Route
, Routes
functions that will be used to handle URL routing for the profile component are imported from the installed react-router-dom
package. The other created components are also imported from the components
folder.
In the App
function, the value object returned when the useToken
function is called is destructured and the values are assigned to the token
, removeToken
and setToken
variables respectively.
const { token, removeToken, setToken } = useToken();
Next, the BrowserRouter
function is made the parent component and in it, the Header
component is placed with the removeToken
function passed as an argument which is called prop
in react.
<Header token={removeToken}/>
Then the javascript conditional ternary operator is used to ensure that the user must have a token before having access to the profile
component. If the user doesn't have a token, the Login
component is rendered with the setToken
function passed as an argument. Else if the user already has a token, the Profile
component with the URL path /profile
is rendered and displayed to the user.
You can read more on how to use React Router
here
Now, you need to create the Login, Header, and Profile functions in your Login
, Header, and Profile
component files respectively.
Login.js
import { useState } from 'react';
import axios from "axios";
function Login(props) {
const [loginForm, setloginForm] = useState({
email: "",
password: ""
})
function logMeIn(event) {
axios({
method: "POST",
url:"/token",
data:{
email: loginForm.email,
password: loginForm.password
}
})
.then((response) => {
props.setToken(response.data.access_token)
}).catch((error) => {
if (error.response) {
console.log(error.response)
console.log(error.response.status)
console.log(error.response.headers)
}
})
setloginForm(({
email: "",
password: ""}))
event.preventDefault()
}
function handleChange(event) {
const {value, name} = event.target
setloginForm(prevNote => ({
...prevNote, [name]: value})
)}
return (
<div>
<h1>Login</h1>
<form className="login">
<input onChange={handleChange}
type="email"
text={loginForm.email}
name="email"
placeholder="Email"
value={loginForm.email} />
<input onChange={handleChange}
type="password"
text={loginForm.password}
name="password"
placeholder="Password"
value={loginForm.password} />
<button onClick={logMeIn}>Submit</button>
</form>
</div>
);
}
export default Login;
The code above should be easy to understand, the summary of what it does is to use the login details provided by the user to make a POST
request to the /token
API endpoint in the backend which then returns the user's token and the token is stored in the local web storage using the setToken
function passed as a prop to the Login function.
Header.js
import logo from '../logo.svg'
import axios from "axios";
function Header(props) {
function logMeOut() {
axios({
method: "POST",
url:"/logout",
})
.then((response) => {
props.token()
}).catch((error) => {
if (error.response) {
console.log(error.response)
console.log(error.response.status)
console.log(error.response.headers)
}
})}
return(
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<button onClick={logMeOut}>
Logout
</button>
</header>
)
}
export default Header;
Once the user clicks on the Logout
button, a POST
request is made to the /logout
API endpoint, and the cookies in which the user's JWToken is stored are cleared on the backend. The Axios
response function is used to call the removeToken
function which deletes the token
stored in the local web storage. Now, if the user tries to access the /profile
page, the user gets redirected to the login page.
Profile.js
import { useState } from 'react'
import axios from "axios";
function Profile(props) {
const [profileData, setProfileData] = useState(null)
function getData() {
axios({
method: "GET",
url:"/profile",
headers: {
Authorization: 'Bearer ' + props.token
}
})
.then((response) => {
const res =response.data
res.access_token && props.setToken(res.access_token)
setProfileData(({
profile_name: res.name,
about_me: res.about}))
}).catch((error) => {
if (error.response) {
console.log(error.response)
console.log(error.response.status)
console.log(error.response.headers)
}
})}
return (
<div className="Profile">
<p>To get your profile details: </p><button onClick={getData}>Click me</button>
{profileData && <div>
<p>Profile name: {profileData.profile_name}</p>
<p>About me: {profileData.about_me}</p>
</div>
}
</div>
);
}
export default Profile;
The piece of code previously in App.js
was moved here. This contains the protected endpoint \profile
. A GET
request method is sent to the endpoint whenever the Click me
button is clicked and it responds with the user's details.
For the user to be able to access the data of the \profile
API endpoint, an Authorization header that contains the token must be added to the axios GET
request.
headers: {
Authorization: 'Bearer ' + props.token
}
If the response contains an access token
, this means that the current token is near expiration and the server has created a new token. So the token stored in the local storage is updated with the newly generated token.
res.access_token && props.setToken(res.access_token)
App.css
You also need to make a change to the CSS style for the header. On line 16 you'll see the style for the header component .App-header
. Comment out or delete the /* min-height: 100vh; */
code so your application can end up looking like 👇:
Now to test your application, start the backend server by running the script below
npm run start-backend
followed by :
npm start
Then navigate to the http://localhost:3000/profile
URL in your web browser and you'll be prompted to login since the page is protected. I hope you still remember the login details: email:test
and password:test
. You can also open up localStorage
under the Application
section in Developer tools
to monitor the token as it gets stored and deleted.
It's been a long ride, but we have finally come to the end of this tutorial. With what you have learned, I believe you can easily authenticate your flask plus react applications. Congratulations on the new knowledge you just acquired.
If you have any questions, feel free to drop them as a comment or send me a message on Linkedin or Twitter and I'll ensure I respond as quickly as I can. Ciao 👋