Skip to content

jwk_key is reparsed 3x per login (and it's expensive) #1263

@cakemanny

Description

@cakemanny

Describe the bug

The OIDC private key is parsed multiple times on each OpenID Connect login. (3 times by our specific observations)

If we zoom in on the flame graph for a request to the token endpoint, we see that there are 3 calls to JWK.from_pem, twice before signing the ID Token and once again afterwards. In total 81% of the time is wasted parsing and loading the private key from its PEM format.

image

To Reproduce

Terse version:

Verbose version:

mkdir dot-jwk-bug && cd dot-jwk-bug
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install django django-oauth-toolkit py-spy
django-admin startproject mysite

Follow DOT installation steps: https://django-oauth-toolkit.readthedocs.io/en/latest/install.html

Edit mysite/mysite/settings.py appending:

OAUTH2_PROVIDER = {
    "OIDC_ENABLED": True,
    "OIDC_RSA_PRIVATE_KEY": os.environ.get("OIDC_RSA_PRIVATE_KEY"),
    "PKCE_REQUIRED": False,  # to simplify repro process
    "SCOPES": {
        "openid": "OpenID Connect scope",
    },
}
openssl genrsa -out oidc.key 4096
export OIDC_RSA_PRIVATE_KEY="$(cat oidc.key)"

(cd mysite
python manage.py migrate
python manage.py createsuperuser
)

Create the account with any uninteresting credentials you will be able to remember

(cd mysite && python manage.py runserver)
  • Go to http://127.0.0.1:8000/admin/oauth2_provider/application/ ,
  • Login with the super user created earlier
  • Create new application
    • Client id: dot_jwk_bug
    • Redirect uris list: http://127.0.0.1:9000/
    • Client type: Public
    • Authorization grant type: Authorization code
    • Copy the Client secret
    • Skip authorization: checked (only to simply reproduction process)
    • Algorithm: RSA with SHA-2 256
export CLIENT_ID=dot_jwk_bug
export CLIENT_SECRET=[client secret here]

Retrieve an oauth access token

We run netcat to listen for the authorisation code:

export CODE=$(echo $'HTTP/1.0 200 OK\r\n\r\nall good' | nc -l 127.0.0.1 9000 | awk '/^GET/ { print $2 }' | awk -F '=' '{ print $2 }')

While that command waits go to the following URL in the the browser where still logged in as the superuser:

http://127.0.0.1:8000/o/authorize/?client_id=dot_jwk_bug&response_type=code&redirect_uri=http://127.0.0.1:9000/&scope=openid

Return to the terminal and finally retrieve an access token:

curl 'http://127.0.0.1:8000/o/token/' -d 'code='"${CODE}"'&grant_type=authorization_code&client_id=dot_jwk_bug&client_secret='"${CLIENT_SECRET}"'&redirect_uri=http://127.0.0.1:9000/'

In order to get the flame chart shown above one can run py-spy while doing this access token retrieval process and ctrl-c once done

sudo py-spy record -o dot-profile.svg -p [PID for the python process]

Expected behaviour

The OIDC private key to be parsed exactly once at application startup, and then never again.

Version

  • I have tested with the latest published release and it's still a problem.
  • I have tested with the master branch and it's still a problem.

Additional context

In case it's useful to point out, this is sitting behind a property 🤷

https://github.com/jazzband/django-oauth-toolkit/blob/769c0a2fd668f1a0dd3ca80ef8bfd76f8082eb57/oauth2_provider/models.py#L217-L225

And we can already see a couple of the places where it is accessed and thus recalculated:
https://github.com/jazzband/django-oauth-toolkit/blob/769c0a2fd668f1a0dd3ca80ef8bfd76f8082eb57/oauth2_provider/oauth2_validators.py#L838-L844

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions