authentik: Non-admin user can retrieve confidential OAuth client_secret via /api/v3/oauth2/access_tokens/
Description
authentik is an open-source identity provider. In versions prior to 2025.12.5 and 2026.2.0-rc1 through 2026.2.2, authenticated non-admin users with at least one OAuth2 access token can retrieve the client_secret of confidential OAuth2 providers they have previously authenticated against, exposing sensitive information to users without the correct permissions. This logic is GET /api/v3/oauth2/access_tokens/. The API response includes a nested provider object containing client_id and client_secret for providers configured with client_type: confidential, which should not be accessible to low-privilege users. This issue has been fixed in versions 2025.12.5 and 2026.2.3.
Affected products
1Patches
24cfb61f83ba3website/docs: fix email link in CVE-2026-40166 (#22331)
1 file changed · +1 −1
website/docs/security/cves/CVE-2026-40166.md+1 −1 modified@@ -24,4 +24,4 @@ Restrict API access to `/api/v3/oauth2/access_tokens/` for non-admin users, or r If you have any questions or comments about this advisory: -- Email us at [[security@goauthentik.io](mailto:security@goauthentik.io)](mailto:security@goauthentik.io) +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
5053167a057einternal: Automated internal backport: CVE-2026-40166.sec.patch to authentik-main (#22299)
5 files changed · +43 −16
authentik/providers/oauth2/api/tokens.py+2 −2 modified@@ -9,18 +9,18 @@ from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.viewsets import GenericViewSet +from authentik.core.api.providers import ProviderSerializer from authentik.core.api.used_by import UsedByMixin from authentik.core.api.users import UserSerializer from authentik.core.api.utils import MetaNameSerializer, ModelSerializer -from authentik.providers.oauth2.api.providers import OAuth2ProviderSerializer from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken class ExpiringBaseGrantModelSerializer(ModelSerializer, MetaNameSerializer): """Serializer for BaseGrantModel and ExpiringBaseGrant""" user = UserSerializer() - provider = OAuth2ProviderSerializer() + provider = ProviderSerializer() scope = ListField(child=CharField()) class Meta:
packages/client-ts/src/models/ExpiringBaseGrantModel.ts+6 −6 modified@@ -12,8 +12,8 @@ * Do not edit the class manually. */ -import type { OAuth2Provider } from "./OAuth2Provider"; -import { OAuth2ProviderFromJSON, OAuth2ProviderToJSON } from "./OAuth2Provider"; +import type { Provider } from "./Provider"; +import { ProviderFromJSON, ProviderToJSON } from "./Provider"; import type { User } from "./User"; import { UserFromJSON, UserToJSON } from "./User"; @@ -31,10 +31,10 @@ export interface ExpiringBaseGrantModel { readonly pk: number; /** * - * @type {OAuth2Provider} + * @type {Provider} * @memberof ExpiringBaseGrantModel */ - provider: OAuth2Provider; + provider: Provider; /** * * @type {User} @@ -86,7 +86,7 @@ export function ExpiringBaseGrantModelFromJSONTyped( } return { pk: json["pk"], - provider: OAuth2ProviderFromJSON(json["provider"]), + provider: ProviderFromJSON(json["provider"]), user: UserFromJSON(json["user"]), isExpired: json["is_expired"], expires: json["expires"] == null ? undefined : new Date(json["expires"]), @@ -107,7 +107,7 @@ export function ExpiringBaseGrantModelToJSONTyped( } return { - provider: OAuth2ProviderToJSON(value["provider"]), + provider: ProviderToJSON(value["provider"]), user: UserToJSON(value["user"]), expires: value["expires"] == null ? value["expires"] : value["expires"].toISOString(), scope: value["scope"],
packages/client-ts/src/models/TokenModel.ts+6 −6 modified@@ -12,8 +12,8 @@ * Do not edit the class manually. */ -import type { OAuth2Provider } from "./OAuth2Provider"; -import { OAuth2ProviderFromJSON, OAuth2ProviderToJSON } from "./OAuth2Provider"; +import type { Provider } from "./Provider"; +import { ProviderFromJSON, ProviderToJSON } from "./Provider"; import type { User } from "./User"; import { UserFromJSON, UserToJSON } from "./User"; @@ -31,10 +31,10 @@ export interface TokenModel { readonly pk: number; /** * - * @type {OAuth2Provider} + * @type {Provider} * @memberof TokenModel */ - provider: OAuth2Provider; + provider: Provider; /** * * @type {User} @@ -96,7 +96,7 @@ export function TokenModelFromJSONTyped(json: any, ignoreDiscriminator: boolean) } return { pk: json["pk"], - provider: OAuth2ProviderFromJSON(json["provider"]), + provider: ProviderFromJSON(json["provider"]), user: UserFromJSON(json["user"]), isExpired: json["is_expired"], expires: json["expires"] == null ? undefined : new Date(json["expires"]), @@ -119,7 +119,7 @@ export function TokenModelToJSONTyped( } return { - provider: OAuth2ProviderToJSON(value["provider"]), + provider: ProviderToJSON(value["provider"]), user: UserToJSON(value["user"]), expires: value["expires"] == null ? value["expires"] : value["expires"].toISOString(), scope: value["scope"],
schema.yml+2 −2 modified@@ -39086,7 +39086,7 @@ components: readOnly: true title: ID provider: - $ref: '#/components/schemas/OAuth2Provider' + $ref: '#/components/schemas/Provider' user: $ref: '#/components/schemas/User' is_expired: @@ -57261,7 +57261,7 @@ components: readOnly: true title: ID provider: - $ref: '#/components/schemas/OAuth2Provider' + $ref: '#/components/schemas/Provider' user: $ref: '#/components/schemas/User' is_expired:
website/docs/security/cves/CVE-2026-40166.md+27 −0 added@@ -0,0 +1,27 @@ +# CVE-2026-40166 + +_Reported by [@Colbascov](https://github.com/Colbascov)_ + +## Non-admin users can read confidential OAuth provider client secrets via the access token endpoint + +### Summary + +Authenticated non-admin users with at least one OAuth2 access token can retrieve the `client_secret` of confidential OAuth2 providers they have previously authenticated against, via `GET /api/v3/oauth2/access_tokens/`. The API response includes a nested `provider` object containing `client_id` and `client_secret` for providers configured with `client_type: confidential`, which should not be accessible to low-privilege users. + +### Patches + +authentik 2025.12.5 and 2026.2.3 fix this issue; for other versions the workaround can be used. + +### Impact + +Any authenticated non-admin user who has previously completed an OAuth2 flow against a confidential provider — and therefore has an access token object returned by `/api/v3/oauth2/access_tokens/` — can read that provider's `client_secret`. Exposure is limited to providers the user has access to and has logged into at least once; users cannot read secrets for providers they have never authenticated against. This could allow unauthorized reuse of confidential client credentials depending on the provider configuration. + +### Workarounds + +Restrict API access to `/api/v3/oauth2/access_tokens/` for non-admin users, or review and limit which users are permitted to complete OAuth2 flows against confidential providers until a patched version can be applied. + +### For more information + +If you have any questions or comments about this advisory: + +- Email us at [[security@goauthentik.io](mailto:security@goauthentik.io)](mailto:security@goauthentik.io)
Vulnerability mechanics
Root cause
"The API serializer for access tokens used the full OAuth2ProviderSerializer, which exposes the client_secret, instead of the restricted ProviderSerializer that omits sensitive fields."
Attack vector
An authenticated non-admin user who has previously completed an OAuth2 flow against a confidential OAuth2 provider (client_type: confidential) can retrieve that provider's `client_secret` by sending a GET request to `/api/v3/oauth2/access_tokens/`. The API response includes a nested `provider` object containing both `client_id` and `client_secret`. The attacker does not need any special privileges beyond being authenticated and having previously obtained an access token from the target provider. Exposure is limited to providers the user has authenticated against at least once.
Affected code
The vulnerability is in the API endpoint `GET /api/v3/oauth2/access_tokens/` defined in `authentik/providers/oauth2/api/tokens.py`. The `ExpiringBaseGrantModelSerializer` used `OAuth2ProviderSerializer` for the nested `provider` field, which returned the full `OAuth2Provider` object including `client_secret` for confidential providers. The same issue existed in the TypeScript client models `TokenModel.ts` and `ExpiringBaseGrantModel.ts` which referenced `OAuth2Provider` instead of the more restricted `Provider` type.
What the fix does
The patch in `authentik/providers/oauth2/api/tokens.py` replaces `OAuth2ProviderSerializer` with `ProviderSerializer` for the `provider` field in `ExpiringBaseGrantModelSerializer` [patch_id=1693025]. The `ProviderSerializer` is a more restricted serializer that does not expose sensitive fields like `client_secret`, whereas `OAuth2ProviderSerializer` returned the full provider object. The corresponding TypeScript client models (`TokenModel.ts` and `ExpiringBaseGrantModel.ts`) were also updated to use the `Provider` type instead of `OAuth2Provider` to match the server-side change. The schema.yml was updated accordingly to reflect the new `$ref` to the `Provider` schema instead of `OAuth2Provider`.
Preconditions
- authAttacker must be an authenticated non-admin user of the authentik instance.
- inputAttacker must have previously completed an OAuth2 flow against a confidential OAuth2 provider (client_type: confidential) to have an access token object associated with their account.
Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3- github.com/goauthentik/authentik/releases/tag/version%2F2025.12.5mitrex_refsource_MISC
- github.com/goauthentik/authentik/releases/tag/version%2F2026.2.3mitrex_refsource_MISC
- github.com/goauthentik/authentik/security/advisories/GHSA-hhpc-rqgm-pxj4mitrex_refsource_CONFIRM
News mentions
0No linked articles in our index yet.