providers/oauth2: Configure allowed grant types (#20363)

* naming cleanup

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* adjust defaults, start adding tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* more tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* gen

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix proxy

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add UI

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* attempt to fix e2e

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* allow refresh token for conformance

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix e2e

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

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