Using with Flask¶
In this page we will see how to implement common CAS uses to protect a webpage by using Flask and PyCAS-SSO.
1. What we target to achieve¶
We want to create a Flask application that contains:
Homepage publicly accessible,
Login route that redirect un-authenticated user to CAS login form,
Callback route to redirect user after sign-in on CAS,
Logout route to log-out user from the application,
Protected page only accessible to authenticated user.
2. Set-up the environment¶
Create a project folder that will contain our code and environment:
mkdir pycas_sso_flask
Next, we need to create a new Python venv in our project folder:
cd pycas_sso_flask && \
python -m venv .pyenv/pycas_sso_flask
This will create a new Python venv in .pyenv/pycas_sso_flask inside our project folder.
Then activate the environment:
source .pyenv/pycas_sso_flask/bin/activate
Now we need to install additional packages that will be needed to make this project, using pip:
pip install Flask python-decouple pycas-sso[requests]
Note
python-decouple is a tool that will help us organize our settings properly inside a .env file.
Note
In this project, we will use pycas-sso with requests HTTP library since Flask is not asynchronous, no need for a library that support it.
Our development environment is now ready!
3. Create the structure of our application¶
Let’s start by creating a new file named main.py then open it with your favorite code editor. This file will contain all the code of our application.
We will start by importing Flask and declaring a new Flask application, then we will create four routes (homepage, login, logout, protected).
from flask import Flask
app = Flask(__name__)
@app.route('/')
def homepage():
"""This will be our homepage publicly accessible."""
@app.route('/login')
def login():
"""
This page will have multiple functions:
- Redirect un-authenticated user to CAS login form,
- If had argument `ticket` it will validate ticket to the CAS service \
then login the user in application if succeed.
- If user is already authenticated, it will redirect to the protected page.
"""
@app.route('/logout')
def logout():
"""Log-out the user from the application and from the CAS service."""
@app.route('/protected')
def protected():
"""Protected page only accessible by authenticated user. It will display the username."""
4. Implementing the logic¶
Homepage¶
First make the homepage returning something:
@app.route('/')
def homepage():
return "Welcome to the Homepage!"
At this stage if you run your application: flask --app main run --debug and go on http://localhost:5000/ you will see the message: Welcome to the Homepage!.
Tip
Use --host=<ip_address> and --port=<port> arguments when using flask to adapt the host and port used by Flask.
Protected page¶
Next we will implement the protected page. For that we will use Flask’s session module. If user is marked authenticated in session, we will display a message with their username stored in session. If user is not authenticated we will display an error.
from flask import Flask, session
# Using session in Flask requires setting the app.secret_key
# see: https://flask.palletsprojects.com/en/stable/quickstart/#sessions
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
...
@app.route('/protected')
def protected():
# Get 'authenticated' from session or return False if not exists
if not session.get('authenticated', False):
# Return HTTP 401: Unauthorized
return "Access denied. Please log in first.", 401
# Retrieve the username from session
username = session['username']
# Display a message with the username
return f"Welcome to the protected page, {username}!"
Login¶
Now we will go for the login page. The first thing we are going to do is to check if the user is already authenticated and if so, redirect the user to the protected page.
from flask import Flask, session, redirect, url_for
...
@app.route('/login')
def login():
if not session.get('authenticated', False):
# User is not authenticated
return ""
return redirect(url_for('protected'))
Next we will check if there is a ticket argument in the querystring and if not redirect the user to the CAS login form. For that we will need a CAS client from PyCAS-SSO and the request object from Flask. We will also use config from python-decouple to get the configuration variables relative to our environment.
from flask import Flask, session, redirect, url_for, request
from decouple import config
from pycas_sso.cas import CASClient
...
@app.route('/login')
def login():
if not session.get('authenticated', False):
# Try to retrieve ticket from querystring or set ticket to None
ticket = request.args.get('ticket', None)
# Create a new CAS client using ContextManager and pycas_sso.CASClient.create
with CASClient.create(
config('CAS_PROVIDER'), config('SERVICE_URL'), config('LOGIN_URL')
) as client:
# If ticket argument is not present, redirect the user to CAS login form
if ticket is None:
# Retrieve CAS login form url and redirect the user
login_form_url = client.login_form_url()
return redirect(login_form_url)
return redirect(url_for('protected'))
Now we will declare our configuration by creating a .env file at the root of our project folder.
# CAS service URL
CAS_PROVIDER="https://cas.example.com/"
# Your service URL (don't forget to authorize this service your CAS service)
SERVICE_URL="http://localhost:5000"
# URL where user will be redirect after successfully logged on CAS
LOGIN_URL="http://localhost:8000/login"
The last step for this route is to validate the ticket with the CAS service and if succeed authenticate the user on the application using session.
from flask import Flask, session, redirect, url_for, request
from decouple import config
from pycas_sso.cas import CASClient
from pycas_sso.errors import CASServiceAuthenticationFailure
...
@app.route('/login')
def login():
if not session.get('authenticated', False):
# Try to retrieve ticket from querystring or set ticket to None
ticket = request.args.get('ticket', None)
# Create a new CAS client using ContextManager and pycas_sso.CASClient.create
with CASClient.create(
config('CAS_PROVIDER'), config('SERVICE_URL'), config('LOGIN_URL')
) as client:
# If ticket argument is not present, redirect the user to CAS login form
if ticket is None:
# Retrieve CAS login form url and redirect the user
login_form_url = client.login_form_url()
return redirect(login_form_url)
try:
# Send a validation request to the CAS service using CAS 2.0 protocol
# and store the data from the response.
validate_data = client.service_validate(ticket)
# Set user's session
session['authenticated'] = True
session['username'] = validate_data.username
except CASServiceAuthenticationFailure as err:
# If validation failed, display an error
return f"Authentication failed: {err}", 401
return redirect(url_for('protected'))
We are done for the login. The final step is to implement the logout.
Logout¶
The logout page will simply destroy user’s session on the application and send the user to the CAS logout URL to logout the user from the CAS service too.
@app.route('/logout')
def logout():
# Clear user's session
session.clear()
with CASClient.create(
config('CAS_PROVIDER'), config('SERVICE_URL'), config('LOGIN_URL')
) as client:
# Retrieve CAS service logout URL and redirect the user
logout_url = client.logout_url()
return redirect(logout_url)
And it’s done. You can start Flask and test if everything works as expected: flask --app main run --debug.
5. Full implementation¶
# file: main.py
from flask import Flask, request, session, redirect, url_for
from decouple import config
from pycas_sso.cas import CASClient
from pycas_sso.errors import CASServiceAuthenticationFailure
app = Flask(__name__)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
@app.route('/')
def homepage():
return "Welcome to the Homepage!"
@app.route('/login')
def login():
if not session.get('authenticated', False):
# Try to retrieve ticket from querystring or set ticket to None
ticket = request.args.get('ticket', None)
# Create a new CAS client using ContextManager and pycas_sso.CASClient.create
with CASClient.create(
config('CAS_PROVIDER'), config('SERVICE_URL'), config('LOGIN_URL')
) as client:
# If ticket argument is not present, redirect the user to CAS login form
if ticket is None:
# Retrieve CAS login form url and redirect the user
login_form_url = client.login_form_url()
return redirect(login_form_url)
try:
# Send a validation request to the CAS service using CAS 2.0 protocol
# and store the data from the response.
validate_data = client.service_validate(ticket)
# Set user's session
session['authenticated'] = True
session['username'] = validate_data.username
except CASServiceAuthenticationFailure as err:
# If validation failed, display an error
return f"Authentication failed: {err}", 401
return redirect(url_for('protected'))
@app.route('/logout')
def logout():
# Clear user's session
session.clear()
with CASClient.create(
config('CAS_PROVIDER'), config('SERVICE_URL'), config('LOGIN_URL')
) as client:
# Retrieve CAS service logout URL and redirect the user
logout_url = client.logout_url()
return redirect(logout_url)
@app.route('/protected')
def protected():
# Get 'authenticated' from session or return False if not exists
if not session.get('authenticated', False):
# Return HTTP 401: Unauthorized
return "Access denied. Please log in first.", 401
# Retrieve the username from session
username = session['username']
# Display a message with the username
return f"Welcome to the protected page, {username}!"
# file: .env
# CAS service URL
CAS_PROVIDER="https://cas.example.com/"
# Your service URL (don't forget to authorize this service your CAS service)
SERVICE_URL="http://localhost:5000"
# URL where user will be redirect after successfully logged on CAS
LOGIN_URL="http://localhost:8000/login"
6. Go Further¶
This is only a basic example of how PyCAS-SSO can be used alongside Flask. You can go further by adapting it to a complete user management system with a database, creating a custom decorator to check if user is authenticated or not and automatically redirect the user to CAS login form if not and way more.