DEV Community

Cover image for How to add login authentication to a Flask and React application.
Faruq Abdulsalam
Faruq Abdulsalam

Posted on • Updated on

How to add login authentication to a Flask and React application.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

To test this, start your backend server with

npm run start-backend
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You'll get a 500 internal server error 👇
500 internal server error
Check your terminal and you'll see the error as well 👇

terminal nonetype error
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.
login details
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"
Enter fullscreen mode Exit fullscreen mode

You can try to pass in a wrong email or password to see the 401 UNAUTHORIZED error
401 UNAUTHORIZED error

Logout API endpoint

@api.route("/logout", methods=["POST"])
def logout():
    response = jsonify({"msg": "logout successful"})
    unset_jwt_cookies(response)
    return response
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You should get the response below 👇
logout api call

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You should still get the json form of the dictionary created in the last article.
json form of profile api call

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
Enter fullscreen mode Exit fullscreen mode

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.

unauthorized error

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
authorization header added

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Then to get the value 53 assigned to the key test above run:

sessionStorage.getItem('test')
Enter fullscreen mode Exit fullscreen mode

session and local storage 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')
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Then to get the assigned value 333:

localStorage.getItem('test')
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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}/>
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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
      }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 👇:

final application look

Now to test your application, start the backend server by running the script below

npm run start-backend
Enter fullscreen mode Exit fullscreen mode

followed by :

npm start
Enter fullscreen mode Exit fullscreen mode

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 👋

Discussion (2)

Collapse
shtrudle profile image
Oded Liphshitz

Great project, well done!

Collapse
nagatodev profile image
Faruq Abdulsalam Author

Thank you