-
Notifications
You must be signed in to change notification settings - Fork 808
Description
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.
To Reproduce
Terse version:
- Assume django application and django-oauth-toolkit installed
- Follow steps in the readme to set up / configure Open ID Connect: https://django-oauth-toolkit.readthedocs.io/en/latest/oidc.html#configuration
- Create an application that uses RSA as the algorithm
- Perform an oauth login
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
- Client id:
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:
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 🤷
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