Skip to content

Fix TypeError at /o/token/#1597

Open
aibaq wants to merge 3 commits intodjango-oauth:masterfrom
aibaq:master
Open

Fix TypeError at /o/token/#1597
aibaq wants to merge 3 commits intodjango-oauth:masterfrom
aibaq:master

Conversation

@aibaq
Copy link
Copy Markdown

@aibaq aibaq commented Sep 16, 2025

Fixes #

Description of the Change

Django User's last_login field is nullable, so /o/token/ endpoint raises TypeError when last_login is null when using oidc

Снимок экрана 2025-09-16 в 13 05 42

This fix sets timezone.now() as a default value for auth_time

Checklist

  • PR only contains one change (considered splitting up PR)
  • unit-test added
  • [o] documentation updated (not needed)
  • CHANGELOG.md updated (only for user relevant changes)
  • author name in AUTHORS

@aibaq aibaq changed the title Fix TypeError at /s/auth/o/token/ Fix TypeError at /o/token/ Sep 16, 2025
@codecov
Copy link
Copy Markdown

codecov bot commented Oct 3, 2025

Codecov Report

❌ Patch coverage is 66.66667% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
oauth2_provider/oauth2_validators.py 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@dopry
Copy link
Copy Markdown
Member

dopry commented Oct 3, 2025

@aibaq this is a great start. to help me understand the issue a bit more, in what scenario did you encounter this issue? I would expect that a user would have logged in at least once by the time get_id_token_dictionary was called. I'd like to understand how this was occurring to both correct my expectations and ensure there isn't a bug somewhere else that is allow a token to be generated for a user that isn't authenticated. It also looks like there is a code path that is uncovered that will need to be tested.

@dopry dopry requested a review from Copilot October 3, 2025 16:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This pull request fixes a TypeError that occurs at the /o/token/ endpoint when using OIDC and the Django User's last_login field is null. The fix adds a null check for last_login and provides a fallback value using the current timestamp.

  • Added null safety check for request.user.last_login before accessing it
  • Set timezone.now().timestamp() as fallback when last_login is null
  • Added test case to verify the fix works with null last_login values

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
oauth2_provider/oauth2_validators.py Added null check for last_login and fallback to current timestamp
tests/test_oauth2_validators.py Added test setup to simulate user with null last_login
CHANGELOG.md Added entry for the TypeError fix
AUTHORS Added contributor name

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@dopry
Copy link
Copy Markdown
Member

dopry commented Oct 31, 2025

@aibaq We won't be able to close this without the test coverage and details so we can reproduce the bug.

@amatissart
Copy link
Copy Markdown

For context, we recently hit the same error after upgrading oauthlib from 3.2 to 3.3. The "id_token" is now included in refresh token responses when the the openid scope is provided (oauthlib/oauthlib#838). A workaround for us was to change the default scopes so client apps that don't explicitly require openid don't get the ID token claims.

However, it's unclear for me how this should behave for users who interact only with OAuth applications. For these users, their User.last_login might remain None (unless this indicates a misconfiguration on our side?). I couldn't find proper guidance on how to update last_login when using django-oauth-toolkit, although some approaches were suggested in #348. So it is not clear whether it's safe to assume that last_login is always non-None in this code path.

@dopry
Copy link
Copy Markdown
Member

dopry commented Jan 9, 2026

@amatissart thanks for the additional context. I'm definitely open to the PR.

As you noted the guidance with regard to last_login is unclear. I see there is a test for the case, but it explicitly sets user.last_login to None. I'd like a clear understanding of how this happens in the wild and for that to be clearly documented in the code at the conditional or in a test.

The test explicitly sets the user.last_login to none, which is a bit synthetic.

I feel like the field being nullabe is probably sufficient for having some sort of fallback... but that raises the question of how should we handle this when user.last_login is not set? Is now appropriate? Should auth_time be omitted in these cases, it is optional after all? Should we look to the token chain and look for a a created time on the earliest token there? Or are we missing a mechanism to track session time that allows us to provide a reliable auth_time that can be used for session expiration? If user.last_login is not set, should we set it and then use that for auth_time?

I want to be sure we're doing the right thing, but I have limited time to research these things as a maintainer. We really depend on the support of the wider community and contributors who are focused on these issues to help us ensure we're doing what's right for the wider user community...

@dopry dopry requested a review from Copilot March 12, 2026 20:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment on lines 899 to 910
expiration_time = timezone.now() + timedelta(seconds=oauth2_settings.ID_TOKEN_EXPIRE_SECONDS)
if request.user.last_login:
auth_time = int(dateformat.format(request.user.last_login, "U"))
else:
auth_time = int(timezone.now().timestamp())
# Required ID Token claims
claims.update(
**{
"iss": self.get_oidc_issuer_endpoint(request),
"exp": int(dateformat.format(expiration_time, "U")),
"auth_time": int(dateformat.format(request.user.last_login, "U")),
"auth_time": auth_time,
"jti": str(uuid.uuid4()),
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

int(timezone.now().timestamp()) can differ from Django’s dateformat.format(..., 'U') behavior (notably when USE_TZ=False, where .timestamp() on naive datetimes is interpreted in local time). For consistency with the existing codepath and Django’s formatting semantics, compute the fallback using the same mechanism as the non-null branch (i.e., format timezone.now() with 'U').

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +64
self.user.last_login = None
self.user.save()
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Setting last_login = None in setUp() adds an extra DB write for every test in this class and makes the entire class depend on a special user state (even in tests unrelated to OIDC auth_time). Prefer moving this setup into the specific test(s) that exercise the last_login is None path, or create a dedicated user within that test to keep unrelated tests isolated and faster.

Suggested change
self.user.last_login = None
self.user.save()

Copilot uses AI. Check for mistakes.
* #1252 Fix crash when 'client' is in token request body
* #1496 Fix error when Bearer token string is empty but preceded by `Bearer` keyword.
* #1630 use token_checksum for lookup in _get_token_from_authentication_server
* #1597 Fix: TypeError at /s/auth/o/token/
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

The PR description refers to the /o/token/ endpoint, but the changelog entry records /s/auth/o/token/. This looks inconsistent and may mislead users; update the changelog entry to match the actual endpoint and wording used by the project.

Copilot uses AI. Check for mistakes.
@dopry
Copy link
Copy Markdown
Member

dopry commented Mar 13, 2026

@aibaq can you address the copilot comments and rebase this branch?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants