Skip to content

feat(api-tokens): API token expiry and revoke-by-key endpoint#14932

Open
svader0 wants to merge 9 commits into
DefectDojo:devfrom
svader0:token-revocation
Open

feat(api-tokens): API token expiry and revoke-by-key endpoint#14932
svader0 wants to merge 9 commits into
DefectDojo:devfrom
svader0:token-revocation

Conversation

@svader0

@svader0 svader0 commented May 29, 2026

Copy link
Copy Markdown
Contributor

This PR addresses issue/feature request #14890.

NOTE: This PR description has been updated away from the original commit, which was significantly different.

Description

Currently, DefectDojo has no mechanism for managing API tokens via the API. In case a token gets leaked or needs to be revoked, the only existing solution is to call reset_api_token, which simply rotates the token. Superusers also have no way to see what tokens exist, who owns them, or when they were issued. This PR is an attempt to close the gap a little.

This PR introduces a per-user token expiry. A superuser can set a token_expiry on a specific user via the existing user_contact_infos endpoint. Expired tokens are rejected at authentication time. In addition to this, a new instance-wide environment variable DD_API_TOKEN_DEFAULT_EXPIRY_DAYS (talk about a mouthful...) applies a default lifetime to all generated tokens. The default is 0, a.k.a. no expiry, so there should be no issues with existing deployments unless they choose to opt into the change.

This PR also introduces one new endpoint POST /api/v2/api-tokens/revoke/, where you supply a key via JSON payload, and it gets revoked. Currently, you can only rotate a user's key by user lookup, you can't eradicate a specific key itself. This would solve the use case where, "uh oh!", a token gets leaked, and I want to eradicate it immediately and programmatically without tracking down its owner and rotating it.

Documentation

docs/content/automation/api/api-v2-docs.md updated with new endpoint, expiry behavior, and the DD_API_TOKEN_DEFAULT_EXPIRY_DAYS variable.

Checklist

This checklist is for your information.

  • Make sure to rebase your PR against the very latest dev.
  • Features/Changes should be submitted against the dev.
  • Bugfixes should be submitted against the bugfix branch.
  • Give a meaningful name to your PR, as it may end up being used in the release notes.
  • Your code is Ruff compliant (see ruff.toml).
  • Your code is python 3.13 compliant.
  • If this is a new feature and not a bug fix, you've included the proper documentation in the docs at https://github.com/DefectDojo/django-DefectDojo/tree/dev/docs as part of this PR.
  • Model changes must include the necessary migrations in the dojo/db_migrations folder.
  • Add applicable tests to the unit tests.
  • Add the proper label to categorize your PR.

svader0 added 7 commits May 26, 2026 12:13
- Add token_expiry DateTimeField to UserContactInfo (was missing from
  initial commit despite migration referencing it)
- Register ApiTokenViewSet at api-tokens/ in urls.py (was missing from
  initial commit despite ViewSet existing in views.py)
- Add unit tests for list, retrieve, revoke, expiry enforcement, and
  default-expiry-on-reset behaviours
@svader0 svader0 requested review from Maffooch and mtesauro as code owners May 29, 2026 23:59
@github-actions github-actions Bot added New Migration Adding a new migration file. Take care when merging. settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR apiv2 docs unittests ui labels May 30, 2026
@dryrunsecurity

dryrunsecurity Bot commented May 30, 2026

Copy link
Copy Markdown

DryRun Security

This pull request triggers multiple critical findings from the configured codepaths analyzer, indicating sensitive edits across core files such as models.py, urls.py, authentication.py, views.py, and serializers.py, which may bypass intended security controls; additionally, a medium-severity issue reveals that API token auto-creation in user/views.py bypasses token expiry policies by failing to initialize the token_expiry field, potentially allowing indefinite token validity.

🔴 Configured Codepaths Edit in dojo/api_v2/serializers.py (drs_0220be96)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/api_v2/views.py (drs_e0bcfe7f)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/db_migrations/0269_usercontactinfo_token_expiry.py (drs_850baef5)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/models.py (drs_0aa4b6d6)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/templates/dojo/api_v2_key.html (drs_0d836839)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/urls.py (drs_d1116fbb)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/user/authentication.py (drs_bbd8f2de)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/user/views.py (drs_f521fc46)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/models.py (drs_b492fa69)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/urls.py (drs_76d499ca)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/user/authentication.py (drs_a6fb3017)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/user/authentication.py (drs_637a190b)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/api_v2/views.py (drs_f690e06f)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/user/authentication.py (drs_b4eef600)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/models.py (drs_0b272573)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/urls.py (drs_8620e6e2)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/api_v2/serializers.py (drs_0ed544d0)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/api_v2/views.py (drs_32b9ac12)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/urls.py (drs_2a60837c)
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🟡 Token auto-creation bypasses expiry policy in dojo/user/views.py (drs_34ed9c3e)
Vulnerability Token auto-creation bypasses expiry policy
Description In dojo/user/views.py, the api_v2_key view automatically creates an API token for users who do not have one. This creation process does not initialize the token_expiry field in the user's UserContactInfo. As a result, newly created tokens via this path have token_expiry set to None (or effectively uninitialized). The ExpiringTokenAuthentication middleware in dojo/user/authentication.py only enforces expiration if uci.token_expiry is set and in the past; if it is None, the token remains valid indefinitely, allowing users to effectively bypass any configured expiration policy.

except Token.DoesNotExist:
api_key = Token.objects.create(user=request.user)

We've notified @mtesauro.


Comment to provide feedback on these findings.

Report false positive: @dryrunsecurity fp [FINDING ID] [FEEDBACK]
Report low-impact: @dryrunsecurity nit [FINDING ID] [FEEDBACK]

Example: @dryrunsecurity fp drs_90eda195 This code is not user-facing

All finding details can be found in the DryRun Security Dashboard.

@valentijnscholten

Copy link
Copy Markdown
Member

Thank you for the PR. At first glance there seem to be various functionalities in this PR. I can imagine having an expiry datetime/period for tokens is good to have and not to hard to maintain. But I'm unsure of the other items like allowing super users to manage other users tokens etc. Do you have good example of use cases for each feature introduced by the PR?

@svader0

svader0 commented May 31, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look! I definitely agree about the expiry date/time period. After thinking about it for a while, it seems that the practical use-case of the token-management stuff was a little too overstated, so I made a few changes to slim it down.

Token expiry: Like you said, it's fairly easy implementation and maintenance, and having an expiration date on API keys is pretty standard behavior. If a key were to get leaked, having an expiration date will reduce risk. I think this should stay as-is.

Token Management: The thinking was that if a token was compromised, as a superuser you may want to revoke it immediately without issuing a replacement. I was originally following the train of logic set about by the issue I was responding to, where you'd want a superuser to be able to have a list of existing tokens and then be able to revoke them selectively. But I think that in trying to solve that problem, I accidentally made things a little convoluted here, so I went ahead and simplified the solution.

Now there is one new endpoint in this PR, POST /api/v2/api-tokens/revoke/, where you supply a key via JSON payload, and it gets revoked. Currently, you can only rotate a user's key by user lookup, you can't eradicate a specific key itself. With this implementation, we avoid the API bloat of needing to look up all existing tokens, filter, and revoke like I had previously.
This would solve the use case where, "uh oh!", a token gets leaked, and I want to eradicate it immediately and programmatically without tracking down its owner and rotating it.

If you think these two additions are too distinct to be in one PR, I am happy to open up a second and split them up.

@svader0 svader0 changed the title feat(api-tokens): list, revoke, and per-user expiry feat(api-tokens): API token expiry and revoke-by-key endpoint May 31, 2026
@valentijnscholten

Copy link
Copy Markdown
Member

So this PR is hard to review not because of the implementation, but because it needs some careful thinking about API design, features, security, etc. My feeling is that it cannot be merged as-is. I have asked Claude also to review it and that review below contains some of my concerns. My suggestion would be to switch to django-restknow (or another library if it exists) and do it right. Might be a good time right now while we're "in transition" to v3. I'm going to have to ask @Maffooch and @mtesauro to lean in here as well, and will discuss it with them.

@valentijnscholten

Copy link
Copy Markdown
Member

Thanks for working on this — token lifecycle management is a real gap. I reviewed the full diff; some of this is blocking-ish (correctness gaps), and there's a bigger-picture question at the end about foundation.

Correctness

What's solid:

  • ExpiringTokenAuthentication cleanly wraps DRF TokenAuthentication and is swapped in via DEFAULT_AUTHENTICATION_CLASSES.
  • RevokeApiTokenView is gated by IsSuperUserOrGlobalOwner, and user_contact_infos is IsSuperUser-gated, so the "superuser-only" claims actually hold. UserContactInfoSerializer uses fields = "__all__", so token_expiry is genuinely PATCH-able there.

Gaps:

  1. Default expiry is bypassed on 2 of the 3 token-creation paths. It's only applied inside reset_token_for_user. But api_v2_key auto-creates a token on GET (Token.objects.create(...)), and api-token-auth (obtain_auth_token) does get_or_create — both with no expiry. So after a superuser revokes a leaked token, the owner can just reload /api/key-v2 (or hit api-token-auth) and get a fresh non-expiring token, even with DD_API_TOKEN_DEFAULT_EXPIRY_DAYS=90. The "applies a default lifetime to all generated tokens" claim doesn't hold, and revoke isn't durable. All token creation should funnel through one place that applies the default.

  2. Expiry is decoupled from the token (stored on UserContactInfo, not the Token). Rotation has to remember to update it, and reset_token_for_user clobbers any superuser-set per-user expiry back to the instance default (documented, but a confusing wart). Any token created outside that one function has no expiry — see Same name as commercial software #1.

  3. 401 vs 403: AuthenticationFailed normally yields 401; this returns 403 only because a non-token authenticator happens to be first in the list so authenticate_header is None. Works today but is order-dependent/fragile (and 401 is arguably more correct for an expired credential).

  4. Migration collision: 0269_usercontactinfo_token_expiry depends on 0268, but dev already has 0269_normalize_blank_finding_components. Needs renumbering to 0270 before it'll merge.

  5. No audit trail / owner notification on revoke — for an incident-response feature that's a notable omission.

Missing features

  • The PR description still advertises a /api/v2/api-tokens/ list/retrieve viewset for superuser-wide token visibility, but that's not in the diff (only revoke/ + expiry). Worth updating the description.
  • No per-token expiry and no multiple-tokens-per-user (both blocked by DRF's one-token-per-user model).
  • Tokens remain stored in plaintext in the DB (DRF default); the revoke endpoint looks them up by raw key. Expiry without hashed storage leaves the larger exposure untouched.

Foundation: should this be built on django-rest-knox?

This is the bigger question. django-rest-knox provides natively almost everything being hand-rolled here: per-token expiry (on the token itself, no UserContactInfo decoupling), multiple tokens per user, hashed token storage (a DB leak doesn't expose usable tokens), and built-in logout/revoke endpoints + an enforcing auth class.

The catch is that adopting it is a breaking, multi-release migration, not a tweak to this PR: DefectDojo's whole ecosystem uses DRF's Token <key> format, so a switch changes token format/length and the api-token-auth flow and breaks every existing integration unless both auth backends run in parallel during a deprecation window with token re-issuance. That's an epic, well beyond this PR's scope.

So my suggestion is not to retrofit Knox here. Either:

  • (a) land a corrected incremental version — funnel all token creation through one default-expiry path (fixes Same name as commercial software #1), drop the "all tokens" overclaim, add an audit entry, renumber the migration — accepting per-user/single-token semantics for now; or
  • (b) if multi-token / hashed storage / per-token expiry is the actual goal, treat Knox as a dedicated foundation epic and supersede this PR.

The decoupled-expiry and bypass-path issues above are essentially symptoms of forcing per-token semantics onto DRF's single-plaintext-token model — which is exactly what Knox exists to replace. Happy to help scope the Knox migration if the team wants to go that route.

@svader0

svader0 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Makes a lot of sense. You're right in that this is more of a shotgun-surgery type of fix; I wasn't quite sure how drastic we wanted to go. Happy to help with implementing a more robust solution depending on what the higher-ups decide.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@Maffooch

Copy link
Copy Markdown
Contributor

@valentijnscholten thanks for the careful consideration here. I think you're right that this would be a better API v3 ONLY change. There have been no expirations on API v2 for many years, so this would be a big shift.

I would feel better if this new mechanism only impacted v3 so that the correct expectation can be set from the get go

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

Labels

apiv2 conflicts-detected docs New Migration Adding a new migration file. Take care when merging. settings_changes Needs changes to settings.py based on changes in settings.dist.py included in this PR ui unittests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants