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:
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.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.
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, callssdk.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:
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
andsetNameEmail
functions.In the
getProfileData
function, we have a fetch that calls our/get_secret_message
endpoint and we pass in oursessionToken
variable from ourdescope.js
file. In the then statement, we check if the status is a 401 unauthorized response and redirect to the login URL.The
setNameEmail
function calls thesdk.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:
The decorator
token_required
takes in a function f as a parameter (whatever it may be).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 functionf
. We use@wraps(f)
to accomplish this.We allow our nested function decorator to take in any number of arguments denoted by the
args
andkwargs
. This is in case thef
function has any arguments.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.
Then we validate our session token and store the JWT in the
jwt_response
variable.We then return the function
f
with thejwt_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.
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.
The profile page showcases our name, email, logout button, and a link back to the home 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.