Skip to content

Add endpoint for JWT refresh tokens #3270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

stveit
Copy link
Contributor

@stveit stveit commented Jan 29, 2025

Based on #3268

Adds API endpoint for JWT refresh token. It accepts valid refresh tokens and returns an access token and a new refresh token while invalidating the old one.

Adds docs for local tokens, with caveat that its not fully implemented yet

@stveit stveit added the JWT label Jan 29, 2025
@stveit stveit self-assigned this Jan 29, 2025
Copy link

github-actions bot commented Jan 29, 2025

🦙 MegaLinter status: ✅ SUCCESS

Descriptor Linter Files Fixed Errors Warnings Elapsed time
✅ PYTHON ruff 6 0 0 0.21s

See detailed report in MegaLinter reports
Set VALIDATE_ALL_CODEBASE: true in mega-linter.yml to validate all sources, not only the diff

MegaLinter is graciously provided by OX Security

Copy link

github-actions bot commented Jan 29, 2025

Test results

   12 files     12 suites   11m 59s ⏱️
2 196 tests 2 196 ✅ 0 💤 0 ❌
6 063 runs  6 063 ✅ 0 💤 0 ❌

Results for commit 1b61d8b.

♻️ This comment has been updated with latest results.

Copy link

codecov bot commented Jan 29, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 60.72%. Comparing base (51a22b1) to head (91f2633).
Report is 3 commits behind head on jwt-refresh-token-model.

Additional details and impacted files
@@                     Coverage Diff                     @@
##           jwt-refresh-token-model    #3270      +/-   ##
===========================================================
- Coverage                    60.73%   60.72%   -0.01%     
===========================================================
  Files                          606      606              
  Lines                        43769    43800      +31     
  Branches                        48       48              
===========================================================
+ Hits                         26581    26597      +16     
- Misses                       17176    17191      +15     
  Partials                        12       12              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@stveit stveit force-pushed the jwt-refresh-token-endpoint branch from 257a05c to b384528 Compare January 29, 2025 23:16
@stveit stveit force-pushed the jwt-refresh-token-model branch from f3f4c12 to d16dda5 Compare January 29, 2025 23:26
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch from b384528 to c3c9e16 Compare January 29, 2025 23:28
@stveit stveit force-pushed the jwt-refresh-token-model branch from d16dda5 to c2568eb Compare January 29, 2025 23:35
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch 2 times, most recently from c3c9e16 to ec2ead6 Compare January 29, 2025 23:47
@stveit stveit force-pushed the jwt-refresh-token-model branch 3 times, most recently from 1417b06 to f118619 Compare February 4, 2025 01:56
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch 3 times, most recently from 66952f5 to 2190bad Compare February 4, 2025 03:17
@stveit stveit force-pushed the jwt-refresh-token-model branch from f118619 to 49c7894 Compare February 4, 2025 18:56
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch 2 times, most recently from 0f0ed15 to 825d9f3 Compare February 6, 2025 07:02
@stveit stveit force-pushed the jwt-refresh-token-model branch from 49c7894 to c87e7a1 Compare February 6, 2025 07:46
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch from 825d9f3 to c35d5b6 Compare February 6, 2025 07:47
@stveit stveit force-pushed the jwt-refresh-token-model branch from c87e7a1 to 50e46b4 Compare February 6, 2025 07:57
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch 2 times, most recently from 2889fe6 to 315df0c Compare February 6, 2025 08:25
@stveit stveit force-pushed the jwt-refresh-token-model branch from 50e46b4 to 15b2279 Compare February 21, 2025 10:09
@stveit stveit force-pushed the jwt-refresh-token-model branch 2 times, most recently from 14e45e7 to 4805ebf Compare February 21, 2025 11:43
@stveit stveit force-pushed the jwt-refresh-token-model branch from 4805ebf to 63b9bc5 Compare February 21, 2025 12:15
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch from 315df0c to d1bfbc3 Compare February 21, 2025 12:17
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch 2 times, most recently from a29801e to 8403bc7 Compare February 25, 2025 08:59
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch 2 times, most recently from 83c5074 to 511b5e1 Compare February 25, 2025 11:33
Copy link
Contributor

@johannaengland johannaengland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only nitpicks

Copy link

sonarqubecloud bot commented Mar 6, 2025

@stveit stveit requested a review from lunkwill42 April 29, 2025 09:48
@stveit stveit force-pushed the jwt-refresh-token-model branch from a3db8c2 to 5f15bb6 Compare May 5, 2025 11:52
Copy link
Member

@lunkwill42 lunkwill42 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got questions (that aren't necessarily showstoppers at all, just me being forgetful), but my major gripe is missing input validation.

Otherwise, this seems good :)


def post(self, request):
incoming_token = request.data.get('refresh_token')
token_hash = hash_token(incoming_token)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I browsed http://127.0.0.1:8000/api/refresh/ and posted an empty request, and this line crashed terribly with an AttributeError ('NoneType' object has no attribute 'encode').

Some input validation would be proper here - bad input should result in possibly a 400 Bad Request (depending on how other parts of the API behave with bad input).

Copy link
Contributor Author

@stveit stveit May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validation that input was given and that its a string. That should be enough, I dont think its necessary to check that the string has the format of a real token - if its not valid then there will be no match for the hash in the db

@@ -73,4 +73,5 @@
name="prefix-usage-detail",
),
re_path(r'^', include(router.urls)),
re_path(r'^refresh/$', views.JWTRefreshViewSet.as_view(), name='jwt-refresh'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to satisfy my own curiosity: I cannot recall why some API views are added to the DRF router instance, while some URLs are added manually to the URL config.

The noticeable difference (for me) is that URLs that are added manually like this do not show up in the official list of API endpoints (swagger/OpenAPI) when just visiting /api/.

These URLs will also not be part of the versioned API. We might need a discussion on why we have this setup and whether we should keep doing it (@hmpf )

Copy link
Contributor Author

@stveit stveit May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at this point I dont remember either 😛

probably had to do with it not being a modelview or something? similar to issues I had with #3354 perhaps

Comment on lines +1175 to +1181
try:
# If hash exists in the database, then we know it is a real token
db_token = JWTRefreshToken.objects.get(hash=token_hash)
except JWTRefreshToken.DoesNotExist:
return Response("Invalid token", status=status.HTTP_403_FORBIDDEN)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I'm sort of thinking that as long as we can verify that this token was signed by our key, it should be treated as valid (until its claims or dates are determined to be invalid, that is).

At the moment, I've entirely forgotten why we need to keep a reference to issued refresh tokens in the database, I just remember pointing out that storing the hash should be enough.

Only thing I can think of at the moment is that we need it to keep an audit trail?

Copy link
Contributor Author

@stveit stveit May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that we need some way to revoke tokens that may have fallen in the wrong hands. If we accept all tokens that are signed by us and an "attacker" gets their hands on a refresh token, then they can keep generating tokens forever. The only way to stop them would be to change our keys so the signature doesn't validate anymore. Storing the hash is a type of whitelisting that can easily be revoked by deleting the hash in the db (which can be done via the planned frontend)

A side effect of storing the hash is that we don't need to validate the signature in this endpoint, checking the hash is enough

Comment on lines 51 to 53
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
# Don't add passphrase
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come openssl cannot also be use to generate the key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be possible, this is how I've been doing it and it has worked well. I'll see if I can find the openssl equivalent just to have fewer dependencies here

Copy link
Contributor Author

@stveit stveit May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openssl genrsa -out jwtRS256.key 4096 seems to be the equivalent

Base automatically changed from jwt-refresh-token-model to master May 5, 2025 14:45
@stveit stveit force-pushed the jwt-refresh-token-endpoint branch from 91f2633 to a869e1d Compare May 6, 2025 07:48
@stveit
Copy link
Contributor Author

stveit commented May 6, 2025

shit got all messed up after merging #3268 so I needed to force push to remove unwanted commits

stveit added 2 commits May 6, 2025 10:23
Adds some basic validation of the incoming token.
No need to really check the format or anything,
if its not a valid token in any way it will fail
when we check the hash against the database.
The important part is that the incoming token is
a string that can actually be hashed
Copy link

sonarqubecloud bot commented May 6, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants