2023-04-23

Auth0 Python quickstart - validation of signature in ID token

I'm building a Python Flask app with Auth0 as my OpenID Connect provider. The Auth0 website provides some skeleton code, which I'm using as the starting point for my app. The skeleton code works and is easily extensible; however, there are some ambiguities as to what the code is doing and why the behaviour adheres to modern security standards. I can't afford to be less than 100% confident when it comes to security, so I would like to run these ambiguities past the experts here at StackOverflow.

My understanding of the skeleton code

The following is the sequence of events that occur when a user interacts with this skeleton app. (See below for the code.)

  1. The user opens http://localhost:3000/ in the browser. This invokes the home endpoint in the Flask app. At this point in time, the user does not have a session cookie, so the response is some HTML containing the words "Hello guest" and a login button.
  2. The user clicks on the login button, which is a link to http://localhost:3000/login. This invokes the login endpoint in the Flask app, which redirects the user to Auth0's login box.
  3. The user enters their email and username into Auth0's login box. The user is redirected to http://localhost:3000/callback; the authorisation code generated by the successful login is passed in the query string in this URL.
  4. The callback endpoint in the Flask app is invoked. The authorisation code is sent to Auth0 in exchange for an ID token and an access token. A session cookie is set, containing this ID token and access token. The user is redirected to http://localhost:3000/.
  5. The home endpoint is invoked again. This time, the user has a session cookie. The endpoint returns some HTML containing the text Welcome {username}, plus further info about the user contained inside the ID token in the session cookie.

And how exactly is the session cookie constructed? My understanding is that the session cookie contains the ID token and access token, plus a signature. The signature is created using the Flask app's secret key (see line 8 in the code sample below).


Question 1: JWT signature verification

The ID token is a JWT. Being a JWT, the ID token contains a signature. This signature is signed using Auth0's private key, and can be verified using Auth0's public key (also known as the JWK).

Since Auth0 goes to the trouble of signing the ID token, I would expect that any endpoint in our Flask app that uses the ID token as a proof of the user's identity ought to verify the signature on the ID token using Auth0's public key. Otherwise, what's the point in Auth0 signing the ID token?

But the home endpoint uses the ID token as proof of the user's identity, and it does not verify the signature on the ID token! (At least, that's the impression I get by reading the code and ctrl-clicking through the library methods. That said, I'm not too confident in this assertion since the authlib.integrations.flask_client library is not very friendly for ctrl-clicking.)

My questions are:

  • Am I correct that the signature inside the ID token does not get verified by the home endpoint?
  • Assuming I'm correct, then is this a problem? Should I fix it?

Warning: There are two signatures in this setup:

  • The signature inside the ID token, which is signed by Auth0's private key and can be verified using Auth0's public key.
  • The signature on the session cookie, which is signed using the Flask app's secret key.

It is impossible for an attacker to forge a session cookie, because the attacker doesn't have the Flask app's secret key, and so is unable to create a valid signature for the session cookie. So to my naive mind, the app seems secure. Sure, the app fails to verify the signature in the ID token, but it makes up for this by verifying the signature on the session cookie.

Still, I suspect that the signature in the ID token has got to be there for a good reason, presumably to defend against some kind of attack that my non-expert brain hasn't thought of. It's likely that I'm missing something.

Question 2: Expiry time verification

The ID token contains an expiry time. As far as I can tell, the home endpoint doesn't check that the ID tokens hasn't expired.

Again, I'm not 100% sure if I'm right about this. Maybe the session cookie has a concept of an expiry time, which gets checked by Flask? I can't tell because I don't know how to decode the session cookie so that I can inspect it.

My questions are:

  • Is it really the case that the home endpoint doesn't check for expiry?
  • If so, then is this a mistake?

Finally, feel free to correct any misconceptions I have and/or suggest better choice of terminology. I'm not an expert and I'm here to learn.


Code samples:

server.py

import json
from os import environ as env

from authlib.integrations.flask_client import OAuth
from flask import Flask, redirect, render_template, session, url_for
    
app = Flask(__name__)
app.secret_key = env.get("APP_SECRET_KEY")

oauth = OAuth(app)

oauth.register(
    "auth0",
    client_id=env.get("AUTH0_CLIENT_ID"),
    client_secret=env.get("AUTH0_CLIENT_SECRET"),
    client_kwargs={"scope": "openid profile email"},
    server_metadata_url=f'https://{env.get("AUTH0_DOMAIN")}/.well-known/openid-configuration'
)

@app.route("/login")
def login():
    return oauth.auth0.authorize_redirect(
        redirect_uri=url_for("callback", _external=True)
    )

@app.route("/callback", methods=["GET", "POST"])
def callback():
    token = oauth.auth0.authorize_access_token()
    session["user"] = token
    return redirect("/")

@app.route("/")
def home():
    return render_template(
      "home.html",
      session=session.get('user'),
      pretty=json.dumps(session.get('user'), indent=4)
    )

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=env.get("PORT", 3000))

templates/home.html

<html>
<head>
  <meta charset="utf-8" />
  <title>Auth0 Example</title>
</head>
<body>
  
    <h1>Welcome Guest</h1>
    <p><a href="/login">Login</a></p>
  
</body>
</html>


No comments:

Post a Comment