goauthentik / goauthentik/authentik
OAuth2 token ViewSets bypass RBAC — get_queryset hardcodes is_superuser, unlike AuthenticatedSessionViewSet
- Dominant language
- Python
- Stars
- 25.6k
- Forks
- 2k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 644
Description
### Is your feature request related to a problem?
Yes — least-privilege service accounts cannot manage OAuth2 tokens. On authentik 2026.5.3, `RefreshTokenViewSet` / `AccessTokenViewSet` / `AuthorizationCodeViewSet` (`authentik/providers/oauth2/api/tokens.py`) implement:
```python
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)
```
This runs before the RBAC layer, so a service account holding the **global `authentik_providers_oauth2.view_refreshtoken` / `delete_refreshtoken` permissions** still only ever sees its own tokens. By contrast, `AuthenticatedSessionViewSet` respects the generic RBAC `ObjectFilter` / `ObjectPermissions` — global `view_/delete_authenticatedsession` grants work as expected there.
Concrete use case: an application backend implementing **"log out all devices"** for its users. Killing sessions works with a tightly-scoped Role, but session deletion intentionally spares refresh tokens (`RefreshToken.session` is `SET_DEFAULT` — persisting `offline_access` past session termination is deliberate and documented in the model), so *completing* the logout requires revoking the user's refresh tokens — which today forces the application backend to hold a **full superuser token**, a much larger blast radius than the operation needs.
### Describe the solution you'd like
Honor object-level RBAC on the OAuth2 token ViewSets the same way `AuthenticatedSessionViewSet` does (drop the `is_superuser` short-circuit in `get_queryset()` in favor of the `ObjectFilter` path), so a Role granting `view_`/`delete_` on refresh tokens can administer them.
A bulk "revoke all tokens for user X" action would additionally simplify the logout-all case (sessions have `bulk_delete`; the token ViewSets have no equivalent), but the RBAC fix alone is enough.
### Describe alternatives that you've considered
- **Superuser-bound API token held by the application backend**, tightly encapsulated (dedicated service account, network isolation, every call audited) — works, and is what we'll do meanwhile, but it grants far more than the operation needs.
- **RFC 7009 revocation endpoint** — only revokes a token the client presents; the backend does not hold other devices' refresh tokens, so logout-all cannot be built on it.
- **Deleting sessions only** — deliberately spares `offline_access` refresh tokens, so a stolen refresh token would survive the user pressing "log out everywhere".
### Additional context
Version: authentik 2026.5.3, self-hosted (official helm chart). Verified against the source at tag `version/2026.5.3`: `authentik/providers/oauth2/api/tokens.py` `get_queryset` (L101-105, byte-identical on current `main`); the sessions-side comparison is `authentik/core/api/authenticated_sessions.py` + `authentik/rbac/filters.py::ObjectFilter`. Happy to provide more detail or test a patch.
*(Research assisted by AI; all cited code paths were verified by hand against the tag.)*
Contributor guide
Research direction
Start in authentik/providers/oauth2/api/tokens.py and compare the token ViewSets with AuthenticatedSessionViewSet in authentik/core/api/authenticated_sessions.py. Read the ObjectFilter path in authentik/rbac/filters.py, then identify the relevant existing API tests before changing behavior. Done means appropriately scoped global view_ and delete_ permissions can administer OAuth2 tokens without requiring superuser access.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, authorization, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100