Skip to main contentArrow Right

In this tutorial, we will learn how to add authentication to your Flask app with a simple HTML webpage. The user will be able to sign up, sign in, logout, and view their profile.

Prerequisites

All the code for the tutorial will be in the GitHub Repository at https://github.com/descope-sample-apps/flask-sample-app. Instructions on the installation are on the README. 

If you're a total beginner or getting started with Descope, feel free to follow along! Visit the Descope website to sign up for a free account. There’s also the Descope Get Started Docs, and the Flask Quick Start guide to help you get a general understanding. 

Descope + HTML = No problem

The templates folder contains all our HTML files. To make Descope accessible within our client-side app, we import the packages in the head of the base.html file. 

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>{% block title %} {% endblock %}</title>
   <script src="https://unpkg.com/@descope/web-component@latest/dist/index.js"></script>
   <script src="https://unpkg.com/@descope/web-js-sdk@latest/dist/index.umd.js"></script>

Code snippet: base.html file head

Home page

The index.html page serves as the home page. The profile and login links are conditionally shown, depending on whether the refresh token is valid.

 <div class="layout row">
       <a class="link" href="{{ url_for('home') }}">Home</a>
       <a id="loginOrProfile" class="link" href=""></a>
   </div>
   <script src="{{url_for('static', filename='descope.js')}}"></script>
   <script>
       const loginOrProfile = document.getElementById('loginOrProfile');


       const refresh = Promise.resolve(sdk.refresh())
       refresh.then((res) => {
           if (res.ok) {
               loginOrProfile.href = "{{ url_for('profile') }}"
               loginOrProfile.innerHTML = "Profile"
           } else {
               return Promise.reject(res);
           }
       }).catch((error) => {
           loginOrProfile.href = "{{ url_for('login') }}"
           loginOrProfile.innerHTML = "Login"
           console.log("Error: ", error);
       });
   </script>

Code snippet: index.html file conditional rendering

Here is a step-by-step of how we conditionally render the links:

  1. We call the sdk.refresh() function to check if we are logged in. The refresh method uses our refresh token to “refresh” and get a new valid session token. We wait for the Promise to resolve.

  2. Then we check if the response is okay and set the anchor element to the profile URL. If refresh fails, we reject the Promise and pass in the response as the reason. 

  3. We catch the error and set the login URL in the anchor element. 

Let’s look at how we initialize our Descope app in descope.js.

Establishing the global variables

Below is the descope.js file that is imported into every HTML page. It contains variables that will act as global variables that can be used throughout the pages. 

const projectId = ""
const sdk = Descope({ projectId: projectId, persistTokens: true, autoRefresh: true })
const sessionToken = sdk.getSessionToken()

Code snippet: descope.js file

Globally accessible variables include the following:

  • projectId: From your Descope project.

  • sdk: We initialize our Descope app and pass our project id.

  • sessionToken: We get our current session token string.

Next, let’s see an example of how we use these variables in our login page.

Login with Descope in HTML

The login logic can be found in the login.html file. Our descope.js file is first imported to make sure we have access to the global variables.

{% extends 'base.html' %}


{% block content %}
   <h1 class="title">{% block title %} Login {% endblock %}</h1>
   <div id="container"></div>


   <script src="{{url_for('static', filename='descope.js')}}"></script>
   <script>
       sdk.refresh()
       const container = document.getElementById('container')


       if (!sessionToken) {
           container.innerHTML = '<descope-wc project-id="' + projectId + '" flow-id="sign-up-or-in"></descope-wc>'
           const wcElement = document.getElementsByTagName('descope-wc')[0]


           const onSuccess = (e) => {
               sdk.refresh()
               window.location.href = "/profile"
           }


           const onError = (err) => console.log(err);


           wcElement.addEventListener('success', onSuccess)
           wcElement.addEventListener('error', onError)
       } else {
           window.location.href = "/profile"
       }
   </script>
{% endblock %}

There are three key takeaways from the JavaScript code: 

  • At the top of every HTML page, we will call the sdk.refresh() to jumpstart the autorefresh process and use our refresh token to get a new valid session token. 

  • The if statement checks if the session token is invalid and displays the descope login widget. If the session token is valid, we reroute to the profile page.  

  • The onSuccess arrow function captures the event, calls sdk.refresh() to start the autorefresh process, and redirects us to the profile page where we will validate the session token. 

Let’s see how we display our profile information in the profile.html file.

Displaying our profile 

Once the user is logged in, we can access our profile route in the profile.html file.

<script src="{{url_for('static', filename='descope.js')}}"></script>
   <script>
       sdk.refresh()
       const userName = document.getElementById('userName')
       const userEmail = document.getElementById('userEmail')
       const secretMsg = document.getElementById('secretMsg')
       const profileContainer = document.getElementById('profile')


       function getProfileData(){
           fetch('/get_secret_message', { // call the api endpoint from the flask server
               headers: {
                   Accept: 'application/json',
                   Authorization: 'Bearer ' + sessionToken,
               }
           }).then(data => {
               if (data.status === 401) { // error
                   window.location.href = '/login'
               }
               return data.json()
           }).then(jsonData => {
               console.log(jsonData)
               secretMsg.innerHTML = jsonData.secret_msg
           }).catch((err) => {
               console.log(err) // error
               window.location.href = '/login'
           })
       }


       async function setNameEmail() {
           const profile = await sdk.me()
           userName.innerHTML = profile.data.name
           userEmail.innerHTML = profile.data.email
       }


       if (sessionToken) {
           getProfileData();
           setNameEmail()
       } else {
           window.location.href = "/login"
       }


       async function logout() {
           await sdk.logout()
           window.location.href = "/login"
       }
   </script>

Code snippet: profile.html file scripts

Here is a step-by-step of how we display our profile information:

  1. First, looking at the bottom of the code we see the if-else condition. In the if-else condition, we check if the session token is valid and call the getProfileData and setNameEmail functions. 

  2. In the getProfileData function, we have a fetch that calls our /get_secret_message endpoint and we pass in our sessionToken variable from our descope.js file. In the then statement, we check if the status is a 401 unauthorized response and redirect to the login URL. 

  3. The setNameEmail function calls the sdk.me() method which gets us our user information and we set the HTML elements with the corresponding values.

Last is our logout function, which is called upon by the logout button. We have an await because we need to wait for the logout method to clear our tokens.

Again, notice how sdk.refresh() is called at the top of every page. This is to jumpstart the autorefresh process and use our refresh token to get a new valid session token.

Let’s finally check out our app.py file to see our endpoints and how we validate the session token. 

Validating the session token 

Before we implement our Flask endpoints, we need to build an authentication decorator to prevent unauthorized users from accessing our endpoints. We are going to name our decorator token_required. A decorator adds more functionality to a function by wrapping it with another function. 

def token_required(f): # auth decorator
   @wraps(f)
   def decorator(*args, **kwargs):
       session_token = None


       if 'Authorization' in request.headers: # check if token in request
           auth_request = request.headers['Authorization']
           session_token = auth_request.replace('Bearer ', '')
       if not session_token: # throw error
           return make_response(jsonify({"error": "❌ invalid session token!"}), 401)


       try: # validate token
           jwt_response = descope_client.validate_session(session_token=session_token)
       except:
           return make_response(jsonify({"error": "❌ invalid session token!"}), 401)


       return f(jwt_response, *args, **kwargs)

   return decorator

Code snippet: app.py file, token_required decorator 

There are five important things to understand about the decorator: 

  1. The decorator token_required takes in a function f as a parameter (whatever it may be).

  2. Since we are wrapping our function f with the token_required decorator, we need to make sure the inner function called decorator preserves the original values of the original function f. We use @wraps(f) to accomplish this.

  3. We allow our nested function decorator to take in any number of arguments denoted by the args and kwargs. This is in case the f function has any arguments. 

  4. In our inner function decorator, we check if the Authorization is in the headers and set the session token. If not, we return an error message. 

  5. Then we validate our session token and store the JWT in the jwt_response variable.

  6. We then return the function f with the jwt_response because we will need our JWT to access other information in our endpoints. 

Now we can use our authentication decorator in our Flask endpoints!

Getting our secret message

To protect the /get_secret_message endpoint, we wrap the function with the token_required decorator. When our profile.html file fetch calls the get_secret_message endpoint, our token_required decorator will first process the request to see if it’s authorized. If all goes well, we will continue with the get_secret_message function and the jwt_response that was passed in from the decorator.

@app.route('/get_secret_message', methods=["GET"])
@token_required
def get_secret_message(jwt_response):
   print(jwt_response)
   return {"secret_msg": "This is the secret message. Congrats!"}

Code snippet: app.py file, get_secret_message endpoint

The roles variable is set to the response object roles which will be a list. Once the role value is set, an object containing the secretMessage and role is returned.

Back in our profile.html, the data is fetched and set to our HTML element.

The demo 

Run your app to see the result! Below is our homepage which includes a route to the login page.

Fig: Home page

When the user is not logged in, the login page is accessible. If the user is already logged in, they will automatically be redirected to the profile page.

Fig: Login page

The profile page showcases our name, email, logout button, and a link back to the home page.

Fig: Profile page

That's it!

With just Python and HTML, we were able to integrate Descope authentication into our Flask app without the use of any complex frameworks or libraries. 

If you have any questions or want to start your Descope journey, sign up for our platform and join AuthTown, our open user community for developers to learn about authentication.