# [Python] SSO using Flask, requests-oauthlib and pyjwt **Published by:** [The Digital Meadow](https://paragraph.com/@digitalmeadow/) **Published on:** 2021-11-28 **Categories:** python, oidc, sso, selenium **URL:** https://paragraph.com/@digitalmeadow/%5Bpython%5D-sso-using-flask%2C-requests-oauthlib-and-pyjwt ## Content I am currently developing an application that will need Single Sign-On to delegate user management and authorization to a third party Identity Provider (IdP). Although the domain of the application is not entirely finished and I have not started developing the API, the Proof of Concept of using the backend to generate and retrieve the JWT from an IdP is done and maybe it will be of use to guide other in this process. This article is basically tying up two examples and an article, namely requests-oatuhlib’s [Web App Example](https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html), pyjwt’s [Retrieve RSA signing keys from a JWKS endpoint](https://pyjwt.readthedocs.io/en/stable/usage.html#retrieve-rsa-signing-keys-from-a-jwks-endpoint), and Auth0’s [How to Verify a JWT](https://auth0.com/blog/how-to-handle-jwt-in-python/#How-to-Verify-a-JWT). Adding to that, we also use Selenium’s outstanding API to open a browser session and wait for the authentication flow to complete before retrieving the session cookies. ## Background Before diving into the code in all its prototypical glory, I think it is important to at least refer to the OIDC authentication flow. If you feel comfortable with the flow already you can skip to **Pre-requisites**. I think the best source of high level information about OIDC comes from Auth0 articles. What we are implementing here is a form of [Authorization Code Flow](https://auth0.com/docs/authorization/flows/authorization-code-flow), one of the [many flow types](https://auth0.com/docs/authorization/flows) supported by OIDC. In this flow we are relying on redirecting the user from our backend’s `/login` endpoint to the Identity Provider authentication page, in order to generate code and state after a successful authentication. This ensures better compatibility at the expense of having a browser as a dependency for the backend. We will show how we can still leverage a browser in a CLI for authentication using Selenium. The code and state is then sent to the backend’s `/callback` endpoint via redirect, which in turn generate the JWT token with user claims and send it back to the user using [Flask’s native session API](https://flask.palletsprojects.com/en/2.0.x/api/#sessions). The last step is to extract the token from the session cookies. JWT is designed to be self contained and relying on Flask’s session adds a point of failure if we decide to rotate the application’s session secret. ## Design Decisions For this example we will rely heavily on [OIDC’s configuration endpoint](https://swagger.io/docs/specification/authentication/openid-connect-discovery/) which is based on [RFC5785 well-known URIs](https://datatracker.ietf.org/doc/html/rfc5785). This is supported by [Auth0’s OIDC Discovery endpoint](https://auth0.com/docs/configure/applications/configure-applications-with-oidc-discovery), which is the IdP I used to validate the code in this article. All the necessary endpoints, like `authorization_endpoint`, `token_endpoint` and `jwks_uri` are present in the Auth0’s configuration endpoint response. I can’t say that this will be supported by every IdP, but the examples in this article can be expanded to include explicitly defined endpoints for those three resources we need in order to authorize the user. ## Pre-requesites For the examples in this article you will need: * An Auth0 tenant * An Auth0 Application configured to accept connections from `http://localhost:5000` * A user in your Auth0 Tenant * A python virtual environment with the following `requirements.txt` installed: ``` flask==2.0.2 requests-oauthlib==1.3.0 requests==2.26.0 pyjwt[crypto]==2.3.0 selenium==4.1.0 ``` All code was tested using python 3.9.9. ## Authentication Flow The first thing is to setup a flask app. We need to set app’s config `SECRET_KEY` so that we can later use flask’s `session`. ```python from flask import Flask from uuid import uuid4 app = Flask(__name__) app.config["SECRET_KEY"] = str(uuid4()) if __name__ == "__main__": app.run() ``` Then we declare our IdP settings: ```python IDP_CONFIG = { "well_known_url": "Identity Provider wellknown url: https://{TENANT}.auth0.com/.well-known/openid-configuration", "client_id": "Your app client ID", "client_secret": "Your app client secret", "scope": ["profile", "email", "openid"] } ``` Make sure to replace the necessary fields with your tenant openid configuration endpoint, client ID and secret. You could also try to use a different Identity Provider. Just make sure you replace the `scope` list with the corresponding scopes. What we need is to be able to produce a JWT with identity claims containg email and username. Now we can implement the endpoints for the auth flow, namely `/login` and `/callback`, but first let’s implement two functions that will help us fetch the well-known metadata and create a oauth2 session: ```python import requests from flask import url_for from requests_oauthlib import OAuth2Session def get_well_known_metadata(): response = requests.get(IDP_CONFIG["well_known_url"]) response.raise_for_status() return response.json() def get_oauth2_session(**kwargs): oauth2_session = OAuth2Session(IDP_CONFIG["client_id"], scope=IDP_CONFIG["scope"], redirect_uri=url_for(".callback", _external=True), **kwargs) return oauth2_session ``` The first function, `get_well_known_metadata` will get and parse the json response of the well-known endpoint url. The second function, `get_oauth2_session` will create an `OAuth2Session` instance with out client id, scopes and a `redirect_uri` for our callback endpoint that we will implement next. The `**kwargs` will be used in the callback endpoint to pass the authorization state to the `OAuth2Session` to prevent CSRF attacks. We can then implement our login and callback endpoints. First, the login: ```python from flask import redirect, session @app.route("/login") def login(): well_known_metadata = get_well_known_metadata() oauth2_session = get_oauth2_session() authorization_url, state = oauth2_session.authorization_url(well_known_metadata["authorization_endpoint"]) session["oauth_state"] = state return redirect(authorization_url) ``` This piece of code is pretty straight forward and very similar to the original `demo` endpoint implemented in the requests-oauthlib Web App example. We are producing an authorization URL to which we are going to redirect our user. The `authorization_endpoint` is extracted from the JSON response of the IdP’s well-known configuration endpoint. The state is saved in the session to be used later. Then, we have the callback endpoint: ```python from flask import request @app.route("/callback") def callback(): well_known_metadata = get_well_known_metadata() oauth2_session = get_oauth2_session(state=session["oauth_state"]) session["oauth_token"] = oauth2_session.fetch_token(well_known_metadata["token_endpoint"], client_secret=IDP_CONFIG["client_secret"], code=request.args["code"])["id_token"] return "ok" ``` This endpoint creates another OAuth2Session instance, but passing the previous oauth state. We then use the `token_endpoint`, `client_secret` and the `code` from the query string to get the token metadata and save the `id_token` in the session. I am not entirely sure the `token_endpoint` response fields are consistent across Identity Providers. The [OAuth2 RFC6749 Secion 3.2](https://datatracker.ietf.org/doc/html/rfc6749#section-3.2) defines what the token endpoint is, but doesn’t seem to enforce the response schema. By now we have the basic authentication flow to generate a valid token and populate the client’s session cookies with it. We now want to provide an endpoint so that the user can request the JWT token in plain text to be extracted from the session cookies: ```python @app.route("/user/token") def get_user_token(): return session["oauth_token"] ``` This endpoint is very simple and just returns the `oauth_token` field included in the session, the one which we populated in the `callback` endpoint. ## Validating the JWT token Now that we have a valid JWT token both in the session cookies and in plaintext, we can create an interceptor to validate the token before the request is processed by whichever endpoint controller the request is being sent to. We provide two ways to send the token to the backend, in the session cookies or using the `Authorization` header. The following code extracts the token and validates it using `pyjwt`: ```python import jwt from jwt import PyJWKClient from jwt.exceptions import DecodeError from werkzeug.exceptions import InternalServerError, Unauthorized def get_jwks_client(): well_known_metadata = get_well_known_metadata() jwks_client = PyJWKClient(well_known_metadata["jwks_uri"]) return jwks_client jwks_client = get_jwks_client() @app.before_request def verify_and_decode_token(): if request.endpoint not in {"login", "callback"}: if "Authorization" in request.headers: token = request.headers["Authorization"].split()[1] elif "oauth_token" in session: token = session["oauth_token"] else: return Unauthorized("Missing authorization token") try: signing_key = jwks_client.get_signing_key_from_jwt(token) header_data = jwt.get_unverified_header(token) request.user_data = jwt.decode(token, signing_key.key, algorithms=[header_data['alg']], audience=IDP_CONFIG["client_id"]) except DecodeError: return Unauthorized("Authorization token is invalid") except Exception: return InternalServerError("Error authenticating client") ``` The function `get_jwks_client` produces a `PyJWKClient` instance using the `jwks_uri` we retrieved from the IdP’s well-known configuration endpoint. We have to make sure both `login` and `callback` endpoints are whitelisted from the token verification inteceptor, because these endpoints are necessary to produce the token in the first place. Then we can test whether the token is in the `Authorization` header or in the `session`. If we can’t find it, we just return an `Unauthorized` response telling the user that the authorization token is missing. To retrieve the token from the `Authorization` header we have to do a little string manipulation, because these headers are usually in the form `Authorization: Basic `. Therefore we split the header value and get the second occurrence. Then we can use the `PyJWKClient` example from `pyjwt` to exrtact the signing key. The example uses hardcoded algorithms, but we can circumvent it using jwt’s function `get_unverified_header`. The function will return the JWT token header before verification, which will include the `alg` field, telling which algorithm was used to sign the token. The use of this function was illustrated from Auth0’s article How to Handle JWT in Python, namely in the How to Verify a JWT session. After decoding the token we can just populate a custom field in the request, called `user_data` which will contain all JWT claims to be used throughout the request lifespan. To illustrate the use, let’s create an endpoint to return the signed-in user’s email: ```python @app.route("/user/id") def get_user_id(): return request.user_data["email"] ``` Calling this endpoint with either the `Authorization` header set or the original session cookies will return your Auth0 user’s email. Note that `/user/id` actually answers with the user’s **email**. This is because my application will eventually use the user’s email for unique identification. You can later change it to `/user/email` or return the actual user id, if you so desire. ## Using the login flow in a CLI Part of the scope of my original project is to provide means for the user to interact with the API using a CLI. However, to ensure the best consistency with the Identity Providers authentication flow implementation, I decided to implement a comunication flow that will open a browser session from the CLI and retrieve the JWT using the driver’s session cookies. For this exercise we will use Selenium and chromium. We start by creating a chromium driver that will direct the user to the backend’s login page: ```python from selenium import webdriver from selenium.webdriver.chrome.options import Options as ChromeOptions chrome_options = ChromeOptions() chrome_options.add_argument("--user-data-dir=chrome-data") # ensure data persitence across cli calls chrome_options.add_argument("--app=http://localhost:5000/login") # open a browser session without tabs directly to our endpoint of interest driver = webdriver.Chrome(options=chrome_options) ``` This will open a browser session directly to the login page, without a tabs and URL bar, and with data persistence. ![Login from a CLI using Selenium](https://storage.googleapis.com/papyrus_images/b365994cc802b4d02730fc1fd139dd6b) Before filling the login page with your credentials, we can first leverage Selenium’s `WebDriverWait` and automate both session cookies retrieval and driver closure. The backend callback endpoint returns `ok` if the authentication flow is successful, so we can use that to wait for the response: ```python from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC WebDriverWait(driver, 300).until( EC.visibility_of_element_located((By.XPATH, "//*[contains(text(), 'ok')]"))) ``` This is not robust **at.. all…** but serves to illustrate the concept. We could for instance return a div with a UUID for an ID and look for the element using Selenium’s class selectors. The code will lock and wait until the `ok` is sent. You can now proceed to login with your client as usual. When `ok` is sent, the wait lock is released and we can fetch the session cookies and close the driver: ```python chromium_cookies = driver.get_cookies() driver.close() ``` With the session cookies at hand, we can proceed to use python’s `requests` to fetch the actual token. We need first to map the session cookies to something that requests understands. The chromium session cookies are in the following format: ```json [ { "domain": "localhost", "httpOnly": true, "name": "session", "path": "/", "secure": false, "value": "value" } ] ``` Python `requests` expects cookies to be in the form of a key-value dictionary. We can convert the chromium cookies using dict comprehension: ```python requests_cookies = {c["name"]: c["value"] for c in chromium_cookies} ``` Then we can retrieve the plaintext token using the `/user/token` endpoint: ```javascript import requests token_response = requests.get("http://localhost:5000/user/token", cookies=requests_cookies) token_response.raise_for_status() token = token_response.text ``` Now we have a JWT token in our CLI that does not depend on the backend application secret. Let’s test the token by sending a request to the `/user/id`: ```python userid_response = requests.get("http://localhost:5000/user/id", headers={"Authorization": f"Bearer {token}"}) userid_response.raise_for_status() print(userid_response.text) ``` This should produce your user’s email. # Conclusion We laid in this article a simple building block with which we can build a backend with OIDC SSO and a frontend CLI that successfully communicates with the backend. The examples in this article are fairly simple and a direct result of the well-documented API’s it uses, but they can hopefully serve as a starting point for others to build their applications expanding upon them. The complete, uninterrupted code for both the backend and the CLI prototype can be found here: If you have any questions or suggestions, please feel free to contact me anytime! ## Publication Information - [The Digital Meadow](https://paragraph.com/@digitalmeadow/): Publication homepage - [All Posts](https://paragraph.com/@digitalmeadow/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@digitalmeadow): Subscribe to updates - [Twitter](https://twitter.com/gchamon): Follow on Twitter ## Optional - [Collect as NFT](https://paragraph.com/@digitalmeadow/%5Bpython%5D-sso-using-flask%2C-requests-oauthlib-and-pyjwt): Support the author by collecting this post - [View Collectors](https://paragraph.com/@digitalmeadow/%5Bpython%5D-sso-using-flask%2C-requests-oauthlib-and-pyjwt/collectors): See who has collected this post