Compare commits
42 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| 748a8e560f | |||
| d6c35787b0 | |||
| cc214a0eb7 | |||
| 0c9fd5f056 | |||
| 92a1f7e01a | |||
| 1a727b9ea0 | |||
| 28cc75af29 | |||
| 0ad245f7f6 | |||
| b10957e5df | |||
| 3adf79c493 | |||
| f478593826 | |||
| edf4de7271 | |||
| db43869e25 | |||
| 8a668af5f6 | |||
| eef233fd11 | |||
| 833b350c42 | |||
| b388265d98 | |||
| faefd9776d | |||
| a5ee159189 | |||
| 35c739ee84 | |||
| e9764333ea | |||
| 22af17be2c | |||
| 679bf17d6f | |||
| cbfa51fb31 | |||
| 5f8c21cc88 | |||
| 69b3d1722b | |||
| fa4ce1d629 | |||
| e4a392834f | |||
| 31fe0e5923 | |||
| 8b619635ea | |||
| 1f1db523c0 | |||
| bbc23e1d77 | |||
| c30b7ee3e9 | |||
| 2ba79627bc | |||
| 198cbe1d9d | |||
| db6da159d5 | |||
| 9862e32078 | |||
| a7714e2892 | |||
| 073e1d241b | |||
| 5c5cc1c7da | |||
| 3dccce1095 | |||
| 78f997fbee |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.2.0-rc3
|
||||
current_version = 2025.2.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
|
||||
@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | --------- |
|
||||
| 2024.10.x | ✅ |
|
||||
| 2024.12.x | ✅ |
|
||||
| 2025.2.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.2.0"
|
||||
__version__ = "2025.2.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer):
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
actual_value = value
|
||||
if raw_session in actual_value:
|
||||
if raw_session is not None and raw_session in actual_value:
|
||||
actual_value = actual_value.replace(
|
||||
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
||||
)
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
"""User API Views"""
|
||||
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
from json import loads
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.db.models.functions import ExtractHour
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
@ -91,6 +92,7 @@ from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
LOGGER = get_logger()
|
||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
class UserGroupSerializer(ModelSerializer):
|
||||
@ -373,7 +375,7 @@ class UsersFilter(FilterSet):
|
||||
method="filter_attributes",
|
||||
)
|
||||
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||
uuid = UUIDFilter(field_name="uuid")
|
||||
|
||||
path = CharFilter(field_name="path")
|
||||
@ -391,6 +393,11 @@ class UsersFilter(FilterSet):
|
||||
queryset=Group.objects.all().order_by("name"),
|
||||
)
|
||||
|
||||
def filter_is_superuser(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(ak_groups__is_superuser=True).distinct()
|
||||
return queryset.exclude(ak_groups__is_superuser=True).distinct()
|
||||
|
||||
def filter_attributes(self, queryset, name, value):
|
||||
"""Filter attributes by query args"""
|
||||
try:
|
||||
@ -769,7 +776,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
if not instance.is_active:
|
||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||
session_ids = sessions.values_list("session_key", flat=True)
|
||||
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
||||
for session in session_ids:
|
||||
SessionStore(session).delete()
|
||||
sessions.delete()
|
||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||
return response
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
"""authentik core signals"""
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.core.cache import cache
|
||||
from django.core.signals import Signal
|
||||
from django.db.models import Model
|
||||
@ -25,6 +28,7 @@ password_changed = Signal()
|
||||
login_failed = Signal()
|
||||
|
||||
LOGGER = get_logger()
|
||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@receiver(post_save, sender=Application)
|
||||
@ -60,8 +64,7 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||
"""Delete session when authenticated session is deleted"""
|
||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||
cache.delete(cache_key)
|
||||
SessionStore(instance.session_key).delete()
|
||||
|
||||
|
||||
@receiver(pre_save)
|
||||
|
||||
@ -36,6 +36,7 @@ from authentik.flows.planner import (
|
||||
)
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||
from authentik.lib.utils.urls import is_url_absolute
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.utils import delete_none_values
|
||||
@ -208,6 +209,8 @@ class SourceFlowManager:
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
)
|
||||
if not is_url_absolute(final_redirect):
|
||||
final_redirect = "authentik_core:if-user"
|
||||
flow_context.update(
|
||||
{
|
||||
# Since we authenticate the user by their token, they have no backend set
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
build: "{{ build }}",
|
||||
api: {
|
||||
base: "{{ base_url }}",
|
||||
relBase: "{{ base_url_rel }}",
|
||||
},
|
||||
};
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""Test Users API"""
|
||||
|
||||
from datetime import datetime
|
||||
from json import loads
|
||||
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
@ -15,7 +16,11 @@ from authentik.core.models import (
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||
from authentik.core.tests.utils import (
|
||||
create_test_admin_user,
|
||||
create_test_brand,
|
||||
create_test_flow,
|
||||
)
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.stages.email.models import EmailStage
|
||||
@ -41,6 +46,32 @@ class TestUsersAPI(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_filter_is_superuser(self):
|
||||
"""Test API filtering by superuser status"""
|
||||
self.client.force_login(self.admin)
|
||||
# Test superuser
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"is_superuser": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 1)
|
||||
self.assertEqual(body["results"][0]["username"], self.admin.username)
|
||||
# Test non-superuser
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"is_superuser": False,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 1, body)
|
||||
self.assertEqual(body["results"][0]["username"], self.user.username)
|
||||
|
||||
def test_list_with_groups(self):
|
||||
"""Test listing with groups"""
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
@ -55,7 +55,7 @@ class RedirectToAppLaunch(View):
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
raise Http404 from None
|
||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
||||
plan.append_stage(in_memory_stage(RedirectToAppStage))
|
||||
return plan.to_redirect(request, flow)
|
||||
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ class InterfaceView(TemplateView):
|
||||
kwargs["build"] = get_build_hash()
|
||||
kwargs["url_kwargs"] = self.kwargs
|
||||
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
|
||||
kwargs["base_url_rel"] = CONFIG.get("web.path", "/")
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
|
||||
@ -89,9 +89,9 @@ class SourceStageFinal(StageView):
|
||||
This stage uses the override flow token to resume execution of the initial flow the
|
||||
source stage is bound to."""
|
||||
|
||||
def dispatch(self):
|
||||
def dispatch(self, *args, **kwargs):
|
||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||
self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||
plan = token.plan
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||
response = plan.to_redirect(self.request, token.flow)
|
||||
|
||||
@ -4,7 +4,8 @@ from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
||||
from authentik.enterprise.stages.source.models import SourceStage
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
|
||||
from authentik.enterprise.stages.source.stage import SourceStageFinal
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken, in_memory_stage
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
@ -87,6 +88,7 @@ class TestSourceStage(FlowTestCase):
|
||||
self.assertIsNotNone(flow_token)
|
||||
session = self.client.session
|
||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||
plan.insert_stage(in_memory_stage(SourceStageFinal), index=0)
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
@ -96,4 +98,6 @@ class TestSourceStage(FlowTestCase):
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
self.assertStageRedirects(
|
||||
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||
)
|
||||
|
||||
@ -76,10 +76,10 @@ class FlowPlan:
|
||||
self.bindings.append(binding)
|
||||
self.markers.append(marker or StageMarker())
|
||||
|
||||
def insert_stage(self, stage: Stage, marker: StageMarker | None = None):
|
||||
def insert_stage(self, stage: Stage, marker: StageMarker | None = None, index=1):
|
||||
"""Insert stage into plan, as immediate next stage"""
|
||||
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
|
||||
self.markers.insert(1, marker or StageMarker())
|
||||
self.bindings.insert(index, FlowStageBinding(stage=stage, order=0))
|
||||
self.markers.insert(index, marker or StageMarker())
|
||||
|
||||
def redirect(self, destination: str):
|
||||
"""Insert a redirect stage as next stage"""
|
||||
|
||||
@ -282,16 +282,14 @@ class ConfigLoader:
|
||||
|
||||
def get_optional_int(self, path: str, default=None) -> int | None:
|
||||
"""Wrapper for get that converts value into int or None if set"""
|
||||
value = self.get(path, default)
|
||||
value = self.get(path, UNSET)
|
||||
if value is UNSET:
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError) as exc:
|
||||
if value is None or (isinstance(value, str) and value.lower() == "null"):
|
||||
return default
|
||||
if value is UNSET:
|
||||
return default
|
||||
return None
|
||||
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
||||
return default
|
||||
|
||||
@ -372,9 +370,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
||||
"sslcert": config.get("postgresql.sslcert"),
|
||||
"sslkey": config.get("postgresql.sslkey"),
|
||||
},
|
||||
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
|
||||
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
|
||||
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
|
||||
"CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0),
|
||||
"CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False),
|
||||
"DISABLE_SERVER_SIDE_CURSORS": config.get_bool(
|
||||
"postgresql.disable_server_side_cursors", False
|
||||
),
|
||||
"TEST": {
|
||||
@ -383,8 +381,8 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
||||
}
|
||||
}
|
||||
|
||||
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
|
||||
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
||||
conn_max_age = config.get_optional_int("postgresql.conn_max_age", UNSET)
|
||||
disable_server_side_cursors = config.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
||||
if config.get_bool("postgresql.use_pgpool", False):
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
||||
if disable_server_side_cursors is not UNSET:
|
||||
|
||||
@ -158,6 +158,18 @@ class TestConfig(TestCase):
|
||||
test_obj = Test()
|
||||
dumps(test_obj, indent=4, cls=AttrEncoder)
|
||||
|
||||
def test_get_optional_int(self):
|
||||
config = ConfigLoader()
|
||||
self.assertEqual(config.get_optional_int("foo", 21), 21)
|
||||
self.assertEqual(config.get_optional_int("foo"), None)
|
||||
config.set("foo", "21")
|
||||
self.assertEqual(config.get_optional_int("foo"), 21)
|
||||
self.assertEqual(config.get_optional_int("foo", 0), 21)
|
||||
self.assertEqual(config.get_optional_int("foo", "null"), 21)
|
||||
config.set("foo", "null")
|
||||
self.assertEqual(config.get_optional_int("foo"), None)
|
||||
self.assertEqual(config.get_optional_int("foo", 21), None)
|
||||
|
||||
@mock.patch.dict(environ, check_deprecations_env_vars)
|
||||
def test_check_deprecations(self):
|
||||
"""Test config key re-write for deprecated env vars"""
|
||||
@ -221,6 +233,16 @@ class TestConfig(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_db_conn_max_age(self):
|
||||
"""Test DB conn_max_age Config"""
|
||||
config = ConfigLoader()
|
||||
config.set("postgresql.conn_max_age", "null")
|
||||
conf = django_db_config(config)
|
||||
self.assertEqual(
|
||||
conf["default"]["CONN_MAX_AGE"],
|
||||
None,
|
||||
)
|
||||
|
||||
def test_db_read_replicas(self):
|
||||
"""Test read replicas"""
|
||||
config = ConfigLoader()
|
||||
|
||||
@ -71,7 +71,7 @@ class CodeValidatorView(PolicyAccessView):
|
||||
except FlowNonApplicableException:
|
||||
LOGGER.warning("Flow not applicable to user")
|
||||
return None
|
||||
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||
plan.append_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||
return plan.to_redirect(self.request, self.token.provider.authorization_flow)
|
||||
|
||||
|
||||
|
||||
@ -34,5 +34,5 @@ class EndSessionView(PolicyAccessView):
|
||||
PLAN_CONTEXT_APPLICATION: self.application,
|
||||
},
|
||||
)
|
||||
plan.insert_stage(in_memory_stage(SessionEndStage))
|
||||
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||
return plan.to_redirect(self.request, self.flow)
|
||||
|
||||
@ -36,17 +36,17 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
def reconciler_name() -> str:
|
||||
return "ingress"
|
||||
|
||||
def _check_annotations(self, reference: V1Ingress):
|
||||
def _check_annotations(self, current: V1Ingress, reference: V1Ingress):
|
||||
"""Check that all annotations *we* set are correct"""
|
||||
for key, value in self.get_ingress_annotations().items():
|
||||
if key not in reference.metadata.annotations:
|
||||
for key, value in reference.metadata.annotations.items():
|
||||
if key not in current.metadata.annotations:
|
||||
raise NeedsUpdate()
|
||||
if reference.metadata.annotations[key] != value:
|
||||
if current.metadata.annotations[key] != value:
|
||||
raise NeedsUpdate()
|
||||
|
||||
def reconcile(self, current: V1Ingress, reference: V1Ingress):
|
||||
super().reconcile(current, reference)
|
||||
self._check_annotations(reference)
|
||||
self._check_annotations(current, reference)
|
||||
# Create a list of all expected host and tls hosts
|
||||
expected_hosts = []
|
||||
expected_hosts_tls = []
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
"""RAC app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikProviderRAC(AppConfig):
|
||||
class AuthentikProviderRAC(ManagedAppConfig):
|
||||
"""authentik rac app config"""
|
||||
|
||||
name = "authentik.providers.rac"
|
||||
|
||||
@ -4,8 +4,7 @@ from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.db.models.signals import post_delete, post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
|
||||
@ -46,12 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Endpoint)
|
||||
def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
|
||||
"""Clear user's endpoint cache upon endpoint creation"""
|
||||
if not created: # pragma: no cover
|
||||
return
|
||||
|
||||
# Delete user endpoint cache
|
||||
@receiver([post_save, post_delete], sender=Endpoint)
|
||||
def post_save_post_delete_endpoint(**_):
|
||||
"""Clear user's endpoint cache upon endpoint creation or deletion"""
|
||||
keys = cache.keys(user_endpoint_cache_key("*"))
|
||||
cache.delete_many(keys)
|
||||
|
||||
@ -46,7 +46,7 @@ class RACStartView(PolicyAccessView):
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
raise Http404 from None
|
||||
plan.insert_stage(
|
||||
plan.append_stage(
|
||||
in_memory_stage(
|
||||
RACFinalStage,
|
||||
application=self.application,
|
||||
|
||||
@ -61,7 +61,7 @@ class SAMLSLOView(PolicyAccessView):
|
||||
PLAN_CONTEXT_APPLICATION: self.application,
|
||||
},
|
||||
)
|
||||
plan.insert_stage(in_memory_stage(SessionEndStage))
|
||||
plan.append_stage(in_memory_stage(SessionEndStage))
|
||||
return plan.to_redirect(self.request, self.flow)
|
||||
|
||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
"""User client"""
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils.http import urlencode
|
||||
from pydantic import ValidationError
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.sync.outgoing.exceptions import ObjectExistsSyncException, StopSync
|
||||
from authentik.policies.utils import delete_none_values
|
||||
from authentik.providers.scim.clients.base import SCIMClient
|
||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||
@ -55,18 +57,35 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
|
||||
def create(self, user: User):
|
||||
"""Create user from scratch and create a connection object"""
|
||||
scim_user = self.to_schema(user, None)
|
||||
response = self._request(
|
||||
"POST",
|
||||
"/Users",
|
||||
json=scim_user.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
scim_id = response.get("id")
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
return SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
|
||||
with transaction.atomic():
|
||||
try:
|
||||
response = self._request(
|
||||
"POST",
|
||||
"/Users",
|
||||
json=scim_user.model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
except ObjectExistsSyncException as exc:
|
||||
if not self._config.filter.supported:
|
||||
raise exc
|
||||
users = self._request(
|
||||
"GET", f"/Users?{urlencode({'filter': f'userName eq {scim_user.userName}'})}"
|
||||
)
|
||||
users_res = users.get("Resources", [])
|
||||
if len(users_res) < 1:
|
||||
raise exc
|
||||
return SCIMProviderUser.objects.create(
|
||||
provider=self.provider, user=user, scim_id=users_res[0]["id"]
|
||||
)
|
||||
else:
|
||||
scim_id = response.get("id")
|
||||
if not scim_id or scim_id == "":
|
||||
raise StopSync("SCIM Response with missing or invalid `id`")
|
||||
return SCIMProviderUser.objects.create(
|
||||
provider=self.provider, user=user, scim_id=scim_id
|
||||
)
|
||||
|
||||
def update(self, user: User, connection: SCIMProviderUser):
|
||||
"""Update existing user"""
|
||||
|
||||
@ -68,8 +68,6 @@ class OAuth2Client(BaseOAuthClient):
|
||||
error_desc = self.get_request_arg("error_description", None)
|
||||
return {"error": error_desc or error or _("No token received.")}
|
||||
args = {
|
||||
"client_id": self.get_client_id(),
|
||||
"client_secret": self.get_client_secret(),
|
||||
"redirect_uri": callback,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
|
||||
@ -28,7 +28,7 @@ def update_well_known_jwks(self: SystemTask):
|
||||
LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text)
|
||||
messages.append(f"Failed to update OIDC configuration for {source.slug}")
|
||||
continue
|
||||
config = well_known_config.json()
|
||||
config: dict = well_known_config.json()
|
||||
try:
|
||||
dirty = False
|
||||
source_attr_key = (
|
||||
@ -40,7 +40,9 @@ def update_well_known_jwks(self: SystemTask):
|
||||
for source_attr, config_key in source_attr_key:
|
||||
# Check if we're actually changing anything to only
|
||||
# save when something has changed
|
||||
if getattr(source, source_attr, "") != config[config_key]:
|
||||
if config_key not in config:
|
||||
continue
|
||||
if getattr(source, source_attr, "") != config.get(config_key, ""):
|
||||
dirty = True
|
||||
setattr(source, source_attr, config[config_key])
|
||||
except (IndexError, KeyError) as exc:
|
||||
|
||||
@ -33,6 +33,7 @@ from authentik.flows.planner import (
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import is_url_absolute
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.providers.saml.utils.encoding import nice64
|
||||
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
||||
@ -73,6 +74,8 @@ class InitiateView(View):
|
||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||
)
|
||||
if not is_url_absolute(final_redirect):
|
||||
final_redirect = "authentik_core:if-user"
|
||||
kwargs.update(
|
||||
{
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
|
||||
@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import StageInvalidException
|
||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||
@ -71,6 +72,14 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
def component(self) -> str:
|
||||
return "ak-stage-authenticator-email-form"
|
||||
|
||||
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.friendly_name or str(self._meta.verbose_name),
|
||||
"component": "ak-user-settings-authenticator-email",
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def backend_class(self) -> type[BaseEmailBackend]:
|
||||
"""Get the email backend class to use"""
|
||||
|
||||
@ -300,9 +300,11 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(device.confirmed)
|
||||
# Session key should be removed after device is saved
|
||||
device.save()
|
||||
self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, self.client.session)
|
||||
# Get a fresh session to check if the key was removed
|
||||
session = self.client.session
|
||||
session.save()
|
||||
session.load()
|
||||
self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, session)
|
||||
|
||||
def test_model_properties_and_methods(self):
|
||||
"""Test model properties"""
|
||||
|
||||
@ -12,6 +12,7 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction, TaskStatus
|
||||
from authentik.events.system_tasks import SystemTask
|
||||
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
|
||||
from authentik.stages.email.models import EmailStage
|
||||
@ -32,9 +33,10 @@ def send_mails(
|
||||
Celery group promise for the email sending tasks
|
||||
"""
|
||||
tasks = []
|
||||
stage_class = stage.__class__
|
||||
# Use the class path instead of the class itself for serialization
|
||||
stage_class_path = class_to_path(stage.__class__)
|
||||
for message in messages:
|
||||
tasks.append(send_mail.s(message.__dict__, stage_class, str(stage.pk)))
|
||||
tasks.append(send_mail.s(message.__dict__, stage_class_path, str(stage.pk)))
|
||||
lazy_group = group(*tasks)
|
||||
promise = lazy_group()
|
||||
return promise
|
||||
@ -61,7 +63,7 @@ def get_email_body(email: EmailMultiAlternatives) -> str:
|
||||
def send_mail(
|
||||
self: SystemTask,
|
||||
message: dict[Any, Any],
|
||||
stage_class: EmailStage | AuthenticatorEmailStage = EmailStage,
|
||||
stage_class_path: str | None = None,
|
||||
email_stage_pk: str | None = None,
|
||||
):
|
||||
"""Send Email for Email Stage. Retries are scheduled automatically."""
|
||||
@ -69,9 +71,10 @@ def send_mail(
|
||||
message_id = make_msgid(domain=DNS_NAME)
|
||||
self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
|
||||
try:
|
||||
if not email_stage_pk:
|
||||
stage: EmailStage | AuthenticatorEmailStage = stage_class(use_global_settings=True)
|
||||
if not stage_class_path or not email_stage_pk:
|
||||
stage = EmailStage(use_global_settings=True)
|
||||
else:
|
||||
stage_class = path_to_class(stage_class_path)
|
||||
stages = stage_class.objects.filter(pk=email_stage_pk)
|
||||
if not stages.exists():
|
||||
self.set_status(
|
||||
|
||||
@ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
@ -67,6 +67,36 @@ class TestEmailStageSending(FlowTestCase):
|
||||
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
|
||||
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
||||
|
||||
def test_newlines_long_name(self):
|
||||
"""Test with pending user"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
long_user = create_test_user()
|
||||
long_user.name = "Test User\r\n Many Words\r\n"
|
||||
long_user.save()
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = long_user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
with patch(
|
||||
"authentik.stages.email.models.EmailStage.backend_class",
|
||||
PropertyMock(return_value=EmailBackend),
|
||||
):
|
||||
response = self.client.post(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
response_errors={
|
||||
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
|
||||
|
||||
def test_pending_fake_user(self):
|
||||
"""Test with pending (fake) user"""
|
||||
self.flow.designation = FlowDesignation.RECOVERY
|
||||
|
||||
58
authentik/stages/email/tests/test_tasks.py
Normal file
58
authentik/stages/email/tests/test_tasks.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""Test email stage tasks"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import get_email_body, send_mails
|
||||
|
||||
|
||||
class TestEmailTasks(TestCase):
|
||||
"""Test email stage tasks"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_test_admin_user()
|
||||
self.stage = EmailStage.objects.create(
|
||||
name="test-email",
|
||||
use_global_settings=True,
|
||||
)
|
||||
self.auth_stage = AuthenticatorEmailStage.objects.create(
|
||||
name="test-auth-email",
|
||||
use_global_settings=True,
|
||||
)
|
||||
|
||||
def test_get_email_body_html(self):
|
||||
"""Test get_email_body with HTML alternative"""
|
||||
message = EmailMultiAlternatives()
|
||||
message.body = "plain text"
|
||||
message.attach_alternative("<p>html content</p>", "text/html")
|
||||
self.assertEqual(get_email_body(message), "<p>html content</p>")
|
||||
|
||||
def test_get_email_body_plain(self):
|
||||
"""Test get_email_body with plain text only"""
|
||||
message = EmailMultiAlternatives()
|
||||
message.body = "plain text"
|
||||
self.assertEqual(get_email_body(message), "plain text")
|
||||
|
||||
def test_send_mails_email_stage(self):
|
||||
"""Test send_mails with EmailStage"""
|
||||
message = EmailMultiAlternatives()
|
||||
with patch("authentik.stages.email.tasks.send_mail") as mock_send:
|
||||
send_mails(self.stage, message)
|
||||
mock_send.s.assert_called_once_with(
|
||||
message.__dict__, class_to_path(EmailStage), str(self.stage.pk)
|
||||
)
|
||||
|
||||
def test_send_mails_authenticator_stage(self):
|
||||
"""Test send_mails with AuthenticatorEmailStage"""
|
||||
message = EmailMultiAlternatives()
|
||||
with patch("authentik.stages.email.tasks.send_mail") as mock_send:
|
||||
send_mails(self.auth_stage, message)
|
||||
mock_send.s.assert_called_once_with(
|
||||
message.__dict__, class_to_path(AuthenticatorEmailStage), str(self.auth_stage.pk)
|
||||
)
|
||||
@ -32,7 +32,14 @@ class TemplateEmailMessage(EmailMultiAlternatives):
|
||||
sanitized_to = []
|
||||
# Ensure that all recipients are valid
|
||||
for recipient_name, recipient_email in to:
|
||||
sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8"))
|
||||
# Remove any newline characters from name and email before sanitizing
|
||||
clean_name = (
|
||||
recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else ""
|
||||
)
|
||||
clean_email = (
|
||||
recipient_email.replace("\n", "").replace("\r", "") if recipient_email else ""
|
||||
)
|
||||
sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8"))
|
||||
super().__init__(to=sanitized_to, **kwargs)
|
||||
if not template_name:
|
||||
return
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.2.0 Blueprint schema",
|
||||
"title": "authentik 2025.2.3 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
|
||||
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -54,7 +54,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
||||
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.2.0"
|
||||
const VERSION = "2025.2.3"
|
||||
|
||||
@ -35,13 +35,19 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
||||
req PaginatorRequest[Treq, Tres],
|
||||
opts PaginatorOptions,
|
||||
) ([]Tobj, error) {
|
||||
if opts.Logger == nil {
|
||||
opts.Logger = log.NewEntry(log.StandardLogger())
|
||||
}
|
||||
var bfreq, cfreq interface{}
|
||||
fetchOffset := func(page int32) (Tres, error) {
|
||||
bfreq = req.Page(page)
|
||||
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
|
||||
res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||
res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
|
||||
if err != nil {
|
||||
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
|
||||
if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
@ -51,6 +57,9 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
|
||||
for {
|
||||
apiObjects, err := fetchOffset(page)
|
||||
if err != nil {
|
||||
if page == 1 {
|
||||
return objects, err
|
||||
}
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -1,5 +1,64 @@
|
||||
package ak
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/api/v3"
|
||||
)
|
||||
|
||||
type fakeAPIType struct{}
|
||||
|
||||
type fakeAPIResponse struct {
|
||||
results []fakeAPIType
|
||||
pagination api.Pagination
|
||||
}
|
||||
|
||||
func (fapi *fakeAPIResponse) GetResults() []fakeAPIType { return fapi.results }
|
||||
func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination }
|
||||
|
||||
type fakeAPIRequest struct {
|
||||
res *fakeAPIResponse
|
||||
http *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest { return fapi }
|
||||
func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi }
|
||||
func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) {
|
||||
return fapi.res, fapi.http, fapi.err
|
||||
}
|
||||
|
||||
func Test_Simple(t *testing.T) {
|
||||
req := &fakeAPIRequest{
|
||||
res: &fakeAPIResponse{
|
||||
results: []fakeAPIType{
|
||||
{},
|
||||
},
|
||||
pagination: api.Pagination{
|
||||
TotalPages: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := Paginator(req, PaginatorOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 1)
|
||||
}
|
||||
|
||||
func Test_BadRequest(t *testing.T) {
|
||||
req := &fakeAPIRequest{
|
||||
http: &http.Response{
|
||||
StatusCode: 400,
|
||||
},
|
||||
err: errors.New("foo"),
|
||||
}
|
||||
res, err := Paginator(req, PaginatorOptions{})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, []fakeAPIType{}, res)
|
||||
}
|
||||
|
||||
// func Test_PaginatorCompile(t *testing.T) {
|
||||
// req := api.ApiCoreUsersListRequest{}
|
||||
// Paginator(req, PaginatorOptions{
|
||||
|
||||
@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.2.0
|
||||
Default: 2025.2.3
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.2.0",
|
||||
"version": "2025.2.3",
|
||||
"private": true
|
||||
}
|
||||
|
||||
368
poetry.lock
generated
368
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2025.2.0"
|
||||
version = "2025.2.3"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
@ -123,7 +123,7 @@ kubernetes = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
msgraph-sdk = "*"
|
||||
opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf", extras = ["reggie"] }
|
||||
opencontainers = { git = "https://github.com/BeryJu/oci-python", rev = "c791b19056769cd67957322806809ab70f5bead8", extras = ["reggie"] }
|
||||
packaging = "*"
|
||||
paramiko = "*"
|
||||
psycopg = { extras = ["c"], version = "*" }
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2025.2.0
|
||||
version: 2025.2.3
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
||||
@ -94,7 +94,7 @@ export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
|
||||
}
|
||||
|
||||
renderExpanded(item: ApplicationEntitlement): TemplateResult {
|
||||
return html` <td></td>
|
||||
return html`<td></td>
|
||||
<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<div class="pf-c-content">
|
||||
|
||||
@ -58,7 +58,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
|
||||
get bindingsAsColumns() {
|
||||
return this.wizard.bindings.map((binding, index) => {
|
||||
const { order, enabled, timeout } = binding;
|
||||
const isSet = P.string.minLength(1);
|
||||
const isSet = P.union(P.string.minLength(1), P.number);
|
||||
const policy = match(binding)
|
||||
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
|
||||
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))
|
||||
|
||||
@ -21,12 +21,22 @@ export class RelatedApplicationButton extends AKElement {
|
||||
@property({ attribute: false })
|
||||
provider?: Provider;
|
||||
|
||||
@property()
|
||||
mode: "primary" | "backchannel" = "primary";
|
||||
|
||||
render(): TemplateResult {
|
||||
if (this.provider?.assignedApplicationSlug) {
|
||||
if (this.mode === "primary" && this.provider?.assignedApplicationSlug) {
|
||||
return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}">
|
||||
${this.provider.assignedApplicationName}
|
||||
</a>`;
|
||||
}
|
||||
if (this.mode === "backchannel" && this.provider?.assignedBackchannelApplicationSlug) {
|
||||
return html`<a
|
||||
href="#/core/applications/${this.provider.assignedBackchannelApplicationSlug}"
|
||||
>
|
||||
${this.provider.assignedBackchannelApplicationName}
|
||||
</a>`;
|
||||
}
|
||||
return html`<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Create")} </span>
|
||||
<span slot="header"> ${msg("Create Application")} </span>
|
||||
|
||||
@ -7,10 +7,10 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/SyncStatusCard";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/sync/SyncStatusCard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||
|
||||
@ -9,10 +9,10 @@ import "@goauthentik/components/events/ObjectChangelog";
|
||||
import MDSCIMProvider from "@goauthentik/docs/add-secure-apps/providers/scim/index.md";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/SyncStatusCard";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/sync/SyncStatusCard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||
@ -173,6 +173,7 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-provider-related-application
|
||||
mode="backchannel"
|
||||
.provider=${this.provider}
|
||||
></ak-provider-related-application>
|
||||
</div>
|
||||
|
||||
@ -8,11 +8,11 @@ import MDSourceKerberosBrowser from "@goauthentik/docs/users-sources/sources/pro
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/SyncStatusCard";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import "@goauthentik/elements/sync/SyncStatusCard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
|
||||
@ -6,11 +6,11 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/SyncStatusCard";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import "@goauthentik/elements/sync/SyncStatusCard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
|
||||
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2025.2.0";
|
||||
export const VERSION = "2025.2.3";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
||||
157
web/src/elements/sync/SyncStatusCard.stories.ts
Normal file
157
web/src/elements/sync/SyncStatusCard.stories.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html } from "lit";
|
||||
|
||||
import { LogLevelEnum, SyncStatus, SystemTaskStatusEnum } from "@goauthentik/api";
|
||||
|
||||
import "./SyncStatusCard";
|
||||
|
||||
const metadata: Meta<SyncStatus> = {
|
||||
title: "Elements/<ak-sync-status-card>",
|
||||
component: "ak-sync-status-card",
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
export const Running: StoryObj = {
|
||||
args: {
|
||||
status: {
|
||||
isRunning: true,
|
||||
tasks: [],
|
||||
} as SyncStatus,
|
||||
},
|
||||
// @ts-ignore
|
||||
render: ({ status }: SyncStatus) => {
|
||||
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
|
||||
<ak-sync-status-card
|
||||
.fetch=${async () => {
|
||||
return status;
|
||||
}}
|
||||
></ak-sync-status-card>
|
||||
</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleTask: StoryObj = {
|
||||
args: {
|
||||
status: {
|
||||
isRunning: false,
|
||||
tasks: [
|
||||
{
|
||||
uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b",
|
||||
name: "Single task",
|
||||
fullName: "foo:bar:baz",
|
||||
status: SystemTaskStatusEnum.Successful,
|
||||
messages: [
|
||||
{
|
||||
logger: "foo",
|
||||
event: "bar",
|
||||
attributes: {
|
||||
foo: "bar",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
logLevel: LogLevelEnum.Info,
|
||||
},
|
||||
],
|
||||
description: "foo",
|
||||
startTimestamp: new Date(),
|
||||
finishTimestamp: new Date(),
|
||||
duration: 0,
|
||||
},
|
||||
],
|
||||
} as SyncStatus,
|
||||
},
|
||||
// @ts-ignore
|
||||
render: ({ status }: SyncStatus) => {
|
||||
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
|
||||
<ak-sync-status-card
|
||||
.fetch=${async () => {
|
||||
return status;
|
||||
}}
|
||||
></ak-sync-status-card>
|
||||
</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleTasks: StoryObj = {
|
||||
args: {
|
||||
status: {
|
||||
isRunning: false,
|
||||
tasks: [
|
||||
{
|
||||
uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b",
|
||||
name: "Single task",
|
||||
fullName: "foo:bar:baz",
|
||||
status: SystemTaskStatusEnum.Successful,
|
||||
messages: [
|
||||
{
|
||||
logger: "foo",
|
||||
event: "bar",
|
||||
attributes: {
|
||||
foo: "bar",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
logLevel: LogLevelEnum.Info,
|
||||
},
|
||||
],
|
||||
description: "foo",
|
||||
startTimestamp: new Date(),
|
||||
finishTimestamp: new Date(),
|
||||
duration: 0,
|
||||
},
|
||||
{
|
||||
uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b",
|
||||
name: "Single task",
|
||||
fullName: "foo:bar:baz",
|
||||
status: SystemTaskStatusEnum.Successful,
|
||||
messages: [
|
||||
{
|
||||
logger: "foo",
|
||||
event: "bar",
|
||||
attributes: {
|
||||
foo: "bar",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
logLevel: LogLevelEnum.Info,
|
||||
},
|
||||
],
|
||||
description: "foo",
|
||||
startTimestamp: new Date(),
|
||||
finishTimestamp: new Date(),
|
||||
duration: 0,
|
||||
},
|
||||
{
|
||||
uuid: "9ff42169-8249-4b67-ae3d-e455d822de2b",
|
||||
name: "Single task",
|
||||
fullName: "foo:bar:baz",
|
||||
status: SystemTaskStatusEnum.Successful,
|
||||
messages: [
|
||||
{
|
||||
logger: "foo",
|
||||
event: "bar",
|
||||
attributes: {
|
||||
foo: "bar",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
logLevel: LogLevelEnum.Info,
|
||||
},
|
||||
],
|
||||
description: "foo",
|
||||
startTimestamp: new Date(),
|
||||
finishTimestamp: new Date(),
|
||||
duration: 0,
|
||||
},
|
||||
],
|
||||
} as SyncStatus,
|
||||
},
|
||||
// @ts-ignore
|
||||
render: ({ status }: SyncStatus) => {
|
||||
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
|
||||
<ak-sync-status-card
|
||||
.fetch=${async () => {
|
||||
return status;
|
||||
}}
|
||||
></ak-sync-status-card>
|
||||
</div>`;
|
||||
},
|
||||
};
|
||||
@ -3,17 +3,92 @@ import { getRelativeTime } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/events/LogViewer";
|
||||
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html, nothing } from "lit";
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { SyncStatus, SystemTask, SystemTaskStatusEnum } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-sync-status-table")
|
||||
export class SyncStatusTable extends Table<SystemTask> {
|
||||
@property({ attribute: false })
|
||||
tasks: SystemTask[] = [];
|
||||
|
||||
expandable = true;
|
||||
|
||||
static get styles() {
|
||||
return super.styles.concat(css`
|
||||
code:not(:last-of-type)::after {
|
||||
content: "-";
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<SystemTask>> {
|
||||
return {
|
||||
pagination: {
|
||||
next: 0,
|
||||
previous: 0,
|
||||
count: this.tasks.length,
|
||||
current: 1,
|
||||
totalPages: 1,
|
||||
startIndex: 0,
|
||||
endIndex: this.tasks.length,
|
||||
},
|
||||
results: this.tasks,
|
||||
};
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(msg("Task")),
|
||||
new TableColumn(msg("Status")),
|
||||
new TableColumn(msg("Finished")),
|
||||
];
|
||||
}
|
||||
|
||||
row(item: SystemTask): TemplateResult[] {
|
||||
const nameParts = item.fullName.split(":");
|
||||
nameParts.shift();
|
||||
return [
|
||||
html`<div>${item.name}</div>
|
||||
<small>${nameParts.map((part) => html`<code>${part}</code>`)}</small>`,
|
||||
html`<ak-status-label
|
||||
?good=${item.status === SystemTaskStatusEnum.Successful}
|
||||
good-label=${msg("Finished successfully")}
|
||||
bad-label=${msg("Finished with errors")}
|
||||
></ak-status-label>`,
|
||||
html`<div>${getRelativeTime(item.finishTimestamp)}</div>
|
||||
<small>${item.finishTimestamp.toLocaleString()}</small>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: SystemTask): TemplateResult {
|
||||
return html`<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-log-viewer .logs=${item?.messages}></ak-log-viewer>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
renderToolbarContainer() {
|
||||
return html``;
|
||||
}
|
||||
|
||||
renderTablePagination() {
|
||||
return html``;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ak-sync-status-card")
|
||||
export class SyncStatusCard extends AKElement {
|
||||
@state()
|
||||
@ -29,7 +104,7 @@ export class SyncStatusCard extends AKElement {
|
||||
triggerSync!: () => Promise<unknown>;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFCard];
|
||||
return [PFBase, PFCard, PFTable];
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
@ -40,25 +115,6 @@ export class SyncStatusCard extends AKElement {
|
||||
});
|
||||
}
|
||||
|
||||
renderSyncTask(task: SystemTask): TemplateResult {
|
||||
return html`<li>
|
||||
${(this.syncState?.tasks || []).length > 1 ? html`<span>${task.name}</span>` : nothing}
|
||||
<span
|
||||
><ak-status-label
|
||||
?good=${task.status === SystemTaskStatusEnum.Successful}
|
||||
good-label=${msg("Finished successfully")}
|
||||
bad-label=${msg("Finished with errors")}
|
||||
></ak-status-label
|
||||
></span>
|
||||
<span
|
||||
>${msg(
|
||||
str`Finished ${getRelativeTime(task.finishTimestamp)} (${task.finishTimestamp.toLocaleString()})`,
|
||||
)}</span
|
||||
>
|
||||
<ak-log-viewer .logs=${task?.messages}></ak-log-viewer>
|
||||
</li> `;
|
||||
}
|
||||
|
||||
renderSyncStatus(): TemplateResult {
|
||||
if (this.loading) {
|
||||
return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
|
||||
@ -72,13 +128,7 @@ export class SyncStatusCard extends AKElement {
|
||||
if (this.syncState.tasks.length < 1) {
|
||||
return html`${msg("Not synced yet.")}`;
|
||||
}
|
||||
return html`
|
||||
<ul class="pf-c-list">
|
||||
${this.syncState.tasks.map((task) => {
|
||||
return this.renderSyncTask(task);
|
||||
})}
|
||||
</ul>
|
||||
`;
|
||||
return html`<ak-sync-status-table .tasks=${this.syncState.tasks}></ak-sync-status-table>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
@ -120,6 +170,7 @@ export class SyncStatusCard extends AKElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-sync-status-table": SyncStatusTable;
|
||||
"ak-sync-status-card": SyncStatusCard;
|
||||
}
|
||||
}
|
||||
@ -269,7 +269,7 @@ export class InputPassword extends AKElement {
|
||||
|
||||
toggleElement.setAttribute(
|
||||
"aria-label",
|
||||
msg(masked ? Visibility.Reveal.label : Visibility.Mask.label),
|
||||
masked ? Visibility.Reveal.label : Visibility.Mask.label,
|
||||
);
|
||||
|
||||
const iconElement = toggleElement.querySelector("i")!;
|
||||
@ -285,7 +285,7 @@ export class InputPassword extends AKElement {
|
||||
|
||||
return html`<button
|
||||
${ref(this.toggleVisibilityRef)}
|
||||
aria-label=${msg(label)}
|
||||
aria-label=${label}
|
||||
@click=${this.togglePasswordVisibility}
|
||||
class="pf-c-button pf-m-control"
|
||||
type="button"
|
||||
|
||||
@ -3,7 +3,7 @@ import "@goauthentik/elements/forms/FormElement";
|
||||
import { BaseDeviceStage } from "@goauthentik/flow/stages/authenticator_validate/base";
|
||||
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@ -35,7 +35,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
switch (this.deviceChallenge?.deviceClass) {
|
||||
case DeviceClassesEnum.Email: {
|
||||
const email = this.deviceChallenge.challenge?.email;
|
||||
return msg(`A code has been sent to you via email${email ? ` ${email}` : ""}`);
|
||||
return msg(str`A code has been sent to you via email${email ? ` ${email}` : ""}`);
|
||||
}
|
||||
case DeviceClassesEnum.Sms:
|
||||
return msg("A code has been sent to you via SMS.");
|
||||
@ -70,52 +70,57 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
return html`<ak-empty-state loading> </ak-empty-state>`;
|
||||
}
|
||||
return html`<div class="pf-c-login__main-body">
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
${this.renderUserInfo()}
|
||||
<div class="icon-description">
|
||||
<i class="fa ${this.deviceIcon()}" aria-hidden="true"></i>
|
||||
<p>${this.deviceMessage()}</p>
|
||||
</div>
|
||||
<ak-form-element
|
||||
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||
? msg("Static token")
|
||||
: msg("Authentication code")}"
|
||||
required
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
||||
<form
|
||||
class="pf-c-form"
|
||||
@submit=${(e: Event) => {
|
||||
this.submitForm(e);
|
||||
}}
|
||||
>
|
||||
<!-- @ts-ignore -->
|
||||
<input
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||
? "text"
|
||||
: "numeric"}"
|
||||
pattern="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||
? "[0-9a-zA-Z]*"
|
||||
: "[0-9]*"}"
|
||||
placeholder="${msg("Please enter your code")}"
|
||||
autofocus=""
|
||||
autocomplete="one-time-code"
|
||||
class="pf-c-form-control"
|
||||
value="${PasswordManagerPrefill.totp || ""}"
|
||||
${this.renderUserInfo()}
|
||||
<div class="icon-description">
|
||||
<i class="fa ${this.deviceIcon()}" aria-hidden="true"></i>
|
||||
<p>${this.deviceMessage()}</p>
|
||||
</div>
|
||||
<ak-form-element
|
||||
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||
? msg("Static token")
|
||||
: msg("Authentication code")}"
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
class="pf-c-form__group"
|
||||
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
||||
>
|
||||
<!-- @ts-ignore -->
|
||||
<input
|
||||
type="text"
|
||||
name="code"
|
||||
inputmode="${this.deviceChallenge?.deviceClass ===
|
||||
DeviceClassesEnum.Static
|
||||
? "text"
|
||||
: "numeric"}"
|
||||
pattern="${this.deviceChallenge?.deviceClass ===
|
||||
DeviceClassesEnum.Static
|
||||
? "[0-9a-zA-Z]*"
|
||||
: "[0-9]*"}"
|
||||
placeholder="${msg("Please enter your code")}"
|
||||
autofocus=""
|
||||
autocomplete="one-time-code"
|
||||
class="pf-c-form-control"
|
||||
value="${PasswordManagerPrefill.totp || ""}"
|
||||
required
|
||||
/>
|
||||
</ak-form-element>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
</button>
|
||||
${this.renderReturnToDevicePicker()}
|
||||
</div>
|
||||
</form>
|
||||
</div>`;
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Continue")}
|
||||
</button>
|
||||
${this.renderReturnToDevicePicker()}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
<ul class="pf-c-login__main-footer-links"></ul>
|
||||
</footer>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -72,7 +72,9 @@ export class BaseStage<
|
||||
}
|
||||
return this.host?.submit(object as unknown as Tout).then((successful) => {
|
||||
if (successful) {
|
||||
this.cleanup();
|
||||
this.onSubmitSuccess();
|
||||
} else {
|
||||
this.onSubmitFailure();
|
||||
}
|
||||
return successful;
|
||||
});
|
||||
@ -124,7 +126,11 @@ export class BaseStage<
|
||||
`;
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
onSubmitSuccess(): void {
|
||||
// Method that can be overridden by stages
|
||||
return;
|
||||
}
|
||||
onSubmitFailure(): void {
|
||||
// Method that can be overridden by stages
|
||||
return;
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import { randomId } from "@goauthentik/elements/utils/randomId";
|
||||
import "@goauthentik/flow/FormStatic";
|
||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||
import { P, match } from "ts-pattern";
|
||||
import type { TurnstileObject } from "turnstile-types";
|
||||
import type * as _ from "turnstile-types";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
@ -24,10 +24,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
|
||||
|
||||
interface TurnstileWindow extends Window {
|
||||
turnstile: TurnstileObject;
|
||||
}
|
||||
|
||||
type TokenHandler = (token: string) => void;
|
||||
|
||||
type Dims = { height: number };
|
||||
@ -52,6 +48,8 @@ type CaptchaHandler = {
|
||||
name: string;
|
||||
interactive: () => Promise<unknown>;
|
||||
execute: () => Promise<unknown>;
|
||||
refreshInteractive: () => Promise<unknown>;
|
||||
refresh: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
// A container iframe for a hosted Captcha, with an event emitter to monitor when the Captcha forces
|
||||
@ -119,6 +117,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
this.host.submit({ component: "ak-stage-captcha", token });
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
refreshedAt = new Date();
|
||||
|
||||
@state()
|
||||
activeHandler?: CaptchaHandler = undefined;
|
||||
|
||||
@state()
|
||||
error?: string;
|
||||
|
||||
@ -127,16 +131,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
name: "grecaptcha",
|
||||
interactive: this.renderGReCaptchaFrame,
|
||||
execute: this.executeGReCaptcha,
|
||||
refreshInteractive: this.refreshGReCaptchaFrame,
|
||||
refresh: this.refreshGReCaptcha,
|
||||
},
|
||||
{
|
||||
name: "hcaptcha",
|
||||
interactive: this.renderHCaptchaFrame,
|
||||
execute: this.executeHCaptcha,
|
||||
refreshInteractive: this.refreshHCaptchaFrame,
|
||||
refresh: this.refreshHCaptcha,
|
||||
},
|
||||
{
|
||||
name: "turnstile",
|
||||
interactive: this.renderTurnstileFrame,
|
||||
execute: this.executeTurnstile,
|
||||
refreshInteractive: this.refreshTurnstileFrame,
|
||||
refresh: this.refreshTurnstile,
|
||||
},
|
||||
];
|
||||
|
||||
@ -230,6 +240,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
});
|
||||
}
|
||||
|
||||
async refreshGReCaptchaFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.grecaptcha.reset();
|
||||
}
|
||||
|
||||
async refreshGReCaptcha() {
|
||||
window.grecaptcha.reset();
|
||||
window.grecaptcha.execute();
|
||||
}
|
||||
|
||||
async renderHCaptchaFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
@ -251,6 +270,15 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
);
|
||||
}
|
||||
|
||||
async refreshHCaptchaFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.hcaptcha.reset();
|
||||
}
|
||||
|
||||
async refreshHCaptcha() {
|
||||
window.hcaptcha.reset();
|
||||
window.hcaptcha.execute();
|
||||
}
|
||||
|
||||
async renderTurnstileFrame() {
|
||||
this.renderFrame(
|
||||
html`<div
|
||||
@ -262,13 +290,18 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
}
|
||||
|
||||
async executeTurnstile() {
|
||||
return (window as unknown as TurnstileWindow).turnstile.render(
|
||||
this.captchaDocumentContainer,
|
||||
{
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
},
|
||||
);
|
||||
return window.turnstile.render(this.captchaDocumentContainer, {
|
||||
sitekey: this.challenge.siteKey,
|
||||
callback: this.onTokenChange,
|
||||
});
|
||||
}
|
||||
|
||||
async refreshTurnstileFrame() {
|
||||
(this.captchaFrame.contentWindow as typeof window)?.turnstile.reset();
|
||||
}
|
||||
|
||||
async refreshTurnstile() {
|
||||
window.turnstile.reset();
|
||||
}
|
||||
|
||||
async renderFrame(captchaElement: TemplateResult) {
|
||||
@ -336,16 +369,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
const handlers = this.handlers.filter(({ name }) => Object.hasOwn(window, name));
|
||||
let lastError = undefined;
|
||||
let found = false;
|
||||
for (const { name, interactive, execute } of handlers) {
|
||||
console.debug(`authentik/stages/captcha: trying handler ${name}`);
|
||||
for (const handler of handlers) {
|
||||
console.debug(`authentik/stages/captcha: trying handler ${handler.name}`);
|
||||
try {
|
||||
const runner = this.challenge.interactive ? interactive : execute;
|
||||
const runner = this.challenge.interactive
|
||||
? handler.interactive
|
||||
: handler.execute;
|
||||
await runner.apply(this);
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: handler succeeded`);
|
||||
found = true;
|
||||
this.activeHandler = handler;
|
||||
break;
|
||||
} catch (exc) {
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler failed`);
|
||||
console.debug(`authentik/stages/captcha[${handler.name}]: handler failed`);
|
||||
console.debug(exc);
|
||||
lastError = exc;
|
||||
}
|
||||
@ -370,6 +406,19 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
document.body.appendChild(this.captchaDocumentContainer);
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (!changedProperties.has("refreshedAt") || !this.challenge) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug("authentik/stages/captcha: refresh triggered");
|
||||
if (this.challenge.interactive) {
|
||||
this.activeHandler?.refreshInteractive.apply(this);
|
||||
} else {
|
||||
this.activeHandler?.refresh.apply(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@ -49,6 +49,8 @@ export class IdentificationStage extends BaseStage<
|
||||
|
||||
@state()
|
||||
captchaToken = "";
|
||||
@state()
|
||||
captchaRefreshedAt = new Date();
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
@ -179,12 +181,16 @@ export class IdentificationStage extends BaseStage<
|
||||
this.form.appendChild(totp);
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
onSubmitSuccess(): void {
|
||||
if (this.form) {
|
||||
this.form.remove();
|
||||
}
|
||||
}
|
||||
|
||||
onSubmitFailure(): void {
|
||||
this.captchaRefreshedAt = new Date();
|
||||
}
|
||||
|
||||
renderSource(source: LoginSource): TemplateResult {
|
||||
const icon = renderSourceIcon(source.name, source.iconUrl);
|
||||
return html`<li class="pf-c-login__main-footer-links-item">
|
||||
@ -287,6 +293,7 @@ export class IdentificationStage extends BaseStage<
|
||||
.onTokenChange=${(token: string) => {
|
||||
this.captchaToken = token;
|
||||
}}
|
||||
.refreshedAt=${this.captchaRefreshedAt}
|
||||
embedded
|
||||
></ak-stage-captcha>
|
||||
`
|
||||
|
||||
@ -97,9 +97,7 @@ export class LibraryApplication extends AKElement {
|
||||
return html``;
|
||||
}
|
||||
if (this.application?.launchUrl === "goauthentik.io://providers/rac/launch") {
|
||||
return html`<ak-library-rac-endpoint-launch .app=${this.application}>
|
||||
</ak-library-rac-endpoint-launch>
|
||||
<div class="pf-c-card__header">
|
||||
return html`<div class="pf-c-card__header">
|
||||
<a
|
||||
@click=${() => {
|
||||
this.racEndpointLaunch?.onClick();
|
||||
@ -120,7 +118,9 @@ export class LibraryApplication extends AKElement {
|
||||
>
|
||||
${this.application.name}
|
||||
</a>
|
||||
</div>`;
|
||||
</div>
|
||||
<ak-library-rac-endpoint-launch .app=${this.application}>
|
||||
</ak-library-rac-endpoint-launch>`;
|
||||
}
|
||||
return html`<div class="pf-c-card__header">
|
||||
<a
|
||||
|
||||
@ -165,13 +165,21 @@ class UserInterfacePresentation extends AKElement {
|
||||
}
|
||||
|
||||
return html`<a
|
||||
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
|
||||
href="${globalAK().api.base}if/admin/"
|
||||
slot="extra"
|
||||
>
|
||||
${msg("Admin interface")}
|
||||
</a>`;
|
||||
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
|
||||
href="${globalAK().api.base}if/admin/"
|
||||
slot="extra"
|
||||
>
|
||||
${msg("Admin interface")}
|
||||
</a>
|
||||
<a
|
||||
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none-on-md pf-u-display-block"
|
||||
href="${globalAK().api.base}if/admin/"
|
||||
slot="extra"
|
||||
>
|
||||
${msg("Admin")}
|
||||
</a>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
// The `!` in the field definitions above only re-assure typescript and eslint that the
|
||||
// values *should* be available, not that they *are*. Thus this contract check; it asserts
|
||||
|
||||
@ -59,6 +59,10 @@ export class UserSettingsPage extends AKElement {
|
||||
:host([theme="dark"]) .pf-c-page__main-section {
|
||||
--pf-c-page__main-section--BackgroundColor: transparent;
|
||||
}
|
||||
.pf-c-page__main {
|
||||
min-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@media screen and (min-width: 1200px) {
|
||||
:host {
|
||||
width: 90rem;
|
||||
|
||||
@ -4,29 +4,21 @@ title: Manage applications
|
||||
|
||||
Managing the applications that your team uses involves several tasks, from initially adding the application and provider, to controlling access and visibility of the application, to providing access URLs.
|
||||
|
||||
## Add new applications
|
||||
|
||||
Learn how to add new applications from our video or follow the instructions below.
|
||||
|
||||
### Video
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/broUAWrIWDI;start=22" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
### Instructions
|
||||
|
||||
To add an application to authentik and have it display on users' **My applications** page, you can use the Application Wizard, which creates both the new application and the required provider at the same time.
|
||||
To add an application to authentik and have it display on users' **My applications** page, follow these steps:
|
||||
|
||||
1. Log into authentik as an admin, and navigate to **Applications --> Applications**.
|
||||
1. Log in to authentik as an admin, and open the authentik Admin interface.
|
||||
|
||||
2. Click **Create with Wizard**. (Alternatively, use our legacy process and click **Create**. The legacy process requires that the application and its authentication provider be configured separately.)
|
||||
2. Navigate to **Applications -> Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can create only an application, without a provider, by clicking **Create.)**
|
||||
|
||||
3. In the **New application** wizard, define the application details, the provider type, bindings for the application.
|
||||
3. In the **New application** box, define the application details, the provider type and configuration settings, and bindings for the application.
|
||||
|
||||
- **Application**: provide a name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
|
||||
- **Choose a Provider**: select the provider types for this application.
|
||||
|
||||
- **Configure a Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and any additional required configurations.
|
||||
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and any additional required configurations.
|
||||
|
||||
- **Configure Bindings**: to manage the listing and access to applications on a user's **My applications** page, you can optionally create a [binding](../flows-stages/bindings/index.md) between the application and a specific policy, group, or user. Note that if you do not define any bindings, then all users have access to the application. For more information about user access, refer to our documentation about [authorization](#policy-driven-authorization) and [hiding an application](#hide-applications).
|
||||
|
||||
@ -83,8 +75,8 @@ return {
|
||||
3. Click the **Application entitlements** tab at the top of the page, and then click **Create entitlement**. Provide a name for the entitlement, enter any optional **Attributes**, and then click **Create**.
|
||||
4. In the list locate the entitlement to which you want to bind a user or group, and then **click the caret (>) to expand the entitlement details.**
|
||||
5. In the expanded area, click **Bind existing Group/User**.
|
||||
6. In the **Create Binding** modal box, select either the tab for **Group** or **User**, and then in the drop-down list, select the group or user.
|
||||
7. Optionally, configure additional settings for the binding, and then click **Create** to create the binding and close the modal box.
|
||||
6. In the **Create Binding** box, select either the tab for **Group** or **User**, and then in the drop-down list, select the group or user.
|
||||
7. Optionally, configure additional settings for the binding, and then click **Create** to create the binding and close the box.
|
||||
|
||||
## Hide applications
|
||||
|
||||
|
||||
@ -9,5 +9,5 @@ For instructions to create a binding, refer to the documentation for the specifi
|
||||
- [Bind a stage to a flow](../stages/index.md#bind-a-stage-to-a-flow)
|
||||
- [Bind a policy to a flow or stage](../../../customize/policies/working_with_policies#bind-a-policy-to-a-flow-or-stage)
|
||||
- [Bind users or groups to a specific application with an Application Entitlement](../../applications/manage_apps.md#application-entitlements)
|
||||
- [Bind a policy to a specific application when you create a new app using the Wizard](../../applications/manage_apps.md#instructions)
|
||||
- [Bind a policy to a specific application when you create a new application and provider](../../applications/manage_apps.md#instructions)
|
||||
- [Bind users and groups to a stage binding, to define whether or not that stage is shown](../stages/index.md#bind-users-and-groups-to-a-flows-stage-binding)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Duo authenticator setup stage
|
||||
title: Duo Authenticator Setup stage
|
||||
---
|
||||
|
||||
This stage configures a Duo authenticator. To get the API Credentials for this stage, open your Duo Admin dashboard.
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Email Authenticator Setup stage
|
||||
---
|
||||
|
||||
<span class="badge badge--version">authentik 2025.2+</span>
|
||||
|
||||
This stage configures an email-based authenticator that sends a one-time code to a user's email address for authentication.
|
||||
|
||||
When a user goes through a flow that includes this stage, they are prompted for their email address (if not already set). The user then receives an email with a one-time code, which they enter into the authentik Login panel.
|
||||
|
||||
The email address will be saved and can be used with the [Authenticator validation](../authenticator_validate/index.md) stage for future authentications.
|
||||
|
||||
## Flow integration
|
||||
|
||||
To use the Email Authenticator Setup stage in a flow, follow these steps:
|
||||
|
||||
1. [Create](../../flow/index.md#create-a-custom-flow) a new flow or edit an existing one.
|
||||
2. On the flow's **Stage Bindings** tab, click **Create and bind stage** to create and add the Email Authenticator Setup stage. (If the stage already exists, click **Bind existing stage**.)
|
||||
3. Configure the stage settings as described below.
|
||||
|
||||
- **Name**: provide a descriptive name, such as Email Authenticator Setup.
|
||||
- **Authenticator type name**: define the display name for this stage.
|
||||
- **Use global connection settings**: the stage can be configured in two ways: global settings or stage-specific settings.
|
||||
|
||||
- Enable (toggle on) the **Use global connection settings** option to use authentik's global email configuration. Note that you must already have configured your environment variables to use the global settings. See instructions for [Docker Compose](../../../../install-config/install/docker-compose#email-configuration-optional-but-recommended) and for [Kubernetes](../../../../install-config/install/kubernetes#optional-step-configure-global-email-credentials).
|
||||
|
||||
- If you need different email settings for this stage, disable (toggle off) **Use global connection settings** and configure the following options:
|
||||
|
||||
- **Connection settings**:
|
||||
|
||||
- **SMTP Host**: SMTP server hostname (default: localhost)
|
||||
- **SMTP Port**: SMTP server port number(default: 25)
|
||||
- **SMTP Username**: SMTP authentication username (optional)
|
||||
- **SMTP Password**: SMTP authentication password (optional)
|
||||
- **Use TLS**: Enable TLS encryption
|
||||
- **Use SSL**: Enable SSL encryption
|
||||
- **Timeout**: Connection timeout in seconds (default: 10)
|
||||
- **From Address**: Email address that messages are sent from (default: system@authentik.local)
|
||||
|
||||
- **Stage-specific settings**:
|
||||
|
||||
- **Subject**: Email subject line (default: "authentik Sign-in code")
|
||||
- **Token Expiration**: Time in minutes that the sent token is valid (default: 30)
|
||||
- **Configuration flow**: select the flow to which you are binding this stage.
|
||||
|
||||
4. Click **Update** to complete the creation and binding of the stage to the flow.
|
||||
|
||||
The new Email Authenticator Setup stage now appears on the **Stage Bindings** tab for the flow.
|
||||
@ -28,7 +28,7 @@ For detailed instructions, refer to Google documentation.
|
||||
### Create a Google cloud project
|
||||
|
||||
1. Open the Google Cloud Console (https://cloud.google.com/cloud-console).
|
||||
2. In upper left, click the drop-down box to open the **Select a project** modal box, and then select **New Project**.
|
||||
2. In upper left, click the drop-down box to open the **Select a project** box, and then select **New Project**.
|
||||
3. Create a new project and give it a name like "authentik GWS".
|
||||
4. Use the search bar at the top of your new project page to search for "API Library".
|
||||
5. On the **API Library** page, use the search bar again to find "Chrome Verified Access API".
|
||||
@ -49,7 +49,7 @@ For detailed instructions, refer to Google documentation.
|
||||
|
||||
1. On the **Service accounts** page, click the account that you just created.
|
||||
2. Click the **Keys** tab at top of the page, the click **Add Key -> Create new key**.
|
||||
3. In the Create modal box, select JSON as the key type, and then click **Create**.
|
||||
3. In the Create box, select JSON as the key type, and then click **Create**.
|
||||
A pop-up displays with the private key, and the key is saved to your computer as a JSON file.
|
||||
Later, when you create the stage in authentik, you will add this key in the **Credentials** field.
|
||||
4. On the service account page, click the **Details** tab, and expand the **Advanced settings** area.
|
||||
@ -66,7 +66,7 @@ For detailed instructions, refer to Google documentation.
|
||||
|
||||
2. In the Admin interface, navigate to **Flows -> Stages**.
|
||||
|
||||
3. Click **Create**, and select **Endpoint Authenticator Google Device Trust Connector Stage**, and in the **New stage** modal box, define the following fields:
|
||||
3. Click **Create**, and select **Endpoint Authenticator Google Device Trust Connector Stage**, and in the **New stage** box, define the following fields:
|
||||
|
||||
- **Name**: define a descriptive name, such as "chrome-device-trust".
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: SMS authenticator setup stage
|
||||
title: SMS Authenticator Setup stage
|
||||
---
|
||||
|
||||
This stage configures an SMS-based authenticator using either Twilio, or a generic HTTP endpoint.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Static authenticator setup stage
|
||||
title: Static Authenticator Setup stage
|
||||
---
|
||||
|
||||
This stage configures static Tokens, which can be used as a backup method to time-based OTP tokens.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: TOTP authenticator setup stage
|
||||
title: TOTP Authenticator Setup stage
|
||||
---
|
||||
|
||||
This stage configures a time-based OTP Device, such as Google Authenticator or Authy.
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
---
|
||||
title: Authenticator validation stage
|
||||
title: Authenticator Validation stage
|
||||
---
|
||||
|
||||
This stage validates an already configured Authenticator Device. This device has to be configured using any of the other authenticator stages:
|
||||
|
||||
- [Duo authenticator stage](../authenticator_duo/index.md)
|
||||
- [Email authenticator stage](../authenticator_email/index.md)
|
||||
- [SMS authenticator stage](../authenticator_sms/index.md)
|
||||
- [Static authenticator stage](../authenticator_static/index.md)
|
||||
- [TOTP authenticator stage](../authenticator_totp/index.md)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: WebAuthn authenticator setup stage
|
||||
title: WebAuthn Authenticator Setup stage
|
||||
---
|
||||
|
||||
This stage configures a WebAuthn-based Authenticator. This can either be a browser, biometrics or a Security stick like a YubiKey.
|
||||
|
||||
@ -70,8 +70,8 @@ To bind a user or a group to a stage binding for a specific flow, follow these s
|
||||

|
||||
|
||||
6. In the expanded area, click **Bind existing policy/group/user**.
|
||||
7. In the **Create Binding** modal box, select either the tab for **Group** or **User**.
|
||||
7. In the **Create Binding** box, select either the tab for **Group** or **User**.
|
||||
8. In the drop-down list, select the group or user.
|
||||
9. Optionally, configure additional settings for the binding, and then click **Create** to create the binding and close the modal box.
|
||||
9. Optionally, configure additional settings for the binding, and then click **Create** to create the binding and close the box.
|
||||
|
||||
Learn more about [bindings](../bindings/index.md) and [working with them](../bindings/work_with_bindings.md).
|
||||
|
||||
@ -35,7 +35,7 @@ Any change made to the outpost's associated app or provider immediately triggers
|
||||
- **Applications**: select the applications that you want the outpost to serve
|
||||
- **Advanced settings** (*optional*): For further optional configuration settings, refer to [Configuration](#configuration) below.
|
||||
|
||||
4. Click **Create** to save your new outpost settings and close the modal.
|
||||
4. Click **Create** to save your new outpost settings and close the box.
|
||||
|
||||
Upon creation, a service account and a token is generated. The service account only has permissions to read the outpost and provider configuration. This token is used by the outpost to connect to authentik.
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ As detailed in the steps below, when you add an Entra ID provider in authentik y
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Applications -> Providers**.
|
||||
3. Click **Create**, and in the **New provider** modal box select **Microsoft Entra Provider** as the type and click **Next**.
|
||||
3. Click **Create**, and in the **New provider** box select **Microsoft Entra Provider** as the type and click **Next**.
|
||||
4. Define the following fields:
|
||||
|
||||
- **Name**: define a descriptive name, such as "Entra provider".
|
||||
@ -49,7 +49,7 @@ As detailed in the steps below, when you add an Entra ID provider in authentik y
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Applications -> Applications**.
|
||||
3. Click **Create**, and in the **Create Application** modal box define the following fields:
|
||||
3. Click **Create**, and define the following fields:
|
||||
|
||||
- **Name**: provide a descriptive name.
|
||||
- **Slug**: enter the name of the app as you want it to appear in the URL.
|
||||
|
||||
@ -22,7 +22,7 @@ When adding the Google Workspace provider in authentik, you must define the **Ba
|
||||
|
||||
2. In the Admin interface, navigate to **Applications -> Providers**.
|
||||
|
||||
3. Click **Create**, and select **Google Workspace Provider**, and in the **New provider** modal box, define the following fields:
|
||||
3. Click **Create**, and select **Google Workspace Provider**, and in the **New provider** box, define the following fields:
|
||||
|
||||
- **Name**: define a descriptive name, such as "GWS provider".
|
||||
|
||||
@ -53,7 +53,7 @@ When adding the Google Workspace provider in authentik, you must define the **Ba
|
||||
:::info
|
||||
If you have also configured Google Workspace to log in using authentik following [these](https://docs.goauthentik.io/integrations/services/google/index), then this configuration can be done on the same app by adding this new provider as a backchannel provider on the existing app instead of creating a new app.
|
||||
:::
|
||||
3. Click **Create**, and in the **New provider** modal box, and define the following fields:
|
||||
3. Click **Create**, and in the **New provider** box, and define the following fields:
|
||||
|
||||
- **Slug**: enter the name of the app as you want it to appear in the URL.
|
||||
- **Provider**: when _not_ used in conjunction with the Google SAML configuration should be left empty.
|
||||
|
||||
@ -23,7 +23,7 @@ For detailed instructions, refer to Google documentation.
|
||||
### Create a Google cloud project
|
||||
|
||||
1. Open the Google Cloud Console (https://cloud.google.com/cloud-console).
|
||||
2. In upper left, click the drop-down box to open the **Select a project** modal box, and then select **New Project**.
|
||||
2. In upper left, click the drop-down box to open the **Select a project** box, and then select **New Project**.
|
||||
3. Create a new project and give it a name like "authentik GWS"
|
||||
4. Use the search bar at the top of your new project page to search for "API Library".
|
||||
5. On the **API Library** page, use the search bar again to find "Admin SDK API".
|
||||
@ -44,7 +44,7 @@ For detailed instructions, refer to Google documentation.
|
||||
|
||||
1. On the **Service accounts** page, click the account that you just created.
|
||||
2. Click the **Keys** tab at top of the page, the click **Add Key -> Create new key**.
|
||||
3. In the Create modal box, select JSON as the key type, and then click **Create**.
|
||||
3. In the Create box, select JSON as the key type, and then click **Create**.
|
||||
A pop-up displays with the private key, and the key is saved to your computer as a JSON file.
|
||||
Later, when you create your authentik provider for Google Workspace, you will add this key in the **Credentials** field.
|
||||
4. On the service account page, click the **Details** tab, and expand the **Advanced settings** area.
|
||||
@ -52,7 +52,7 @@ For detailed instructions, refer to Google documentation.
|
||||
6. Log in to the Admin Console, and then navigate to **Security -> Access and data control -> API controls**.
|
||||
7. On the **API controls** page, click **Manage Domain Wide Delegation**.
|
||||
8. On the **Domain Wide Delegation** page, click **Add new**.
|
||||
9. In the **Add a new client ID** modal box, paste in the Client ID that you copied from the Admin console earlier (the value from the downloaded JSON file) and paste in the following scope documents:
|
||||
9. In the **Add a new client ID** box, paste in the Client ID that you copied from the Admin console earlier (the value from the downloaded JSON file) and paste in the following scope documents:
|
||||
- `https://www.googleapis.com/auth/admin.directory.user`
|
||||
- `https://www.googleapis.com/auth/admin.directory.group`
|
||||
- `https://www.googleapis.com/auth/admin.directory.group.member`
|
||||
|
||||
@ -11,7 +11,7 @@ Providers are the "other half" of [applications](../applications/index.md). They
|
||||
|
||||
Applications can use additional providers to augment the functionality of the main provider. For more information, see [Backchannel providers](../applications/manage_apps.md#backchannel-providers).
|
||||
|
||||
You can create a new provider in the Admin interface, or you can use the [Application wizard](../applications/manage_apps.md#instructions) to create a new application and its provider at the same time.
|
||||
You can create a new provider in the Admin interface, or you can use the [**Create with provider** option](../applications/manage_apps.md#instructions) to create a new application and its provider at the same time.
|
||||
|
||||
When you create certain types of providers, you need to select specific [flows](../flows-stages/flow/index.md) to apply to users who access authentik via the provider. To learn more, refer to our [default flow documentation](../flows-stages/flow/examples/default_flows.md).
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ Note: The `default-authentication-flow` validates MFA by default, and currently
|
||||
|
||||
### Create LDAP Application & Provider
|
||||
|
||||
1. Create the LDAP Application under _Applications_ -> _Applications_ -> _Create With Wizard_ and name it `LDAP`.
|
||||
1. Create the LDAP Application under _Applications_ -> _Applications_ -> _Create With provider_ and name it `LDAP`.
|
||||

|
||||

|
||||
|
||||
@ -55,7 +55,7 @@ Note: The `default-authentication-flow` validates MFA by default, and currently
|
||||
1. Navigate to the LDAP Provider under _Applications_ -> _Providers_ -> `Provider for LDAP`.
|
||||
2. Switch to the _Permissions_ tab.
|
||||
3. Click the _Assign to new user_ button to select a user to assign the full directory search permission to.
|
||||
4. Select the `ldapservice` user in the modal by typing in its username. Select the _Search full LDAP directory_ permission and click _Assign_
|
||||
4. Select the `ldapservice` user typing in its username. Select the _Search full LDAP directory_ permission and click _Assign_
|
||||
|
||||
### Create LDAP Outpost
|
||||
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
title: Create an OAuth2 provider
|
||||
---
|
||||
|
||||
To add a provider (and the application that uses the provider for authentication) use the Application Wizard, which creates both the new application and the required provider at the same time. For typical scenarios, authentik recommends that you use the Wizard to create both the application and the provider together. (Alternatively, use our legacy process: navigate to **Applications --> Providers**, and then click **Create**.)
|
||||
To add a provider (and the application that uses the provider for authentication) use the ** Create with provider** option, which creates both the new application and the required provider at the same time. For typical scenarios, authentik recommends that you create both the application and the provider together. (Alternatively, use our legacy process: navigate to **Applications --> Providers**, and then click **Create**.)
|
||||
|
||||
1. Log into authentik as an admin, and navigate to **Applications --> Applications**.
|
||||
1. Log in to authentik as an admin, and open the authentik Admin interface.
|
||||
|
||||
2. Click **Create with Wizard**.
|
||||
2. Navigate to **Applications -> Applications** and click **Create with provider** to create an application and provider pair. (Alternatively you can create only an application, without a provider, by clicking **Create**.)
|
||||
|
||||
3. In the **New application** wizard, define the application details, and then click **Next**.
|
||||
3. In the **New application** box, define the application details, and then click **Next**.
|
||||
|
||||
4. Select the **Provider Type** of **OAuth2/OIDC**, and then click **Next**.
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ The first step is to create the RAC app and provider.
|
||||
|
||||
2. In the Admin interface, navigate to **Applications -> Applications**.
|
||||
|
||||
3. Click **Create with Wizard**. Follow the [instructions](../../applications/manage_apps.md#instructions) to create your RAC application and provider.
|
||||
3. Click **Create with provider**. Follow the [instructions](../../applications/manage_apps.md#instructions) to create your RAC application and provider.
|
||||
|
||||
### Step 2. Create RAC property mapping
|
||||
|
||||
@ -36,7 +36,7 @@ Next, you need to add a property mapping for each of the remote machines you wan
|
||||
|
||||
2. On the **Property Mappings** page, click **Create**.
|
||||
|
||||
3. On the **New property mapping** modal, set the following:
|
||||
3. On the **New property mapping** box, set the following:
|
||||
|
||||
- **Select Type**: RAC Property Mappings
|
||||
- **Create RAC Property Mapping**:
|
||||
@ -52,7 +52,7 @@ Next, you need to add a property mapping for each of the remote machines you wan
|
||||
- Advanced settings:
|
||||
- **Expressions**: optional, using Python you can define custom [expressions](../property-mappings/expression.mdx).
|
||||
|
||||
4. Click **Finish** to save your settings and close the modal.
|
||||
4. Click **Finish** to save your settings and close the box.
|
||||
|
||||
### Step 3. Create Endpoints for the Provider
|
||||
|
||||
@ -64,7 +64,7 @@ Finally, you need to create an endpoint for each remote machine. Endpoints are d
|
||||
|
||||
3. On the Provider page, under **Endpoints**, click **Create**.
|
||||
|
||||
4. On the **Create Endpoint** modal, provide the following settings:
|
||||
4. On the **Create Endpoint** box, provide the following settings:
|
||||
|
||||
- **Name**: define a name for the endpoint, perhaps include the type of connection (RDP, SSH, VNC)
|
||||
- **Protocol**: select the appropriate protocol
|
||||
@ -73,7 +73,7 @@ Finally, you need to create an endpoint for each remote machine. Endpoints are d
|
||||
- **Property mapping**: select either the property mapping that you created in Step 2, or use one of the default settings.
|
||||
- **Advance settings**: optional
|
||||
|
||||
5. Click **Create** to save your settings and close the modal.
|
||||
5. Click **Create** to save your settings and close the box.
|
||||
|
||||
### Access the remote machine
|
||||
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Configure an SSF provider
|
||||
---
|
||||
|
||||
The workflow to implement an SSF provider as a [backchannel provider](../../applications/manage_apps#backchannel-providers) for an application/provider pair is as follows:
|
||||
|
||||
1. Create the SSF provider (which serves as the backchannel provider).
|
||||
2. Create an OIDC provider (which serves as the protocol provider for the application).
|
||||
3. Create the application, and assign both the OIDC provider and the SSF provider.
|
||||
|
||||
## Create the SSF provider
|
||||
|
||||
1. Log in to authentik as an admin, and in the Admin interface navigate to **Applications -> Providers**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. In the modal, select the **Provider Type** of **SSF**, and then click **Next**.
|
||||
|
||||
4. On the **New provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
|
||||
|
||||
5. Click **Finish** to create and save the provider.
|
||||
|
||||
## Create the OIDC provider
|
||||
|
||||
1. Log in to authentik as an admin, and in the Admin interface navigate to **Applications -> Providers**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. In the modal, select the **Provider Type** of **OIDC**, and then click **Next**.
|
||||
|
||||
4. Define the settings for the provider, and then click **Finish** to save the new provider.
|
||||
|
||||
## Create the application
|
||||
|
||||
1. Log in to authentik as an admin, and in the Admin interface navigate to **Applications -> Applications**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
3. Define the settings for the application:
|
||||
|
||||
- **Name**: define a descriptive name ofr the application.
|
||||
- **Slug**: optionally define the internal application name used in URLs.
|
||||
- **Group**: optionally select a group that you want to have access to this application.
|
||||
- **Provider**: select the OIDC provider that you created.
|
||||
- **Backchannel Providers**: select the SSF provider you created.
|
||||
- **Policy engine mode**: define policy-based access.
|
||||
- **UI Settings**: optionally define a launch URL, an icon, and other UI elements.
|
||||
|
||||
4. Click **Create** to save the new application.
|
||||
|
||||
The new application, with its OIDC provider and the backchannel SFF provider, should now appear in your list of Applications.
|
||||
48
website/docs/add-secure-apps/providers/ssf/index.md
Normal file
48
website/docs/add-secure-apps/providers/ssf/index.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Shared Signals Framework (SSF) Provider
|
||||
sidebar_label: SSF Provider
|
||||
---
|
||||
|
||||
<span class="badge badge--preview">Preview</span>
|
||||
<span class="badge badge--version">authentik 2025.2+</span>
|
||||
|
||||
|
||||
Shared Signals Framework (SSF) is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected, secure webhooks.
|
||||
|
||||
## About Shared Signals Framework
|
||||
|
||||
In authentik, an SSF provider allows applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
|
||||
|
||||
Events in authentik that are tracked via SSF include when an MFA device is added or removed, logouts, sessions being revoked by Admin or user clicking logout, or credentials changed.
|
||||
|
||||
## Example use cases
|
||||
|
||||
A common use case for SSF is when an Admin wants to know if a user logs out of authentik, so that the user is then also automaticlaly logged out of all other work-focused applications.
|
||||
|
||||
Another example use case is when an application uses SSF to subscribe to authorization events because the application needs to know if a user changed their password in authentik. If a user did change their password, then the application receives a POST request to write the fact that the password was changed.
|
||||
|
||||
## About using SSF in authentik
|
||||
|
||||
Let's look at a few details about using SSF in authentik.
|
||||
|
||||
The SSF provider in authentik serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application. Thus you will still need to [create a typical application/provider pair](../../applications/manage_apps#instructions) (using an OIDC provider), and when creating the application, assign the SSF provider as a backchannel provider.
|
||||
|
||||
When an authentik Admin [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
|
||||
|
||||
### The application (the receiver)
|
||||
|
||||
Within the application, the admin creates an SSF stream (which comprises all the signals that the app wants to subscribe to) and defines the audience, called `aud` in the specification (the URL that identifies the stream). A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can change or delete the stream.
|
||||
|
||||
Note that authentik doesn't specify which events to subscribe to; instead the application defines which they want to listen for.
|
||||
|
||||
### authentik (the transmitter)
|
||||
|
||||
To configure authentik as a shared signals transmitter, the authentik Admin [creates a new provider](./create-ssf-provider), selecting the type "SSF", to serve as the backchannelprovider for the application.
|
||||
|
||||
When creating the SSF provider you will need to select a signing key. This is the key that the Security Event Tokens (SET) is signed with.
|
||||
|
||||
Optionally, you can specify a event retention time period: this value determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
|
||||
|
||||
:::info
|
||||
Be aware that the SET events are different events than those displayed in the authentik Admin interface under **Events**.
|
||||
:::
|
||||
@ -26,6 +26,22 @@ See [Expression Policy](./expression.mdx).
|
||||
|
||||
Use this policy for simple GeoIP lookups, such as country or ASN matching. (For a more advanced GeoIP lookup, use an [Expression policy](./expression.mdx).)
|
||||
|
||||
With the GeoIP policy, you can use the **Distance Settings** options to set travel "expectations" and control login attempts based on GeoIP location. The GeoIP policy calculates the values defined for travel distances (in kilometers), and then either passes or fails based on the results. If the GeoIP policy failed, the current login attempt is not allowed.
|
||||
|
||||
- **Maximum distance**: define the allowed maximum distance between a login's initial GeoIP location and the GeoIP location of a subsequent login attempt.
|
||||
|
||||
- **Distance tolerance**: optionally, add an additional "tolerance" distance. This value is added to the **Maximum distance** value, then the total is used in the calculations that determine if the policy fails or passes.
|
||||
|
||||
- **Historical Login Count**: define the number of login events that you want to use for the distance calculations. For example, with the default value of 5, the policy will check the distance between each of the past 5 login attempts, and if any of those distances exceed the **Maximum distance** PLUS the **Distance tolerance**, then the policy will fail and the current login attempt will not be allowed.
|
||||
|
||||
- **Check impossible travel**: this option, when enabled, provides an additional layer of calculations to the policy. With Impossible travel, a built-in value of 1,000 kilometers is used as the base distance. This distance, PLUS the value defined for **Impossible travel tolerance**, is the maximum allowed distance for the policy to pass. Note that the value defined in **Historical Login Count** (the number of login events to check) is also used for Impossible travel calculations.
|
||||
|
||||
- **Impossible travel tolerance**: optionally, you can add an additional "tolerance" distance. This value is added to the built-in allowance of 1000 kilometers per hour, then the total is used in the calculations that run against each of the login events (to determine if the travel would have been possible in the amount of time since the previous login event) to determine if the policy fails or passes.
|
||||
|
||||
:::info
|
||||
GeoIP is included in every release of authentik and does not require any additional setup for creating GeoIP policies. For information about advanced uses (configuring your own database, etc.) and system management of GeoIP data, refer to our [GeoIP documentation](../../sys-mgmt/ops/geoip.mdx).
|
||||
:::
|
||||
|
||||
### Password-Expiry Policy
|
||||
|
||||
This policy can enforce regular password rotation by expiring set passwords after a finite amount of time. This forces users to set a new password.
|
||||
|
||||
@ -8,7 +8,7 @@ authentik provides several [standard policy types](./index.md#standard-policies)
|
||||
|
||||
We also document how to use a policy to [whitelist email domains](./expression/whitelist_email.md) and to [ensure unique email addresses](./expression/unique_email.md).
|
||||
|
||||
To learn more see also [bindings](../../add-secure-apps/flows-stages/bindings/index.md) and how to use the [authentik Wizard to bind policy bindings to the new application](../../add-secure-apps/applications/manage_apps.md#add-new-applications) (for example, to configure application-specific access).
|
||||
To learn more see also [bindings](../../add-secure-apps/flows-stages/bindings/index.md) and how to [bind policy bindings to a new application when yo create the application](../../add-secure-apps/applications/manage_apps.md#instructions) (for example, to configure application-specific access).
|
||||
|
||||
## Create a policy
|
||||
|
||||
|
||||
BIN
website/docs/developer-docs/setup/debug_vscode.png
Normal file
BIN
website/docs/developer-docs/setup/debug_vscode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 805 KiB |
53
website/docs/developer-docs/setup/debugging.md
Normal file
53
website/docs/developer-docs/setup/debugging.md
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Debugging authentik
|
||||
---
|
||||
|
||||
This page describes how to debug different components of an authentik instance, running either in production or in a development setup. To learn more about the structure of authentik, refer to our [architecture documentation](../../core/architecture).
|
||||
|
||||
## authentik Server & Worker (Python)
|
||||
|
||||
The majority of the authentik codebase is in Python, running in Gunicorn for the server and Celery for the worker. These instructions show how this code can be debugged/inspected. The local debugging setup requires a setup as described in [Full development environment](./full-dev-environment.mdx)
|
||||
|
||||
Note that authentik uses [debugpy](https://github.com/microsoft/debugpy), which relies on the "Debug Adapter Protocol" (DAP). These instructions demonstrate debugging using [Visual Studio Code](https://code.visualstudio.com/), however they should be adaptable to other editors that support DAP.
|
||||
|
||||
To enable the debugging server, set the environment variable `AUTHENTIK_DEBUGGER` to `true`. This will launch the debugging server (by default on port _9901_).
|
||||
|
||||
With this setup in place, you can set Breakpoints in VS Code. To connect to the debugging server, run the command `> Debug: Start Debugging" in VS Code.
|
||||
|
||||

|
||||
|
||||
:::info
|
||||
Note that due to the Python debugger for VS Code, when a Python file in authentik is saved and the Django process restarts, you must manually reconnect the Debug session. Automatic re-connection is not supported for the Python debugger (see [here](https://github.com/microsoft/vscode-python/issues/19998) and [here](https://github.com/microsoft/vscode-python/issues/1182)).
|
||||
:::
|
||||
|
||||
#### Debugging in containers
|
||||
|
||||
When debugging an authentik instance running in containers, there are some additional steps that need to be taken in addition to the steps above.
|
||||
|
||||
A local clone of the authentik repository is required to be able to set breakpoints in the code. The locally checked out repository must be on the same version/commit as the authentik version running in the containers. To checkout version 2024.12.3 for example, you can run `git checkout version/2024.12.3`.
|
||||
|
||||
The debug port needs to be accessible on the local machine. By default, this is port 9901. Additionally, the container being debugged must be started as `root`, because additional dependencies need to be installed on startup.
|
||||
|
||||
When running in Docker Compose, a file `docker-compose.override.yml` can be created next to the authentik docker-compose.yml file to expose the port, change the user, and enable debug mode.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# Replace `server` with `worker` to debug the worker container.
|
||||
server:
|
||||
user: root
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
AUTHENTIK_DEBUGGER: "true"
|
||||
AUTHENTIK_LOG_LEVEL: "debug"
|
||||
ports:
|
||||
- 9901:9901
|
||||
```
|
||||
|
||||
After re-creating the containers with `AUTHENTIK_DEBUGGER` set to `true` and the port mapped, the steps are identical to the steps above.
|
||||
|
||||
If the authentik instance is running on a remote server, the `.vscode/launch.json` file needs to be adjusted to point to the IP of the remote server. Alternatively, it is also possible to forward the debug port via an SSH tunnel, using `-L 9901:127.0.0.1:9901`.
|
||||
|
||||
## authentik Server / Outposts (Golang)
|
||||
|
||||
Outposts, as well as some auxiliary code of the authentik server, are written in Go. These components can be debugged using standard Golang tooling, such as [Delve](https://github.com/go-delve/delve).
|
||||
@ -2,9 +2,9 @@
|
||||
title: Full development environment
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
|
||||
|
||||
## Requirements
|
||||
|
||||
@ -70,7 +70,9 @@ instructions](https://golangci-lint.run/welcome/install/#other-ci).
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="windows">[We request community input on running the full dev environment on Windows]</TabItem>
|
||||
<TabItem value="windows">
|
||||
[We request community input on running the full dev environment on Windows]
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
@ -77,7 +77,7 @@ To check if your config has been applied correctly, you can run the following co
|
||||
- `AUTHENTIK_POSTGRESQL__SSLCERT`: Path to x509 client certificate to authenticate to server
|
||||
- `AUTHENTIK_POSTGRESQL__SSLKEY`: Path to private key of `SSLCERT` certificate
|
||||
- `AUTHENTIK_POSTGRESQL__CONN_MAX_AGE`: Database connection lifetime. Defaults to `0` (no persistent connections). Can be set to `null` for unlimited persistent connections. See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/settings/#conn-max-age) for more details.
|
||||
- `AUTHENTIK_POSTGRESQL__CONN_HEALTH_CHECK`: Existing persistent database connections will be health checked before they are reused if set to `true`. Defaults to `false`. See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/settings/#conn-health-checks) for more details.
|
||||
- `AUTHENTIK_POSTGRESQL__CONN_HEALTH_CHECKS`: Existing persistent database connections will be health checked before they are reused if set to `true`. Defaults to `false`. See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/settings/#conn-health-checks) for more details.
|
||||
- `AUTHENTIK_POSTGRESQL__DISABLE_SERVER_SIDE_CURSORS`: Disable server side cursors when set to `true`. Defaults to `false`. See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/settings/#disable-server-side-cursors) for more details.
|
||||
|
||||
The PostgreSQL settings `HOST`, `PORT`, `USER`, and `PASSWORD` support hot-reloading. Adding and removing read replicas doesn't support hot-reloading.
|
||||
@ -108,7 +108,7 @@ The same PostgreSQL settings as described above are used for each read replica.
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__SSLCERT`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__SSLKEY`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__CONN_MAX_AGE`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__CONN_HEALTH_CHECK`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__CONN_HEALTH_CHECKS`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__DISABLE_SERVER_SIDE_CURSORS`
|
||||
|
||||
### Using a PostgreSQL connection pooler (PgBouncer or PgPool)
|
||||
@ -125,7 +125,7 @@ When your PostgreSQL database(s) are running behind a connection pooler, like Pg
|
||||
|
||||
Using a connection pooler in transaction pool mode (e.g. PgPool, or PgBouncer in transaction or statement pool mode) requires disabling server-side cursors, so this setting must be set to `false`.
|
||||
|
||||
Additionally, you can set `AUTHENTIK_POSTGRESQL__CONN_HEALTH_CHECK` to perform health checks on persistent database connections before they are reused.
|
||||
Additionally, you can set `AUTHENTIK_POSTGRESQL__CONN_HEALTH_CHECKS` to perform health checks on persistent database connections before they are reused.
|
||||
|
||||
## Redis Settings
|
||||
|
||||
@ -175,6 +175,7 @@ Additionally, you can set `AUTHENTIK_POSTGRESQL__CONN_HEALTH_CHECK` to perform h
|
||||
- `AUTHENTIK_LISTEN__LDAPS`: Listening address:port (e.g. `0.0.0.0:6636`) for LDAPS (Applies to LDAP outpost)
|
||||
- `AUTHENTIK_LISTEN__METRICS`: Listening address:port (e.g. `0.0.0.0:9300`) for Prometheus metrics (Applies to All)
|
||||
- `AUTHENTIK_LISTEN__DEBUG`: Listening address:port (e.g. `0.0.0.0:9900`) for Go Debugging metrics (Applies to All)
|
||||
- `AUTHENTIK_LISTEN__DEBUG_PY`: Listening address:port (e.g. `0.0.0.0:9901`) for Python debugging server (Applies to Server, see [Debugging](../../developer-docs/setup/debugging.md))
|
||||
- `AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS`: List of comma-separated CIDRs that proxy headers should be accepted from (Applies to Server)
|
||||
|
||||
Defaults to `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fe80::/10`, `::1/128`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
23
website/docs/security/cves/CVE-2025-29928.md
Normal file
23
website/docs/security/cves/CVE-2025-29928.md
Normal file
@ -0,0 +1,23 @@
|
||||
# CVE-2025-29928
|
||||
|
||||
## Deletion of sessions did not revoke sessions when using database session storage
|
||||
|
||||
### Summary
|
||||
|
||||
When authentik was configured to use the database for session storage (which is a non-default setting), deleting sessions via the Web Interface or the API would not revoke the session and the session holder would continue to have access to authentik.
|
||||
|
||||
This also affects automatic session deletion when a user is set to inactive or a user is deleted.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2025.2.3 and 2024.12.4 fix this issue.
|
||||
|
||||
### Workarounds
|
||||
|
||||
Switching to the cache-based session storage until the authentik instance can be upgraded is recommended.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
|
||||
@ -51,12 +51,12 @@ To assign or remove _object_ permissions for a specific user:
|
||||
1. Click the **User Object Permissions** tab, and then click **Assign to new user**.
|
||||
2. In the **User** drop-down, select the user object.
|
||||
3. Use the toggles to set which permissions on that selected user object you want to grant to (or remove from) the specific user.
|
||||
4. Click **Assign** to save your settings and close the modal.
|
||||
4. Click **Assign** to save your settings and close the box.
|
||||
5. To assign or remove permissions that another _role_ has on this specific user:
|
||||
1. Click the **Role Object Permissions** tab, and then click **Assign to new role**.
|
||||
2. In the **User** drop-down, select the user object.
|
||||
3. Use the toggles to set which permissions you want to grant to (or remove from) the selected role.
|
||||
4. Click **Assign** to save your settings and close the modal.
|
||||
4. Click **Assign** to save your settings and close the box.
|
||||
|
||||
To assign or remove _global_ permissions for a user:
|
||||
|
||||
@ -65,8 +65,8 @@ To assign or remove _global_ permissions for a user:
|
||||
3. Click the **Permissions** tab at the top of the page.
|
||||
4. Click **Assigned Global Permissions** to the left.
|
||||
5. In the **Assign permissions** area, click **Assign Permission**.
|
||||
6. In the **Assign permission to user** modal box, click the plus sign (**+**) and then click the checkbox beside each permission that you want to assign to the user. To remove permissions, deselect the checkbox.
|
||||
7. Click **Add**, and then click **Assign** to save your changes and close the modal.
|
||||
6. In the **Assign permission to user** box, click the plus sign (**+**) and then click the checkbox beside each permission that you want to assign to the user. To remove permissions, deselect the checkbox.
|
||||
7. Click **Add**, and then click **Assign** to save your changes and close the box.
|
||||
|
||||
### Assign or remove permissions on a specific group
|
||||
|
||||
@ -84,12 +84,12 @@ To assign or remove _object_ permissions on a specific group by users and roles:
|
||||
1. Click **User Object Permissions** to the left, and then click **Assign to new user**.
|
||||
2. In the **User** drop-down, select the user object.
|
||||
3. Use the toggles to set which permissions on that selected group you want to grant to (or remove from) the specific user.
|
||||
4. Click **Assign** to save your settings and close the modal.
|
||||
4. Click **Assign** to save your settings and close the box.
|
||||
4. To assign or remove permissions that another _role_ has on this specific group:
|
||||
1. Click **Role Object Permissions** to the left, and then click **Assign to new role**.
|
||||
2. In the **Role** drop-down, select the role.
|
||||
3. Use the toggles to set which permissions you want to grant to (or remove from ) the selected role.
|
||||
4. Click **Assign** to save your settings and close the modal.
|
||||
4. Click **Assign** to save your settings and close the box.
|
||||
|
||||
### Assign or remove permissions for a specific role
|
||||
|
||||
@ -102,12 +102,12 @@ To assign or remove _object_ permissions for a specific role:
|
||||
1. Click **User Object Permissions** to the left, and then click **Assign to new user**.
|
||||
2. In the **User** drop-down, select the user object.
|
||||
3. Use the toggles to set which permissions on that role you want to grant to (or remove from) the selected user.
|
||||
4. Click **Assign** to save your settings and close the modal.
|
||||
4. Click **Assign** to save your settings and close the box.
|
||||
4. To assign or remove permissions that another _role_ has on this specific group:
|
||||
1. Click **Role Object Permissions** to the left, and then click **Assign to new role**.
|
||||
2. In the **Role** drop-down, select the role.
|
||||
3. Use the toggles to set which permissions you want to grant to (or remove from) the selected role.
|
||||
4. Click **Assign** to save your settings and close the modal.
|
||||
4. Click **Assign** to save your settings and close the box.
|
||||
|
||||
To assign or remove _global_ permissions for a role:
|
||||
|
||||
@ -115,8 +115,8 @@ To assign or remove _global_ permissions for a role:
|
||||
2. Select a specific role by clicking on the role's name.
|
||||
3. Click the **Permissions** tab at the top of the page.
|
||||
4. Click **Assigned Global Permissions** to the left, and then click **Assign Permission**.
|
||||
5. In the **Assign permissions to role** modal, click the plus sign (**+**) and then click the checkbox beside each permission that you want to assign to the role. To remove permissions, deselect the checkbox.
|
||||
6. Click **Assign** to save your changes and close the modal.
|
||||
5. In the **Assign permissions to role** box, click the plus sign (**+**) and then click the checkbox beside each permission that you want to assign to the role. To remove permissions, deselect the checkbox.
|
||||
6. Click **Assign** to save your changes and close the box.
|
||||
|
||||
### Assign or remove flow permissions
|
||||
|
||||
@ -129,4 +129,4 @@ To assign or remove _global_ permissions for a role:
|
||||
|
||||
1. Go to the Admin interface and navigate to **Flows and Stages -> Stagess**.
|
||||
2. On the row for the specific stage that you want to manage permissions, click the **lock icon**.
|
||||
3. On the **Update Permissions** modal window, you can add or remove the assigned permissions using the **User Object Permissions** and the **Role Object Permissions** tabs.
|
||||
3. On the **Update Permissions** box, you can add or remove the assigned permissions using the **User Object Permissions** and the **Role Object Permissions** tabs.
|
||||
|
||||
@ -11,7 +11,7 @@ To create a new group, follow these steps:
|
||||
|
||||
1. In the Admin interface, navigate to **Directory > Groups**.
|
||||
2. Click **Create** at the top of the Groups page.
|
||||
3. In the Create modal, define the following:
|
||||
3. In the Create box, define the following:
|
||||
- **Name** of the group
|
||||
- Whether or not users in that group will all be **super-users** (means anyone in that group has all permissions on everything)
|
||||
- The **Parent** group
|
||||
@ -25,9 +25,11 @@ To create a super-user, you need to add the user to a group that has super-user
|
||||
|
||||
## Modify a group
|
||||
|
||||
To edit the group's name, parent group, whether or not the group is for superusers, associated roles, and any custom attributes, click the Edit icon beside the role's name. Make the changes, and then click **Update**.
|
||||
To edit the group's name, parent group, whether the group grants superuser permissions, associated roles, and any custom attributes, click the Edit icon beside the role's name. Make the changes and then click **Update**.
|
||||
|
||||
To [add or remove users](../user/user_basic_operations.md#add-a-user-to-a-group) from the group, or to manage permissions assigned to the group, click on the name of the group to go to the group's detail page.
|
||||
Starting with authentik version 2025.2, the permission to change super-user status has been separated from the permission required to change the group. Now, the `Enable superuser status` and `Disable superuser status` permissions are explicitly required to enable and disable the super-user status.
|
||||
|
||||
To [add or remove users](../user/user_basic_operations.md#add-a-user-to-a-group) from the group, or to manage permissions assigned to the group, click on the name of the group to go to the group's detail page and then click on the **Permissions** tab.
|
||||
|
||||
For more information about permissions, refer to [Assign or remove permissions for a specific group](../access-control/manage_permissions.md#assign-or-remove-permissions-on-a-specific-group).
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ In authentik, we assign roles to groups, not to individual users.
|
||||
To create a new role, follow these steps:
|
||||
|
||||
1. In the Admin interface, navigate to **Directory > Roles**.
|
||||
2. Click **Create**, enter the name of the role, and then click **Create** in the modal.
|
||||
2. Click **Create**, enter the name of the role, and then click **Create** in the box.
|
||||
3. Next, [assign permissions to the role](../access-control/manage_permissions.md#assign-or-remove-permissions-for-a-specific-role).
|
||||
|
||||
## Modify a role
|
||||
@ -44,5 +44,5 @@ In authentik, each role can only be applied to a single group at a time.
|
||||
1. To assign the role to a group, navigate to **Directory -> Groups**.
|
||||
2. Click the name of the group to which you want to add a role.
|
||||
3. On the group's detail page, on the Overview tab, click **Edit** in the **Group Info** area.
|
||||
4. On the **Update Group** modal, in the **Roles** field, select the roles you want to assign to the group from the list of **Available Roles** in the left box (you can select multiple roles at once by holding the Shift key while selecting the roles), and then click the appropriate arrow icon to move them into the **Selected Roles** box.
|
||||
5. Click **Update** to add the role(s) and close the modal.
|
||||
4. On the **Update Group** box, in the **Roles** field, select the roles you want to assign to the group from the list of **Available Roles** in the left box (you can select multiple roles at once by holding the Shift key while selecting the roles), and then click the appropriate arrow icon to move them into the **Selected Roles** box.
|
||||
5. Click **Update** to add the role(s) and close the box.
|
||||
|
||||
@ -8,7 +8,7 @@ The base SCIM URL is in the format of `https://authentik.company/source/scim/<so
|
||||
|
||||
## First steps
|
||||
|
||||
To set up an SCIM source, log in as an administrator into authentik. Navigate to **Directory->Federation & Social login**, and click on **Create**. Select the **SCIM Source** type in the wizard, and give the source a name.
|
||||
To set up an SCIM source, log in as an administrator into authentik. Navigate to **Directory->Federation & Social login**, and click on **Create**. Select the **SCIM Source** type, and give the source a name.
|
||||
|
||||
After the source is created, click on the name of the source in the list, and you will see the **SCIM Base URL** which is used by the SCIM client. Use the **Click to copy token** button to copy the token which is used by the client to authenticate SCIM requests.
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ Finally, you need to publish the Facebook app.
|
||||
|
||||
1. Log into authentik as admin, and then navigate to **Directory -> Federation & Social login**
|
||||
2. Click **Create**.
|
||||
3. In the **New Source** modal box, for **Select type** select **Facebook OAuth Source** and then click **Next**.
|
||||
3. In the **New Source** box, for **Select type** select **Facebook OAuth Source** and then click **Next**.
|
||||
4. Define the following fields:
|
||||
- **Name**: provide a descriptive name
|
||||
- **Slug**: leave default value (If you choose a different slug then the default, the URL will need to be updated to reflect the change)
|
||||
@ -65,7 +65,7 @@ Finally, you need to publish the Facebook app.
|
||||
- **Flow settings**
|
||||
- **Authentication flow**: leave the default `default-source-authentication` option.
|
||||
- **Enrollment flow**: leave the default `default-source-enrollment` option.
|
||||
5. Click **Finish** to save your settings and close the modal box.
|
||||
5. Click **Finish** to save your settings and close the box.
|
||||
|
||||
You now have Facebook as a source. Verify by checking that appears on the **Directory -> Federation & Social login** page in authentik.
|
||||
|
||||
|
||||
@ -138,7 +138,7 @@ Start by logging into your authentik instance as an administrator and navigating
|
||||
|
||||
In the Admin interface, navigate to **Directory -> Federation & Social login** and press **Create**.
|
||||
|
||||
In the **New source** modal, choose **SAML Source** and continue by filling in the following fields:
|
||||
In the **New source** box, choose **SAML Source** and continue by filling in the following fields:
|
||||
|
||||
| Field | Value |
|
||||
| ----- | ---------------- |
|
||||
|
||||
@ -31,7 +31,7 @@ At the top of the Flows page, click **Import**, and then select the `flows-enrol
|
||||
|
||||
**Step 3. Create the invitation object**
|
||||
|
||||
In the Admin UI, navigate to **Directory --> Invitations**, and then click **Create** to open the **Create Invitation** modal. Define the following fields:
|
||||
In the Admin UI, navigate to **Directory --> Invitations**, and then click **Create** to open the **Create Invitation** box. Define the following fields:
|
||||
|
||||
- **Name**: provide a name for your invitation object.
|
||||
- **Expires**: select a date for when you want the invitation to expire.
|
||||
@ -42,7 +42,7 @@ In the Admin UI, navigate to **Directory --> Invitations**, and then click **Cre
|
||||
|
||||
- **Single use**: specify whether or not you want the invitation to expire after a single use.
|
||||
|
||||
Click **Save** to save the new invitation and close the modal and return to the **Invitations** page.
|
||||
Click **Save** to save the new invitation and close the box and return to the **Invitations** page.
|
||||
|
||||
**Step 3. Email the invitation**
|
||||
|
||||
|
||||
@ -27,11 +27,12 @@ This documentation lists only the settings that you need to change from their de
|
||||
## authentik configuration
|
||||
|
||||
1. From the authentik Admin interface navigate to **Applications** -> **Applications** on the left sidebar.
|
||||
2. Create an application and an OAuth2/OpenID provider using the [wizard](https://docs.goauthentik.io/docs/add-secure-apps/applications/manage_apps#add-new-applications).
|
||||
|
||||
2. Create an application and an OAuth2/OpenID provider using the [Application modal](https://docs.goauthentik.io/docs/add-secure-apps/applications/manage_apps#instructions).
|
||||
- Note the application slug, client ID, and client secret, as they will be required later.
|
||||
- Set a strict redirect URI to `https://chronograf.company/oauth/authentik/callback`.
|
||||
- Choose a signing key (any available key is acceptable).
|
||||
3. Complete and submit the settings to close the wizard.
|
||||
3. Complete and submit the settings to close the modal.
|
||||
|
||||
## Chronograf configuration
|
||||
|
||||
|
||||
@ -2,13 +2,14 @@ import { generateVersionDropdown } from "./src/utils.js";
|
||||
import apiReference from "./docs/developer-docs/api/reference/sidebar";
|
||||
|
||||
const releases = [
|
||||
"releases/2025/v2025.2",
|
||||
"releases/2024/v2024.12",
|
||||
"releases/2024/v2024.10",
|
||||
"releases/2024/v2024.8",
|
||||
{
|
||||
type: "category",
|
||||
label: "Previous versions",
|
||||
items: [
|
||||
"releases/2024/v2024.8",
|
||||
"releases/2024/v2024.6",
|
||||
"releases/2024/v2024.4",
|
||||
"releases/2024/v2024.2",
|
||||
@ -200,8 +201,6 @@ export default {
|
||||
"add-secure-apps/providers/oauth2/github-compatibility",
|
||||
],
|
||||
},
|
||||
"add-secure-apps/providers/saml/index",
|
||||
"add-secure-apps/providers/radius/index",
|
||||
{
|
||||
type: "category",
|
||||
label: "Proxy Provider",
|
||||
@ -228,7 +227,6 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
"add-secure-apps/providers/scim/index",
|
||||
{
|
||||
type: "category",
|
||||
label: "RAC (Remote Access Control) Provider",
|
||||
@ -238,6 +236,20 @@ export default {
|
||||
},
|
||||
items: ["add-secure-apps/providers/rac/how-to-rac"],
|
||||
},
|
||||
"add-secure-apps/providers/radius/index",
|
||||
"add-secure-apps/providers/saml/index",
|
||||
"add-secure-apps/providers/scim/index",
|
||||
{
|
||||
type: "category",
|
||||
label: "SSF Provider",
|
||||
link: {
|
||||
type: "doc",
|
||||
id: "add-secure-apps/providers/ssf/index",
|
||||
},
|
||||
items: [
|
||||
"add-secure-apps/providers/ssf/create-ssf-provider",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -286,11 +298,12 @@ export default {
|
||||
items: [
|
||||
"add-secure-apps/flows-stages/stages/authenticator_duo/index",
|
||||
"add-secure-apps/flows-stages/stages/authenticator_endpoint_gdtc/index",
|
||||
"add-secure-apps/flows-stages/stages/authenticator_email/index",
|
||||
"add-secure-apps/flows-stages/stages/authenticator_sms/index",
|
||||
"add-secure-apps/flows-stages/stages/authenticator_static/index",
|
||||
"add-secure-apps/flows-stages/stages/authenticator_totp/index",
|
||||
"add-secure-apps/flows-stages/stages/authenticator_validate/index",
|
||||
"add-secure-apps/flows-stages/stages/authenticator_webauthn/index",
|
||||
"add-secure-apps/flows-stages/stages/authenticator_validate/index",
|
||||
"add-secure-apps/flows-stages/stages/captcha/index",
|
||||
"add-secure-apps/flows-stages/stages/deny",
|
||||
"add-secure-apps/flows-stages/stages/email/index",
|
||||
@ -607,11 +620,12 @@ export default {
|
||||
items: [
|
||||
{
|
||||
type: "category",
|
||||
label: "Setup",
|
||||
label: "Development environment",
|
||||
items: [
|
||||
"developer-docs/setup/full-dev-environment",
|
||||
"developer-docs/setup/frontend-dev-environment",
|
||||
"developer-docs/setup/website-dev-environment",
|
||||
"developer-docs/setup/debugging",
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -689,6 +703,11 @@ export default {
|
||||
type: "category",
|
||||
label: "CVEs",
|
||||
items: [
|
||||
{
|
||||
type: "category",
|
||||
label: "2024",
|
||||
items: ["security/cves/CVE-2025-29928"],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "2024",
|
||||
|
||||
Reference in New Issue
Block a user