goauthentik / goauthentik/authentik
Property-mapping test-execution endpoint doesn't enforce the same return-type rules as real token issuance, giving false confidence
- Dominant language
- Python
- Stars
- 25.6k
- Forks
- 2k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 644
Description
### Summary
`POST /api/v3/propertymappings/all/{id}/test/` evaluates a `ScopeMapping`'s expression standalone and returns whatever the expression returns, reporting `"successful": true` as long as the expression doesn't raise. The real token-issuance path (`UserInfoView.get_claims()` in `authentik/providers/oauth2/views/userinfo.py`) has an additional requirement the test endpoint doesn't enforce or even warn about: **the returned value must be a `dict`**, or it's silently discarded (logged only as a `LOGGER.warning`, invisible to the admin) and contributes nothing to the token/userinfo response.
This means an admin can write a scope mapping expression like:
```python
return ["some-value"]
```
...test it via the UI/API, see `{"result": "[\"some-value\"]", "successful": true}`, reasonably conclude it works, wire up an OIDC-consuming application around it — and have it silently do nothing in every real login, with the only trace being a `LOGGER.warning` in Authentik's own server logs that the affected downstream application never sees.
### Environment
- Authentik version: `2026.5.0`
### Steps to reproduce
1. Create a custom Scope Mapping (`authentik_property_mapping_provider_scope`) with an expression that returns a plain list, e.g.:
```python
return ["consoleAdmin"]
```
2. Attach it to an OAuth2/OIDC provider's `property_mappings`, with a `scope_name` matching a scope the client will actually request.
3. Test it via `POST /api/v3/propertymappings/all/{id}/test/` with a real user PK.
4. Separately, complete a full OIDC login through that provider and inspect the resulting ID token / userinfo response (e.g. via `GET /api/v3/oauth2/access_tokens/`).
### Expected
Either:
- The test endpoint rejects/flags non-dict returns the same way `get_claims()` does, so the admin finds out immediately, or
- `get_claims()` itself is more forgiving (e.g. wraps a non-dict return under the mapping's `scope_name` as the key, rather than discarding it) — many admins would reasonably expect `return [...]` for a "groups"/"roles"-style mapping to work this way, and the current MinIO/RustFS-style downstream integrations often want exactly a bare list.
### Actual
1. The test endpoint returns `{"result": "[\"consoleAdmin\"]", "successful": true}` — appears correct.
2. The real ID token contains no trace of the mapping's intended claim at all; the only evidence anything went wrong is a `LOGGER.warning("Scope returned a non-dict value, ignoring", scope=scope, value=value)` in Authentik's server-side logs, which a typical admin debugging a downstream application (RustFS/MinIO/etc., in our case) would have no reason to go looking at.
### Suggested fix
At minimum, have the test-execution endpoint surface a warning (not just `"successful": true`) when the expression's return value wouldn't actually be included in a real token — e.g. `"successful": true, "warning": "non-dict return values are ignored during real claim assembly"`. This closes the gap between "the test tool says it works" and "it silently does nothing in production" without changing any existing behavior for expressions that already return dicts correctly.
Contributor guide
Research direction
Start with UserInfoView.get_claims() in authentik/providers/oauth2/views/userinfo.py, then trace the POST /api/v3/propertymappings/all/{id}/test/ endpoint that evaluates the expression. Compare how non-dict returns are handled in both paths and add a visible warning for test results that real claim assembly would ignore. Done means the endpoint flags a list return while preserving successful behavior for valid dict returns.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, authentication, authorization
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 67/100