mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
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:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Generated
+1
@@ -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
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user