diff --git a/authentik/common/oauth/constants.py b/authentik/common/oauth/constants.py index db1955d8eb..0a0dc24a20 100644 --- a/authentik/common/oauth/constants.py +++ b/authentik/common/oauth/constants.py @@ -40,6 +40,8 @@ SCOPE_OPENID_PROFILE = "profile" SCOPE_OPENID_EMAIL = "email" SCOPE_OFFLINE_ACCESS = "offline_access" SCOPE_BOUND_KEY = "bound_key" +SCOPE_AUTHENTIK_API = "goauthentik.io/api" +SCOPE_AUTHENTIK_DCR = "goauthentik.io/oidc/dcr" UI_LOCALES = "ui_locales" @@ -50,8 +52,6 @@ PKCE_METHOD_S256 = "S256" TOKEN_TYPE = "Bearer" # nosec JWT_TYPE_DPOP_ID_TOKEN = "dpop+id_token" -SCOPE_AUTHENTIK_API = "goauthentik.io/api" - # URI schemes that are forbidden for redirect URIs FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"} diff --git a/authentik/enterprise/providers/oauth2/__init__.py b/authentik/enterprise/providers/oauth2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/enterprise/providers/oauth2/api.py b/authentik/enterprise/providers/oauth2/api.py new file mode 100644 index 0000000000..a799338f08 --- /dev/null +++ b/authentik/enterprise/providers/oauth2/api.py @@ -0,0 +1,38 @@ +"""OAuth2 Dynamic Client Registration API""" + +from rest_framework.viewsets import ModelViewSet + +from authentik.core.api.utils import ModelSerializer +from authentik.enterprise.api import EnterpriseRequiredMixin +from authentik.providers.oauth2.models import ( + OAuth2DynamicClientRegistration, +) + + +class OAuth2DynamicClientRegistrationSerializer(EnterpriseRequiredMixin, ModelSerializer): + """Serializer for OAuth2DynamicClientRegistration""" + + class Meta: + model = OAuth2DynamicClientRegistration + fields = [ + "pbm_uuid", + "provider", + "default_application_group", + "override_authorization_flow", + "override_invalidation_flow", + "override_property_mappings", + "access_token_validity", + "refresh_token_validity", + "allowed_grant_types", + "policy_engine_mode", + ] + + +class OAuth2DynamicClientRegistrationViewSet(ModelViewSet): + """OAuth2 Dynamic Client Registration configuration ViewSet""" + + queryset = OAuth2DynamicClientRegistration.objects.all() + serializer_class = OAuth2DynamicClientRegistrationSerializer + filterset_fields = ["provider"] + search_fields = ["provider__name"] + ordering = ["provider__name"] diff --git a/authentik/enterprise/providers/oauth2/apps.py b/authentik/enterprise/providers/oauth2/apps.py new file mode 100644 index 0000000000..8dd62000c6 --- /dev/null +++ b/authentik/enterprise/providers/oauth2/apps.py @@ -0,0 +1,10 @@ +from authentik.enterprise.apps import EnterpriseConfig + + +class AuthentikEnterpriseProviderOAuth2Config(EnterpriseConfig): + + name = "authentik.enterprise.providers.oauth2" + label = "authentik_enterprise_providers_oauth2" + verbose_name = "authentik Enterprise.Providers.OAuth2" + default = True + mountpoint = "application/o/" diff --git a/authentik/enterprise/providers/oauth2/tests/__init__.py b/authentik/enterprise/providers/oauth2/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/enterprise/providers/oauth2/tests/test_dcr.py b/authentik/enterprise/providers/oauth2/tests/test_dcr.py new file mode 100644 index 0000000000..34e8e970b9 --- /dev/null +++ b/authentik/enterprise/providers/oauth2/tests/test_dcr.py @@ -0,0 +1,344 @@ +"""Tests for OAuth2 Dynamic Client Registration (RFC 7591)""" + +import json +from datetime import timedelta + +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone + +from authentik.blueprints.tests import apply_blueprint +from authentik.common.oauth.constants import SCOPE_AUTHENTIK_DCR +from authentik.core.models import Application +from authentik.core.tests.utils import create_test_flow, create_test_user +from authentik.lib.generators import generate_id +from authentik.policies.dummy.models import DummyPolicy +from authentik.policies.models import PolicyBinding +from authentik.providers.oauth2.models import ( + AccessToken, + ClientType, + GrantType, + OAuth2DynamicClientRegistration, + OAuth2Provider, + RedirectURI, + RedirectURIMatchingMode, + ScopeMapping, +) + + +class TestDynamicClientRegistration(TestCase): + """RFC 7591 Dynamic Client Registration tests""" + + @apply_blueprint("system/providers-oauth2.yaml") + def setUp(self): + self.flow = create_test_flow() + self.provider = OAuth2Provider.objects.create( + name=generate_id(), + authorization_flow=self.flow, + redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], + ) + self.app = Application.objects.create( + name=generate_id(), + slug=generate_id(), + provider=self.provider, + ) + self.dcr = OAuth2DynamicClientRegistration.objects.create( + provider=self.provider, + override_authorization_flow=self.flow, + ) + self.register_url = reverse( + "authentik_enterprise_providers_oauth2:dynamic-client-registration", + kwargs={"application_slug": self.app.slug}, + ) + + def _access_token(self, scope: str = SCOPE_AUTHENTIK_DCR, **kwargs) -> AccessToken: + return AccessToken.objects.create( + user=kwargs.pop("user", None) or create_test_user(), + provider=kwargs.pop("provider", None) or self.provider, + token=generate_id(), + auth_time=timezone.now(), + _scope=scope, + _id_token=json.dumps({}), + **kwargs, + ) + + def _post(self, body: dict, token: str | None = None) -> object: + headers = {"content_type": "application/json"} + if token: + headers["HTTP_AUTHORIZATION"] = f"Bearer {token}" + return self.client.post(self.register_url, json.dumps(body), **headers) + + def test_registration_success(self): + """Basic registration creates a new OAuth2Provider and Application.""" + token = self._access_token() + response = self._post( + {"redirect_uris": ["https://example.com/callback"], "client_name": "Test Client"}, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + self.assertIn("client_id", body) + self.assertIn("client_secret", body) + self.assertEqual(body["redirect_uris"], ["https://example.com/callback"]) + self.assertEqual(body["client_name"], "Test Client") + # Provider created + self.assertTrue(OAuth2Provider.objects.filter(client_id=body["client_id"]).exists()) + # Application created + provider = OAuth2Provider.objects.get(client_id=body["client_id"]) + self.assertIsNotNone(provider.application) + + def test_requires_redirect_uris(self): + """redirect_uris is required.""" + token = self._access_token() + response = self._post({"client_name": "No URIs"}, token=token.token) + self.assertEqual(response.status_code, 400) + body = json.loads(response.content) + self.assertEqual(body["error"], "invalid_redirect_uri") + + def test_invalid_json(self): + """Non-JSON body is rejected.""" + token = self._access_token() + response = self.client.post( + self.register_url, + "not-json", + content_type="application/json", + HTTP_AUTHORIZATION=f"Bearer {token.token}", + ) + self.assertEqual(response.status_code, 400) + body = json.loads(response.content) + self.assertEqual(body["error"], "invalid_client_metadata") + + def test_requires_access_token(self): + """Requests without a bearer token are rejected.""" + response = self._post({"redirect_uris": ["https://example.com/cb"]}) + self.assertEqual(response.status_code, 401) + body = json.loads(response.content) + self.assertEqual(body["error"], "invalid_token") + + def test_access_token_missing_scope_rejected(self): + """An access token without the DCR scope is rejected.""" + token = self._access_token(scope="") + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 401) + body = json.loads(response.content) + self.assertEqual(body["error"], "invalid_token") + + def test_valid_access_token(self): + """A valid access token with the DCR scope grants registration.""" + token = self._access_token() + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + + def test_expired_access_token_rejected(self): + """An expired access token cannot be used to register clients.""" + token = self._access_token(expires=timezone.now() - timedelta(hours=1)) + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 401) + + def test_grant_type_restriction(self): + """Grant types not in allowed_grant_types are filtered out.""" + self.dcr.allowed_grant_types = [GrantType.AUTHORIZATION_CODE] + self.dcr.save() + token = self._access_token() + response = self._post( + { + "redirect_uris": ["https://example.com/cb"], + "grant_types": [GrantType.AUTHORIZATION_CODE, GrantType.CLIENT_CREDENTIALS], + }, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + self.assertNotIn(GrantType.CLIENT_CREDENTIALS, body["grant_types"]) + self.assertIn(GrantType.AUTHORIZATION_CODE, body["grant_types"]) + + def test_public_client_no_secret(self): + """Public client (auth_method=none) does not receive a client_secret.""" + token = self._access_token() + response = self._post( + { + "redirect_uris": ["https://example.com/cb"], + "token_endpoint_auth_method": "none", + }, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + self.assertNotIn("client_secret", body) + provider = OAuth2Provider.objects.get(client_id=body["client_id"]) + self.assertEqual(provider.client_type, ClientType.PUBLIC) + + def test_policy_access_denied(self): + """A failing policy binding on the application rejects registration with 403.""" + policy = DummyPolicy.objects.create(name=generate_id(), result=False) + PolicyBinding.objects.create(target=self.app, policy=policy, order=0) + token = self._access_token() + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 403) + body = json.loads(response.content) + self.assertEqual(body["error"], "access_denied") + + def test_override_authorization_flow_applied(self): + """override_authorization_flow is applied to the registered provider.""" + override_flow = create_test_flow() + self.dcr.override_authorization_flow = override_flow + self.dcr.save() + token = self._access_token() + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + provider = OAuth2Provider.objects.get(client_id=body["client_id"]) + self.assertEqual(provider.authorization_flow, override_flow) + + def test_default_application_group_applied(self): + """default_application_group is applied to the created application.""" + group = generate_id() + self.dcr.default_application_group = group + self.dcr.save() + token = self._access_token() + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + provider = OAuth2Provider.objects.get(client_id=body["client_id"]) + self.assertEqual(provider.application.group, group) + + def test_all_grant_types_allowed_when_unrestricted(self): + """With an empty allowed_grant_types, all requested grant types are kept.""" + token = self._access_token() + response = self._post( + { + "redirect_uris": ["https://example.com/cb"], + "grant_types": [GrantType.AUTHORIZATION_CODE, GrantType.CLIENT_CREDENTIALS], + }, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + self.assertIn(GrantType.AUTHORIZATION_CODE, body["grant_types"]) + self.assertIn(GrantType.CLIENT_CREDENTIALS, body["grant_types"]) + + def test_override_property_mappings_applied(self): + """override_property_mappings are applied to the registered provider.""" + mapping = ScopeMapping.objects.create( + name=generate_id(), scope_name=generate_id(), expression="return {}" + ) + self.dcr.override_property_mappings.set([mapping]) + token = self._access_token() + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + provider = OAuth2Provider.objects.get(client_id=body["client_id"]) + self.assertTrue(provider.property_mappings.filter(pk=mapping.pk).exists()) + + def test_policy_bindings_copied_from_dcr(self): + """Policy bindings on the DCR config are copied onto the new application.""" + policy = DummyPolicy.objects.create(name=generate_id(), result=True) + source_binding = PolicyBinding.objects.create(target=self.dcr, policy=policy, order=0) + token = self._access_token() + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + provider = OAuth2Provider.objects.get(client_id=body["client_id"]) + copied = PolicyBinding.objects.filter(target=provider.application, policy=policy) + self.assertEqual(copied.count(), 1) + # The copy has its own primary key, not the source binding's. + self.assertNotEqual(copied.first().pk, source_binding.pk) + + def test_policy_bindings_copied_from_application(self): + """When the DCR config has no bindings, the base application's bindings are copied.""" + policy = DummyPolicy.objects.create(name=generate_id(), result=True) + PolicyBinding.objects.create(target=self.app, policy=policy, order=0) + token = self._access_token() + response = self._post( + {"redirect_uris": ["https://example.com/cb"]}, + token=token.token, + ) + self.assertEqual(response.status_code, 201) + body = json.loads(response.content) + provider = OAuth2Provider.objects.get(client_id=body["client_id"]) + self.assertTrue( + PolicyBinding.objects.filter(target=provider.application, policy=policy).exists() + ) + + def test_registration_endpoint_in_openid_config(self): + """registration_endpoint is advertised in .well-known/openid-configuration.""" + response = self.client.get( + reverse( + "authentik_providers_oauth2:provider-info", + kwargs={"application_slug": self.app.slug}, + ) + ) + self.assertEqual(response.status_code, 200) + config = json.loads(response.content) + self.assertIn("registration_endpoint", config) + self.assertIn("/register/", config["registration_endpoint"]) + + def test_no_registration_endpoint_when_dcr_disabled(self): + """registration_endpoint is absent when DCR is not configured.""" + # Create a separate provider/app without DCR + provider2 = OAuth2Provider.objects.create( + name=generate_id(), + authorization_flow=self.flow, + redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], + ) + app2 = Application.objects.create( + name=generate_id(), + slug=generate_id(), + provider=provider2, + ) + response = self.client.get( + reverse( + "authentik_providers_oauth2:provider-info", + kwargs={"application_slug": app2.slug}, + ) + ) + self.assertEqual(response.status_code, 200) + config = json.loads(response.content) + self.assertNotIn("registration_endpoint", config) + + def test_register_endpoint_404_without_dcr(self): + """Registration endpoint returns 404 when DCR is not configured on that provider.""" + provider2 = OAuth2Provider.objects.create( + name=generate_id(), + authorization_flow=self.flow, + redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver")], + ) + app2_slug = generate_id().lower() + app2 = Application.objects.create( + name=generate_id(), + slug=app2_slug, + provider=provider2, + ) + response = self.client.post( + reverse( + "authentik_enterprise_providers_oauth2:dynamic-client-registration", + kwargs={"application_slug": app2.slug}, + ), + json.dumps({"redirect_uris": ["https://x.com/cb"]}), + content_type="application/json", + ) + self.assertEqual(response.status_code, 404) diff --git a/authentik/enterprise/providers/oauth2/urls.py b/authentik/enterprise/providers/oauth2/urls.py new file mode 100644 index 0000000000..9bd90a973a --- /dev/null +++ b/authentik/enterprise/providers/oauth2/urls.py @@ -0,0 +1,18 @@ +"""OAuth2 Dynamic Client Registration URLs""" + +from django.urls import path + +from authentik.enterprise.providers.oauth2.api import OAuth2DynamicClientRegistrationViewSet +from authentik.enterprise.providers.oauth2.views.dcr import DynamicClientRegistrationView + +urlpatterns = [ + path( + "/register/", + DynamicClientRegistrationView.as_view(), + name="dynamic-client-registration", + ), +] + +api_urlpatterns = [ + ("providers/oauth2-dcr", OAuth2DynamicClientRegistrationViewSet), +] diff --git a/authentik/enterprise/providers/oauth2/views/__init__.py b/authentik/enterprise/providers/oauth2/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/authentik/enterprise/providers/oauth2/views/dcr.py b/authentik/enterprise/providers/oauth2/views/dcr.py new file mode 100644 index 0000000000..d7c11238d1 --- /dev/null +++ b/authentik/enterprise/providers/oauth2/views/dcr.py @@ -0,0 +1,236 @@ +"""authentik OAuth2 Dynamic Client Registration (RFC 7591)""" + +import json +import time +from typing import Any + +from django.db import transaction +from django.http import HttpRequest, HttpResponse, JsonResponse +from django.shortcuts import get_object_or_404 +from django.utils.text import slugify +from django.views import View +from structlog.stdlib import get_logger + +from authentik.api.authentication import validate_auth +from authentik.common.oauth.constants import SCOPE_AUTHENTIK_DCR +from authentik.core.apps import AppAccessWithoutBindings +from authentik.core.models import Application, User +from authentik.lib.generators import generate_id +from authentik.policies.engine import PolicyEngine +from authentik.policies.models import PolicyBinding +from authentik.providers.oauth2.models import ( + AccessToken, + ClientType, + GrantType, + OAuth2DynamicClientRegistration, + OAuth2Provider, + RedirectURI, + RedirectURIMatchingMode, + generate_client_secret, +) + +LOGGER = get_logger() + +AUTH_METHOD_TO_CLIENT_TYPE = { + "client_secret_basic": ClientType.CONFIDENTIAL, + "client_secret_post": ClientType.CONFIDENTIAL, + "none": ClientType.PUBLIC, +} + + +def _dcr_error(error: str, description: str, status: int = 400) -> JsonResponse: + return JsonResponse({"error": error, "error_description": description}, status=status) + + +class DynamicClientRegistrationView(View): + """RFC 7591 Dynamic Client Registration endpoint. + + POST /application/o//register/ + """ + + dcr: OAuth2DynamicClientRegistration + application: Application + provider: OAuth2Provider + + def _authenticate_access_token(self, request: HttpRequest) -> AccessToken | None: + """Authenticate the request via a Bearer `AccessToken` carrying the + `goauthentik.io/oidc/dcr` scope, mirroring how `SCOPE_AUTHENTIK_API` + authenticates access to authentik's own API.""" + raw_token = validate_auth(request.headers.get("Authorization", "").encode()) + if not raw_token: + return None + access_token = AccessToken.objects.filter( + token=raw_token, _scope__icontains=SCOPE_AUTHENTIK_DCR + ).first() + if not access_token or SCOPE_AUTHENTIK_DCR not in access_token.scope: + return None + return access_token + + def _check_policy_access(self, request: HttpRequest, user: User) -> bool: + engine = PolicyEngine(self.application, user, request) + engine.empty_result = AppAccessWithoutBindings.get() + engine.use_cache = False + engine.mode = self.application.policy_engine_mode + engine.build() + return engine.result.passing + + def _unique_app_slug(self, base: str) -> str: + slug = slugify(base)[:200] or "client" + candidate = slug + counter = 1 + while Application.objects.filter(slug=candidate).exists(): + candidate = f"{slug}-{counter}" + counter += 1 + return candidate + + @transaction.atomic + def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: + # --- Access control ----------------------------------------------- + access_token = self._authenticate_access_token(request) + if access_token is None: + return _dcr_error( + "invalid_token", + "A valid access token with the " + f"'{SCOPE_AUTHENTIK_DCR}' scope is required to register clients.", + status=401, + ) + + if not self._check_policy_access(request, access_token.user): + LOGGER.info("DCR registration rejected by policy", slug=application_slug) + return _dcr_error( + "access_denied", "Policy check failed for client registration.", status=403 + ) + + # --- Parse request body ------------------------------------------ + try: + metadata: dict[str, Any] = json.loads(request.body) + except json.JSONDecodeError, UnicodeDecodeError: + return _dcr_error("invalid_client_metadata", "Request body must be valid JSON.") + + if not isinstance(metadata, dict): + return _dcr_error("invalid_client_metadata", "Request body must be a JSON object.") + + raw_uris = metadata.get("redirect_uris") + if not raw_uris or not isinstance(raw_uris, list): + return _dcr_error( + "invalid_redirect_uri", "redirect_uris is required and must be a non-empty list." + ) + if not all(isinstance(u, str) for u in raw_uris): + return _dcr_error("invalid_redirect_uri", "All redirect_uris must be strings.") + + redirect_uris = [ + RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT, url=uri) for uri in raw_uris + ] + + # --- Resolve settings -------------------------------------------- + auth_method = metadata.get("token_endpoint_auth_method", "client_secret_basic") + client_type = AUTH_METHOD_TO_CLIENT_TYPE.get(auth_method, ClientType.CONFIDENTIAL) + + requested_grants = metadata.get("grant_types", [GrantType.AUTHORIZATION_CODE]) + if not isinstance(requested_grants, list): + return _dcr_error("invalid_client_metadata", "grant_types must be an array.") + if self.dcr.allowed_grant_types: + requested_grants = [g for g in requested_grants if g in self.dcr.allowed_grant_types] + grant_types = [g for g in requested_grants if g in GrantType.values] or [ + GrantType.AUTHORIZATION_CODE + ] + + client_name = metadata.get("client_name", "") + + # --- Create OAuth2Provider --------------------------------------- + provider = OAuth2Provider( + name=client_name or generate_id(), + client_id=generate_id(), + client_secret=generate_client_secret(), + client_type=client_type, + grant_types=grant_types, + authorization_flow=self.dcr.override_authorization_flow + or self.provider.authorization_flow, + invalidation_flow=self.dcr.override_invalidation_flow + or self.provider.invalidation_flow, + access_token_validity=self.dcr.access_token_validity + or self.provider.access_token_validity, + refresh_token_validity=self.dcr.refresh_token_validity + or self.provider.refresh_token_validity, + ) + provider.redirect_uris = redirect_uris + provider.save() + + if self.dcr.override_property_mappings.exists(): + provider.property_mappings.set(self.dcr.override_property_mappings.all()) + else: + provider.property_mappings.set(self.provider.property_mappings.all()) + + app_slug = self._unique_app_slug(client_name or provider.client_id) + app = Application.objects.create( + name=client_name or provider.client_id, + slug=app_slug, + provider=provider, + group=self.dcr.default_application_group, + policy_engine_mode=self.dcr.policy_engine_mode, + ) + bindings = PolicyBinding.objects.filter(target=self.dcr) + if not bindings.exists(): + bindings = PolicyBinding.objects.filter(target=self.application) + new_bindings = [] + for binding in bindings: + new_bindings.append( + PolicyBinding( + # Copy over all fields except the target and primary key + **{ + k.name: getattr(binding, k.name) + for k in PolicyBinding._meta.concrete_fields + if k.name != "target" and not k.primary_key + }, + target=app, + ) + ) + PolicyBinding.objects.bulk_create(new_bindings) + + LOGGER.info( + "DCR: registered new client", + client_id=provider.client_id, + client_name=provider.name, + registered_by=access_token.user.username, + ) + + # --- RFC 7591 ยง3.2.1 response ------------------------------------- + response_data: dict[str, Any] = { + "client_id": provider.client_id, + "client_id_issued_at": int(time.time()), + "redirect_uris": raw_uris, + "grant_types": grant_types, + "token_endpoint_auth_method": ( + auth_method if auth_method in AUTH_METHOD_TO_CLIENT_TYPE else "client_secret_basic" + ), + } + + if client_type == ClientType.CONFIDENTIAL: + response_data["client_secret"] = provider.client_secret + response_data["client_secret_expires_at"] = 0 + + if client_name: + response_data["client_name"] = client_name + + return JsonResponse(response_data, status=201) + + def dispatch( + self, request: HttpRequest, application_slug: str, *args: Any, **kwargs: Any + ) -> HttpResponse: + self.application = get_object_or_404(Application, slug=application_slug) + self.provider = get_object_or_404(OAuth2Provider, pk=self.application.provider_id) + + try: + self.dcr = OAuth2DynamicClientRegistration.objects.get(provider=self.provider) + except OAuth2DynamicClientRegistration.DoesNotExist: + return JsonResponse( + { + "error": "not_found", + "error_description": ( + "Dynamic client registration is not enabled for this provider." + ), + }, + status=404, + ) + + return super().dispatch(request, application_slug, *args, **kwargs) diff --git a/authentik/enterprise/settings.py b/authentik/enterprise/settings.py index 5978ed6fbd..426b461dcc 100644 --- a/authentik/enterprise/settings.py +++ b/authentik/enterprise/settings.py @@ -9,6 +9,7 @@ TENANT_APPS = [ "authentik.enterprise.policies.unique_password", "authentik.enterprise.providers.google_workspace", "authentik.enterprise.providers.microsoft_entra", + "authentik.enterprise.providers.oauth2", "authentik.enterprise.providers.radius", "authentik.enterprise.providers.scim", "authentik.enterprise.providers.ssf", diff --git a/authentik/providers/oauth2/migrations/0036_dynamicclientregistrationpropertymapping_and_more.py b/authentik/providers/oauth2/migrations/0036_dynamicclientregistrationpropertymapping_and_more.py new file mode 100644 index 0000000000..1ce86fc7cd --- /dev/null +++ b/authentik/providers/oauth2/migrations/0036_dynamicclientregistrationpropertymapping_and_more.py @@ -0,0 +1,169 @@ +# Generated by Django 5.2.15 on 2026-07-29 17:10 + +import authentik.lib.models +import authentik.lib.utils.time +import django.contrib.postgres.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_flows", "0031_alter_flow_layout"), + ("authentik_policies", "0012_alter_policybinding_managers_policybinding_expires_and_more"), + ("authentik_providers_oauth2", "0035_oauth2providerjwtfederationprovider_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="DynamicClientRegistrationPropertyMapping", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "property_mapping", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_providers_oauth2.scopemapping", + ), + ), + ], + options={ + "verbose_name": "Dynamic Client Registration Property Mapping", + "verbose_name_plural": "Dynamic Client Registration Property Mappings", + }, + bases=(models.Model, authentik.lib.models.InternallyManagedMixin), + ), + migrations.CreateModel( + name="OAuth2DynamicClientRegistration", + fields=[ + ( + "policybindingmodel_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authentik_policies.policybindingmodel", + ), + ), + ( + "default_application_group", + models.TextField( + blank=True, + default="", + help_text="Group to assign to automatically created applications.", + verbose_name="Default application group", + ), + ), + ( + "access_token_validity", + models.TextField( + default="hours=1", + help_text="Maximum access token validity for registered clients (Format: hours=1;minutes=2;seconds=3).", + validators=[authentik.lib.utils.time.timedelta_string_validator], + verbose_name="Access token validity", + ), + ), + ( + "refresh_token_validity", + models.TextField( + default="days=30", + help_text="Maximum refresh token validity for registered clients (Format: hours=1;minutes=2;seconds=3).", + validators=[authentik.lib.utils.time.timedelta_string_validator], + verbose_name="Refresh token validity", + ), + ), + ( + "allowed_grant_types", + 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"), + ( + "urn:ietf:params:oauth:grant-type:token-exchange", + "Token Exchange", + ), + ] + ), + blank=True, + default=list, + help_text="If empty, all grant types are allowed.", + size=None, + verbose_name="Allowed grant types", + ), + ), + ( + "override_authorization_flow", + models.ForeignKey( + blank=True, + help_text="Authorization flow applied to dynamically registered clients.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="dcr_override_authorization_flow", + to="authentik_flows.flow", + verbose_name="Override authorization flow", + ), + ), + ( + "override_invalidation_flow", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="dcr_override_invalidation_flow", + to="authentik_flows.flow", + verbose_name="Override invalidation flow", + ), + ), + ( + "override_property_mappings", + models.ManyToManyField( + blank=True, + help_text="Scope mappings applied to dynamically registered clients.", + through="authentik_providers_oauth2.DynamicClientRegistrationPropertyMapping", + to="authentik_providers_oauth2.scopemapping", + verbose_name="Override property mappings", + ), + ), + ( + "provider", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="dcr_configuration", + to="authentik_providers_oauth2.oauth2provider", + verbose_name="Provider", + ), + ), + ], + options={ + "verbose_name": "OAuth2 Dynamic Client Registration", + "verbose_name_plural": "OAuth2 Dynamic Client Registrations", + }, + bases=("authentik_policies.policybindingmodel", models.Model), + ), + migrations.AddField( + model_name="dynamicclientregistrationpropertymapping", + name="dynamic_client_registration", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="authentik_providers_oauth2.oauth2dynamicclientregistration", + ), + ), + migrations.AlterUniqueTogether( + name="dynamicclientregistrationpropertymapping", + unique_together={("property_mapping", "dynamic_client_registration")}, + ), + ] diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index aa14bf6245..aad63a4ebd 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -63,6 +63,7 @@ from authentik.lib.models import ( SimpleThroughModel, ) from authentik.lib.utils.time import timedelta_string_validator +from authentik.policies.models import PolicyBindingModel from authentik.sources.oauth.models import OAuthSource if TYPE_CHECKING: @@ -739,3 +740,107 @@ class DeviceToken(InternallyManagedMixin, ExpiringModel): def __str__(self): return f"Device Token for {self.provider_id}" + + +class OAuth2DynamicClientRegistration(SerializerModel, PolicyBindingModel): + """Configuration for Dynamic Client Registration (RFC 7591) on an OAuth2Provider.""" + + provider = models.OneToOneField( + OAuth2Provider, + on_delete=models.CASCADE, + related_name="dcr_configuration", + verbose_name=_("Provider"), + ) + + default_application_group = models.TextField( + blank=True, + default="", + verbose_name=_("Default application group"), + help_text=_("Group to assign to automatically created applications."), + ) + + override_authorization_flow = models.ForeignKey( + "authentik_flows.Flow", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("Override authorization flow"), + help_text=_("Authorization flow applied to dynamically registered clients."), + related_name="dcr_override_authorization_flow", + ) + override_invalidation_flow = models.ForeignKey( + "authentik_flows.Flow", + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name=_("Override invalidation flow"), + related_name="dcr_override_invalidation_flow", + ) + override_property_mappings = models.ManyToManyField( + ScopeMapping, + blank=True, + verbose_name=_("Override property mappings"), + help_text=_("Scope mappings applied to dynamically registered clients."), + through="DynamicClientRegistrationPropertyMapping", + ) + + access_token_validity = models.TextField( + default="hours=1", + validators=[timedelta_string_validator], + verbose_name=_("Access token validity"), + help_text=_( + "Maximum access token validity for registered clients " + "(Format: hours=1;minutes=2;seconds=3)." + ), + ) + refresh_token_validity = models.TextField( + default="days=30", + validators=[timedelta_string_validator], + verbose_name=_("Refresh token validity"), + help_text=_( + "Maximum refresh token validity for registered clients " + "(Format: hours=1;minutes=2;seconds=3)." + ), + ) + + allowed_grant_types = ArrayField( + models.TextField(choices=GrantType.choices), + default=list, + blank=True, + verbose_name=_("Allowed grant types"), + help_text=_("If empty, all grant types are allowed."), + ) + + @property + def serializer(self) -> type[Serializer]: + from authentik.enterprise.providers.oauth2.api import ( + OAuth2DynamicClientRegistrationSerializer, + ) + + return OAuth2DynamicClientRegistrationSerializer + + def __str__(self): + return f"DCR Configuration for {self.provider_id}" + + class Meta: + verbose_name = _("OAuth2 Dynamic Client Registration") + verbose_name_plural = _("OAuth2 Dynamic Client Registrations") + + +class DynamicClientRegistrationPropertyMapping(SimpleThroughModel): + property_mapping = models.ForeignKey(ScopeMapping, on_delete=models.CASCADE) + dynamic_client_registration = models.ForeignKey( + OAuth2DynamicClientRegistration, on_delete=models.CASCADE + ) + + class Meta: + unique_together = (("property_mapping", "dynamic_client_registration"),) + verbose_name = _("Dynamic Client Registration Property Mapping") + verbose_name_plural = _("Dynamic Client Registration Property Mappings") + + def __str__(self): + return ( + "DynamicClientRegistrationPropertyMapping for DCR " + f"{self.dynamic_client_registration_id} and PropertyMapping " + f"{self.property_mapping_id}." + ) diff --git a/authentik/providers/oauth2/views/provider.py b/authentik/providers/oauth2/views/provider.py index 19a9932cfe..eb105de619 100644 --- a/authentik/providers/oauth2/views/provider.py +++ b/authentik/providers/oauth2/views/provider.py @@ -2,6 +2,7 @@ from typing import Any +from django.apps import apps from django.http import HttpRequest, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, reverse from django.views import View @@ -24,6 +25,7 @@ from authentik.core.expression.exceptions import PropertyMappingExpressionExcept from authentik.core.models import Application from authentik.providers.oauth2.dpop import DPOP_SUPPORTED_ALGS from authentik.providers.oauth2.models import ( + OAuth2DynamicClientRegistration, OAuth2Provider, ResponseMode, ResponseTypes, @@ -123,6 +125,17 @@ class ProviderInfoView(View): if provider.encryption_key: config["id_token_encryption_alg_values_supported"] = ["RSA-OAEP-256"] config["id_token_encryption_enc_values_supported"] = ["A256CBC-HS512"] + try: + _ = provider.dcr_configuration + if apps.get_app_config("authentik_enterprise").enabled(): + config["registration_endpoint"] = self.request.build_absolute_uri( + reverse( + "authentik_enterprise_providers_oauth2:dynamic-client-registration", + kwargs={"application_slug": provider.application.slug}, + ) + ) + except OAuth2DynamicClientRegistration.DoesNotExist: + pass return config def get_claims(self, provider: OAuth2Provider) -> list[str]: diff --git a/authentik/root/settings.py b/authentik/root/settings.py index f240ef26a0..71ed3eb570 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -212,6 +212,8 @@ SPECTACULAR_SETTINGS = { "TaskStatusEnum": "django_dramatiq_postgres.models.TaskState", "TransportModeEnum": "authentik.events.models.TransportMode", "RequestStatus": "authentik.enterprise.requests.models.RequestStatus", + "ClientTypeEnum": "authentik.providers.oauth2.models.ClientType", + "GrantTypeEnum": "authentik.providers.oauth2.models.GrantType", "UserTypeEnum": "authentik.core.models.UserTypes", "UserVerificationEnum": "authentik.stages.authenticator_webauthn.models.UserVerification", "WebAuthnHintEnum": "authentik.stages.authenticator_webauthn.models.WebAuthnHint", diff --git a/blueprints/schema.json b/blueprints/schema.json index 39e92ed187..3d33e4cc6a 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -2376,6 +2376,46 @@ } } }, + { + "type": "object", + "required": [ + "model", + "identifiers" + ], + "properties": { + "model": { + "const": "authentik_providers_oauth2.oauth2dynamicclientregistration" + }, + "id": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "absent", + "created", + "must_created", + "present" + ], + "default": "present" + }, + "conditions": { + "type": "array", + "items": { + "type": "boolean" + } + }, + "permissions": { + "$ref": "#/$defs/model_authentik_providers_oauth2.oauth2dynamicclientregistration_permissions" + }, + "attrs": { + "$ref": "#/$defs/model_authentik_providers_oauth2.oauth2dynamicclientregistration" + }, + "identifiers": { + "$ref": "#/$defs/model_authentik_providers_oauth2.oauth2dynamicclientregistration" + } + } + }, { "type": "object", "required": [ @@ -6308,6 +6348,8 @@ "authentik_providers_oauth2.add_accesstoken", "authentik_providers_oauth2.add_authorizationcode", "authentik_providers_oauth2.add_devicetoken", + "authentik_providers_oauth2.add_dynamicclientregistrationpropertymapping", + "authentik_providers_oauth2.add_oauth2dynamicclientregistration", "authentik_providers_oauth2.add_oauth2provider", "authentik_providers_oauth2.add_oauth2providerjwtfederationprovider", "authentik_providers_oauth2.add_oauth2providerjwtfederationsource", @@ -6316,6 +6358,8 @@ "authentik_providers_oauth2.change_accesstoken", "authentik_providers_oauth2.change_authorizationcode", "authentik_providers_oauth2.change_devicetoken", + "authentik_providers_oauth2.change_dynamicclientregistrationpropertymapping", + "authentik_providers_oauth2.change_oauth2dynamicclientregistration", "authentik_providers_oauth2.change_oauth2provider", "authentik_providers_oauth2.change_oauth2providerjwtfederationprovider", "authentik_providers_oauth2.change_oauth2providerjwtfederationsource", @@ -6324,6 +6368,8 @@ "authentik_providers_oauth2.delete_accesstoken", "authentik_providers_oauth2.delete_authorizationcode", "authentik_providers_oauth2.delete_devicetoken", + "authentik_providers_oauth2.delete_dynamicclientregistrationpropertymapping", + "authentik_providers_oauth2.delete_oauth2dynamicclientregistration", "authentik_providers_oauth2.delete_oauth2provider", "authentik_providers_oauth2.delete_oauth2providerjwtfederationprovider", "authentik_providers_oauth2.delete_oauth2providerjwtfederationsource", @@ -6332,6 +6378,8 @@ "authentik_providers_oauth2.view_accesstoken", "authentik_providers_oauth2.view_authorizationcode", "authentik_providers_oauth2.view_devicetoken", + "authentik_providers_oauth2.view_dynamicclientregistrationpropertymapping", + "authentik_providers_oauth2.view_oauth2dynamicclientregistration", "authentik_providers_oauth2.view_oauth2provider", "authentik_providers_oauth2.view_oauth2providerjwtfederationprovider", "authentik_providers_oauth2.view_oauth2providerjwtfederationsource", @@ -9882,6 +9930,7 @@ "authentik.enterprise.policies.unique_password", "authentik.enterprise.providers.google_workspace", "authentik.enterprise.providers.microsoft_entra", + "authentik.enterprise.providers.oauth2", "authentik.enterprise.providers.radius", "authentik.enterprise.providers.scim", "authentik.enterprise.providers.ssf", @@ -9939,6 +9988,7 @@ "authentik_providers_ldap.ldapprovider", "authentik_providers_oauth2.scopemapping", "authentik_providers_oauth2.oauth2provider", + "authentik_providers_oauth2.oauth2dynamicclientregistration", "authentik_providers_proxy.proxyprovider", "authentik_providers_rac.racprovider", "authentik_providers_rac.endpoint", @@ -10876,6 +10926,107 @@ } } }, + "model_authentik_providers_oauth2.oauth2dynamicclientregistration": { + "type": "object", + "properties": { + "provider": { + "type": "integer", + "title": "Provider" + }, + "default_application_group": { + "type": "string", + "title": "Default application group", + "description": "Group to assign to automatically created applications." + }, + "override_authorization_flow": { + "type": "string", + "format": "uuid", + "title": "Override authorization flow", + "description": "Authorization flow applied to dynamically registered clients." + }, + "override_invalidation_flow": { + "type": "string", + "format": "uuid", + "title": "Override invalidation flow" + }, + "override_property_mappings": { + "type": "array", + "items": { + "type": "string", + "format": "uuid", + "description": "Scope mappings applied to dynamically registered clients." + }, + "title": "Override property mappings", + "description": "Scope mappings applied to dynamically registered clients." + }, + "access_token_validity": { + "type": "string", + "minLength": 1, + "title": "Access token validity", + "description": "Maximum access token validity for registered clients (Format: hours=1;minutes=2;seconds=3)." + }, + "refresh_token_validity": { + "type": "string", + "minLength": 1, + "title": "Refresh token validity", + "description": "Maximum refresh token validity for registered clients (Format: hours=1;minutes=2;seconds=3)." + }, + "allowed_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", + "urn:ietf:params:oauth:grant-type:token-exchange" + ], + "title": "Allowed grant types" + }, + "title": "Allowed grant types", + "description": "If empty, all grant types are allowed." + }, + "policy_engine_mode": { + "type": "string", + "enum": [ + "all", + "any" + ], + "title": "Policy engine mode" + } + }, + "required": [] + }, + "model_authentik_providers_oauth2.oauth2dynamicclientregistration_permissions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "permission" + ], + "properties": { + "permission": { + "type": "string", + "enum": [ + "add_oauth2dynamicclientregistration", + "change_oauth2dynamicclientregistration", + "delete_oauth2dynamicclientregistration", + "view_oauth2dynamicclientregistration" + ] + }, + "user": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, "model_authentik_providers_oauth2.oauth2provider": { "type": "object", "properties": { @@ -12577,6 +12728,8 @@ "authentik_providers_oauth2.add_accesstoken", "authentik_providers_oauth2.add_authorizationcode", "authentik_providers_oauth2.add_devicetoken", + "authentik_providers_oauth2.add_dynamicclientregistrationpropertymapping", + "authentik_providers_oauth2.add_oauth2dynamicclientregistration", "authentik_providers_oauth2.add_oauth2provider", "authentik_providers_oauth2.add_oauth2providerjwtfederationprovider", "authentik_providers_oauth2.add_oauth2providerjwtfederationsource", @@ -12585,6 +12738,8 @@ "authentik_providers_oauth2.change_accesstoken", "authentik_providers_oauth2.change_authorizationcode", "authentik_providers_oauth2.change_devicetoken", + "authentik_providers_oauth2.change_dynamicclientregistrationpropertymapping", + "authentik_providers_oauth2.change_oauth2dynamicclientregistration", "authentik_providers_oauth2.change_oauth2provider", "authentik_providers_oauth2.change_oauth2providerjwtfederationprovider", "authentik_providers_oauth2.change_oauth2providerjwtfederationsource", @@ -12593,6 +12748,8 @@ "authentik_providers_oauth2.delete_accesstoken", "authentik_providers_oauth2.delete_authorizationcode", "authentik_providers_oauth2.delete_devicetoken", + "authentik_providers_oauth2.delete_dynamicclientregistrationpropertymapping", + "authentik_providers_oauth2.delete_oauth2dynamicclientregistration", "authentik_providers_oauth2.delete_oauth2provider", "authentik_providers_oauth2.delete_oauth2providerjwtfederationprovider", "authentik_providers_oauth2.delete_oauth2providerjwtfederationsource", @@ -12601,6 +12758,8 @@ "authentik_providers_oauth2.view_accesstoken", "authentik_providers_oauth2.view_authorizationcode", "authentik_providers_oauth2.view_devicetoken", + "authentik_providers_oauth2.view_dynamicclientregistrationpropertymapping", + "authentik_providers_oauth2.view_oauth2dynamicclientregistration", "authentik_providers_oauth2.view_oauth2provider", "authentik_providers_oauth2.view_oauth2providerjwtfederationprovider", "authentik_providers_oauth2.view_oauth2providerjwtfederationsource", diff --git a/blueprints/system/providers-oauth2.yaml b/blueprints/system/providers-oauth2.yaml index 71f529ce14..7c96840955 100644 --- a/blueprints/system/providers-oauth2.yaml +++ b/blueprints/system/providers-oauth2.yaml @@ -78,6 +78,17 @@ entries: # This scope grants the application the ability to access the authentik API # on behalf of the authorizing user return {} + - identifiers: + managed: goauthentik.io/providers/oauth2/scope-dcr + model: authentik_providers_oauth2.scopemapping + attrs: + name: "authentik default OAuth Mapping: authentik Dynamic Client Registration" + scope_name: goauthentik.io/oidc/dcr + description: "authentik Dynamic Client Registration (RFC 7591)" + expression: | + # This scope grants the application the ability to register new + # OAuth2 clients via Dynamic Client Registration (RFC 7591) + return {} - identifiers: managed: goauthentik.io/providers/oauth2/scope-bound_key model: authentik_providers_oauth2.scopemapping diff --git a/packages/client-ts/src/apis/ProvidersApi.ts b/packages/client-ts/src/apis/ProvidersApi.ts index 4faa3e0a71..0c6d16fda9 100644 --- a/packages/client-ts/src/apis/ProvidersApi.ts +++ b/packages/client-ts/src/apis/ProvidersApi.ts @@ -66,6 +66,14 @@ import { type MicrosoftEntraProviderUserRequest, MicrosoftEntraProviderUserRequestToJSON, } from "../models/MicrosoftEntraProviderUserRequest"; +import { + type OAuth2DynamicClientRegistration, + OAuth2DynamicClientRegistrationFromJSON, +} from "../models/OAuth2DynamicClientRegistration"; +import { + type OAuth2DynamicClientRegistrationRequest, + OAuth2DynamicClientRegistrationRequestToJSON, +} from "../models/OAuth2DynamicClientRegistrationRequest"; import { type OAuth2Provider, OAuth2ProviderFromJSON } from "../models/OAuth2Provider"; import { type OAuth2ProviderRequest, @@ -103,6 +111,10 @@ import { type PaginatedMicrosoftEntraProviderUserList, PaginatedMicrosoftEntraProviderUserListFromJSON, } from "../models/PaginatedMicrosoftEntraProviderUserList"; +import { + type PaginatedOAuth2DynamicClientRegistrationList, + PaginatedOAuth2DynamicClientRegistrationListFromJSON, +} from "../models/PaginatedOAuth2DynamicClientRegistrationList"; import { type PaginatedOAuth2ProviderList, PaginatedOAuth2ProviderListFromJSON, @@ -159,6 +171,10 @@ import { type PatchedMicrosoftEntraProviderRequest, PatchedMicrosoftEntraProviderRequestToJSON, } from "../models/PatchedMicrosoftEntraProviderRequest"; +import { + type PatchedOAuth2DynamicClientRegistrationRequest, + PatchedOAuth2DynamicClientRegistrationRequestToJSON, +} from "../models/PatchedOAuth2DynamicClientRegistrationRequest"; import { type PatchedOAuth2ProviderRequest, PatchedOAuth2ProviderRequestToJSON, @@ -508,6 +524,36 @@ export interface ProvidersOauth2CreateRequest { oAuth2ProviderRequest: OAuth2ProviderRequest; } +export interface ProvidersOauth2DcrCreateRequest { + oAuth2DynamicClientRegistrationRequest: OAuth2DynamicClientRegistrationRequest; +} + +export interface ProvidersOauth2DcrDestroyRequest { + pbmUuid: string; +} + +export interface ProvidersOauth2DcrListRequest { + ordering?: string; + page?: number; + pageSize?: number; + provider?: number; + search?: string; +} + +export interface ProvidersOauth2DcrPartialUpdateRequest { + pbmUuid: string; + patchedOAuth2DynamicClientRegistrationRequest?: PatchedOAuth2DynamicClientRegistrationRequest; +} + +export interface ProvidersOauth2DcrRetrieveRequest { + pbmUuid: string; +} + +export interface ProvidersOauth2DcrUpdateRequest { + pbmUuid: string; + oAuth2DynamicClientRegistrationRequest: OAuth2DynamicClientRegistrationRequest; +} + export interface ProvidersOauth2DestroyRequest { id: number; } @@ -4544,6 +4590,430 @@ export class ProvidersApi extends runtime.BaseAPI { return await response.value(); } + /** + * Creates request options for providersOauth2DcrCreate without sending the request + */ + async providersOauth2DcrCreateRequestOpts( + requestParameters: ProvidersOauth2DcrCreateRequest, + ): Promise { + if (requestParameters["oAuth2DynamicClientRegistrationRequest"] == null) { + throw new runtime.RequiredError( + "oAuth2DynamicClientRegistrationRequest", + 'Required parameter "oAuth2DynamicClientRegistrationRequest" was null or undefined when calling providersOauth2DcrCreate().', + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters["Content-Type"] = "application/json"; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("authentik", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/providers/oauth2-dcr/`; + + return { + path: urlPath, + method: "POST", + headers: headerParameters, + query: queryParameters, + body: OAuth2DynamicClientRegistrationRequestToJSON( + requestParameters["oAuth2DynamicClientRegistrationRequest"], + ), + }; + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrCreateRaw( + requestParameters: ProvidersOauth2DcrCreateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.providersOauth2DcrCreateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => + OAuth2DynamicClientRegistrationFromJSON(jsonValue), + ); + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrCreate( + requestParameters: ProvidersOauth2DcrCreateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.providersOauth2DcrCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for providersOauth2DcrDestroy without sending the request + */ + async providersOauth2DcrDestroyRequestOpts( + requestParameters: ProvidersOauth2DcrDestroyRequest, + ): Promise { + if (requestParameters["pbmUuid"] == null) { + throw new runtime.RequiredError( + "pbmUuid", + 'Required parameter "pbmUuid" was null or undefined when calling providersOauth2DcrDestroy().', + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("authentik", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/providers/oauth2-dcr/{pbm_uuid}/`; + urlPath = urlPath.replace( + "{pbm_uuid}", + encodeURIComponent(String(requestParameters["pbmUuid"])), + ); + + return { + path: urlPath, + method: "DELETE", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrDestroyRaw( + requestParameters: ProvidersOauth2DcrDestroyRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.providersOauth2DcrDestroyRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrDestroy( + requestParameters: ProvidersOauth2DcrDestroyRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + await this.providersOauth2DcrDestroyRaw(requestParameters, initOverrides); + } + + /** + * Creates request options for providersOauth2DcrList without sending the request + */ + async providersOauth2DcrListRequestOpts( + requestParameters: ProvidersOauth2DcrListRequest, + ): Promise { + const queryParameters: any = {}; + + if (requestParameters["ordering"] != null) { + queryParameters["ordering"] = requestParameters["ordering"]; + } + + if (requestParameters["page"] != null) { + queryParameters["page"] = requestParameters["page"]; + } + + if (requestParameters["pageSize"] != null) { + queryParameters["page_size"] = requestParameters["pageSize"]; + } + + if (requestParameters["provider"] != null) { + queryParameters["provider"] = requestParameters["provider"]; + } + + if (requestParameters["search"] != null) { + queryParameters["search"] = requestParameters["search"]; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("authentik", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/providers/oauth2-dcr/`; + + return { + path: urlPath, + method: "GET", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrListRaw( + requestParameters: ProvidersOauth2DcrListRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.providersOauth2DcrListRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => + PaginatedOAuth2DynamicClientRegistrationListFromJSON(jsonValue), + ); + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrList( + requestParameters: ProvidersOauth2DcrListRequest = {}, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.providersOauth2DcrListRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for providersOauth2DcrPartialUpdate without sending the request + */ + async providersOauth2DcrPartialUpdateRequestOpts( + requestParameters: ProvidersOauth2DcrPartialUpdateRequest, + ): Promise { + if (requestParameters["pbmUuid"] == null) { + throw new runtime.RequiredError( + "pbmUuid", + 'Required parameter "pbmUuid" was null or undefined when calling providersOauth2DcrPartialUpdate().', + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters["Content-Type"] = "application/json"; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("authentik", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/providers/oauth2-dcr/{pbm_uuid}/`; + urlPath = urlPath.replace( + "{pbm_uuid}", + encodeURIComponent(String(requestParameters["pbmUuid"])), + ); + + return { + path: urlPath, + method: "PATCH", + headers: headerParameters, + query: queryParameters, + body: PatchedOAuth2DynamicClientRegistrationRequestToJSON( + requestParameters["patchedOAuth2DynamicClientRegistrationRequest"], + ), + }; + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrPartialUpdateRaw( + requestParameters: ProvidersOauth2DcrPartialUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = + await this.providersOauth2DcrPartialUpdateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => + OAuth2DynamicClientRegistrationFromJSON(jsonValue), + ); + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrPartialUpdate( + requestParameters: ProvidersOauth2DcrPartialUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.providersOauth2DcrPartialUpdateRaw( + requestParameters, + initOverrides, + ); + return await response.value(); + } + + /** + * Creates request options for providersOauth2DcrRetrieve without sending the request + */ + async providersOauth2DcrRetrieveRequestOpts( + requestParameters: ProvidersOauth2DcrRetrieveRequest, + ): Promise { + if (requestParameters["pbmUuid"] == null) { + throw new runtime.RequiredError( + "pbmUuid", + 'Required parameter "pbmUuid" was null or undefined when calling providersOauth2DcrRetrieve().', + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("authentik", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/providers/oauth2-dcr/{pbm_uuid}/`; + urlPath = urlPath.replace( + "{pbm_uuid}", + encodeURIComponent(String(requestParameters["pbmUuid"])), + ); + + return { + path: urlPath, + method: "GET", + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrRetrieveRaw( + requestParameters: ProvidersOauth2DcrRetrieveRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.providersOauth2DcrRetrieveRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => + OAuth2DynamicClientRegistrationFromJSON(jsonValue), + ); + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrRetrieve( + requestParameters: ProvidersOauth2DcrRetrieveRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.providersOauth2DcrRetrieveRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for providersOauth2DcrUpdate without sending the request + */ + async providersOauth2DcrUpdateRequestOpts( + requestParameters: ProvidersOauth2DcrUpdateRequest, + ): Promise { + if (requestParameters["pbmUuid"] == null) { + throw new runtime.RequiredError( + "pbmUuid", + 'Required parameter "pbmUuid" was null or undefined when calling providersOauth2DcrUpdate().', + ); + } + + if (requestParameters["oAuth2DynamicClientRegistrationRequest"] == null) { + throw new runtime.RequiredError( + "oAuth2DynamicClientRegistrationRequest", + 'Required parameter "oAuth2DynamicClientRegistrationRequest" was null or undefined when calling providersOauth2DcrUpdate().', + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters["Content-Type"] = "application/json"; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("authentik", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/providers/oauth2-dcr/{pbm_uuid}/`; + urlPath = urlPath.replace( + "{pbm_uuid}", + encodeURIComponent(String(requestParameters["pbmUuid"])), + ); + + return { + path: urlPath, + method: "PUT", + headers: headerParameters, + query: queryParameters, + body: OAuth2DynamicClientRegistrationRequestToJSON( + requestParameters["oAuth2DynamicClientRegistrationRequest"], + ), + }; + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrUpdateRaw( + requestParameters: ProvidersOauth2DcrUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + const requestOptions = await this.providersOauth2DcrUpdateRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => + OAuth2DynamicClientRegistrationFromJSON(jsonValue), + ); + } + + /** + * OAuth2 Dynamic Client Registration configuration ViewSet + */ + async providersOauth2DcrUpdate( + requestParameters: ProvidersOauth2DcrUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.providersOauth2DcrUpdateRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * Creates request options for providersOauth2Destroy without sending the request */ diff --git a/packages/client-ts/src/models/AppEnum.ts b/packages/client-ts/src/models/AppEnum.ts index 43586d217f..e71666d6e7 100644 --- a/packages/client-ts/src/models/AppEnum.ts +++ b/packages/client-ts/src/models/AppEnum.ts @@ -89,6 +89,7 @@ export const AppEnum = { AuthentikEnterprisePoliciesUniquePassword: "authentik.enterprise.policies.unique_password", AuthentikEnterpriseProvidersGoogleWorkspace: "authentik.enterprise.providers.google_workspace", AuthentikEnterpriseProvidersMicrosoftEntra: "authentik.enterprise.providers.microsoft_entra", + AuthentikEnterpriseProvidersOauth2: "authentik.enterprise.providers.oauth2", AuthentikEnterpriseProvidersRadius: "authentik.enterprise.providers.radius", AuthentikEnterpriseProvidersScim: "authentik.enterprise.providers.scim", AuthentikEnterpriseProvidersSsf: "authentik.enterprise.providers.ssf", diff --git a/packages/client-ts/src/models/GrantTypesEnum.ts b/packages/client-ts/src/models/GrantTypeEnum.ts similarity index 51% rename from packages/client-ts/src/models/GrantTypesEnum.ts rename to packages/client-ts/src/models/GrantTypeEnum.ts index 12ac28bbba..bfa4dae37f 100644 --- a/packages/client-ts/src/models/GrantTypesEnum.ts +++ b/packages/client-ts/src/models/GrantTypeEnum.ts @@ -16,7 +16,7 @@ * * @export */ -export const GrantTypesEnum = { +export const GrantTypeEnum = { AuthorizationCode: "authorization_code", Implicit: "implicit", Hybrid: "hybrid", @@ -27,12 +27,12 @@ export const GrantTypesEnum = { UrnIetfParamsOauthGrantTypeTokenExchange: "urn:ietf:params:oauth:grant-type:token-exchange", UnknownDefaultOpenApi: "11184809", } as const; -export type GrantTypesEnum = (typeof GrantTypesEnum)[keyof typeof GrantTypesEnum]; +export type GrantTypeEnum = (typeof GrantTypeEnum)[keyof typeof GrantTypeEnum]; -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) { +export function instanceOfGrantTypeEnum(value: any): boolean { + for (const key in GrantTypeEnum) { + if (Object.prototype.hasOwnProperty.call(GrantTypeEnum, key)) { + if (GrantTypeEnum[key as keyof typeof GrantTypeEnum] === value) { return true; } } @@ -40,24 +40,18 @@ export function instanceOfGrantTypesEnum(value: any): boolean { return false; } -export function GrantTypesEnumFromJSON(json: any): GrantTypesEnum { - return GrantTypesEnumFromJSONTyped(json, false); +export function GrantTypeEnumFromJSON(json: any): GrantTypeEnum { + return GrantTypeEnumFromJSONTyped(json, false); } -export function GrantTypesEnumFromJSONTyped( - json: any, - ignoreDiscriminator: boolean, -): GrantTypesEnum { - return json as GrantTypesEnum; +export function GrantTypeEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): GrantTypeEnum { + return json as GrantTypeEnum; } -export function GrantTypesEnumToJSON(value?: GrantTypesEnum | null): any { +export function GrantTypeEnumToJSON(value?: GrantTypeEnum | null): any { return value as any; } -export function GrantTypesEnumToJSONTyped( - value: any, - ignoreDiscriminator: boolean, -): GrantTypesEnum { - return value as GrantTypesEnum; +export function GrantTypeEnumToJSONTyped(value: any, ignoreDiscriminator: boolean): GrantTypeEnum { + return value as GrantTypeEnum; } diff --git a/packages/client-ts/src/models/ModelEnum.ts b/packages/client-ts/src/models/ModelEnum.ts index a0e9c98db0..1afb5158ff 100644 --- a/packages/client-ts/src/models/ModelEnum.ts +++ b/packages/client-ts/src/models/ModelEnum.ts @@ -57,6 +57,8 @@ export const ModelEnum = { AuthentikProvidersLdapLdapprovider: "authentik_providers_ldap.ldapprovider", AuthentikProvidersOauth2Scopemapping: "authentik_providers_oauth2.scopemapping", AuthentikProvidersOauth2Oauth2provider: "authentik_providers_oauth2.oauth2provider", + AuthentikProvidersOauth2Oauth2dynamicclientregistration: + "authentik_providers_oauth2.oauth2dynamicclientregistration", AuthentikProvidersProxyProxyprovider: "authentik_providers_proxy.proxyprovider", AuthentikProvidersRacRacprovider: "authentik_providers_rac.racprovider", AuthentikProvidersRacEndpoint: "authentik_providers_rac.endpoint", diff --git a/packages/client-ts/src/models/OAuth2DynamicClientRegistration.ts b/packages/client-ts/src/models/OAuth2DynamicClientRegistration.ts new file mode 100644 index 0000000000..7b8c46bc87 --- /dev/null +++ b/packages/client-ts/src/models/OAuth2DynamicClientRegistration.ts @@ -0,0 +1,182 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.8.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. + */ + +import type { GrantTypeEnum } from "./GrantTypeEnum"; +import { GrantTypeEnumFromJSON, GrantTypeEnumToJSON } from "./GrantTypeEnum"; +import type { PolicyEngineMode } from "./PolicyEngineMode"; +import { PolicyEngineModeFromJSON, PolicyEngineModeToJSON } from "./PolicyEngineMode"; + +/** + * Serializer for OAuth2DynamicClientRegistration + * @export + * @interface OAuth2DynamicClientRegistration + */ +export interface OAuth2DynamicClientRegistration { + /** + * + * @type {string} + * @memberof OAuth2DynamicClientRegistration + */ + readonly pbmUuid: string; + /** + * + * @type {number} + * @memberof OAuth2DynamicClientRegistration + */ + provider: number; + /** + * Group to assign to automatically created applications. + * @type {string} + * @memberof OAuth2DynamicClientRegistration + */ + defaultApplicationGroup?: string; + /** + * Authorization flow applied to dynamically registered clients. + * @type {string} + * @memberof OAuth2DynamicClientRegistration + */ + overrideAuthorizationFlow?: string | null; + /** + * + * @type {string} + * @memberof OAuth2DynamicClientRegistration + */ + overrideInvalidationFlow?: string | null; + /** + * Scope mappings applied to dynamically registered clients. + * @type {Array} + * @memberof OAuth2DynamicClientRegistration + */ + overridePropertyMappings?: Array; + /** + * Maximum access token validity for registered clients (Format: hours=1;minutes=2;seconds=3). + * @type {string} + * @memberof OAuth2DynamicClientRegistration + */ + accessTokenValidity?: string; + /** + * Maximum refresh token validity for registered clients (Format: hours=1;minutes=2;seconds=3). + * @type {string} + * @memberof OAuth2DynamicClientRegistration + */ + refreshTokenValidity?: string; + /** + * If empty, all grant types are allowed. + * @type {Array} + * @memberof OAuth2DynamicClientRegistration + */ + allowedGrantTypes?: Array; + /** + * + * @type {PolicyEngineMode} + * @memberof OAuth2DynamicClientRegistration + */ + policyEngineMode?: PolicyEngineMode; +} + +/** + * Check if a given object implements the OAuth2DynamicClientRegistration interface. + */ +export function instanceOfOAuth2DynamicClientRegistration( + value: object, +): value is OAuth2DynamicClientRegistration { + if ( + (!("pbmUuid" in (value as Record)) && + !("pbm_uuid" in (value as Record))) || + ((value as Record)["pbmUuid"] === undefined && + (value as Record)["pbm_uuid"] === undefined) + ) + return false; + if (!("provider" in value) || value["provider"] === undefined) return false; + return true; +} + +export function OAuth2DynamicClientRegistrationFromJSON( + json: any, +): OAuth2DynamicClientRegistration { + return OAuth2DynamicClientRegistrationFromJSONTyped(json, false); +} + +export function OAuth2DynamicClientRegistrationFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): OAuth2DynamicClientRegistration { + if (json == null) { + return json; + } + return { + pbmUuid: json["pbm_uuid"], + provider: json["provider"], + defaultApplicationGroup: + json["default_application_group"] == null + ? undefined + : json["default_application_group"], + overrideAuthorizationFlow: + json["override_authorization_flow"] === undefined + ? undefined + : json["override_authorization_flow"] === null + ? null + : json["override_authorization_flow"], + overrideInvalidationFlow: + json["override_invalidation_flow"] === undefined + ? undefined + : json["override_invalidation_flow"] === null + ? null + : json["override_invalidation_flow"], + overridePropertyMappings: + json["override_property_mappings"] == null + ? undefined + : json["override_property_mappings"], + accessTokenValidity: + json["access_token_validity"] == null ? undefined : json["access_token_validity"], + refreshTokenValidity: + json["refresh_token_validity"] == null ? undefined : json["refresh_token_validity"], + allowedGrantTypes: + json["allowed_grant_types"] == null + ? undefined + : (json["allowed_grant_types"] as Array).map(GrantTypeEnumFromJSON), + policyEngineMode: + json["policy_engine_mode"] == null + ? undefined + : PolicyEngineModeFromJSON(json["policy_engine_mode"]), + }; +} + +export function OAuth2DynamicClientRegistrationToJSON(json: any): OAuth2DynamicClientRegistration { + return OAuth2DynamicClientRegistrationToJSONTyped(json, false); +} + +export function OAuth2DynamicClientRegistrationToJSONTyped( + value?: Omit | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return { + provider: value["provider"], + default_application_group: value["defaultApplicationGroup"], + override_authorization_flow: value["overrideAuthorizationFlow"], + override_invalidation_flow: value["overrideInvalidationFlow"], + override_property_mappings: value["overridePropertyMappings"], + access_token_validity: value["accessTokenValidity"], + refresh_token_validity: value["refreshTokenValidity"], + allowed_grant_types: + value["allowedGrantTypes"] == null + ? undefined + : (value["allowedGrantTypes"] as Array).map(GrantTypeEnumToJSON), + policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), + }; +} diff --git a/packages/client-ts/src/models/OAuth2DynamicClientRegistrationRequest.ts b/packages/client-ts/src/models/OAuth2DynamicClientRegistrationRequest.ts new file mode 100644 index 0000000000..c4bef993f6 --- /dev/null +++ b/packages/client-ts/src/models/OAuth2DynamicClientRegistrationRequest.ts @@ -0,0 +1,170 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.8.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. + */ + +import type { GrantTypeEnum } from "./GrantTypeEnum"; +import { GrantTypeEnumFromJSON, GrantTypeEnumToJSON } from "./GrantTypeEnum"; +import type { PolicyEngineMode } from "./PolicyEngineMode"; +import { PolicyEngineModeFromJSON, PolicyEngineModeToJSON } from "./PolicyEngineMode"; + +/** + * Serializer for OAuth2DynamicClientRegistration + * @export + * @interface OAuth2DynamicClientRegistrationRequest + */ +export interface OAuth2DynamicClientRegistrationRequest { + /** + * + * @type {number} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + provider: number; + /** + * Group to assign to automatically created applications. + * @type {string} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + defaultApplicationGroup?: string; + /** + * Authorization flow applied to dynamically registered clients. + * @type {string} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + overrideAuthorizationFlow?: string | null; + /** + * + * @type {string} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + overrideInvalidationFlow?: string | null; + /** + * Scope mappings applied to dynamically registered clients. + * @type {Array} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + overridePropertyMappings?: Array; + /** + * Maximum access token validity for registered clients (Format: hours=1;minutes=2;seconds=3). + * @type {string} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + accessTokenValidity?: string; + /** + * Maximum refresh token validity for registered clients (Format: hours=1;minutes=2;seconds=3). + * @type {string} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + refreshTokenValidity?: string; + /** + * If empty, all grant types are allowed. + * @type {Array} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + allowedGrantTypes?: Array; + /** + * + * @type {PolicyEngineMode} + * @memberof OAuth2DynamicClientRegistrationRequest + */ + policyEngineMode?: PolicyEngineMode; +} + +/** + * Check if a given object implements the OAuth2DynamicClientRegistrationRequest interface. + */ +export function instanceOfOAuth2DynamicClientRegistrationRequest( + value: object, +): value is OAuth2DynamicClientRegistrationRequest { + if (!("provider" in value) || value["provider"] === undefined) return false; + return true; +} + +export function OAuth2DynamicClientRegistrationRequestFromJSON( + json: any, +): OAuth2DynamicClientRegistrationRequest { + return OAuth2DynamicClientRegistrationRequestFromJSONTyped(json, false); +} + +export function OAuth2DynamicClientRegistrationRequestFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): OAuth2DynamicClientRegistrationRequest { + if (json == null) { + return json; + } + return { + provider: json["provider"], + defaultApplicationGroup: + json["default_application_group"] == null + ? undefined + : json["default_application_group"], + overrideAuthorizationFlow: + json["override_authorization_flow"] === undefined + ? undefined + : json["override_authorization_flow"] === null + ? null + : json["override_authorization_flow"], + overrideInvalidationFlow: + json["override_invalidation_flow"] === undefined + ? undefined + : json["override_invalidation_flow"] === null + ? null + : json["override_invalidation_flow"], + overridePropertyMappings: + json["override_property_mappings"] == null + ? undefined + : json["override_property_mappings"], + accessTokenValidity: + json["access_token_validity"] == null ? undefined : json["access_token_validity"], + refreshTokenValidity: + json["refresh_token_validity"] == null ? undefined : json["refresh_token_validity"], + allowedGrantTypes: + json["allowed_grant_types"] == null + ? undefined + : (json["allowed_grant_types"] as Array).map(GrantTypeEnumFromJSON), + policyEngineMode: + json["policy_engine_mode"] == null + ? undefined + : PolicyEngineModeFromJSON(json["policy_engine_mode"]), + }; +} + +export function OAuth2DynamicClientRegistrationRequestToJSON( + json: any, +): OAuth2DynamicClientRegistrationRequest { + return OAuth2DynamicClientRegistrationRequestToJSONTyped(json, false); +} + +export function OAuth2DynamicClientRegistrationRequestToJSONTyped( + value?: OAuth2DynamicClientRegistrationRequest | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return { + provider: value["provider"], + default_application_group: value["defaultApplicationGroup"], + override_authorization_flow: value["overrideAuthorizationFlow"], + override_invalidation_flow: value["overrideInvalidationFlow"], + override_property_mappings: value["overridePropertyMappings"], + access_token_validity: value["accessTokenValidity"], + refresh_token_validity: value["refreshTokenValidity"], + allowed_grant_types: + value["allowedGrantTypes"] == null + ? undefined + : (value["allowedGrantTypes"] as Array).map(GrantTypeEnumToJSON), + policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), + }; +} diff --git a/packages/client-ts/src/models/OAuth2Provider.ts b/packages/client-ts/src/models/OAuth2Provider.ts index b0d282b1ca..ac4ec068dc 100644 --- a/packages/client-ts/src/models/OAuth2Provider.ts +++ b/packages/client-ts/src/models/OAuth2Provider.ts @@ -14,8 +14,8 @@ import type { ClientTypeEnum } from "./ClientTypeEnum"; import { ClientTypeEnumFromJSON, ClientTypeEnumToJSON } from "./ClientTypeEnum"; -import type { GrantTypesEnum } from "./GrantTypesEnum"; -import { GrantTypesEnumFromJSON, GrantTypesEnumToJSON } from "./GrantTypesEnum"; +import type { GrantTypeEnum } from "./GrantTypeEnum"; +import { GrantTypeEnumFromJSON, GrantTypeEnumToJSON } from "./GrantTypeEnum"; import type { IssuerModeEnum } from "./IssuerModeEnum"; import { IssuerModeEnumFromJSON, IssuerModeEnumToJSON } from "./IssuerModeEnum"; import type { OAuth2ProviderLogoutMethodEnum } from "./OAuth2ProviderLogoutMethodEnum"; @@ -126,10 +126,10 @@ export interface OAuth2Provider { clientType?: ClientTypeEnum; /** * - * @type {Array} + * @type {Array} * @memberof OAuth2Provider */ - grantTypes?: Array; + grantTypes?: Array; /** * * @type {string} @@ -344,7 +344,7 @@ export function OAuth2ProviderFromJSONTyped( grantTypes: json["grant_types"] == null ? undefined - : (json["grant_types"] as Array).map(GrantTypesEnumFromJSON), + : (json["grant_types"] as Array).map(GrantTypeEnumFromJSON), clientId: json["client_id"] == null ? undefined : json["client_id"], clientSecret: json["client_secret"] == null ? undefined : json["client_secret"], accessCodeValidity: @@ -420,7 +420,7 @@ export function OAuth2ProviderToJSONTyped( grant_types: value["grantTypes"] == null ? undefined - : (value["grantTypes"] as Array).map(GrantTypesEnumToJSON), + : (value["grantTypes"] as Array).map(GrantTypeEnumToJSON), 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 39a16cd71a..d2108c710c 100644 --- a/packages/client-ts/src/models/OAuth2ProviderRequest.ts +++ b/packages/client-ts/src/models/OAuth2ProviderRequest.ts @@ -14,8 +14,8 @@ import type { ClientTypeEnum } from "./ClientTypeEnum"; import { ClientTypeEnumFromJSON, ClientTypeEnumToJSON } from "./ClientTypeEnum"; -import type { GrantTypesEnum } from "./GrantTypesEnum"; -import { GrantTypesEnumFromJSON, GrantTypesEnumToJSON } from "./GrantTypesEnum"; +import type { GrantTypeEnum } from "./GrantTypeEnum"; +import { GrantTypeEnumFromJSON, GrantTypeEnumToJSON } from "./GrantTypeEnum"; import type { IssuerModeEnum } from "./IssuerModeEnum"; import { IssuerModeEnumFromJSON, IssuerModeEnumToJSON } from "./IssuerModeEnum"; import type { OAuth2ProviderLogoutMethodEnum } from "./OAuth2ProviderLogoutMethodEnum"; @@ -72,10 +72,10 @@ export interface OAuth2ProviderRequest { clientType?: ClientTypeEnum; /** * - * @type {Array} + * @type {Array} * @memberof OAuth2ProviderRequest */ - grantTypes?: Array; + grantTypes?: Array; /** * * @type {string} @@ -230,7 +230,7 @@ export function OAuth2ProviderRequestFromJSONTyped( grantTypes: json["grant_types"] == null ? undefined - : (json["grant_types"] as Array).map(GrantTypesEnumFromJSON), + : (json["grant_types"] as Array).map(GrantTypeEnumFromJSON), clientId: json["client_id"] == null ? undefined : json["client_id"], clientSecret: json["client_secret"] == null ? undefined : json["client_secret"], accessCodeValidity: @@ -295,7 +295,7 @@ export function OAuth2ProviderRequestToJSONTyped( grant_types: value["grantTypes"] == null ? undefined - : (value["grantTypes"] as Array).map(GrantTypesEnumToJSON), + : (value["grantTypes"] as Array).map(GrantTypeEnumToJSON), client_id: value["clientId"], client_secret: value["clientSecret"], access_code_validity: value["accessCodeValidity"], diff --git a/packages/client-ts/src/models/PaginatedOAuth2DynamicClientRegistrationList.ts b/packages/client-ts/src/models/PaginatedOAuth2DynamicClientRegistrationList.ts new file mode 100644 index 0000000000..36067049a7 --- /dev/null +++ b/packages/client-ts/src/models/PaginatedOAuth2DynamicClientRegistrationList.ts @@ -0,0 +1,100 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.8.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. + */ + +import type { OAuth2DynamicClientRegistration } from "./OAuth2DynamicClientRegistration"; +import { + OAuth2DynamicClientRegistrationFromJSON, + OAuth2DynamicClientRegistrationToJSON, +} from "./OAuth2DynamicClientRegistration"; +import type { Pagination } from "./Pagination"; +import { PaginationFromJSON, PaginationToJSON } from "./Pagination"; + +/** + * + * @export + * @interface PaginatedOAuth2DynamicClientRegistrationList + */ +export interface PaginatedOAuth2DynamicClientRegistrationList { + /** + * + * @type {Pagination} + * @memberof PaginatedOAuth2DynamicClientRegistrationList + */ + pagination: Pagination; + /** + * + * @type {Array} + * @memberof PaginatedOAuth2DynamicClientRegistrationList + */ + results: Array; + /** + * + * @type {{ [key: string]: any; }} + * @memberof PaginatedOAuth2DynamicClientRegistrationList + */ + autocomplete: { [key: string]: any }; +} + +/** + * Check if a given object implements the PaginatedOAuth2DynamicClientRegistrationList interface. + */ +export function instanceOfPaginatedOAuth2DynamicClientRegistrationList( + value: object, +): value is PaginatedOAuth2DynamicClientRegistrationList { + if (!("pagination" in value) || value["pagination"] === undefined) return false; + if (!("results" in value) || value["results"] === undefined) return false; + if (!("autocomplete" in value) || value["autocomplete"] === undefined) return false; + return true; +} + +export function PaginatedOAuth2DynamicClientRegistrationListFromJSON( + json: any, +): PaginatedOAuth2DynamicClientRegistrationList { + return PaginatedOAuth2DynamicClientRegistrationListFromJSONTyped(json, false); +} + +export function PaginatedOAuth2DynamicClientRegistrationListFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): PaginatedOAuth2DynamicClientRegistrationList { + if (json == null) { + return json; + } + return { + pagination: PaginationFromJSON(json["pagination"]), + results: (json["results"] as Array).map(OAuth2DynamicClientRegistrationFromJSON), + autocomplete: json["autocomplete"], + }; +} + +export function PaginatedOAuth2DynamicClientRegistrationListToJSON( + json: any, +): PaginatedOAuth2DynamicClientRegistrationList { + return PaginatedOAuth2DynamicClientRegistrationListToJSONTyped(json, false); +} + +export function PaginatedOAuth2DynamicClientRegistrationListToJSONTyped( + value?: PaginatedOAuth2DynamicClientRegistrationList | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return { + pagination: PaginationToJSON(value["pagination"]), + results: (value["results"] as Array).map(OAuth2DynamicClientRegistrationToJSON), + autocomplete: value["autocomplete"], + }; +} diff --git a/packages/client-ts/src/models/PatchedOAuth2DynamicClientRegistrationRequest.ts b/packages/client-ts/src/models/PatchedOAuth2DynamicClientRegistrationRequest.ts new file mode 100644 index 0000000000..f6096711c2 --- /dev/null +++ b/packages/client-ts/src/models/PatchedOAuth2DynamicClientRegistrationRequest.ts @@ -0,0 +1,169 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.8.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. + */ + +import type { GrantTypeEnum } from "./GrantTypeEnum"; +import { GrantTypeEnumFromJSON, GrantTypeEnumToJSON } from "./GrantTypeEnum"; +import type { PolicyEngineMode } from "./PolicyEngineMode"; +import { PolicyEngineModeFromJSON, PolicyEngineModeToJSON } from "./PolicyEngineMode"; + +/** + * Serializer for OAuth2DynamicClientRegistration + * @export + * @interface PatchedOAuth2DynamicClientRegistrationRequest + */ +export interface PatchedOAuth2DynamicClientRegistrationRequest { + /** + * + * @type {number} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + provider?: number; + /** + * Group to assign to automatically created applications. + * @type {string} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + defaultApplicationGroup?: string; + /** + * Authorization flow applied to dynamically registered clients. + * @type {string} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + overrideAuthorizationFlow?: string | null; + /** + * + * @type {string} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + overrideInvalidationFlow?: string | null; + /** + * Scope mappings applied to dynamically registered clients. + * @type {Array} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + overridePropertyMappings?: Array; + /** + * Maximum access token validity for registered clients (Format: hours=1;minutes=2;seconds=3). + * @type {string} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + accessTokenValidity?: string; + /** + * Maximum refresh token validity for registered clients (Format: hours=1;minutes=2;seconds=3). + * @type {string} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + refreshTokenValidity?: string; + /** + * If empty, all grant types are allowed. + * @type {Array} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + allowedGrantTypes?: Array; + /** + * + * @type {PolicyEngineMode} + * @memberof PatchedOAuth2DynamicClientRegistrationRequest + */ + policyEngineMode?: PolicyEngineMode; +} + +/** + * Check if a given object implements the PatchedOAuth2DynamicClientRegistrationRequest interface. + */ +export function instanceOfPatchedOAuth2DynamicClientRegistrationRequest( + value: object, +): value is PatchedOAuth2DynamicClientRegistrationRequest { + return true; +} + +export function PatchedOAuth2DynamicClientRegistrationRequestFromJSON( + json: any, +): PatchedOAuth2DynamicClientRegistrationRequest { + return PatchedOAuth2DynamicClientRegistrationRequestFromJSONTyped(json, false); +} + +export function PatchedOAuth2DynamicClientRegistrationRequestFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): PatchedOAuth2DynamicClientRegistrationRequest { + if (json == null) { + return json; + } + return { + provider: json["provider"] == null ? undefined : json["provider"], + defaultApplicationGroup: + json["default_application_group"] == null + ? undefined + : json["default_application_group"], + overrideAuthorizationFlow: + json["override_authorization_flow"] === undefined + ? undefined + : json["override_authorization_flow"] === null + ? null + : json["override_authorization_flow"], + overrideInvalidationFlow: + json["override_invalidation_flow"] === undefined + ? undefined + : json["override_invalidation_flow"] === null + ? null + : json["override_invalidation_flow"], + overridePropertyMappings: + json["override_property_mappings"] == null + ? undefined + : json["override_property_mappings"], + accessTokenValidity: + json["access_token_validity"] == null ? undefined : json["access_token_validity"], + refreshTokenValidity: + json["refresh_token_validity"] == null ? undefined : json["refresh_token_validity"], + allowedGrantTypes: + json["allowed_grant_types"] == null + ? undefined + : (json["allowed_grant_types"] as Array).map(GrantTypeEnumFromJSON), + policyEngineMode: + json["policy_engine_mode"] == null + ? undefined + : PolicyEngineModeFromJSON(json["policy_engine_mode"]), + }; +} + +export function PatchedOAuth2DynamicClientRegistrationRequestToJSON( + json: any, +): PatchedOAuth2DynamicClientRegistrationRequest { + return PatchedOAuth2DynamicClientRegistrationRequestToJSONTyped(json, false); +} + +export function PatchedOAuth2DynamicClientRegistrationRequestToJSONTyped( + value?: PatchedOAuth2DynamicClientRegistrationRequest | null, + ignoreDiscriminator: boolean = false, +): any { + if (value == null) { + return value; + } + + return { + provider: value["provider"], + default_application_group: value["defaultApplicationGroup"], + override_authorization_flow: value["overrideAuthorizationFlow"], + override_invalidation_flow: value["overrideInvalidationFlow"], + override_property_mappings: value["overridePropertyMappings"], + access_token_validity: value["accessTokenValidity"], + refresh_token_validity: value["refreshTokenValidity"], + allowed_grant_types: + value["allowedGrantTypes"] == null + ? undefined + : (value["allowedGrantTypes"] as Array).map(GrantTypeEnumToJSON), + policy_engine_mode: PolicyEngineModeToJSON(value["policyEngineMode"]), + }; +} diff --git a/packages/client-ts/src/models/PatchedOAuth2ProviderRequest.ts b/packages/client-ts/src/models/PatchedOAuth2ProviderRequest.ts index 2cd78091f0..c744dfbacf 100644 --- a/packages/client-ts/src/models/PatchedOAuth2ProviderRequest.ts +++ b/packages/client-ts/src/models/PatchedOAuth2ProviderRequest.ts @@ -14,8 +14,8 @@ import type { ClientTypeEnum } from "./ClientTypeEnum"; import { ClientTypeEnumFromJSON, ClientTypeEnumToJSON } from "./ClientTypeEnum"; -import type { GrantTypesEnum } from "./GrantTypesEnum"; -import { GrantTypesEnumFromJSON, GrantTypesEnumToJSON } from "./GrantTypesEnum"; +import type { GrantTypeEnum } from "./GrantTypeEnum"; +import { GrantTypeEnumFromJSON, GrantTypeEnumToJSON } from "./GrantTypeEnum"; import type { IssuerModeEnum } from "./IssuerModeEnum"; import { IssuerModeEnumFromJSON, IssuerModeEnumToJSON } from "./IssuerModeEnum"; import type { OAuth2ProviderLogoutMethodEnum } from "./OAuth2ProviderLogoutMethodEnum"; @@ -72,10 +72,10 @@ export interface PatchedOAuth2ProviderRequest { clientType?: ClientTypeEnum; /** * - * @type {Array} + * @type {Array} * @memberof PatchedOAuth2ProviderRequest */ - grantTypes?: Array; + grantTypes?: Array; /** * * @type {string} @@ -211,7 +211,7 @@ export function PatchedOAuth2ProviderRequestFromJSONTyped( grantTypes: json["grant_types"] == null ? undefined - : (json["grant_types"] as Array).map(GrantTypesEnumFromJSON), + : (json["grant_types"] as Array).map(GrantTypeEnumFromJSON), clientId: json["client_id"] == null ? undefined : json["client_id"], clientSecret: json["client_secret"] == null ? undefined : json["client_secret"], accessCodeValidity: @@ -279,7 +279,7 @@ export function PatchedOAuth2ProviderRequestToJSONTyped( grant_types: value["grantTypes"] == null ? undefined - : (value["grantTypes"] as Array).map(GrantTypesEnumToJSON), + : (value["grantTypes"] as Array).map(GrantTypeEnumToJSON), 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 7720d976e8..4fd40b03a4 100644 --- a/packages/client-ts/src/models/index.ts +++ b/packages/client-ts/src/models/index.ts @@ -225,7 +225,7 @@ export * from "./GoogleWorkspaceProviderUser"; export * from "./GoogleWorkspaceProviderUserRequest"; export * from "./GrantRequest"; export * from "./GrantRequestCreateRequest"; -export * from "./GrantTypesEnum"; +export * from "./GrantTypeEnum"; export * from "./Group"; export * from "./GroupKerberosSourceConnection"; export * from "./GroupKerberosSourceConnectionRequest"; @@ -335,6 +335,8 @@ export * from "./NotificationTransportRequest"; export * from "./NotificationTransportTest"; export * from "./NotificationWebhookMapping"; export * from "./NotificationWebhookMappingRequest"; +export * from "./OAuth2DynamicClientRegistration"; +export * from "./OAuth2DynamicClientRegistrationRequest"; export * from "./OAuth2Provider"; export * from "./OAuth2ProviderLogoutMethodEnum"; export * from "./OAuth2ProviderRequest"; @@ -445,6 +447,7 @@ export * from "./PaginatedNotificationList"; export * from "./PaginatedNotificationRuleList"; export * from "./PaginatedNotificationTransportList"; export * from "./PaginatedNotificationWebhookMappingList"; +export * from "./PaginatedOAuth2DynamicClientRegistrationList"; export * from "./PaginatedOAuth2ProviderList"; export * from "./PaginatedOAuthSourceList"; export * from "./PaginatedOAuthSourcePropertyMappingList"; @@ -608,6 +611,7 @@ export * from "./PatchedNotificationRequest"; export * from "./PatchedNotificationRuleRequest"; export * from "./PatchedNotificationTransportRequest"; export * from "./PatchedNotificationWebhookMappingRequest"; +export * from "./PatchedOAuth2DynamicClientRegistrationRequest"; export * from "./PatchedOAuth2ProviderRequest"; export * from "./PatchedOAuthSourcePropertyMappingRequest"; export * from "./PatchedOAuthSourceRequest"; diff --git a/schema.yml b/schema.yml index cc62058954..b004716b7d 100644 --- a/schema.yml +++ b/schema.yml @@ -18549,6 +18549,170 @@ paths: $ref: '#/components/responses/ValidationErrorResponse' '403': $ref: '#/components/responses/GenericErrorResponse' + /providers/oauth2-dcr/: + get: + operationId: providers_oauth2_dcr_list + description: OAuth2 Dynamic Client Registration configuration ViewSet + parameters: + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QueryPaginationPage' + - $ref: '#/components/parameters/QueryPaginationPageSize' + - in: query + name: provider + schema: + type: integer + - $ref: '#/components/parameters/QuerySearch' + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedOAuth2DynamicClientRegistrationList' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + post: + operationId: providers_oauth2_dcr_create + description: OAuth2 Dynamic Client Registration configuration ViewSet + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2DynamicClientRegistrationRequest' + required: true + security: + - authentik: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2DynamicClientRegistration' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + /providers/oauth2-dcr/{pbm_uuid}/: + get: + operationId: providers_oauth2_dcr_retrieve + description: OAuth2 Dynamic Client Registration configuration ViewSet + parameters: + - in: path + name: pbm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this OAuth2 Dynamic Client Registration. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2DynamicClientRegistration' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + put: + operationId: providers_oauth2_dcr_update + description: OAuth2 Dynamic Client Registration configuration ViewSet + parameters: + - in: path + name: pbm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this OAuth2 Dynamic Client Registration. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2DynamicClientRegistrationRequest' + required: true + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2DynamicClientRegistration' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + patch: + operationId: providers_oauth2_dcr_partial_update + description: OAuth2 Dynamic Client Registration configuration ViewSet + parameters: + - in: path + name: pbm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this OAuth2 Dynamic Client Registration. + required: true + tags: + - providers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOAuth2DynamicClientRegistrationRequest' + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth2DynamicClientRegistration' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' + delete: + operationId: providers_oauth2_dcr_destroy + description: OAuth2 Dynamic Client Registration configuration ViewSet + parameters: + - in: path + name: pbm_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this OAuth2 Dynamic Client Registration. + required: true + tags: + - providers + security: + - authentik: [] + responses: + '204': + description: No response body + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' /providers/oauth2/{id}/: get: operationId: providers_oauth2_retrieve @@ -35607,6 +35771,7 @@ components: - authentik.enterprise.policies.unique_password - authentik.enterprise.providers.google_workspace - authentik.enterprise.providers.microsoft_entra + - authentik.enterprise.providers.oauth2 - authentik.enterprise.providers.radius - authentik.enterprise.providers.scim - authentik.enterprise.providers.ssf @@ -41784,7 +41949,7 @@ components: Clamped to the granting rule binding(s)' expiry_granted_max. required: - pbms - GrantTypesEnum: + GrantTypeEnum: enum: - authorization_code - implicit @@ -44805,6 +44970,7 @@ components: - authentik_providers_ldap.ldapprovider - authentik_providers_oauth2.scopemapping - authentik_providers_oauth2.oauth2provider + - authentik_providers_oauth2.oauth2dynamicclientregistration - authentik_providers_proxy.proxyprovider - authentik_providers_rac.racprovider - authentik_providers_rac.endpoint @@ -45373,6 +45539,95 @@ components: required: - expression - name + OAuth2DynamicClientRegistration: + type: object + description: Serializer for OAuth2DynamicClientRegistration + properties: + pbm_uuid: + type: string + format: uuid + readOnly: true + provider: + type: integer + default_application_group: + type: string + description: Group to assign to automatically created applications. + override_authorization_flow: + type: string + format: uuid + nullable: true + description: Authorization flow applied to dynamically registered clients. + override_invalidation_flow: + type: string + format: uuid + nullable: true + override_property_mappings: + type: array + items: + type: string + format: uuid + description: Scope mappings applied to dynamically registered clients. + access_token_validity: + type: string + description: 'Maximum access token validity for registered clients (Format: + hours=1;minutes=2;seconds=3).' + refresh_token_validity: + type: string + description: 'Maximum refresh token validity for registered clients (Format: + hours=1;minutes=2;seconds=3).' + allowed_grant_types: + type: array + items: + $ref: '#/components/schemas/GrantTypeEnum' + description: If empty, all grant types are allowed. + policy_engine_mode: + $ref: '#/components/schemas/PolicyEngineMode' + required: + - pbm_uuid + - provider + OAuth2DynamicClientRegistrationRequest: + type: object + description: Serializer for OAuth2DynamicClientRegistration + properties: + provider: + type: integer + default_application_group: + type: string + description: Group to assign to automatically created applications. + override_authorization_flow: + type: string + format: uuid + nullable: true + description: Authorization flow applied to dynamically registered clients. + override_invalidation_flow: + type: string + format: uuid + nullable: true + override_property_mappings: + type: array + items: + type: string + format: uuid + description: Scope mappings applied to dynamically registered clients. + access_token_validity: + type: string + minLength: 1 + description: 'Maximum access token validity for registered clients (Format: + hours=1;minutes=2;seconds=3).' + refresh_token_validity: + type: string + minLength: 1 + description: 'Maximum refresh token validity for registered clients (Format: + hours=1;minutes=2;seconds=3).' + allowed_grant_types: + type: array + items: + $ref: '#/components/schemas/GrantTypeEnum' + description: If empty, all grant types are allowed. + policy_engine_mode: + $ref: '#/components/schemas/PolicyEngineMode' + required: + - provider OAuth2Provider: type: object description: OAuth2Provider Serializer @@ -45446,7 +45701,7 @@ components: grant_types: type: array items: - $ref: '#/components/schemas/GrantTypesEnum' + $ref: '#/components/schemas/GrantTypeEnum' client_id: type: string maxLength: 255 @@ -45571,7 +45826,7 @@ components: grant_types: type: array items: - $ref: '#/components/schemas/GrantTypesEnum' + $ref: '#/components/schemas/GrantTypeEnum' client_id: type: string minLength: 1 @@ -47693,6 +47948,21 @@ components: - autocomplete - pagination - results + PaginatedOAuth2DynamicClientRegistrationList: + type: object + properties: + pagination: + $ref: '#/components/schemas/Pagination' + results: + type: array + items: + $ref: '#/components/schemas/OAuth2DynamicClientRegistration' + autocomplete: + $ref: '#/components/schemas/Autocomplete' + required: + - autocomplete + - pagination + - results PaginatedOAuth2ProviderList: type: object properties: @@ -51375,6 +51645,47 @@ components: expression: type: string minLength: 1 + PatchedOAuth2DynamicClientRegistrationRequest: + type: object + description: Serializer for OAuth2DynamicClientRegistration + properties: + provider: + type: integer + default_application_group: + type: string + description: Group to assign to automatically created applications. + override_authorization_flow: + type: string + format: uuid + nullable: true + description: Authorization flow applied to dynamically registered clients. + override_invalidation_flow: + type: string + format: uuid + nullable: true + override_property_mappings: + type: array + items: + type: string + format: uuid + description: Scope mappings applied to dynamically registered clients. + access_token_validity: + type: string + minLength: 1 + description: 'Maximum access token validity for registered clients (Format: + hours=1;minutes=2;seconds=3).' + refresh_token_validity: + type: string + minLength: 1 + description: 'Maximum refresh token validity for registered clients (Format: + hours=1;minutes=2;seconds=3).' + allowed_grant_types: + type: array + items: + $ref: '#/components/schemas/GrantTypeEnum' + description: If empty, all grant types are allowed. + policy_engine_mode: + $ref: '#/components/schemas/PolicyEngineMode' PatchedOAuth2ProviderRequest: type: object description: OAuth2Provider Serializer @@ -51409,7 +51720,7 @@ components: grant_types: type: array items: - $ref: '#/components/schemas/GrantTypesEnum' + $ref: '#/components/schemas/GrantTypeEnum' client_id: type: string minLength: 1 diff --git a/web/src/admin/providers/oauth2/OAuth2DCRForm.ts b/web/src/admin/providers/oauth2/OAuth2DCRForm.ts new file mode 100644 index 0000000000..893ed7e6a5 --- /dev/null +++ b/web/src/admin/providers/oauth2/OAuth2DCRForm.ts @@ -0,0 +1,68 @@ +import { renderForm } from "./OAuth2DCRFormForm.js"; + +import { aki } from "#common/api/client"; + +import { ModelForm } from "#elements/forms/ModelForm"; +import { SlottedTemplateResult } from "#elements/types"; + +import { OAuth2DynamicClientRegistration, ProvidersApi } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { customElement, property } from "lit/decorators.js"; + +/** + * Form page for OAuth2 Dynamic Client Registration configuration + * + * @element ak-provider-oauth2-dcr-form + * + */ +@customElement("ak-provider-oauth2-dcr-form") +export class OAuth2DCRForm extends ModelForm { + public static override verboseName = msg("Dynamic Client Registration"); + public static override verboseNamePlural = msg("Dynamic Client Registration"); + public static override createLabel = msg("Create"); + + /** + * The provider this configuration is (or will be) attached to. + * Only used when creating a new configuration. + */ + @property({ type: Number }) + public providerID: number | null = null; + + public override getSuccessMessage(): string { + return this.instance + ? msg("Successfully updated Dynamic Client Registration.") + : msg("Successfully enabled Dynamic Client Registration."); + } + + protected override loadInstance(pk: string): Promise { + return aki(ProvidersApi).providersOauth2DcrRetrieve({ + pbmUuid: pk, + }); + } + + public override async send( + data: OAuth2DynamicClientRegistration, + ): Promise { + if (this.instance) { + return aki(ProvidersApi).providersOauth2DcrUpdate({ + pbmUuid: this.instance.pbmUuid, + oAuth2DynamicClientRegistrationRequest: data, + }); + } + data.provider = this.providerID || 0; + return aki(ProvidersApi).providersOauth2DcrCreate({ + oAuth2DynamicClientRegistrationRequest: data, + }); + } + + protected override renderForm(): SlottedTemplateResult { + return renderForm({ dcr: this.instance }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-provider-oauth2-dcr-form": OAuth2DCRForm; + } +} diff --git a/web/src/admin/providers/oauth2/OAuth2DCRFormForm.ts b/web/src/admin/providers/oauth2/OAuth2DCRFormForm.ts new file mode 100644 index 0000000000..c9a14450ab --- /dev/null +++ b/web/src/admin/providers/oauth2/OAuth2DCRFormForm.ts @@ -0,0 +1,131 @@ +import "#admin/common/ak-flow-search/ak-flow-search"; +import "#components/ak-switch-input"; +import "#components/ak-text-input"; +import "#elements/LicenseNotice"; +import "#elements/ak-checkbox-group/ak-checkbox-group"; +import "#elements/ak-dual-select/ak-dual-select-dynamic-selected-provider"; +import "#elements/forms/FormGroup"; +import "#elements/forms/HorizontalFormElement"; +import "#elements/forms/Radio"; +import "#elements/utils/TimeDeltaHelp"; +import "#components/ak-radio-input"; + +import { propertyMappingsProvider, propertyMappingsSelector } from "./OAuth2ProviderFormHelpers.js"; + +import { policyEngineModes } from "#admin/policies/PolicyEngineModes"; +import { GrantTypeCheckboxItems } from "#admin/providers/oauth2/labels"; + +import { FlowDesignationEnum, OAuth2DynamicClientRegistration } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +export interface OAuth2DCRFormProps { + dcr?: Partial | null; +} + +export function renderForm({ dcr }: OAuth2DCRFormProps) { + dcr ||= {}; + return html` + + + +

+ ${msg( + "Authorization flow applied to dynamically registered clients. When not selected, authorization flow of the parent provider is used.", + )} +

+
+ + +

+ ${msg( + "Invalidation flow applied to dynamically registered clients. When not selected, authorization flow of the parent provider is used.", + )} +

+
+ + +

+ ${msg( + "Scope mappings applied to dynamically registered clients. When not selected, authorization flow of the parent provider is used.", + )} +

+
+ +
+ + ${msg("Maximum access token validity for registered clients.")} +

+ `} + >
+ + ${msg("Maximum refresh token validity for registered clients.")} +

+ `} + >
+ + +

+ ${msg("If none are selected, all grant types are allowed.")} +

+
+ +
+
`; +} diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts index 23dab71f74..58458fca1d 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts @@ -30,7 +30,7 @@ import { AKLabel } from "#components/ak-label"; import { ClientTypeEnum, FlowDesignationEnum, - GrantTypesEnum, + GrantTypeEnum, IssuerModeEnum, MatchingModeEnum, OAuth2Provider, @@ -134,25 +134,25 @@ 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")], - [GrantTypesEnum.UrnIetfParamsOauthGrantTypeTokenExchange, msg("Token exchange")], + [GrantTypeEnum.AuthorizationCode, msg("Authorization Code")], + [GrantTypeEnum.Implicit, msg("Implicit")], + [GrantTypeEnum.Hybrid, msg("Hybrid")], + [GrantTypeEnum.RefreshToken, msg("Refresh token")], + [GrantTypeEnum.ClientCredentials, msg("Client credentials")], + [GrantTypeEnum.Password, msg("Password")], + [GrantTypeEnum.UrnIetfParamsOauthGrantTypeDeviceCode, msg("Device-code")], + [GrantTypeEnum.UrnIetfParamsOauthGrantTypeTokenExchange, msg("Token exchange")], ]; const defaultGrantTypes = [ // TODO: Clean up defaults after 2026 - GrantTypesEnum.AuthorizationCode, - GrantTypesEnum.Implicit, - GrantTypesEnum.Hybrid, - GrantTypesEnum.RefreshToken, - GrantTypesEnum.ClientCredentials, - GrantTypesEnum.Password, - GrantTypesEnum.UrnIetfParamsOauthGrantTypeDeviceCode, + GrantTypeEnum.AuthorizationCode, + GrantTypeEnum.Implicit, + GrantTypeEnum.Hybrid, + GrantTypeEnum.RefreshToken, + GrantTypeEnum.ClientCredentials, + GrantTypeEnum.Password, + GrantTypeEnum.UrnIetfParamsOauthGrantTypeDeviceCode, ]; type ShowClientSecret = (show: boolean) => void; diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts b/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts index b85ee39ea3..5cf230968d 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderViewPage.ts @@ -13,6 +13,7 @@ import "#elements/ak-mdx/index"; import "#elements/buttons/ModalButton"; import "#elements/buttons/SpinnerButton/index"; import "#elements/Divider"; +import "#admin/policies/BoundPoliciesList"; import { aki } from "#common/api/client"; import { EVENT_REFRESH } from "#common/constants"; @@ -24,6 +25,7 @@ import { SlottedTemplateResult } from "#elements/types"; import renderDescriptionList from "#components/DescriptionList"; import { taskCard } from "#components/tasks/taskCard"; +import { OAuth2DCRForm } from "#admin/providers/oauth2/OAuth2DCRForm"; import { OAuth2ProviderFormPage } from "#admin/providers/oauth2/OAuth2ProviderForm"; import { @@ -31,6 +33,7 @@ import { CoreApi, CoreUsersListRequest, ModelEnum, + OAuth2DynamicClientRegistration, OAuth2Provider, OAuth2ProviderLogoutMethodEnum, OAuth2ProviderSetupURLs, @@ -101,6 +104,9 @@ export class OAuth2ProviderViewPage extends AKElement { @state() previewUser?: User; + @state() + dcrConfig?: OAuth2DynamicClientRegistration | null; + static styles: CSSResult[] = [ PFButton, PFPage, @@ -130,6 +136,19 @@ export class OAuth2ProviderViewPage extends AKElement { .then((preview) => (this.preview = preview)); } + fetchDCRConfig(): void { + aki(ProvidersApi) + .providersOauth2DcrList({ + provider: this.provider?.pk, + }) + .then((response) => { + this.dcrConfig = response.results[0] ?? null; + }) + .catch(() => { + this.dcrConfig = null; + }); + } + render(): SlottedTemplateResult { if (!this.provider) { return nothing; @@ -166,6 +185,18 @@ export class OAuth2ProviderViewPage extends AKElement { > ${this.renderTabPreview()} +
{ + this.fetchDCRConfig(); + }} + > + ${this.renderTabDCR()} +
`; } + + renderTabDCR(): SlottedTemplateResult { + if (this.dcrConfig === undefined) { + return html``; + } + if (this.dcrConfig === null) { + return html`
+
+
+ + ${msg("Dynamic Client Registration is not enabled.")} +

+ ${msg( + "Allow OAuth2/OIDC clients to register themselves against this provider (RFC 7591).", + )} +

+
+ +
+
+
+
+
`; + } + const dcr = this.dcrConfig; + return html`
+
+
${msg("Dynamic Client Registration")}
+
+ ${renderDescriptionList([ + [ + msg("Default application group"), + html`${dcr.defaultApplicationGroup !== "" + ? dcr.defaultApplicationGroup + : "-"}`, + ], + [ + msg("Allowed grant types"), + html`${(dcr.allowedGrantTypes || []).length > 0 + ? dcr.allowedGrantTypes?.join(", ") + : msg("All")}`, + ], + [ + msg("Related actions"), + html``, + ], + ])} +
+
+
+
${msg("Dynamic application policies")}
+ + + ${msg( + "Bindings configured here will be copied to dynamically registered applications. If no bindings are created, bindings of this providers' application are copied.", + )} + + +
+
`; + } } declare global { diff --git a/web/src/admin/providers/oauth2/labels.ts b/web/src/admin/providers/oauth2/labels.ts new file mode 100644 index 0000000000..960b7f2e49 --- /dev/null +++ b/web/src/admin/providers/oauth2/labels.ts @@ -0,0 +1,33 @@ +import { MessageFormatter } from "#common/ui/locale/format"; + +import { CheckboxItem } from "#elements/ak-checkbox-group/ak-checkbox-group"; + +import { GrantTypeEnum } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; + +export const GrantTypeLabelRecord: Record> = { + [GrantTypeEnum.AuthorizationCode]: () => msg("Authorization Code"), + [GrantTypeEnum.Implicit]: () => msg("Implicit"), + [GrantTypeEnum.Hybrid]: () => msg("Hybrid"), + [GrantTypeEnum.RefreshToken]: () => msg("Refresh token"), + [GrantTypeEnum.ClientCredentials]: () => msg("Client credentials"), + [GrantTypeEnum.Password]: () => msg("Password"), + [GrantTypeEnum.UrnIetfParamsOauthGrantTypeDeviceCode]: () => msg("Device-code"), + [GrantTypeEnum.UrnIetfParamsOauthGrantTypeTokenExchange]: () => msg("Token exchange"), + [GrantTypeEnum.UnknownDefaultOpenApi]: () => msg("Unknown Grant type"), +}; + +export const GrantTypeCheckboxItems: CheckboxItem[] = [ + GrantTypeEnum.AuthorizationCode, + GrantTypeEnum.Implicit, + GrantTypeEnum.Hybrid, + GrantTypeEnum.RefreshToken, + GrantTypeEnum.ClientCredentials, + GrantTypeEnum.Password, + GrantTypeEnum.UrnIetfParamsOauthGrantTypeDeviceCode, + GrantTypeEnum.UrnIetfParamsOauthGrantTypeTokenExchange, +].map((grantType) => ({ + name: grantType, + label: GrantTypeLabelRecord[grantType](), +})); diff --git a/web/src/elements/ak-checkbox-group/ak-checkbox-group.ts b/web/src/elements/ak-checkbox-group/ak-checkbox-group.ts index 7e3d79cb5c..04bb2c21e5 100644 --- a/web/src/elements/ak-checkbox-group/ak-checkbox-group.ts +++ b/web/src/elements/ak-checkbox-group/ak-checkbox-group.ts @@ -12,11 +12,12 @@ import { map } from "lit/directives/map.js"; import PFCheck from "@patternfly/patternfly/components/Check/check.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css"; -type CheckboxKv = { name: string; label: string | TemplateResult }; -type CheckboxPr = [string, string | TemplateResult]; -export type CheckboxPair = CheckboxKv | CheckboxPr; +export type CheckboxItem = { name: T; label: string | TemplateResult }; +export type CheckboxPair = [name: T, label: string | TemplateResult]; -function* kvToPairs(items: Iterable): Iterable { +export type CheckboxItemInit = CheckboxItem | CheckboxPair; + +function* generateCheckboxKeyValuePairs(items: Iterable): Iterable { for (const item of items) { yield Array.isArray(item) ? item : [item.name, item.label]; } @@ -84,7 +85,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents { } @property({ type: Array }) - public options: CheckboxPair[] = []; + public options: CheckboxItemInit[] = []; @property({ type: Array }) public value: string[] = []; @@ -180,7 +181,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents { }); } - protected renderCheckbox = ([name, label]: CheckboxPr): SlottedTemplateResult => { + protected renderCheckbox = ([name, label]: CheckboxPair): SlottedTemplateResult => { const selected = this.values.includes(name); const blockFwd = (e: Event) => { e.stopImmediatePropagation(); @@ -208,7 +209,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents { protected override render(): SlottedTemplateResult { return html`
- ${map(kvToPairs(this.options), this.renderCheckbox)} + ${map(generateCheckboxKeyValuePairs(this.options), this.renderCheckbox)}
`; } }