Skip to content

Commit 4ef3573

Browse files
providers/oauth2: allow cross provider token introspection for federated providers (cherry-pick #21513 to version-2025.12) (#21747)
Cherry-pick #21513 to version-2025.12 (with conflicts) This cherry-pick has conflicts that need manual resolution. Original PR: #21513 Original commit: c84c8d8 Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens L. <jens@goauthentik.io>
1 parent 8b8ad4d commit 4ef3573

8 files changed

Lines changed: 214 additions & 94 deletions

File tree

authentik/providers/oauth2/tests/test_authorize.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ def test_full_implicit(self):
372372
"nonce": generate_id(),
373373
},
374374
)
375-
token: AccessToken = AccessToken.objects.filter(user=user).first()
375+
token = AccessToken.objects.filter(user=user).first()
376376
expires = timedelta_from_string(provider.access_token_validity).total_seconds()
377377
self.assertEqual(
378378
response.url,
@@ -444,7 +444,7 @@ def test_full_implicit_enc(self):
444444
},
445445
)
446446
self.assertEqual(response.status_code, 302)
447-
token: AccessToken = AccessToken.objects.filter(user=user).first()
447+
token = AccessToken.objects.filter(user=user).first()
448448
expires = timedelta_from_string(provider.access_token_validity).total_seconds()
449449
jwt = self.validate_jwe(token, provider)
450450
self.assertEqual(jwt["amr"], ["pwd"])
@@ -543,7 +543,7 @@ def test_full_form_post_id_token(self):
543543
response = self.client.get(
544544
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
545545
)
546-
token: AccessToken = AccessToken.objects.filter(user=user).first()
546+
token = AccessToken.objects.filter(user=user).first()
547547
self.assertIsNotNone(token)
548548
self.assertJSONEqual(
549549
response.content.decode(),

authentik/providers/oauth2/tests/test_backchannel_logout.py

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@
44

55
import jwt
66
from django.test import RequestFactory
7-
from django.utils import timezone
87
from dramatiq.results.errors import ResultFailure
98
from requests import Response
109
from requests.exceptions import HTTPError, Timeout
1110

12-
from authentik.core.models import Application, AuthenticatedSession, Session
11+
from authentik.core.models import Application
1312
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
1413
from authentik.lib.generators import generate_id
1514
from authentik.providers.oauth2.id_token import hash_session_key
1615
from authentik.providers.oauth2.models import (
17-
AccessToken,
1816
OAuth2LogoutMethod,
1917
OAuth2Provider,
2018
RedirectURI,
2119
RedirectURIMatchingMode,
22-
RefreshToken,
2320
)
2421
from authentik.providers.oauth2.tasks import send_backchannel_logout_request
2522
from authentik.providers.oauth2.tests.utils import OAuthTestCase
@@ -45,52 +42,6 @@ def setUp(self) -> None:
4542
self.app.provider = self.provider
4643
self.app.save()
4744

48-
def _create_session(self, session_key=None):
49-
"""Create a session with the given key or a generated one"""
50-
session_key = session_key or f"session-{generate_id()}"
51-
session = Session.objects.create(
52-
session_key=session_key,
53-
expires=timezone.now() + timezone.timedelta(hours=1),
54-
last_ip="255.255.255.255",
55-
)
56-
auth_session = AuthenticatedSession.objects.create(
57-
session=session,
58-
user=self.user,
59-
)
60-
return auth_session
61-
62-
def _create_token(
63-
self, provider, user, session=None, token_type="access", token_id=None
64-
): # nosec
65-
"""Create a token of the specified type"""
66-
token_id = token_id or f"{token_type}-token-{generate_id()}"
67-
kwargs = {
68-
"provider": provider,
69-
"user": user,
70-
"session": session,
71-
"token": token_id,
72-
"_id_token": "{}",
73-
"auth_time": timezone.now(),
74-
}
75-
76-
if token_type == "access": # nosec
77-
return AccessToken.objects.create(**kwargs)
78-
else: # refresh
79-
return RefreshToken.objects.create(**kwargs)
80-
81-
def _create_provider(self, name=None):
82-
"""Create an OAuth2 provider"""
83-
name = name or f"provider-{generate_id()}"
84-
provider = OAuth2Provider.objects.create(
85-
name=name,
86-
authorization_flow=create_test_flow(),
87-
redirect_uris=[
88-
RedirectURI(RedirectURIMatchingMode.STRICT, f"http://{name}/callback"),
89-
],
90-
signing_key=self.keypair,
91-
)
92-
return provider
93-
9445
def _create_logout_token(
9546
self,
9647
provider: OAuth2Provider | None = None,

authentik/providers/oauth2/tests/test_introspect.py

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from authentik.providers.oauth2.id_token import IDToken
1515
from authentik.providers.oauth2.models import (
1616
AccessToken,
17+
ClientTypes,
1718
OAuth2Provider,
1819
RedirectURI,
1920
RedirectURIMatchingMode,
@@ -43,7 +44,7 @@ def setUp(self) -> None:
4344

4445
def test_introspect_refresh(self):
4546
"""Test introspect"""
46-
token: RefreshToken = RefreshToken.objects.create(
47+
token = RefreshToken.objects.create(
4748
provider=self.provider,
4849
user=self.user,
4950
token=generate_id(),
@@ -75,7 +76,7 @@ def test_introspect_refresh(self):
7576

7677
def test_introspect_access(self):
7778
"""Test introspect"""
78-
token: AccessToken = AccessToken.objects.create(
79+
token = AccessToken.objects.create(
7980
provider=self.provider,
8081
user=self.user,
8182
token=generate_id(),
@@ -130,7 +131,7 @@ def test_introspect_invalid_provider(self):
130131
)
131132
auth = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
132133

133-
token: AccessToken = AccessToken.objects.create(
134+
token = AccessToken.objects.create(
134135
provider=self.provider,
135136
user=self.user,
136137
token=generate_id(),
@@ -169,3 +170,76 @@ def test_introspect_invalid_auth(self):
169170
"active": False,
170171
},
171172
)
173+
174+
def test_introspect_provider_public(self):
175+
"""Test introspect"""
176+
self.provider.client_type = ClientTypes.PUBLIC
177+
self.provider.save()
178+
token = AccessToken.objects.create(
179+
provider=self.provider,
180+
user=self.user,
181+
token=generate_id(),
182+
auth_time=timezone.now(),
183+
_scope="openid user profile",
184+
_id_token=json.dumps(
185+
asdict(
186+
IDToken("foo", "bar"),
187+
)
188+
),
189+
)
190+
res = self.client.post(
191+
reverse("authentik_providers_oauth2:token-introspection"),
192+
HTTP_AUTHORIZATION=f"Basic {self.auth}",
193+
data={"token": token.token},
194+
)
195+
self.assertEqual(res.status_code, 200)
196+
self.assertJSONEqual(
197+
res.content.decode(),
198+
{
199+
"active": False,
200+
},
201+
)
202+
203+
def test_introspect_provider_fed(self):
204+
"""Test introspect with federation. self.provider is a confidential
205+
client and other_provider is a public client."""
206+
other_provider = OAuth2Provider.objects.create(
207+
name=generate_id(),
208+
authorization_flow=create_test_flow(),
209+
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
210+
signing_key=create_test_cert(),
211+
client_type=ClientTypes.PUBLIC,
212+
)
213+
Application.objects.create(name=generate_id(), slug=generate_id(), provider=other_provider)
214+
215+
other_provider.jwt_federation_providers.add(self.provider)
216+
217+
token = AccessToken.objects.create(
218+
provider=other_provider,
219+
user=self.user,
220+
token=generate_id(),
221+
auth_time=timezone.now(),
222+
_scope="openid user profile",
223+
_id_token=json.dumps(
224+
asdict(
225+
IDToken("foo", "bar"),
226+
)
227+
),
228+
)
229+
res = self.client.post(
230+
reverse("authentik_providers_oauth2:token-introspection"),
231+
HTTP_AUTHORIZATION=f"Basic {self.auth}",
232+
data={"token": token.token},
233+
)
234+
self.assertEqual(res.status_code, 200)
235+
self.assertJSONEqual(
236+
res.content.decode(),
237+
{
238+
"acr": ACR_AUTHENTIK_DEFAULT,
239+
"sub": "bar",
240+
"iss": "foo",
241+
"active": True,
242+
"client_id": other_provider.client_id,
243+
"scope": " ".join(token.scope),
244+
},
245+
)

authentik/providers/oauth2/tests/test_revoke.py

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def setUp(self) -> None:
4646

4747
def test_revoke_refresh(self):
4848
"""Test revoke"""
49-
token: RefreshToken = RefreshToken.objects.create(
49+
token = RefreshToken.objects.create(
5050
provider=self.provider,
5151
user=self.user,
5252
token=generate_id(),
@@ -69,7 +69,7 @@ def test_revoke_refresh(self):
6969

7070
def test_revoke_access(self):
7171
"""Test revoke"""
72-
token: AccessToken = AccessToken.objects.create(
72+
token = AccessToken.objects.create(
7373
provider=self.provider,
7474
user=self.user,
7575
token=generate_id(),
@@ -105,7 +105,19 @@ def test_revoke_invalid_auth(self):
105105
"""Test revoke (invalid auth)"""
106106
res = self.client.post(
107107
reverse("authentik_providers_oauth2:token-revoke"),
108-
HTTP_AUTHORIZATION="Basic fqewr",
108+
HTTP_AUTHORIZATION="Basic aaa",
109+
data={
110+
"token": generate_id(),
111+
},
112+
)
113+
self.assertEqual(res.status_code, 401)
114+
115+
def test_revoke_invalid_auth_secret(self):
116+
"""Test revoke (invalid secret)"""
117+
invalid_auth = b64encode(f"{self.provider.client_id}:aaa".encode()).decode()
118+
res = self.client.post(
119+
reverse("authentik_providers_oauth2:token-revoke"),
120+
HTTP_AUTHORIZATION=f"Basic {invalid_auth}",
109121
data={
110122
"token": generate_id(),
111123
},
@@ -116,7 +128,7 @@ def test_revoke_public(self):
116128
"""Test revoke public client"""
117129
self.provider.client_type = ClientTypes.PUBLIC
118130
self.provider.save()
119-
token: AccessToken = AccessToken.objects.create(
131+
token = AccessToken.objects.create(
120132
provider=self.provider,
121133
user=self.user,
122134
token=generate_id(),
@@ -220,3 +232,74 @@ def test_revoke_user_deactivated(self):
220232
self.assertEqual(AccessToken.objects.all().count(), 0)
221233
self.assertEqual(RefreshToken.objects.all().count(), 0)
222234
self.assertEqual(DeviceToken.objects.all().count(), 0)
235+
236+
def test_revoke_provider_fed(self):
237+
"""Test revoke with federation. self.provider is a confidential
238+
client and other_provider is a public client."""
239+
other_provider = OAuth2Provider.objects.create(
240+
name=generate_id(),
241+
authorization_flow=create_test_flow(),
242+
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
243+
signing_key=create_test_cert(),
244+
client_type=ClientTypes.PUBLIC,
245+
)
246+
Application.objects.create(name=generate_id(), slug=generate_id(), provider=other_provider)
247+
248+
other_provider.jwt_federation_providers.add(self.provider)
249+
250+
token = AccessToken.objects.create(
251+
provider=other_provider,
252+
user=self.user,
253+
token=generate_id(),
254+
auth_time=timezone.now(),
255+
_scope="openid user profile",
256+
_id_token=json.dumps(
257+
asdict(
258+
IDToken("foo", "bar"),
259+
)
260+
),
261+
)
262+
res = self.client.post(
263+
reverse("authentik_providers_oauth2:token-revoke"),
264+
HTTP_AUTHORIZATION=f"Basic {self.auth}",
265+
data={"token": token.token},
266+
)
267+
self.assertEqual(res.status_code, 200)
268+
self.assertJSONEqual(res.content.decode(), {})
269+
270+
def test_revoke_provider_fed_public(self):
271+
"""Test revoke with federation. self.provider is a public
272+
client and other_provider is a public client."""
273+
self.provider.client_type = ClientTypes.PUBLIC
274+
self.provider.save()
275+
other_provider = OAuth2Provider.objects.create(
276+
name=generate_id(),
277+
authorization_flow=create_test_flow(),
278+
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")],
279+
signing_key=create_test_cert(),
280+
client_type=ClientTypes.PUBLIC,
281+
)
282+
Application.objects.create(name=generate_id(), slug=generate_id(), provider=other_provider)
283+
284+
other_provider.jwt_federation_providers.add(self.provider)
285+
286+
token = AccessToken.objects.create(
287+
provider=other_provider,
288+
user=self.user,
289+
token=generate_id(),
290+
auth_time=timezone.now(),
291+
_scope="openid user profile",
292+
_id_token=json.dumps(
293+
asdict(
294+
IDToken("foo", "bar"),
295+
)
296+
),
297+
)
298+
auth_public = b64encode(f"{self.provider.client_id}:{generate_id()}".encode()).decode()
299+
res = self.client.post(
300+
reverse("authentik_providers_oauth2:token-revoke"),
301+
HTTP_AUTHORIZATION=f"Basic {auth_public}",
302+
data={"token": token.token},
303+
)
304+
self.assertEqual(res.status_code, 200)
305+
self.assertTrue(AccessToken.objects.filter(token=token.token).exists())

0 commit comments

Comments
 (0)