mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
enterprise/providers/scim: Add SCIM OAuth support (#16903)
* sources/oauth: add expires field to user source connection Signed-off-by: Jens Langhammer <jens@goauthentik.io> * providers/scim: add support for other auth methods Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rest of the owl Signed-off-by: Jens Langhammer <jens@goauthentik.io> * allow specifying any params Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add UI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * delete user when token Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests and fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * sigh Signed-off-by: Jens Langhammer <jens@goauthentik.io> * gen Signed-off-by: Jens Langhammer <jens@goauthentik.io> * better API validation Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix sentry Signed-off-by: Jens Langhammer <jens@goauthentik.io> * one more test and fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@@ -193,6 +193,7 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
|
||||
--git-repo-id authentik \
|
||||
--git-user-id goauthentik
|
||||
|
||||
cd ${PWD}/${GEN_API_TS} && npm i
|
||||
cd ${PWD}/${GEN_API_TS} && npm link
|
||||
cd ${PWD}/web && npm link @goauthentik/api
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.providers.scim.models import SCIMAuthenticationMode
|
||||
|
||||
|
||||
class SCIMProviderSerializerMixin:
|
||||
|
||||
def validate_auth_mode(self, auth_mode: SCIMAuthenticationMode) -> SCIMAuthenticationMode:
|
||||
if auth_mode == SCIMAuthenticationMode.OAUTH:
|
||||
if not LicenseKey.cached_summary().status.is_valid:
|
||||
raise ValidationError(_("Enterprise is required to use the OAuth mode."))
|
||||
return auth_mode
|
||||
@@ -0,0 +1,9 @@
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class AuthentikEnterpriseProviderSCIMConfig(EnterpriseConfig):
|
||||
|
||||
name = "authentik.enterprise.providers.scim"
|
||||
label = "authentik_enterprise_providers_scim"
|
||||
verbose_name = "authentik Enterprise.Providers.SCIM"
|
||||
default = True
|
||||
@@ -0,0 +1,80 @@
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.utils.timezone import now
|
||||
from requests import Request, RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.providers.scim.clients.exceptions import SCIMRequestException
|
||||
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
|
||||
|
||||
class SCIMOAuthException(SCIMRequestException):
|
||||
"""Exceptions related to OAuth operations for SCIM requests"""
|
||||
|
||||
|
||||
class SCIMOAuthAuth:
|
||||
|
||||
def __init__(self, provider: "SCIMProvider"):
|
||||
self.provider = provider
|
||||
self.user = provider.auth_oauth_user
|
||||
self.connection = self.get_connection()
|
||||
self.logger = get_logger().bind()
|
||||
|
||||
def retrieve_token(self):
|
||||
if not self.provider.auth_oauth:
|
||||
return None
|
||||
source: OAuthSource = self.provider.auth_oauth
|
||||
client = OAuth2Client(source, None)
|
||||
access_token_url = source.source_type.access_token_url or ""
|
||||
if source.source_type.urls_customizable and source.access_token_url:
|
||||
access_token_url = source.access_token_url
|
||||
data = client.get_access_token_args(None, None)
|
||||
data["grant_type"] = "password"
|
||||
data.update(self.provider.auth_oauth_params)
|
||||
try:
|
||||
response = client.do_request(
|
||||
"POST",
|
||||
access_token_url,
|
||||
auth=client.get_access_token_auth(),
|
||||
data=data,
|
||||
headers=client._default_headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
body = response.json()
|
||||
if "error" in body:
|
||||
self.logger.info("Failed to get new OAuth token", error=body["error"])
|
||||
raise SCIMOAuthException(response, body["error"])
|
||||
return body
|
||||
except RequestException as exc:
|
||||
raise SCIMOAuthException(exc.response, message="Failed to get OAuth token") from exc
|
||||
|
||||
def get_connection(self):
|
||||
token = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.provider.auth_oauth, user=self.user, expires__gt=now()
|
||||
).first()
|
||||
if token and token.access_token:
|
||||
return token
|
||||
token = self.retrieve_token()
|
||||
access_token = token["access_token"]
|
||||
expires_in = int(token.get("expires_in", 0))
|
||||
token, _ = UserOAuthSourceConnection.objects.update_or_create(
|
||||
source=self.provider.auth_oauth,
|
||||
user=self.user,
|
||||
defaults={
|
||||
"access_token": access_token,
|
||||
"expires": now() + timedelta(seconds=expires_in),
|
||||
},
|
||||
)
|
||||
return token
|
||||
|
||||
def __call__(self, request: Request) -> Request:
|
||||
if not self.connection.is_valid:
|
||||
self.logger.info("OAuth token expired, renewing token")
|
||||
self.connection = self.get_connection()
|
||||
request.headers["Authorization"] = f"Bearer {self.connection.access_token}"
|
||||
return request
|
||||
@@ -0,0 +1,30 @@
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, User, UserTypes
|
||||
from authentik.events.middleware import audit_ignore
|
||||
from authentik.providers.scim.models import SCIMAuthenticationMode, SCIMProvider
|
||||
|
||||
USER_PATH_PROVIDERS_SCIM = USER_PATH_SYSTEM_PREFIX + "/providers/scim"
|
||||
|
||||
|
||||
@receiver(post_save, sender=SCIMProvider)
|
||||
def scim_provider_post_save(sender: type[Model], instance: SCIMProvider, created: bool, **__):
|
||||
"""Create service account before provider is saved"""
|
||||
identifier = f"ak-providers-scim-{instance.pk}"
|
||||
with audit_ignore():
|
||||
if instance.auth_mode == SCIMAuthenticationMode.OAUTH:
|
||||
user, user_created = User.objects.update_or_create(
|
||||
username=identifier,
|
||||
defaults={
|
||||
"name": f"SCIM Provider {instance.name} Service-Account",
|
||||
"type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||
"path": USER_PATH_PROVIDERS_SCIM,
|
||||
},
|
||||
)
|
||||
if created or user_created:
|
||||
instance.auth_oauth_user = user
|
||||
instance.save()
|
||||
elif instance.auth_mode == SCIMAuthenticationMode.TOKEN:
|
||||
User.objects.filter(username=identifier).delete()
|
||||
@@ -0,0 +1,189 @@
|
||||
"""SCIM OAuth tests"""
|
||||
|
||||
from base64 import b64encode
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from requests_mock import Mocker
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, Group, User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
from authentik.enterprise.models import License
|
||||
from authentik.enterprise.tests.test_license import expiry_valid
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.scim.models import SCIMAuthenticationMode, SCIMMapping, SCIMProvider
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
class SCIMOAuthTests(APITestCase):
|
||||
"""SCIM User tests"""
|
||||
|
||||
@apply_blueprint("system/providers-scim.yaml")
|
||||
def setUp(self) -> None:
|
||||
# Delete all users and groups as the mocked HTTP responses only return one ID
|
||||
# which will cause errors with multiple users
|
||||
Tenant.objects.update(avatars="none")
|
||||
User.objects.all().exclude_anonymous().delete()
|
||||
Group.objects.all().delete()
|
||||
self.source = OAuthSource.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
access_token_url="http://localhost/token", # nosec
|
||||
consumer_key=generate_id(),
|
||||
consumer_secret=generate_id(),
|
||||
provider_type="openidconnect",
|
||||
)
|
||||
self.provider = SCIMProvider.objects.create(
|
||||
name=generate_id(),
|
||||
url="https://localhost",
|
||||
auth_mode=SCIMAuthenticationMode.OAUTH,
|
||||
auth_oauth=self.source,
|
||||
auth_oauth_params={
|
||||
"foo": "bar",
|
||||
},
|
||||
exclude_users_service_account=True,
|
||||
)
|
||||
self.app: Application = Application.objects.create(
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
self.provider.property_mappings.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
|
||||
)
|
||||
self.provider.property_mappings_group.add(
|
||||
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
|
||||
)
|
||||
|
||||
def test_retrieve_token(self):
|
||||
"""Test token retrieval"""
|
||||
with Mocker() as mocker:
|
||||
token = generate_id()
|
||||
mocker.post("http://localhost/token", json={"access_token": token, "expires_in": 3600})
|
||||
self.provider.scim_auth()
|
||||
conn = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.source,
|
||||
user=self.provider.auth_oauth_user,
|
||||
).first()
|
||||
self.assertIsNotNone(conn)
|
||||
self.assertTrue(conn.is_valid)
|
||||
auth = (
|
||||
b64encode(
|
||||
b":".join((self.source.consumer_key.encode(), self.source.consumer_secret.encode()))
|
||||
)
|
||||
.strip()
|
||||
.decode()
|
||||
)
|
||||
self.assertEqual(
|
||||
mocker.request_history[0].headers["Authorization"],
|
||||
f"Basic {auth}",
|
||||
)
|
||||
self.assertEqual(mocker.request_history[0].body, "grant_type=password&foo=bar")
|
||||
|
||||
def test_existing_token(self):
|
||||
"""Test existing token"""
|
||||
UserOAuthSourceConnection.objects.create(
|
||||
source=self.source,
|
||||
user=self.provider.auth_oauth_user,
|
||||
access_token=generate_id(),
|
||||
expires=now() + timedelta(hours=3),
|
||||
)
|
||||
with Mocker() as mocker:
|
||||
self.provider.scim_auth()
|
||||
self.assertEqual(len(mocker.request_history), 0)
|
||||
|
||||
@Mocker()
|
||||
def test_user_create(self, mock: Mocker):
|
||||
"""Test user creation"""
|
||||
scim_id = generate_id()
|
||||
token = generate_id()
|
||||
mock.post("http://localhost/token", json={"access_token": token, "expires_in": 3600})
|
||||
mock.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json={},
|
||||
)
|
||||
mock.post(
|
||||
"https://localhost/Users",
|
||||
json={
|
||||
"id": scim_id,
|
||||
},
|
||||
)
|
||||
uid = generate_id()
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.assertEqual(mock.call_count, 3)
|
||||
self.assertEqual(mock.request_history[1].method, "GET")
|
||||
self.assertEqual(mock.request_history[2].method, "POST")
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[2].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
"primary": True,
|
||||
"type": "other",
|
||||
"value": f"{uid}@goauthentik.io",
|
||||
}
|
||||
],
|
||||
"externalId": user.uid,
|
||||
"name": {
|
||||
"familyName": uid,
|
||||
"formatted": f"{uid} {uid}",
|
||||
"givenName": uid,
|
||||
},
|
||||
"displayName": f"{uid} {uid}",
|
||||
"userName": uid,
|
||||
},
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.enterprise.license.LicenseKey.validate",
|
||||
MagicMock(
|
||||
return_value=LicenseKey(
|
||||
aud="",
|
||||
exp=expiry_valid,
|
||||
name=generate_id(),
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_api_create(self):
|
||||
License.objects.create(key=generate_id())
|
||||
self.client.force_login(create_test_admin_user())
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:scimprovider-list"),
|
||||
{
|
||||
"name": generate_id(),
|
||||
"url": "http://localhost",
|
||||
"auth_mode": "oauth",
|
||||
"auth_oauth": str(self.source.pk),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_api_create_no_license(self):
|
||||
self.client.force_login(create_test_admin_user())
|
||||
res = self.client.post(
|
||||
reverse("authentik_api:scimprovider-list"),
|
||||
{
|
||||
"name": generate_id(),
|
||||
"url": "http://localhost",
|
||||
"auth_mode": "oauth",
|
||||
"auth_oauth": str(self.source.pk),
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
self.assertJSONEqual(
|
||||
res.content, {"auth_mode": ["Enterprise is required to use the OAuth mode."]}
|
||||
)
|
||||
@@ -5,6 +5,7 @@ TENANT_APPS = [
|
||||
"authentik.enterprise.policies.unique_password",
|
||||
"authentik.enterprise.providers.google_workspace",
|
||||
"authentik.enterprise.providers.microsoft_entra",
|
||||
"authentik.enterprise.providers.scim",
|
||||
"authentik.enterprise.providers.ssf",
|
||||
"authentik.enterprise.search",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
@@ -62,3 +63,13 @@ def get_env() -> str:
|
||||
if "AK_APPLIANCE" in os.environ:
|
||||
return os.environ["AK_APPLIANCE"]
|
||||
return "custom"
|
||||
|
||||
|
||||
def ConditionalInheritance(path: str):
|
||||
"""Conditionally inherit from a class, intended for things like authentik.enterprise,
|
||||
without which authentik should still be able to run"""
|
||||
try:
|
||||
cls = import_string(path)
|
||||
return cls
|
||||
except ModuleNotFoundError:
|
||||
return object
|
||||
|
||||
@@ -5,11 +5,15 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin
|
||||
from authentik.lib.utils.reflection import ConditionalInheritance
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
from authentik.providers.scim.tasks import scim_sync, scim_sync_objects
|
||||
|
||||
|
||||
class SCIMProviderSerializer(ProviderSerializer):
|
||||
class SCIMProviderSerializer(
|
||||
ConditionalInheritance("authentik.enterprise.providers.scim.api.SCIMProviderSerializerMixin"),
|
||||
ProviderSerializer,
|
||||
):
|
||||
"""SCIMProvider Serializer"""
|
||||
|
||||
class Meta:
|
||||
@@ -28,6 +32,9 @@ class SCIMProviderSerializer(ProviderSerializer):
|
||||
"url",
|
||||
"verify_certificates",
|
||||
"token",
|
||||
"auth_mode",
|
||||
"auth_oauth",
|
||||
"auth_oauth_params",
|
||||
"compatibility_mode",
|
||||
"exclude_users_service_account",
|
||||
"filter_group",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from requests import Request
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.providers.scim.models import SCIMProvider
|
||||
|
||||
|
||||
class SCIMTokenAuth:
|
||||
|
||||
def __init__(self, provider: "SCIMProvider"):
|
||||
self.provider = provider
|
||||
|
||||
def __call__(self, request: Request) -> Request:
|
||||
request.headers["Authorization"] = f"Bearer {self.provider.token}"
|
||||
return request
|
||||
@@ -35,7 +35,6 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
||||
"""SCIM Client"""
|
||||
|
||||
base_url: str
|
||||
token: str
|
||||
|
||||
_session: Session
|
||||
_config: ServiceProviderConfiguration
|
||||
@@ -45,12 +44,12 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
||||
self._session = get_http_session()
|
||||
self._session.verify = provider.verify_certificates
|
||||
self.provider = provider
|
||||
self.auth = provider.scim_auth()
|
||||
# Remove trailing slashes as we assume the URL doesn't have any
|
||||
base_url = provider.url
|
||||
if base_url.endswith("/"):
|
||||
base_url = base_url[:-1]
|
||||
self.base_url = base_url
|
||||
self.token = provider.token
|
||||
self._config = self.get_service_provider_config()
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs) -> dict:
|
||||
@@ -62,8 +61,8 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
|
||||
method,
|
||||
f"{self.base_url}{path}",
|
||||
**kwargs,
|
||||
auth=self.auth,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Accept": "application/scim+json",
|
||||
"Content-Type": "application/scim+json",
|
||||
},
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Generated by Django 5.1.12 on 2025-09-23 12:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_scim", "0013_scimprovidergroup_attributes_and_more"),
|
||||
("authentik_sources_oauth", "0011_useroauthsourceconnection_expires"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scimprovider",
|
||||
name="auth_mode",
|
||||
field=models.TextField(
|
||||
choices=[("token", "Token"), ("oauth", "OAuth")], default="token"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scimprovider",
|
||||
name="auth_oauth",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="OAuth Source used for authentication",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_sources_oauth.oauthsource",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scimprovider",
|
||||
name="auth_oauth_params",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
help_text="Additional OAuth parameters, such as grant_type",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="scimprovider",
|
||||
name="auth_oauth_user",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="scimprovider",
|
||||
name="token",
|
||||
field=models.TextField(blank=True, help_text="Authentication token"),
|
||||
),
|
||||
]
|
||||
@@ -8,12 +8,17 @@ from django.db.models import QuerySet
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dramatiq.actor import Actor
|
||||
from requests.auth import AuthBase
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
|
||||
from authentik.providers.scim.clients.auth import SCIMTokenAuth
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class SCIMProviderUser(SerializerModel):
|
||||
@@ -60,6 +65,13 @@ class SCIMProviderGroup(SerializerModel):
|
||||
return f"SCIM Provider Group {self.group_id} to {self.provider_id}"
|
||||
|
||||
|
||||
class SCIMAuthenticationMode(models.TextChoices):
|
||||
"""SCIM authentication modes"""
|
||||
|
||||
TOKEN = "token", _("Token")
|
||||
OAUTH = "oauth", _("OAuth")
|
||||
|
||||
|
||||
class SCIMCompatibilityMode(models.TextChoices):
|
||||
"""SCIM compatibility mode"""
|
||||
|
||||
@@ -78,7 +90,26 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
)
|
||||
|
||||
url = models.TextField(help_text=_("Base URL to SCIM requests, usually ends in /v2"))
|
||||
token = models.TextField(help_text=_("Authentication token"))
|
||||
|
||||
auth_mode = models.TextField(
|
||||
choices=SCIMAuthenticationMode.choices, default=SCIMAuthenticationMode.TOKEN
|
||||
)
|
||||
|
||||
token = models.TextField(help_text=_("Authentication token"), blank=True)
|
||||
auth_oauth = models.ForeignKey(
|
||||
"authentik_sources_oauth.OAuthSource",
|
||||
on_delete=models.SET_DEFAULT,
|
||||
default=None,
|
||||
null=True,
|
||||
help_text=_("OAuth Source used for authentication"),
|
||||
)
|
||||
auth_oauth_params = models.JSONField(
|
||||
blank=True, default=dict, help_text=_("Additional OAuth parameters, such as grant_type")
|
||||
)
|
||||
auth_oauth_user = models.ForeignKey(
|
||||
"authentik_core.User", on_delete=models.CASCADE, default=None, null=True
|
||||
)
|
||||
|
||||
verify_certificates = models.BooleanField(default=True)
|
||||
|
||||
property_mappings_group = models.ManyToManyField(
|
||||
@@ -96,6 +127,16 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
help_text=_("Alter authentik behavior for vendor-specific SCIM implementations."),
|
||||
)
|
||||
|
||||
def scim_auth(self) -> AuthBase:
|
||||
if self.auth_mode == SCIMAuthenticationMode.OAUTH:
|
||||
try:
|
||||
from authentik.enterprise.providers.scim.auth_oauth2 import SCIMOAuthAuth
|
||||
|
||||
return SCIMOAuthAuth(self)
|
||||
except ImportError:
|
||||
LOGGER.warning("Failed to import SCIM OAuth Client")
|
||||
return SCIMTokenAuth(self)
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/scim.png")
|
||||
|
||||
@@ -175,6 +175,7 @@ SPECTACULAR_SETTINGS = {
|
||||
"SAMLNameIDPolicyEnum": "authentik.sources.saml.models.SAMLNameIDPolicy",
|
||||
"UserTypeEnum": "authentik.core.models.UserTypes",
|
||||
"UserVerificationEnum": "authentik.stages.authenticator_webauthn.models.UserVerification",
|
||||
"SCIMAuthenticationModeEnum": "authentik.providers.scim.models.SCIMAuthenticationMode",
|
||||
},
|
||||
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
||||
"ENUM_GENERATE_CHOICE_DESCRIPTION": False,
|
||||
|
||||
@@ -12,7 +12,7 @@ from authentik.sources.oauth.models import GroupOAuthSourceConnection, UserOAuth
|
||||
class UserOAuthSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
class Meta(UserSourceConnectionSerializer.Meta):
|
||||
model = UserOAuthSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["access_token"]
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["access_token", "expires"]
|
||||
extra_kwargs = {
|
||||
**UserSourceConnectionSerializer.Meta.extra_kwargs,
|
||||
"access_token": {"write_only": True},
|
||||
|
||||
@@ -59,13 +59,15 @@ class OAuth2Client(BaseOAuthClient):
|
||||
"""Get client secret"""
|
||||
return self.source.consumer_secret
|
||||
|
||||
def get_access_token_args(self, callback: str, code: str) -> dict[str, Any]:
|
||||
def get_access_token_args(self, callback: str | None, code: str | None) -> dict[str, Any]:
|
||||
args = {
|
||||
"redirect_uri": callback,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
}
|
||||
if SESSION_KEY_OAUTH_PKCE in self.request.session:
|
||||
if callback:
|
||||
args["redirect_uri"] = callback
|
||||
if code:
|
||||
args["code"] = code
|
||||
if self.request and SESSION_KEY_OAUTH_PKCE in self.request.session:
|
||||
args["code_verifier"] = self.request.session[SESSION_KEY_OAUTH_PKCE]
|
||||
if (
|
||||
self.source.source_type.authorization_code_auth_method
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.12 on 2025-09-21 17:01
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_oauth", "0010_oauthsource_authorization_code_auth_method"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="useroauthsourceconnection",
|
||||
name="expires",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
||||
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
|
||||
from django.db import models
|
||||
from django.http.request import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
@@ -311,6 +312,11 @@ class UserOAuthSourceConnection(UserSourceConnection):
|
||||
"""Authorized remote OAuth provider."""
|
||||
|
||||
access_token = models.TextField(blank=True, null=True, default=None)
|
||||
expires = models.DateTimeField(default=now)
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.expires > now()
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""OAuth Callback Views"""
|
||||
|
||||
from datetime import timedelta
|
||||
from json import JSONDecodeError
|
||||
from typing import Any
|
||||
|
||||
@@ -7,6 +8,7 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import View
|
||||
from structlog.stdlib import get_logger
|
||||
@@ -77,6 +79,7 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
return sfm.get_flow(
|
||||
raw_info=raw_info,
|
||||
access_token=self.token.get("access_token"),
|
||||
expires=self.token.get("expires_in"),
|
||||
)
|
||||
|
||||
def get_callback_url(self, source: OAuthSource) -> str:
|
||||
@@ -119,8 +122,10 @@ class OAuthSourceFlowManager(SourceFlowManager):
|
||||
self,
|
||||
connection: UserOAuthSourceConnection,
|
||||
access_token: str | None = None,
|
||||
expires_in: int | None = None,
|
||||
**_,
|
||||
) -> UserOAuthSourceConnection:
|
||||
"""Set the access_token on the connection"""
|
||||
connection.access_token = access_token
|
||||
connection.expires = now() + timedelta(seconds=expires_in) if expires_in else now()
|
||||
return connection
|
||||
|
||||
+25
-1
@@ -7365,6 +7365,7 @@
|
||||
"authentik.enterprise.policies.unique_password",
|
||||
"authentik.enterprise.providers.google_workspace",
|
||||
"authentik.enterprise.providers.microsoft_entra",
|
||||
"authentik.enterprise.providers.scim",
|
||||
"authentik.enterprise.providers.ssf",
|
||||
"authentik.enterprise.search",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
@@ -9394,10 +9395,28 @@
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Token",
|
||||
"description": "Authentication token"
|
||||
},
|
||||
"auth_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"token",
|
||||
"oauth"
|
||||
],
|
||||
"title": "Auth mode"
|
||||
},
|
||||
"auth_oauth": {
|
||||
"type": "integer",
|
||||
"title": "Auth oauth",
|
||||
"description": "OAuth Source used for authentication"
|
||||
},
|
||||
"auth_oauth_params": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Auth oauth params",
|
||||
"description": "Additional OAuth parameters, such as grant_type"
|
||||
},
|
||||
"compatibility_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -11189,6 +11208,11 @@
|
||||
],
|
||||
"title": "Access token"
|
||||
},
|
||||
"expires": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Expires"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
|
||||
+56
-12
@@ -38573,6 +38573,7 @@ components:
|
||||
- authentik.enterprise.policies.unique_password
|
||||
- authentik.enterprise.providers.google_workspace
|
||||
- authentik.enterprise.providers.microsoft_entra
|
||||
- authentik.enterprise.providers.scim
|
||||
- authentik.enterprise.providers.ssf
|
||||
- authentik.enterprise.search
|
||||
- authentik.enterprise.stages.authenticator_endpoint_gdtc
|
||||
@@ -38757,11 +38758,6 @@ components:
|
||||
required:
|
||||
- name
|
||||
- slug
|
||||
AuthModeEnum:
|
||||
enum:
|
||||
- static
|
||||
- prompt
|
||||
type: string
|
||||
AuthTypeEnum:
|
||||
enum:
|
||||
- basic
|
||||
@@ -42000,7 +41996,7 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
auth_mode:
|
||||
$ref: '#/components/schemas/AuthModeEnum'
|
||||
$ref: '#/components/schemas/EndpointAuthModeEnum'
|
||||
launch_url:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -42021,6 +42017,11 @@ components:
|
||||
- protocol
|
||||
- provider
|
||||
- provider_obj
|
||||
EndpointAuthModeEnum:
|
||||
enum:
|
||||
- static
|
||||
- prompt
|
||||
type: string
|
||||
EndpointDevice:
|
||||
type: object
|
||||
description: Serializer for Endpoint authenticator devices
|
||||
@@ -42073,7 +42074,7 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
auth_mode:
|
||||
$ref: '#/components/schemas/AuthModeEnum'
|
||||
$ref: '#/components/schemas/EndpointAuthModeEnum'
|
||||
maximum_connections:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
@@ -50497,7 +50498,7 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
auth_mode:
|
||||
$ref: '#/components/schemas/AuthModeEnum'
|
||||
$ref: '#/components/schemas/EndpointAuthModeEnum'
|
||||
maximum_connections:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
@@ -52642,8 +52643,18 @@ components:
|
||||
type: boolean
|
||||
token:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Authentication token
|
||||
auth_mode:
|
||||
$ref: '#/components/schemas/SCIMAuthenticationModeEnum'
|
||||
auth_oauth:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: OAuth Source used for authentication
|
||||
auth_oauth_params:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
description: Additional OAuth parameters, such as grant_type
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
@@ -53075,6 +53086,9 @@ components:
|
||||
type: string
|
||||
writeOnly: true
|
||||
nullable: true
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
PatchedUserPlexSourceConnectionRequest:
|
||||
type: object
|
||||
description: User source connection
|
||||
@@ -56057,6 +56071,11 @@ components:
|
||||
- pre_authentication_flow
|
||||
- slug
|
||||
- sso_url
|
||||
SCIMAuthenticationModeEnum:
|
||||
enum:
|
||||
- token
|
||||
- oauth
|
||||
type: string
|
||||
SCIMMapping:
|
||||
type: object
|
||||
description: SCIMMapping Serializer
|
||||
@@ -56177,6 +56196,17 @@ components:
|
||||
token:
|
||||
type: string
|
||||
description: Authentication token
|
||||
auth_mode:
|
||||
$ref: '#/components/schemas/SCIMAuthenticationModeEnum'
|
||||
auth_oauth:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: OAuth Source used for authentication
|
||||
auth_oauth_params:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
description: Additional OAuth parameters, such as grant_type
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
@@ -56199,7 +56229,6 @@ components:
|
||||
- meta_model_name
|
||||
- name
|
||||
- pk
|
||||
- token
|
||||
- url
|
||||
- verbose_name
|
||||
- verbose_name_plural
|
||||
@@ -56275,8 +56304,18 @@ components:
|
||||
type: boolean
|
||||
token:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Authentication token
|
||||
auth_mode:
|
||||
$ref: '#/components/schemas/SCIMAuthenticationModeEnum'
|
||||
auth_oauth:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: OAuth Source used for authentication
|
||||
auth_oauth_params:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
description: Additional OAuth parameters, such as grant_type
|
||||
compatibility_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompatibilityModeEnum'
|
||||
@@ -56294,7 +56333,6 @@ components:
|
||||
the remote system.
|
||||
required:
|
||||
- name
|
||||
- token
|
||||
- url
|
||||
SCIMProviderUser:
|
||||
type: object
|
||||
@@ -58838,6 +58876,9 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- created
|
||||
- identifier
|
||||
@@ -58862,6 +58903,9 @@ components:
|
||||
type: string
|
||||
writeOnly: true
|
||||
nullable: true
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- identifier
|
||||
- source
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ export class ApplicationWizardSCIMProvider extends ApplicationWizardProviderForm
|
||||
return html`<ak-wizard-title>${this.label}</ak-wizard-title>
|
||||
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
${renderForm(
|
||||
this.requestUpdate.bind(this),
|
||||
(this.wizard.provider as SCIMProvider) ?? {},
|
||||
this.wizard.errors.provider,
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import { AuthModeEnum, Endpoint, ProtocolEnum, RacApi } from "@goauthentik/api";
|
||||
import { Endpoint, EndpointAuthModeEnum, ProtocolEnum, RacApi } from "@goauthentik/api";
|
||||
|
||||
import YAML from "yaml";
|
||||
|
||||
@@ -37,7 +37,7 @@ export class EndpointForm extends ModelForm<Endpoint, string> {
|
||||
}
|
||||
|
||||
async send(data: Endpoint): Promise<Endpoint> {
|
||||
data.authMode = AuthModeEnum.Prompt;
|
||||
data.authMode = EndpointAuthModeEnum.Prompt;
|
||||
if (!this.instance) {
|
||||
data.provider = this.providerID || 0;
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { BaseProviderForm } from "#admin/providers/BaseProviderForm";
|
||||
|
||||
import { ProvidersApi, SCIMProvider } from "@goauthentik/api";
|
||||
import { ProvidersApi, SCIMAuthenticationModeEnum, SCIMProvider } from "@goauthentik/api";
|
||||
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@@ -28,8 +28,14 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
|
||||
});
|
||||
}
|
||||
|
||||
get defaultInstance() {
|
||||
return {
|
||||
authMode: SCIMAuthenticationModeEnum.Token,
|
||||
} as SCIMProvider;
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
return renderForm(this.instance ?? {}, []);
|
||||
return renderForm(this.requestUpdate.bind(this), this.instance ?? {}, []);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,106 @@
|
||||
import "#components/ak-hidden-text-input";
|
||||
import "#components/ak-radio-input";
|
||||
import "#elements/ak-dual-select/ak-dual-select-dynamic-selected-provider";
|
||||
import "#elements/forms/FormGroup";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/Radio";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
import "#elements/CodeMirror";
|
||||
import "#admin/common/ak-license-notice";
|
||||
|
||||
import { propertyMappingsProvider, propertyMappingsSelector } from "./SCIMProviderFormHelpers.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { CodeMirrorMode } from "#elements/CodeMirror";
|
||||
|
||||
import {
|
||||
CompatibilityModeEnum,
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
Group,
|
||||
OAuthSource,
|
||||
SCIMAuthenticationModeEnum,
|
||||
SCIMProvider,
|
||||
SourcesApi,
|
||||
SourcesOauthListRequest,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationError = {}) {
|
||||
export function renderAuthToken(provider?: Partial<SCIMProvider>, errors: ValidationError = {}) {
|
||||
return html`<ak-hidden-text-input
|
||||
name="token"
|
||||
label=${msg("Token")}
|
||||
value="${provider?.token ?? ""}"
|
||||
.errorMessages=${errors?.token}
|
||||
required
|
||||
help=${msg("Token to authenticate with.")}
|
||||
input-hint="code"
|
||||
></ak-hidden-text-input>`;
|
||||
}
|
||||
|
||||
export function renderAuthOAuth(provider?: Partial<SCIMProvider>, errors: ValidationError = {}) {
|
||||
return html`<ak-form-element-horizontal label=${msg("OAuth Source")} name="authOauth">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<OAuthSource[]> => {
|
||||
const args: SourcesOauthListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const sources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList(args);
|
||||
return sources.results;
|
||||
}}
|
||||
.renderElement=${(source: OAuthSource): string => {
|
||||
return source.name;
|
||||
}}
|
||||
.value=${(source: OAuthSource | undefined): string | undefined => {
|
||||
return source ? source.pk : undefined;
|
||||
}}
|
||||
.selected=${(source: OAuthSource): boolean => {
|
||||
return source.pk === provider?.authOauth;
|
||||
}}
|
||||
blankable
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Specify OAuth source used for authentication.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("OAuth Parameters")} name="authOauthParams">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(provider?.authOauthParams ?? {})}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Additional OAuth parameters, such as grant_type.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
|
||||
export function renderAuth(provider?: Partial<SCIMProvider>, errors: ValidationError = {}) {
|
||||
switch (provider?.authMode) {
|
||||
default:
|
||||
case SCIMAuthenticationModeEnum.Token:
|
||||
return renderAuthToken(provider, errors);
|
||||
case SCIMAuthenticationModeEnum.Oauth:
|
||||
return renderAuthOAuth(provider, errors);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderForm(
|
||||
update: () => void,
|
||||
provider?: Partial<SCIMProvider>,
|
||||
errors: ValidationError = {},
|
||||
) {
|
||||
return html`
|
||||
<ak-text-input
|
||||
name="name"
|
||||
@@ -51,17 +129,42 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
<ak-hidden-text-input
|
||||
name="token"
|
||||
label=${msg("Token")}
|
||||
value="${provider?.token ?? ""}"
|
||||
.errorMessages=${errors?.token}
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication Mode")}
|
||||
required
|
||||
help=${msg(
|
||||
"Token to authenticate with. Currently only bearer authentication is supported.",
|
||||
)}
|
||||
input-hint="code"
|
||||
></ak-hidden-text-input>
|
||||
name="authMode"
|
||||
>
|
||||
<ak-radio
|
||||
@change=${(ev: CustomEvent<{ value: SCIMAuthenticationModeEnum }>) => {
|
||||
if (!provider) {
|
||||
provider = {};
|
||||
}
|
||||
provider.authMode = ev.detail.value;
|
||||
update();
|
||||
}}
|
||||
.value=${provider?.authMode}
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Token"),
|
||||
value: SCIMAuthenticationModeEnum.Token,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"Authenticate SCIM requests using a static token.",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("OAuth"),
|
||||
value: SCIMAuthenticationModeEnum.Oauth,
|
||||
default: true,
|
||||
description: html`${msg("Authenticate SCIM requests using OAuth.")}
|
||||
<ak-license-notice></ak-license-notice>`,
|
||||
},
|
||||
]}
|
||||
></ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
${renderAuth(provider, errors)}
|
||||
|
||||
<ak-radio-input
|
||||
name="compatibilityMode"
|
||||
label=${msg("Compatibility Mode")}
|
||||
|
||||
@@ -6,14 +6,15 @@ import { readInterfaceRouteParam } from "#elements/router/utils";
|
||||
import { CapabilitiesEnum, ResponseError } from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
type BrowserOptions,
|
||||
browserTracingIntegration,
|
||||
ErrorEvent,
|
||||
EventHint,
|
||||
init,
|
||||
setTag,
|
||||
setUser,
|
||||
spotlightBrowserIntegration,
|
||||
} from "@sentry/browser";
|
||||
import * as Spotlight from "@spotlightjs/spotlight";
|
||||
|
||||
/**
|
||||
* A generic error that can be thrown without triggering Sentry's reporting.
|
||||
@@ -29,7 +30,7 @@ export function configureSentry(canDoPpi = false) {
|
||||
if (!cfg.errorReporting?.enabled && !debug) {
|
||||
return cfg;
|
||||
}
|
||||
init({
|
||||
const opts = {
|
||||
dsn: cfg.errorReporting.sentryDsn,
|
||||
ignoreErrors: [
|
||||
/network/gi,
|
||||
@@ -50,7 +51,8 @@ export function configureSentry(canDoPpi = false) {
|
||||
instrumentPageLoad: false,
|
||||
traceFetch: false,
|
||||
}),
|
||||
],
|
||||
debug ? spotlightBrowserIntegration() : null,
|
||||
].filter((int) => int),
|
||||
tracePropagationTargets: [window.location.origin],
|
||||
tracesSampleRate: debug ? 1.0 : cfg.errorReporting.tracesSampleRate,
|
||||
environment: cfg.errorReporting.environment,
|
||||
@@ -72,22 +74,15 @@ export function configureSentry(canDoPpi = false) {
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
};
|
||||
if (debug) {
|
||||
console.debug("authentik/config: Enabled Sentry Spotlight");
|
||||
}
|
||||
init(opts as BrowserOptions);
|
||||
setTag(TAG_SENTRY_CAPABILITIES, cfg.capabilities.join(","));
|
||||
if (window.location.pathname.includes("if/")) {
|
||||
setTag(TAG_SENTRY_COMPONENT, `web/${readInterfaceRouteParam()}`);
|
||||
}
|
||||
if (debug) {
|
||||
Spotlight.init({
|
||||
injectImmediately: true,
|
||||
integrations: [
|
||||
Spotlight.sentry({
|
||||
injectIntoSDK: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
console.debug("authentik/config: Enabled Sentry Spotlight");
|
||||
}
|
||||
if (cfg.errorReporting.sendPii && canDoPpi) {
|
||||
me().then((user) => {
|
||||
setUser({ email: user.user.email });
|
||||
|
||||
Reference in New Issue
Block a user