diff --git a/authentik/common/oauth/constants.py b/authentik/common/oauth/constants.py index a375bbd072..d7aaf48f8d 100644 --- a/authentik/common/oauth/constants.py +++ b/authentik/common/oauth/constants.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code" GRANT_TYPE_IMPLICIT = "implicit" +GRANT_TYPE_HYBRID = "hybrid" GRANT_TYPE_REFRESH_TOKEN = "refresh_token" # nosec GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials" GRANT_TYPE_PASSWORD = "password" # nosec diff --git a/authentik/providers/oauth2/api/providers.py b/authentik/providers/oauth2/api/providers.py index 3a484ea9ce..1d547d85c5 100644 --- a/authentik/providers/oauth2/api/providers.py +++ b/authentik/providers/oauth2/api/providers.py @@ -65,6 +65,7 @@ class OAuth2ProviderSerializer(ProviderSerializer): fields = ProviderSerializer.Meta.fields + [ "authorization_flow", "client_type", + "grant_types", "client_id", "client_secret", "access_code_validity", diff --git a/authentik/providers/oauth2/errors.py b/authentik/providers/oauth2/errors.py index cc258ddcd1..2443580f1a 100644 --- a/authentik/providers/oauth2/errors.py +++ b/authentik/providers/oauth2/errors.py @@ -7,7 +7,7 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from authentik.events.models import Event, EventAction from authentik.lib.sentry import SentryIgnoredException from authentik.lib.views import bad_request_message -from authentik.providers.oauth2.models import GrantTypes, RedirectURI +from authentik.providers.oauth2.models import GrantType, RedirectURI class OAuth2Error(SentryIgnoredException): @@ -182,7 +182,7 @@ class AuthorizeError(OAuth2Error): # See: # http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError fragment_or_query = ( - "#" if self.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID] else "?" + "#" if self.grant_type in [GrantType.IMPLICIT, GrantType.HYBRID] else "?" ) uri = ( @@ -225,7 +225,7 @@ class TokenError(OAuth2Error): ), } - def __init__(self, error): + def __init__(self, error: str): super().__init__() self.error = error self.description = self.errors[error] diff --git a/authentik/providers/oauth2/migrations/0032_oauth2provider_grant_types.py b/authentik/providers/oauth2/migrations/0032_oauth2provider_grant_types.py new file mode 100644 index 0000000000..852b062514 --- /dev/null +++ b/authentik/providers/oauth2/migrations/0032_oauth2provider_grant_types.py @@ -0,0 +1,69 @@ +# Generated by Django 5.2.11 on 2026-02-17 11:04 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +def migrate_default_grant_types(): + from authentik.providers.oauth2.models import GrantType + + return [ + GrantType.AUTHORIZATION_CODE, + GrantType.HYBRID, + GrantType.IMPLICIT, + GrantType.CLIENT_CREDENTIALS, + GrantType.PASSWORD, + GrantType.DEVICE_CODE, + GrantType.REFRESH_TOKEN, + ] + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "authentik_providers_oauth2", + "0031_remove_oauth2provider_backchannel_logout_uri_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="oauth2provider", + name="grant_types", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField( + choices=[ + ("authorization_code", "Authorization Code"), + ("implicit", "Implicit"), + ("hybrid", "Hybrid"), + ("refresh_token", "Refresh Token"), + ("client_credentials", "Client Credentials"), + ("password", "Password"), + ("urn:ietf:params:oauth:grant-type:device_code", "Device Code"), + ] + ), + default=migrate_default_grant_types, + size=None, + ), + ), + migrations.AlterField( + model_name="oauth2provider", + name="grant_types", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField( + choices=[ + ("authorization_code", "Authorization Code"), + ("implicit", "Implicit"), + ("hybrid", "Hybrid"), + ("refresh_token", "Refresh Token"), + ("client_credentials", "Client Credentials"), + ("password", "Password"), + ("urn:ietf:params:oauth:grant-type:device_code", "Device Code"), + ] + ), + default=list, + size=None, + ), + ), + ] diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index 3a8e72a882..b46be9bc93 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -19,6 +19,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from dacite import Config from dacite.core import from_dict +from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.indexes import HashIndex from django.db import models from django.http import HttpRequest @@ -33,7 +34,16 @@ from rest_framework.serializers import Serializer from structlog.stdlib import get_logger from authentik.brands.models import WebfingerProvider -from authentik.common.oauth.constants import SubModes +from authentik.common.oauth.constants import ( + GRANT_TYPE_AUTHORIZATION_CODE, + GRANT_TYPE_CLIENT_CREDENTIALS, + GRANT_TYPE_DEVICE_CODE, + GRANT_TYPE_HYBRID, + GRANT_TYPE_IMPLICIT, + GRANT_TYPE_PASSWORD, + GRANT_TYPE_REFRESH_TOKEN, + SubModes, +) from authentik.core.models import ( AuthenticatedSession, ExpiringModel, @@ -58,7 +68,7 @@ def generate_client_secret() -> str: return generate_id(128) -class ClientTypes(models.TextChoices): +class ClientType(models.TextChoices): """Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable.""" @@ -66,12 +76,16 @@ class ClientTypes(models.TextChoices): PUBLIC = "public", _("Public") -class GrantTypes(models.TextChoices): +class GrantType(models.TextChoices): """OAuth2 Grant types we support""" - AUTHORIZATION_CODE = "authorization_code" - IMPLICIT = "implicit" - HYBRID = "hybrid" + AUTHORIZATION_CODE = GRANT_TYPE_AUTHORIZATION_CODE + IMPLICIT = GRANT_TYPE_IMPLICIT + HYBRID = GRANT_TYPE_HYBRID + REFRESH_TOKEN = GRANT_TYPE_REFRESH_TOKEN + CLIENT_CREDENTIALS = GRANT_TYPE_CLIENT_CREDENTIALS + PASSWORD = GRANT_TYPE_PASSWORD + DEVICE_CODE = GRANT_TYPE_DEVICE_CODE class ResponseMode(models.TextChoices): @@ -188,14 +202,15 @@ class OAuth2Provider(WebfingerProvider, Provider): client_type = models.CharField( max_length=30, - choices=ClientTypes.choices, - default=ClientTypes.CONFIDENTIAL, + choices=ClientType.choices, + default=ClientType.CONFIDENTIAL, verbose_name=_("Client Type"), help_text=_( "Confidential clients are capable of maintaining the confidentiality " "of their credentials. Public clients are incapable" ), ) + grant_types = ArrayField(models.TextField(choices=GrantType.choices), default=list) client_id = models.CharField( max_length=255, unique=True, diff --git a/authentik/providers/oauth2/tests/test_authorize.py b/authentik/providers/oauth2/tests/test_authorize.py index 2f5f70575a..0789f2fa96 100644 --- a/authentik/providers/oauth2/tests/test_authorize.py +++ b/authentik/providers/oauth2/tests/test_authorize.py @@ -22,7 +22,7 @@ from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, Red from authentik.providers.oauth2.models import ( AccessToken, AuthorizationCode, - GrantTypes, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -41,12 +41,34 @@ class TestAuthorize(OAuthTestCase): super().setUp() self.factory = RequestFactory() + def test_disallowed_grant_type(self): + """Test with disallowed grant type""" + OAuth2Provider.objects.create( + name=generate_id(), + client_id="test", + grant_types=[], + authorization_flow=create_test_flow(), + redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], + ) + with self.assertRaises(AuthorizeError) as cm: + request = self.factory.get( + "/", + data={ + "response_type": "code", + "client_id": "test", + "redirect_uri": "http://local.invalid/Foo", + }, + ) + OAuthAuthorizationParams.from_request(request) + self.assertEqual(cm.exception.error, "invalid_request") + def test_invalid_grant_type(self): """Test with invalid grant type""" OAuth2Provider.objects.create( name=generate_id(), client_id="test", authorization_flow=create_test_flow(), + grant_types=[GrantType.AUTHORIZATION_CODE], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], ) with self.assertRaises(AuthorizeError) as cm: @@ -74,6 +96,7 @@ class TestAuthorize(OAuthTestCase): client_id="test", authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], + grant_types=[GrantType.AUTHORIZATION_CODE], ) with self.assertRaises(AuthorizeError) as cm: request = self.factory.get( @@ -188,6 +211,7 @@ class TestAuthorize(OAuthTestCase): client_id="test", authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.REGEX, ".+")], + grant_types=[GrantType.AUTHORIZATION_CODE], ) request = self.factory.get( "/", @@ -206,6 +230,7 @@ class TestAuthorize(OAuthTestCase): name=generate_id(), client_id="test", authorization_flow=create_test_flow(), + grant_types=[GrantType.AUTHORIZATION_CODE], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid/Foo")], ) provider.property_mappings.set( @@ -227,12 +252,14 @@ class TestAuthorize(OAuthTestCase): ) self.assertEqual( OAuthAuthorizationParams.from_request(request).grant_type, - GrantTypes.AUTHORIZATION_CODE, + GrantType.AUTHORIZATION_CODE, ) self.assertEqual( OAuthAuthorizationParams.from_request(request).redirect_uri, "http://local.invalid/Foo", ) + provider.grant_types = [GrantType.IMPLICIT] + provider.save() request = self.factory.get( "/", data={ @@ -246,7 +273,7 @@ class TestAuthorize(OAuthTestCase): ) self.assertEqual( OAuthAuthorizationParams.from_request(request).grant_type, - GrantTypes.IMPLICIT, + GrantType.IMPLICIT, ) # Implicit without openid scope with self.assertRaises(AuthorizeError) as cm: @@ -261,8 +288,10 @@ class TestAuthorize(OAuthTestCase): ) self.assertEqual( OAuthAuthorizationParams.from_request(request).grant_type, - GrantTypes.IMPLICIT, + GrantType.IMPLICIT, ) + provider.grant_types = [GrantType.HYBRID] + provider.save() request = self.factory.get( "/", data={ @@ -274,7 +303,7 @@ class TestAuthorize(OAuthTestCase): }, ) self.assertEqual( - OAuthAuthorizationParams.from_request(request).grant_type, GrantTypes.HYBRID + OAuthAuthorizationParams.from_request(request).grant_type, GrantType.HYBRID ) with self.assertRaises(AuthorizeError) as cm: request = self.factory.get( @@ -297,6 +326,7 @@ class TestAuthorize(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() @@ -333,6 +363,7 @@ class TestAuthorize(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], signing_key=self.keypair, + grant_types=[GrantType.IMPLICIT], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -404,6 +435,7 @@ class TestAuthorize(OAuthTestCase): redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], signing_key=self.keypair, encryption_key=self.keypair, + grant_types=[GrantType.IMPLICIT], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -466,6 +498,7 @@ class TestAuthorize(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], signing_key=self.keypair, + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() @@ -515,6 +548,7 @@ class TestAuthorize(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], signing_key=self.keypair, + grant_types=[GrantType.IMPLICIT], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -572,6 +606,7 @@ class TestAuthorize(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], signing_key=self.keypair, + grant_types=[GrantType.AUTHORIZATION_CODE], ) app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) state = generate_id() @@ -612,6 +647,7 @@ class TestAuthorize(OAuthTestCase): name=generate_id(), client_id="test", authorization_flow=create_test_flow(), + grant_types=[GrantType.IMPLICIT], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], ) request = self.factory.get( @@ -635,6 +671,7 @@ class TestAuthorize(OAuthTestCase): client_id="test", authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")], + grant_types=[GrantType.IMPLICIT], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -667,6 +704,7 @@ class TestAuthorize(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() @@ -697,6 +735,7 @@ class TestAuthorize(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() @@ -736,6 +775,7 @@ class TestAuthorize(OAuthTestCase): authentication_flow=auth_flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() @@ -762,6 +802,7 @@ class TestAuthorize(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() diff --git a/authentik/providers/oauth2/tests/test_device_backchannel.py b/authentik/providers/oauth2/tests/test_device_backchannel.py index 1c54ca54c4..85413fefb2 100644 --- a/authentik/providers/oauth2/tests/test_device_backchannel.py +++ b/authentik/providers/oauth2/tests/test_device_backchannel.py @@ -10,7 +10,7 @@ from authentik.blueprints.tests import apply_blueprint from authentik.core.models import Application from authentik.core.tests.utils import create_test_flow from authentik.lib.generators import generate_id -from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping +from authentik.providers.oauth2.models import DeviceToken, GrantType, OAuth2Provider, ScopeMapping from authentik.providers.oauth2.tests.utils import OAuthTestCase @@ -22,6 +22,7 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase): name=generate_id(), client_id="test", authorization_flow=create_test_flow(), + grant_types=[GrantType.DEVICE_CODE], ) self.application = Application.objects.create( name=generate_id(), @@ -42,6 +43,21 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase): reverse("authentik_providers_oauth2:device"), ) self.assertEqual(res.status_code, 400) + + def test_backchannel_invalid_no_grant(self): + """Test backchannel""" + self.provider.grant_types = [] + self.provider.save() + res = self.client.post( + reverse("authentik_providers_oauth2:device"), + data={ + "client_id": "test", + }, + ) + self.assertEqual(res.status_code, 400) + + def test_backchannel_invalid_no_app(self): + """Test backchannel""" # test without application self.application.provider = None self.application.save() diff --git a/authentik/providers/oauth2/tests/test_device_init.py b/authentik/providers/oauth2/tests/test_device_init.py index 8c5ac0a31d..5d0ee7b8f8 100644 --- a/authentik/providers/oauth2/tests/test_device_init.py +++ b/authentik/providers/oauth2/tests/test_device_init.py @@ -9,7 +9,7 @@ from authentik.core.models import Application, Group from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow from authentik.lib.generators import generate_id from authentik.policies.models import PolicyBinding -from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider +from authentik.providers.oauth2.models import DeviceToken, GrantType, OAuth2Provider from authentik.providers.oauth2.tests.utils import OAuthTestCase from authentik.providers.oauth2.views.device_init import QS_KEY_CODE @@ -22,6 +22,7 @@ class TesOAuth2DeviceInit(OAuthTestCase): name=generate_id(), client_id="test", authorization_flow=create_test_flow(), + grant_types=[GrantType.DEVICE_CODE], ) self.application = Application.objects.create( name=generate_id(), diff --git a/authentik/providers/oauth2/tests/test_introspect.py b/authentik/providers/oauth2/tests/test_introspect.py index ff4957f63b..bceb00de08 100644 --- a/authentik/providers/oauth2/tests/test_introspect.py +++ b/authentik/providers/oauth2/tests/test_introspect.py @@ -14,7 +14,7 @@ from authentik.lib.generators import generate_id from authentik.providers.oauth2.id_token import IDToken from authentik.providers.oauth2.models import ( AccessToken, - ClientTypes, + ClientType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -173,7 +173,7 @@ class TesOAuth2Introspection(OAuthTestCase): def test_introspect_provider_public(self): """Test introspect""" - self.provider.client_type = ClientTypes.PUBLIC + self.provider.client_type = ClientType.PUBLIC self.provider.save() token = AccessToken.objects.create( provider=self.provider, @@ -208,7 +208,7 @@ class TesOAuth2Introspection(OAuthTestCase): authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")], signing_key=create_test_cert(), - client_type=ClientTypes.PUBLIC, + client_type=ClientType.PUBLIC, ) Application.objects.create(name=generate_id(), slug=generate_id(), provider=other_provider) diff --git a/authentik/providers/oauth2/tests/test_revoke.py b/authentik/providers/oauth2/tests/test_revoke.py index 26a7ce796c..18fa7ef57d 100644 --- a/authentik/providers/oauth2/tests/test_revoke.py +++ b/authentik/providers/oauth2/tests/test_revoke.py @@ -13,7 +13,7 @@ from authentik.lib.generators import generate_id from authentik.providers.oauth2.id_token import IDToken from authentik.providers.oauth2.models import ( AccessToken, - ClientTypes, + ClientType, DeviceToken, OAuth2Provider, RedirectURI, @@ -126,7 +126,7 @@ class TesOAuth2Revoke(OAuthTestCase): def test_revoke_public(self): """Test revoke public client""" - self.provider.client_type = ClientTypes.PUBLIC + self.provider.client_type = ClientType.PUBLIC self.provider.save() token = AccessToken.objects.create( provider=self.provider, @@ -241,7 +241,7 @@ class TesOAuth2Revoke(OAuthTestCase): authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")], signing_key=create_test_cert(), - client_type=ClientTypes.PUBLIC, + client_type=ClientType.PUBLIC, ) Application.objects.create(name=generate_id(), slug=generate_id(), provider=other_provider) @@ -270,14 +270,14 @@ class TesOAuth2Revoke(OAuthTestCase): def test_revoke_provider_fed_public(self): """Test revoke with federation. self.provider is a public client and other_provider is a public client.""" - self.provider.client_type = ClientTypes.PUBLIC + self.provider.client_type = ClientType.PUBLIC self.provider.save() other_provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "")], signing_key=create_test_cert(), - client_type=ClientTypes.PUBLIC, + client_type=ClientType.PUBLIC, ) Application.objects.create(name=generate_id(), slug=generate_id(), provider=other_provider) diff --git a/authentik/providers/oauth2/tests/test_token.py b/authentik/providers/oauth2/tests/test_token.py index 592f41272e..3c7463449d 100644 --- a/authentik/providers/oauth2/tests/test_token.py +++ b/authentik/providers/oauth2/tests/test_token.py @@ -25,6 +25,7 @@ from authentik.providers.oauth2.errors import TokenError from authentik.providers.oauth2.models import ( AccessToken, AuthorizationCode, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -44,11 +45,39 @@ class TestToken(OAuthTestCase): self.factory = RequestFactory() self.app = Application.objects.create(name=generate_id(), slug="test") + def test_invalid_grant_type(self): + """test request param""" + provider = OAuth2Provider.objects.create( + name=generate_id(), + authorization_flow=create_test_flow(), + grant_types=[], + redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://TestServer")], + signing_key=self.keypair, + ) + header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode() + user = create_test_admin_user() + code = AuthorizationCode.objects.create( + code="foobar", provider=provider, user=user, auth_time=timezone.now() + ) + request = self.factory.post( + "/", + data={ + "grant_type": GRANT_TYPE_AUTHORIZATION_CODE, + "code": code.code, + "redirect_uri": "http://TestServer", + }, + HTTP_AUTHORIZATION=f"Basic {header}", + ) + with self.assertRaises(TokenError) as cm: + TokenParams.parse(request, provider, provider.client_id, provider.client_secret) + self.assertEqual(cm.exception.cause, "grant_type_not_configured") + def test_request_auth_code(self): """test request param""" provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.AUTHORIZATION_CODE], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://TestServer")], signing_key=self.keypair, ) @@ -76,6 +105,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.REFRESH_TOKEN], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], signing_key=self.keypair, ) @@ -97,6 +127,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.REFRESH_TOKEN], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], signing_key=self.keypair, ) @@ -139,6 +170,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.AUTHORIZATION_CODE], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], signing_key=self.keypair, ) @@ -179,6 +211,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.AUTHORIZATION_CODE], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], signing_key=self.keypair, encryption_key=self.keypair, @@ -210,6 +243,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.REFRESH_TOKEN], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], signing_key=self.keypair, ) @@ -271,6 +305,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.REFRESH_TOKEN], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], signing_key=self.keypair, ) @@ -328,6 +363,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.REFRESH_TOKEN], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], signing_key=self.keypair, ) @@ -400,6 +436,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.REFRESH_TOKEN], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], signing_key=self.keypair, refresh_token_threshold="hours=1", # nosec @@ -497,6 +534,7 @@ class TestToken(OAuthTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=create_test_flow(), + grant_types=[GrantType.AUTHORIZATION_CODE], redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")], signing_key=self.keypair, include_claims_in_id_token=True, diff --git a/authentik/providers/oauth2/tests/test_token_cc_jwt_provider.py b/authentik/providers/oauth2/tests/test_token_cc_jwt_provider.py index 9f37e0a419..c6850e80a9 100644 --- a/authentik/providers/oauth2/tests/test_token_cc_jwt_provider.py +++ b/authentik/providers/oauth2/tests/test_token_cc_jwt_provider.py @@ -22,6 +22,7 @@ from authentik.lib.generators import generate_id from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.models import ( AccessToken, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -55,6 +56,7 @@ class TestTokenClientCredentialsJWTProvider(OAuthTestCase): authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], signing_key=self.cert, + grant_types=[GrantType.CLIENT_CREDENTIALS], ) self.provider.jwt_federation_providers.add(self.other_provider) self.provider.property_mappings.set(ScopeMapping.objects.all()) diff --git a/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py b/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py index e9612cc56f..bc5ceca62b 100644 --- a/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py +++ b/authentik/providers/oauth2/tests/test_token_cc_jwt_source.py @@ -20,6 +20,7 @@ from authentik.core.tests.utils import create_test_cert, create_test_flow from authentik.lib.generators import generate_id from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.models import ( + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -68,6 +69,7 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase): authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], signing_key=self.cert, + grant_types=[GrantType.CLIENT_CREDENTIALS], ) self.provider.jwt_federation_sources.add(self.source) self.provider.property_mappings.set(ScopeMapping.objects.all()) diff --git a/authentik/providers/oauth2/tests/test_token_cc_standard.py b/authentik/providers/oauth2/tests/test_token_cc_standard.py index 79c7a46eb6..95f0e3703b 100644 --- a/authentik/providers/oauth2/tests/test_token_cc_standard.py +++ b/authentik/providers/oauth2/tests/test_token_cc_standard.py @@ -21,6 +21,7 @@ from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.errors import TokenError from authentik.providers.oauth2.models import ( AccessToken, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -41,6 +42,7 @@ class TestTokenClientCredentialsStandard(OAuthTestCase): authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], signing_key=create_test_cert(), + grant_types=[GrantType.CLIENT_CREDENTIALS, GrantType.PASSWORD], ) self.provider.property_mappings.set(ScopeMapping.objects.all()) self.app = Application.objects.create(name="test", slug="test", provider=self.provider) diff --git a/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py b/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py index 32c41360a9..a186af3133 100644 --- a/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py +++ b/authentik/providers/oauth2/tests/test_token_cc_standard_compat.py @@ -22,6 +22,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_cert, from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.errors import TokenError from authentik.providers.oauth2.models import ( + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -42,6 +43,7 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase): authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], signing_key=create_test_cert(), + grant_types=[GrantType.CLIENT_CREDENTIALS, GrantType.PASSWORD], ) self.provider.property_mappings.set(ScopeMapping.objects.all()) self.app = Application.objects.create(name="test", slug="test", provider=self.provider) diff --git a/authentik/providers/oauth2/tests/test_token_cc_user_pw.py b/authentik/providers/oauth2/tests/test_token_cc_user_pw.py index 6540865b7a..38d9849e4d 100644 --- a/authentik/providers/oauth2/tests/test_token_cc_user_pw.py +++ b/authentik/providers/oauth2/tests/test_token_cc_user_pw.py @@ -25,6 +25,7 @@ from authentik.core.tests.utils import ( from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.errors import TokenError from authentik.providers.oauth2.models import ( + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -45,6 +46,7 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase): authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], signing_key=create_test_cert(), + grant_types=[GrantType.CLIENT_CREDENTIALS, GrantType.PASSWORD], ) self.provider.property_mappings.set(ScopeMapping.objects.all()) self.app = Application.objects.create(name="test", slug="test", provider=self.provider) diff --git a/authentik/providers/oauth2/tests/test_token_device.py b/authentik/providers/oauth2/tests/test_token_device.py index 4a96ab6859..fa252a62ca 100644 --- a/authentik/providers/oauth2/tests/test_token_device.py +++ b/authentik/providers/oauth2/tests/test_token_device.py @@ -17,6 +17,7 @@ from authentik.lib.generators import generate_code_fixed_length, generate_id from authentik.providers.oauth2.models import ( AccessToken, DeviceToken, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -37,6 +38,7 @@ class TestTokenDeviceCode(OAuthTestCase): authorization_flow=create_test_flow(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], signing_key=create_test_cert(), + grant_types=[GrantType.DEVICE_CODE], ) self.provider.property_mappings.set(ScopeMapping.objects.all()) self.app = Application.objects.create(name="test", slug="test", provider=self.provider) diff --git a/authentik/providers/oauth2/tests/test_token_pkce.py b/authentik/providers/oauth2/tests/test_token_pkce.py index 7ffc1d6607..019f7fcd9a 100644 --- a/authentik/providers/oauth2/tests/test_token_pkce.py +++ b/authentik/providers/oauth2/tests/test_token_pkce.py @@ -11,6 +11,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.lib.generators import generate_id from authentik.providers.oauth2.models import ( AuthorizationCode, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -37,6 +38,7 @@ class TestTokenPKCE(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() @@ -95,6 +97,7 @@ class TestTokenPKCE(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() @@ -151,6 +154,7 @@ class TestTokenPKCE(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() @@ -196,6 +200,7 @@ class TestTokenPKCE(OAuthTestCase): authorization_flow=flow, redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")], access_code_validity="seconds=100", + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_id() diff --git a/authentik/providers/oauth2/views/authorize.py b/authentik/providers/oauth2/views/authorize.py index 5bd1cbcc7b..0a62f7a668 100644 --- a/authentik/providers/oauth2/views/authorize.py +++ b/authentik/providers/oauth2/views/authorize.py @@ -57,7 +57,7 @@ from authentik.providers.oauth2.id_token import IDToken from authentik.providers.oauth2.models import ( AccessToken, AuthorizationCode, - GrantTypes, + GrantType, OAuth2Provider, RedirectURIMatchingMode, ResponseMode, @@ -164,28 +164,31 @@ class OAuthAuthorizationParams: """Check grant""" # Determine which flow to use. if self.response_type in [ResponseTypes.CODE]: - self.grant_type = GrantTypes.AUTHORIZATION_CODE + self.grant_type = GrantType.AUTHORIZATION_CODE elif self.response_type in [ ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN_TOKEN, ]: - self.grant_type = GrantTypes.IMPLICIT + self.grant_type = GrantType.IMPLICIT elif self.response_type in [ ResponseTypes.CODE_TOKEN, ResponseTypes.CODE_ID_TOKEN, ResponseTypes.CODE_ID_TOKEN_TOKEN, ]: - self.grant_type = GrantTypes.HYBRID - + self.grant_type = GrantType.HYBRID # Grant type validation. if not self.grant_type: LOGGER.warning("Invalid response type", type=self.response_type) raise AuthorizeError(self.redirect_uri, "unsupported_response_type", "", self.state) + if self.grant_type not in self.provider.grant_types: + LOGGER.warning("Invalid grant_type for provider", grant_type=self.grant_type) + raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type, self.state) + if self.response_mode not in ResponseMode.values: self.response_mode = ResponseMode.QUERY - if self.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: + if self.grant_type in [GrantType.IMPLICIT, GrantType.HYBRID]: self.response_mode = ResponseMode.FRAGMENT def check_redirect_uri(self): @@ -246,7 +249,7 @@ class OAuthAuthorizationParams: ) self.scope = self.scope.intersection(default_scope_names) if SCOPE_OPENID not in self.scope and ( - self.grant_type == GrantTypes.HYBRID + self.grant_type == GrantType.HYBRID or self.response_type in [ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN_TOKEN] ): LOGGER.warning("Missing 'openid' scope.") @@ -597,8 +600,8 @@ class OAuthFulfillmentStage(StageView): code = None if self.params.grant_type in [ - GrantTypes.AUTHORIZATION_CODE, - GrantTypes.HYBRID, + GrantType.AUTHORIZATION_CODE, + GrantType.HYBRID, ]: code = self.params.create_code(self.request) code.save() @@ -613,7 +616,7 @@ class OAuthFulfillmentStage(StageView): if self.params.response_mode == ResponseMode.FRAGMENT: query_fragment = {} - if self.params.grant_type in [GrantTypes.AUTHORIZATION_CODE]: + if self.params.grant_type in [GrantType.AUTHORIZATION_CODE]: query_fragment["code"] = code.code query_fragment["state"] = [str(self.params.state) if self.params.state else ""] else: @@ -627,7 +630,7 @@ class OAuthFulfillmentStage(StageView): if self.params.response_mode == ResponseMode.FORM_POST: post_params = {} - if self.params.grant_type in [GrantTypes.AUTHORIZATION_CODE]: + if self.params.grant_type in [GrantType.AUTHORIZATION_CODE]: post_params["code"] = code.code post_params["state"] = [str(self.params.state) if self.params.state else ""] else: @@ -696,7 +699,7 @@ class OAuthFulfillmentStage(StageView): token.save() # Code parameter must be present if it's Hybrid Flow. - if self.params.grant_type == GrantTypes.HYBRID: + if self.params.grant_type == GrantType.HYBRID: query_fragment["code"] = code.code query_fragment["token_type"] = TOKEN_TYPE diff --git a/authentik/providers/oauth2/views/device_backchannel.py b/authentik/providers/oauth2/views/device_backchannel.py index 301f36492d..6248041bc3 100644 --- a/authentik/providers/oauth2/views/device_backchannel.py +++ b/authentik/providers/oauth2/views/device_backchannel.py @@ -15,7 +15,7 @@ from authentik.core.models import Application from authentik.lib.config import CONFIG from authentik.lib.utils.time import timedelta_from_string from authentik.providers.oauth2.errors import DeviceCodeError -from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping +from authentik.providers.oauth2.models import DeviceToken, GrantType, OAuth2Provider, ScopeMapping from authentik.providers.oauth2.utils import TokenResponse, extract_client_auth from authentik.providers.oauth2.views.device_init import QS_KEY_CODE @@ -42,6 +42,8 @@ class DeviceView(View): _ = provider.application except Application.DoesNotExist: raise DeviceCodeError("invalid_client") from None + if GrantType.DEVICE_CODE not in provider.grant_types: + raise DeviceCodeError("invalid_client") self.provider = provider self.client_id = client_id diff --git a/authentik/providers/oauth2/views/introspection.py b/authentik/providers/oauth2/views/introspection.py index d3703c1141..15e822de5e 100644 --- a/authentik/providers/oauth2/views/introspection.py +++ b/authentik/providers/oauth2/views/introspection.py @@ -11,7 +11,7 @@ from structlog.stdlib import get_logger from authentik.providers.oauth2.errors import TokenIntrospectionError from authentik.providers.oauth2.id_token import IDToken -from authentik.providers.oauth2.models import AccessToken, ClientTypes, OAuth2Provider, RefreshToken +from authentik.providers.oauth2.models import AccessToken, ClientType, OAuth2Provider, RefreshToken from authentik.providers.oauth2.utils import TokenResponse, authenticate_provider LOGGER = get_logger() @@ -45,7 +45,7 @@ class TokenIntrospectionParams: if not provider: LOGGER.info("Failed to authenticate introspection request") raise TokenIntrospectionError - if provider.client_type != ClientTypes.CONFIDENTIAL: + if provider.client_type != ClientType.CONFIDENTIAL: LOGGER.info("Introspection request from public provider, denying.") raise TokenIntrospectionError diff --git a/authentik/providers/oauth2/views/token.py b/authentik/providers/oauth2/views/token.py index 17e1e2e5b8..f280817e97 100644 --- a/authentik/providers/oauth2/views/token.py +++ b/authentik/providers/oauth2/views/token.py @@ -58,7 +58,7 @@ from authentik.providers.oauth2.id_token import IDToken from authentik.providers.oauth2.models import ( AccessToken, AuthorizationCode, - ClientTypes, + ClientType, DeviceToken, OAuth2Provider, RedirectURIMatchingMode, @@ -165,6 +165,10 @@ class TokenParams: raise TokenError("invalid_grant") def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest): + if self.grant_type not in self.provider.grant_types: + LOGGER.warning("Invalid grant_type for provider", grant_type=self.grant_type) + raise TokenError("invalid_grant").with_cause("grant_type_not_configured") + # Confidential clients MUST authenticate to the token endpoint per # RFC 6749 §2.3.1. The device code grant (RFC 8628 §3.4) inherits # that requirement - the device_code alone is not a substitute for @@ -174,7 +178,7 @@ class TokenParams: GRANT_TYPE_REFRESH_TOKEN, GRANT_TYPE_DEVICE_CODE, ]: - if self.provider.client_type == ClientTypes.CONFIDENTIAL and not compare_digest( + if self.provider.client_type == ClientType.CONFIDENTIAL and not compare_digest( self.provider.client_secret, self.client_secret ): LOGGER.warning( @@ -606,10 +610,10 @@ class TokenView(View): if not self.provider: LOGGER.warning("OAuth2Provider does not exist", client_id=client_id) raise TokenError("invalid_client") - CTX_AUTH_VIA.set("oauth_client_secret") self.params = self.params_class.parse( request, self.provider, client_id, client_secret ) + CTX_AUTH_VIA.set("oauth_client_secret") with start_span( op="authentik.providers.oauth2.post.response", diff --git a/authentik/providers/oauth2/views/token_revoke.py b/authentik/providers/oauth2/views/token_revoke.py index 75494bc748..a100c520ac 100644 --- a/authentik/providers/oauth2/views/token_revoke.py +++ b/authentik/providers/oauth2/views/token_revoke.py @@ -10,7 +10,7 @@ from django.views.decorators.csrf import csrf_exempt from structlog.stdlib import get_logger from authentik.providers.oauth2.errors import TokenRevocationError -from authentik.providers.oauth2.models import AccessToken, ClientTypes, OAuth2Provider, RefreshToken +from authentik.providers.oauth2.models import AccessToken, ClientType, OAuth2Provider, RefreshToken from authentik.providers.oauth2.utils import ( TokenResponse, authenticate_provider, @@ -33,11 +33,13 @@ class TokenRevocationParams: raw_token = request.POST.get("token") provider, _, _ = provider_from_request(request) + if provider and provider.client_type == ClientType.CONFIDENTIAL: + provider = authenticate_provider(request) if not provider: raise TokenRevocationError("invalid_client") # By default clients can only revoke their own tokens query = Q(provider=provider, token=raw_token) - if provider.client_type == ClientTypes.CONFIDENTIAL: + if provider.client_type == ClientType.CONFIDENTIAL: provider = authenticate_provider(request) if not provider: raise TokenRevocationError("invalid_client") diff --git a/authentik/providers/proxy/models.py b/authentik/providers/proxy/models.py index 7651bf84b1..3d9f175dfe 100644 --- a/authentik/providers/proxy/models.py +++ b/authentik/providers/proxy/models.py @@ -16,7 +16,8 @@ from authentik.crypto.models import CertificateKeyPair from authentik.lib.models import DomainlessURLValidator, InternallyManagedMixin from authentik.outposts.models import OutpostModel from authentik.providers.oauth2.models import ( - ClientTypes, + ClientType, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -161,7 +162,12 @@ class ProxyProvider(OutpostModel, OAuth2Provider): def set_oauth_defaults(self): """Ensure all OAuth2-related settings are correct""" - self.client_type = ClientTypes.CONFIDENTIAL + self.grant_types = [ + GrantType.AUTHORIZATION_CODE, + GrantType.CLIENT_CREDENTIALS, + GrantType.PASSWORD, + ] + self.client_type = ClientType.CONFIDENTIAL self.signing_key = None self.include_claims_in_id_token = True scopes = ScopeMapping.objects.filter( diff --git a/authentik/providers/proxy/tests.py b/authentik/providers/proxy/tests.py index 25d555f34e..6888229a8d 100644 --- a/authentik/providers/proxy/tests.py +++ b/authentik/providers/proxy/tests.py @@ -9,7 +9,7 @@ from authentik.core.models import Application from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow from authentik.lib.generators import generate_id from authentik.outposts.models import Outpost, OutpostType -from authentik.providers.oauth2.models import ClientTypes +from authentik.providers.oauth2.models import ClientType from authentik.providers.proxy.models import ProxyMode, ProxyProvider @@ -96,7 +96,7 @@ class ProxyProviderTests(APITestCase): ) self.assertEqual(response.status_code, 201) provider: ProxyProvider = ProxyProvider.objects.get(name=name) - self.assertEqual(provider.client_type, ClientTypes.CONFIDENTIAL) + self.assertEqual(provider.client_type, ClientType.CONFIDENTIAL) def test_update_defaults(self): """Test create""" @@ -114,8 +114,8 @@ class ProxyProviderTests(APITestCase): ) self.assertEqual(response.status_code, 201) provider: ProxyProvider = ProxyProvider.objects.get(name=name) - self.assertEqual(provider.client_type, ClientTypes.CONFIDENTIAL) - provider.client_type = ClientTypes.PUBLIC + self.assertEqual(provider.client_type, ClientType.CONFIDENTIAL) + provider.client_type = ClientType.PUBLIC provider.save() response = self.client.put( reverse("authentik_api:proxyprovider-detail", kwargs={"pk": provider.pk}), @@ -130,7 +130,7 @@ class ProxyProviderTests(APITestCase): ) self.assertEqual(response.status_code, 200) provider: ProxyProvider = ProxyProvider.objects.get(name=name) - self.assertEqual(provider.client_type, ClientTypes.CONFIDENTIAL) + self.assertEqual(provider.client_type, ClientType.CONFIDENTIAL) def test_sa_fetch(self): """Test fetching the outpost config as the service account""" diff --git a/blueprints/schema.json b/blueprints/schema.json index f290b1eef1..d33c657fd9 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -9962,6 +9962,23 @@ "title": "Client Type", "description": "Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable" }, + "grant_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "authorization_code", + "implicit", + "hybrid", + "refresh_token", + "client_credentials", + "password", + "urn:ietf:params:oauth:grant-type:device_code" + ], + "title": "Grant types" + }, + "title": "Grant types" + }, "client_id": { "type": "string", "maxLength": 255, diff --git a/blueprints/testing/oidc-conformance.yaml b/blueprints/testing/oidc-conformance.yaml index 564bb0000c..884d734984 100644 --- a/blueprints/testing/oidc-conformance.yaml +++ b/blueprints/testing/oidc-conformance.yaml @@ -75,6 +75,10 @@ entries: url: https://localhost:8443/test/a/authentik/callback - matching_mode: strict url: https://host.docker.internal:8443/test/a/authentik/callback + grant_types: + - authorization_code + - implicit + - refresh_token property_mappings: - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] @@ -106,6 +110,10 @@ entries: url: https://localhost:8443/test/a/authentik/callback - matching_mode: strict url: https://host.docker.internal:8443/test/a/authentik/callback + grant_types: + - authorization_code + - implicit + - refresh_token property_mappings: - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]] - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-email]] diff --git a/packages/client-ts/src/models/GrantTypesEnum.ts b/packages/client-ts/src/models/GrantTypesEnum.ts new file mode 100644 index 0000000000..f9395498c1 --- /dev/null +++ b/packages/client-ts/src/models/GrantTypesEnum.ts @@ -0,0 +1,62 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.5.0-rc1 + * Contact: hello@goauthentik.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + */ +export const GrantTypesEnum = { + AuthorizationCode: "authorization_code", + Implicit: "implicit", + Hybrid: "hybrid", + RefreshToken: "refresh_token", + ClientCredentials: "client_credentials", + Password: "password", + UrnIetfParamsOauthGrantTypeDeviceCode: "urn:ietf:params:oauth:grant-type:device_code", + UnknownDefaultOpenApi: "11184809", +} as const; +export type GrantTypesEnum = (typeof GrantTypesEnum)[keyof typeof GrantTypesEnum]; + +export function instanceOfGrantTypesEnum(value: any): boolean { + for (const key in GrantTypesEnum) { + if (Object.prototype.hasOwnProperty.call(GrantTypesEnum, key)) { + if (GrantTypesEnum[key as keyof typeof GrantTypesEnum] === value) { + return true; + } + } + } + return false; +} + +export function GrantTypesEnumFromJSON(json: any): GrantTypesEnum { + return GrantTypesEnumFromJSONTyped(json, false); +} + +export function GrantTypesEnumFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): GrantTypesEnum { + return json as GrantTypesEnum; +} + +export function GrantTypesEnumToJSON(value?: GrantTypesEnum | null): any { + return value as any; +} + +export function GrantTypesEnumToJSONTyped( + value: any, + ignoreDiscriminator: boolean, +): GrantTypesEnum { + return value as GrantTypesEnum; +} diff --git a/packages/client-ts/src/models/OAuth2Provider.ts b/packages/client-ts/src/models/OAuth2Provider.ts index c1a1cf8428..6c8e0696ca 100644 --- a/packages/client-ts/src/models/OAuth2Provider.ts +++ b/packages/client-ts/src/models/OAuth2Provider.ts @@ -14,6 +14,8 @@ import type { ClientTypeEnum } from "./ClientTypeEnum"; import { ClientTypeEnumFromJSON, ClientTypeEnumToJSON } from "./ClientTypeEnum"; +import type { GrantTypesEnum } from "./GrantTypesEnum"; +import { GrantTypesEnumFromJSON, GrantTypesEnumToJSON } from "./GrantTypesEnum"; import type { IssuerModeEnum } from "./IssuerModeEnum"; import { IssuerModeEnumFromJSON, IssuerModeEnumToJSON } from "./IssuerModeEnum"; import type { OAuth2ProviderLogoutMethodEnum } from "./OAuth2ProviderLogoutMethodEnum"; @@ -122,6 +124,12 @@ export interface OAuth2Provider { * @memberof OAuth2Provider */ clientType?: ClientTypeEnum; + /** + * + * @type {Array} + * @memberof OAuth2Provider + */ + grantTypes?: Array; /** * * @type {string} @@ -279,6 +287,10 @@ export function OAuth2ProviderFromJSONTyped( metaModelName: json["meta_model_name"], clientType: json["client_type"] == null ? undefined : ClientTypeEnumFromJSON(json["client_type"]), + grantTypes: + json["grant_types"] == null + ? undefined + : (json["grant_types"] as Array).map(GrantTypesEnumFromJSON), clientId: json["client_id"] == null ? undefined : json["client_id"], clientSecret: json["client_secret"] == null ? undefined : json["client_secret"], accessCodeValidity: @@ -341,6 +353,10 @@ export function OAuth2ProviderToJSONTyped( invalidation_flow: value["invalidationFlow"], property_mappings: value["propertyMappings"], client_type: ClientTypeEnumToJSON(value["clientType"]), + grant_types: + value["grantTypes"] == null + ? undefined + : (value["grantTypes"] as Array).map(GrantTypesEnumToJSON), client_id: value["clientId"], client_secret: value["clientSecret"], access_code_validity: value["accessCodeValidity"], diff --git a/packages/client-ts/src/models/OAuth2ProviderRequest.ts b/packages/client-ts/src/models/OAuth2ProviderRequest.ts index f2118e3b42..15338bb37e 100644 --- a/packages/client-ts/src/models/OAuth2ProviderRequest.ts +++ b/packages/client-ts/src/models/OAuth2ProviderRequest.ts @@ -14,6 +14,8 @@ import type { ClientTypeEnum } from "./ClientTypeEnum"; import { ClientTypeEnumFromJSON, ClientTypeEnumToJSON } from "./ClientTypeEnum"; +import type { GrantTypesEnum } from "./GrantTypesEnum"; +import { GrantTypesEnumFromJSON, GrantTypesEnumToJSON } from "./GrantTypesEnum"; import type { IssuerModeEnum } from "./IssuerModeEnum"; import { IssuerModeEnumFromJSON, IssuerModeEnumToJSON } from "./IssuerModeEnum"; import type { OAuth2ProviderLogoutMethodEnum } from "./OAuth2ProviderLogoutMethodEnum"; @@ -68,6 +70,12 @@ export interface OAuth2ProviderRequest { * @memberof OAuth2ProviderRequest */ clientType?: ClientTypeEnum; + /** + * + * @type {Array} + * @memberof OAuth2ProviderRequest + */ + grantTypes?: Array; /** * * @type {string} @@ -197,6 +205,10 @@ export function OAuth2ProviderRequestFromJSONTyped( propertyMappings: json["property_mappings"] == null ? undefined : json["property_mappings"], clientType: json["client_type"] == null ? undefined : ClientTypeEnumFromJSON(json["client_type"]), + grantTypes: + json["grant_types"] == null + ? undefined + : (json["grant_types"] as Array).map(GrantTypesEnumFromJSON), clientId: json["client_id"] == null ? undefined : json["client_id"], clientSecret: json["client_secret"] == null ? undefined : json["client_secret"], accessCodeValidity: @@ -248,6 +260,10 @@ export function OAuth2ProviderRequestToJSONTyped( invalidation_flow: value["invalidationFlow"], property_mappings: value["propertyMappings"], client_type: ClientTypeEnumToJSON(value["clientType"]), + grant_types: + value["grantTypes"] == null + ? undefined + : (value["grantTypes"] as Array).map(GrantTypesEnumToJSON), client_id: value["clientId"], client_secret: value["clientSecret"], access_code_validity: value["accessCodeValidity"], diff --git a/packages/client-ts/src/models/PatchedOAuth2ProviderRequest.ts b/packages/client-ts/src/models/PatchedOAuth2ProviderRequest.ts index 7da82e284e..7c509ebeac 100644 --- a/packages/client-ts/src/models/PatchedOAuth2ProviderRequest.ts +++ b/packages/client-ts/src/models/PatchedOAuth2ProviderRequest.ts @@ -14,6 +14,8 @@ import type { ClientTypeEnum } from "./ClientTypeEnum"; import { ClientTypeEnumFromJSON, ClientTypeEnumToJSON } from "./ClientTypeEnum"; +import type { GrantTypesEnum } from "./GrantTypesEnum"; +import { GrantTypesEnumFromJSON, GrantTypesEnumToJSON } from "./GrantTypesEnum"; import type { IssuerModeEnum } from "./IssuerModeEnum"; import { IssuerModeEnumFromJSON, IssuerModeEnumToJSON } from "./IssuerModeEnum"; import type { OAuth2ProviderLogoutMethodEnum } from "./OAuth2ProviderLogoutMethodEnum"; @@ -68,6 +70,12 @@ export interface PatchedOAuth2ProviderRequest { * @memberof PatchedOAuth2ProviderRequest */ clientType?: ClientTypeEnum; + /** + * + * @type {Array} + * @memberof PatchedOAuth2ProviderRequest + */ + grantTypes?: Array; /** * * @type {string} @@ -196,6 +204,10 @@ export function PatchedOAuth2ProviderRequestFromJSONTyped( propertyMappings: json["property_mappings"] == null ? undefined : json["property_mappings"], clientType: json["client_type"] == null ? undefined : ClientTypeEnumFromJSON(json["client_type"]), + grantTypes: + json["grant_types"] == null + ? undefined + : (json["grant_types"] as Array).map(GrantTypesEnumFromJSON), clientId: json["client_id"] == null ? undefined : json["client_id"], clientSecret: json["client_secret"] == null ? undefined : json["client_secret"], accessCodeValidity: @@ -250,6 +262,10 @@ export function PatchedOAuth2ProviderRequestToJSONTyped( invalidation_flow: value["invalidationFlow"], property_mappings: value["propertyMappings"], client_type: ClientTypeEnumToJSON(value["clientType"]), + grant_types: + value["grantTypes"] == null + ? undefined + : (value["grantTypes"] as Array).map(GrantTypesEnumToJSON), client_id: value["clientId"], client_secret: value["clientSecret"], access_code_validity: value["accessCodeValidity"], diff --git a/packages/client-ts/src/models/index.ts b/packages/client-ts/src/models/index.ts index de3ce2683c..9688dc8cb3 100644 --- a/packages/client-ts/src/models/index.ts +++ b/packages/client-ts/src/models/index.ts @@ -221,6 +221,7 @@ export * from "./GoogleWorkspaceProviderMappingRequest"; export * from "./GoogleWorkspaceProviderRequest"; export * from "./GoogleWorkspaceProviderUser"; export * from "./GoogleWorkspaceProviderUserRequest"; +export * from "./GrantTypesEnum"; export * from "./Group"; export * from "./GroupKerberosSourceConnection"; export * from "./GroupKerberosSourceConnectionRequest"; diff --git a/schema.yml b/schema.yml index a746ee831c..e3ff089551 100644 --- a/schema.yml +++ b/schema.yml @@ -39976,6 +39976,16 @@ components: - google_id - provider - user + GrantTypesEnum: + enum: + - authorization_code + - implicit + - hybrid + - refresh_token + - client_credentials + - password + - urn:ietf:params:oauth:grant-type:device_code + type: string Group: type: object description: Group Serializer @@ -43635,6 +43645,10 @@ components: - $ref: '#/components/schemas/ClientTypeEnum' description: Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable + grant_types: + type: array + items: + $ref: '#/components/schemas/GrantTypesEnum' client_id: type: string maxLength: 255 @@ -43756,6 +43770,10 @@ components: - $ref: '#/components/schemas/ClientTypeEnum' description: Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable + grant_types: + type: array + items: + $ref: '#/components/schemas/GrantTypesEnum' client_id: type: string minLength: 1 @@ -49323,6 +49341,10 @@ components: - $ref: '#/components/schemas/ClientTypeEnum' description: Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable + grant_types: + type: array + items: + $ref: '#/components/schemas/GrantTypesEnum' client_id: type: string minLength: 1 diff --git a/tests/e2e/test_provider_oauth2_github.py b/tests/e2e/test_provider_oauth2_github.py index 9f8bfe5b31..120dbabd90 100644 --- a/tests/e2e/test_provider_oauth2_github.py +++ b/tests/e2e/test_provider_oauth2_github.py @@ -13,7 +13,8 @@ from authentik.lib.generators import generate_id, generate_key from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.models import ( - ClientTypes, + ClientType, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -78,11 +79,12 @@ class TestProviderOAuth2Github(SeleniumTestCase): name=generate_id(), client_id=self.client_id, client_secret=self.client_secret, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, redirect_uris=[ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github") ], authorization_flow=authorization_flow, + grant_types=[GrantType.AUTHORIZATION_CODE], ) Application.objects.create( name=generate_id(), @@ -135,11 +137,12 @@ class TestProviderOAuth2Github(SeleniumTestCase): name=generate_id(), client_id=self.client_id, client_secret=self.client_secret, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, redirect_uris=[ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github") ], authorization_flow=authorization_flow, + grant_types=[GrantType.AUTHORIZATION_CODE], ) app = Application.objects.create( name=generate_id(), @@ -208,11 +211,12 @@ class TestProviderOAuth2Github(SeleniumTestCase): name=generate_id(), client_id=self.client_id, client_secret=self.client_secret, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, redirect_uris=[ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/github") ], authorization_flow=authorization_flow, + grant_types=[GrantType.AUTHORIZATION_CODE], ) app = Application.objects.create( name=generate_id(), diff --git a/tests/e2e/test_provider_oauth2_grafana.py b/tests/e2e/test_provider_oauth2_grafana.py index f3b8de9763..3eb49e7533 100644 --- a/tests/e2e/test_provider_oauth2_grafana.py +++ b/tests/e2e/test_provider_oauth2_grafana.py @@ -20,7 +20,8 @@ from authentik.lib.generators import generate_id, generate_key from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.models import ( - ClientTypes, + ClientType, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -85,12 +86,13 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): ) provider = OAuth2Provider.objects.create( name=generate_id(), - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:3000/")], authorization_flow=authorization_flow, + grant_types=[GrantType.AUTHORIZATION_CODE], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -134,7 +136,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): ) provider = OAuth2Provider.objects.create( name=generate_id(), - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), @@ -144,6 +146,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): ) ], authorization_flow=authorization_flow, + grant_types=[GrantType.AUTHORIZATION_CODE], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -207,7 +210,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): invalidation_flow = Flow.objects.get(slug="default-provider-invalidation-flow") provider = OAuth2Provider.objects.create( name=generate_id(), - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), @@ -218,6 +221,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): ], authorization_flow=authorization_flow, invalidation_flow=invalidation_flow, + grant_types=[GrantType.AUTHORIZATION_CODE], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -287,7 +291,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=authorization_flow, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), @@ -296,6 +300,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth" ) ], + grant_types=[GrantType.AUTHORIZATION_CODE], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -371,7 +376,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): provider = OAuth2Provider.objects.create( name=generate_id(), authorization_flow=authorization_flow, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), @@ -380,6 +385,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth" ) ], + grant_types=[GrantType.AUTHORIZATION_CODE], ) provider.property_mappings.set( ScopeMapping.objects.filter( diff --git a/tests/e2e/test_provider_oidc.py b/tests/e2e/test_provider_oidc.py index 96988d9b2d..bea8d70850 100644 --- a/tests/e2e/test_provider_oidc.py +++ b/tests/e2e/test_provider_oidc.py @@ -20,7 +20,8 @@ from authentik.lib.generators import generate_id, generate_key from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.models import ( - ClientTypes, + ClientType, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -70,12 +71,13 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): ) provider = OAuth2Provider.objects.create( name=self.application_slug, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")], authorization_flow=authorization_flow, + grant_types=[GrantType.AUTHORIZATION_CODE], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -119,7 +121,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): ) provider = OAuth2Provider.objects.create( name=self.application_slug, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), @@ -127,6 +129,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback") ], authorization_flow=authorization_flow, + grant_types=[GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -231,13 +234,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): provider = OAuth2Provider.objects.create( name=self.application_slug, authorization_flow=authorization_flow, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), redirect_uris=[ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback") ], + grant_types=[GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -335,13 +339,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): provider = OAuth2Provider.objects.create( name=self.application_slug, authorization_flow=authorization_flow, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), redirect_uris=[ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback") ], + grant_types=[GrantType.AUTHORIZATION_CODE], ) provider.property_mappings.set( ScopeMapping.objects.filter( diff --git a/tests/e2e/test_provider_oidc_implicit.py b/tests/e2e/test_provider_oidc_implicit.py index 7c0f78de22..4632c26b22 100644 --- a/tests/e2e/test_provider_oidc_implicit.py +++ b/tests/e2e/test_provider_oidc_implicit.py @@ -20,7 +20,8 @@ from authentik.lib.generators import generate_id, generate_key from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.models import PolicyBinding from authentik.providers.oauth2.models import ( - ClientTypes, + ClientType, + GrantType, OAuth2Provider, RedirectURI, RedirectURIMatchingMode, @@ -71,12 +72,13 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): ) provider = OAuth2Provider.objects.create( name=self.application_slug, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/")], authorization_flow=authorization_flow, + grant_types=[GrantType.IMPLICIT], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -120,7 +122,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): ) provider = OAuth2Provider.objects.create( name=self.application_slug, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), @@ -128,6 +130,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/") ], authorization_flow=authorization_flow, + grant_types=[GrantType.IMPLICIT], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -192,13 +195,14 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): provider = OAuth2Provider.objects.create( name=self.application_slug, authorization_flow=authorization_flow, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), redirect_uris=[ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/") ], + grant_types=[GrantType.IMPLICIT], ) provider.property_mappings.set( ScopeMapping.objects.filter( @@ -282,13 +286,14 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): provider = OAuth2Provider.objects.create( name=self.application_slug, authorization_flow=authorization_flow, - client_type=ClientTypes.CONFIDENTIAL, + client_type=ClientType.CONFIDENTIAL, client_id=self.client_id, client_secret=self.client_secret, signing_key=create_test_cert(), redirect_uris=[ RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/") ], + grant_types=[GrantType.IMPLICIT], ) provider.property_mappings.set( ScopeMapping.objects.filter( diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts index 7ad1081425..6836cc5cab 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts @@ -14,6 +14,7 @@ import "#elements/forms/Radio"; import "#elements/forms/SearchSelect/index"; import "#elements/utils/TimeDeltaHelp"; import "#admin/providers/oauth2/OAuth2ProviderRedirectURI"; +import "#elements/ak-checkbox-group/ak-checkbox-group"; import { propertyMappingsProvider, propertyMappingsSelector } from "./OAuth2ProviderFormHelpers.js"; import { oauth2ProvidersProvider, oauth2ProvidersSelector } from "./OAuth2ProvidersProvider.js"; @@ -29,6 +30,7 @@ import { AKLabel } from "#components/ak-label"; import { ClientTypeEnum, FlowDesignationEnum, + GrantTypesEnum, IssuerModeEnum, MatchingModeEnum, OAuth2Provider, @@ -131,6 +133,27 @@ const redirectUriHelpMessages: string[] = [ ), ]; +const grantTypes = [ + [GrantTypesEnum.AuthorizationCode, msg("Authorization Code")], + [GrantTypesEnum.Implicit, msg("Implicit")], + [GrantTypesEnum.Hybrid, msg("Hybrid")], + [GrantTypesEnum.RefreshToken, msg("Refresh token")], + [GrantTypesEnum.ClientCredentials, msg("Client credentials")], + [GrantTypesEnum.Password, msg("Password")], + [GrantTypesEnum.UrnIetfParamsOauthGrantTypeDeviceCode, msg("Device-code")], +]; + +const defaultGrantTypes = [ + // TODO: Clean up defaults after 2026 + GrantTypesEnum.AuthorizationCode, + GrantTypesEnum.Implicit, + GrantTypesEnum.Hybrid, + GrantTypesEnum.RefreshToken, + GrantTypesEnum.ClientCredentials, + GrantTypesEnum.Password, + GrantTypesEnum.UrnIetfParamsOauthGrantTypeDeviceCode, +]; + type ShowClientSecret = (show: boolean) => void; type ShowLogoutMethod = (show: boolean) => void; @@ -218,6 +241,26 @@ export function renderForm({ ?hidden=${!showClientSecret} > + + grantType[0]) + .filter( + (type) => + (provider?.grantTypes || defaultGrantTypes).filter( + (isField) => { + return type === isField; + }, + ).length > 0, + )} + > +

+ ${msg("Grant types this provider may use.")} +

+