mirror of
https://github.com/goauthentik/authentik
synced 2026-05-14 19:06:39 +02:00
Compare commits
71 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
680feaefa1 | ||
|
|
8676cd3a43 | ||
|
|
53e0f6b734 | ||
|
|
eaee475662 | ||
|
|
19b672b3bc | ||
|
|
bf0a31ce86 | ||
|
|
28ff561400 | ||
|
|
ff2472a551 | ||
|
|
dac302e8be | ||
|
|
930a6f7c6f | ||
|
|
b661b0bb39 | ||
|
|
5203713ca0 | ||
|
|
94794b106e | ||
|
|
eb8c21cf04 | ||
|
|
8a501377f2 | ||
|
|
5c67a0fecd | ||
|
|
145e6a3a4f | ||
|
|
0219ed73f5 | ||
|
|
88ccb7857b | ||
|
|
e33d6becba | ||
|
|
41417affc0 | ||
|
|
1b4a6c3f6d | ||
|
|
aa98edd661 | ||
|
|
cb70331c82 | ||
|
|
31e105b190 | ||
|
|
08d2615f71 | ||
|
|
bb9e8b1c42 | ||
|
|
6b8d7376a6 | ||
|
|
4f680d8c06 | ||
|
|
3eeb741975 | ||
|
|
39cb638132 | ||
|
|
be8f5d21cb | ||
|
|
acf18836e8 | ||
|
|
d688621de4 | ||
|
|
5173d09191 | ||
|
|
45bab4d32f | ||
|
|
f383e54c72 | ||
|
|
ab6b9b27cc | ||
|
|
db3fb0bf2e | ||
|
|
5859e6a5e5 | ||
|
|
1d3271fec7 | ||
|
|
673c8ef62c | ||
|
|
4614ae320f | ||
|
|
10b103c0bf | ||
|
|
361e64a8a1 | ||
|
|
e56081b863 | ||
|
|
01a44b281b | ||
|
|
7a6631c6e8 | ||
|
|
ada973dd44 | ||
|
|
3a7e962bde | ||
|
|
ae297e2f60 | ||
|
|
2bc2b6bd41 | ||
|
|
8199371172 | ||
|
|
dac1879de5 | ||
|
|
dd7c6b29d9 | ||
|
|
41b7e05f59 | ||
|
|
c5f5714e02 | ||
|
|
d20d8322af | ||
|
|
288f5d5015 | ||
|
|
a640eb9180 | ||
|
|
7d48baab3e | ||
|
|
b1cd6d34fc | ||
|
|
f14d033cef | ||
|
|
ec75e161e2 | ||
|
|
2e8fb8f2c6 | ||
|
|
362bf22139 | ||
|
|
890da9b287 | ||
|
|
7b8dadf945 | ||
|
|
8f70dbb963 | ||
|
|
15505f5caf | ||
|
|
b2d770c0a4 |
@@ -1 +0,0 @@
|
||||
website/docs/developer-docs/index.md
|
||||
4
CONTRIBUTING.md
Normal file
4
CONTRIBUTING.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Contributing to authentik
|
||||
|
||||
Thanks for your interest in contributing! Please see our [contributing guide](https://docs.goauthentik.io/docs/developer-docs/?utm_source=github) for more information.
|
||||
|
||||
27
README.md
27
README.md
@@ -15,15 +15,16 @@
|
||||
|
||||
## What is authentik?
|
||||
|
||||
authentik is an open-source Identity Provider that emphasizes flexibility and versatility, with support for a wide set of protocols.
|
||||
authentik is an open-source Identity Provider (IdP) for modern SSO. It supports SAML, OAuth2/OIDC, LDAP, RADIUS, and more, designed for self-hosting from small labs to large production clusters.
|
||||
|
||||
Our [enterprise offer](https://goauthentik.io/pricing) can also be used as a self-hosted replacement for large-scale deployments of Okta/Auth0, Entra ID, Ping Identity, or other legacy IdPs for employees and B2B2C use.
|
||||
Our [enterprise offering](https://goauthentik.io/pricing) is available for organizations to securely replace existing IdPs such as Okta, Auth0, Entra ID, and Ping Identity for robust, large-scale identity management.
|
||||
|
||||
## Installation
|
||||
|
||||
For small/test setups it is recommended to use Docker Compose; refer to the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github).
|
||||
|
||||
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github).
|
||||
- Docker Compose: recommended for small/test setups. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/docker-compose/).
|
||||
- Kubernetes (Helm Chart): recommended for larger setups. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/kubernetes/) and the Helm chart [repository](https://github.com/goauthentik/helm).
|
||||
- AWS CloudFormation: deploy on AWS using our official templates. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/aws/).
|
||||
- DigitalOcean Marketplace: one-click deployment via the official Marketplace app. See the [app listing](https://marketplace.digitalocean.com/apps/authentik).
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -32,14 +33,20 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Development
|
||||
## Development and contributions
|
||||
|
||||
See [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/?utm_source=github)
|
||||
See the [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/) for information about setting up local build environments, testing your contributions, and our contribution process.
|
||||
|
||||
## Security
|
||||
|
||||
See [SECURITY.md](SECURITY.md)
|
||||
Please see [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Adoption and Contributions
|
||||
## Adoption
|
||||
|
||||
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [contribution guide](https://docs.goauthentik.io/docs/developer-docs?utm_source=github).
|
||||
Using authentik? We'd love to hear your story and feature your logo. Email us at [hello@goauthentik.io](mailto:hello@goauthentik.io) or open a GitHub Issue/PR!
|
||||
|
||||
## License
|
||||
|
||||
[](LICENSE)
|
||||
[](website/LICENSE)
|
||||
[](authentik/enterprise/LICENSE)
|
||||
|
||||
23
SECURITY.md
23
SECURITY.md
@@ -25,7 +25,28 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io). Be sure to include relevant information like which version you've found the issue in, instructions on how to reproduce the issue, and anything else that might make it easier for us to find the issue.
|
||||
If you discover a potential vulnerability, please report it responsibly through one of the following channels:
|
||||
|
||||
- **Email**: [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
- **GitHub**: Submit a private security advisory via our [repository’s advisory portal](https://github.com/goauthentik/authentik/security/advisories/new)
|
||||
|
||||
When submitting a report, please include as much detail as possible, such as:
|
||||
|
||||
- **Affected version(s)**: The version of authentik where the issue was identified.
|
||||
- **Steps to reproduce**: A clear description or proof of concept to help us verify the issue.
|
||||
- **Impact assessment**: How the vulnerability could be exploited and its potential effect.
|
||||
- **Additional information**: Logs, configuration details (if relevant), or any suggested mitigations.
|
||||
|
||||
We kindly ask that you do not disclose the vulnerability publicly until we have confirmed and addressed the issue.
|
||||
|
||||
Our team will:
|
||||
|
||||
- Acknowledge receipt of your report as quickly as possible.
|
||||
- Keep you updated on the investigation and resolution progress.
|
||||
|
||||
## Researcher Recognition
|
||||
|
||||
We value contributions from the security community. For each valid report, we will publish a dedicated entry on our Security Advisory page that optionally includes the reporter’s name (or preferred alias). Please note that while we do not currently offer monetary bounties, we are committed to giving researchers appropriate credit for their efforts in keeping authentik secure.
|
||||
|
||||
## Severity levels
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from functools import lru_cache
|
||||
from os import environ
|
||||
|
||||
VERSION = "2025.8.0"
|
||||
VERSION = "2025.8.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ from authentik.blueprints.v1.oci import OCI_PREFIX
|
||||
from authentik.events.logs import capture_logs
|
||||
from authentik.events.utils import sanitize_dict
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.tasks.apps import PRIORITY_HIGH
|
||||
from authentik.tasks.models import Task
|
||||
from authentik.tasks.schedules.models import Schedule
|
||||
from authentik.tenants.models import Tenant
|
||||
@@ -111,6 +112,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
|
||||
@actor(
|
||||
description=_("Find blueprints as `blueprints_find` does, but return a safe dict."),
|
||||
throws=(DatabaseError, ProgrammingError, InternalError),
|
||||
priority=PRIORITY_HIGH,
|
||||
)
|
||||
def blueprints_find_dict():
|
||||
blueprints = []
|
||||
|
||||
@@ -328,6 +328,12 @@ class SessionUserSerializer(PassiveSerializer):
|
||||
original = UserSelfSerializer(required=False)
|
||||
|
||||
|
||||
class UserPasswordSetSerializer(PassiveSerializer):
|
||||
"""Payload to set a users' password directly"""
|
||||
|
||||
password = CharField(required=True)
|
||||
|
||||
|
||||
class UsersFilter(FilterSet):
|
||||
"""Filter for users"""
|
||||
|
||||
@@ -585,12 +591,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
"UserPasswordSetSerializer",
|
||||
{
|
||||
"password": CharField(required=True),
|
||||
},
|
||||
),
|
||||
request=UserPasswordSetSerializer,
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully changed password"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
@@ -599,9 +600,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
@action(detail=True, methods=["POST"], permission_classes=[])
|
||||
def set_password(self, request: Request, pk: int) -> Response:
|
||||
"""Set password for user"""
|
||||
data = UserPasswordSetSerializer(data=request.data)
|
||||
data.is_valid(raise_exception=True)
|
||||
user: User = self.get_object()
|
||||
try:
|
||||
user.set_password(request.data.get("password"), request=request)
|
||||
user.set_password(data.validated_data["password"], request=request)
|
||||
user.save()
|
||||
except (ValidationError, IntegrityError) as exc:
|
||||
LOGGER.debug("Failed to set password", exc=exc)
|
||||
|
||||
@@ -102,6 +102,16 @@ class TestUsersAPI(APITestCase):
|
||||
self.admin.refresh_from_db()
|
||||
self.assertTrue(self.admin.check_password(new_pw))
|
||||
|
||||
def test_set_password_blank(self):
|
||||
"""Test Direct password set"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}),
|
||||
data={"password": ""},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]})
|
||||
|
||||
def test_recovery(self):
|
||||
"""Test user recovery link"""
|
||||
flow = create_test_flow(
|
||||
|
||||
@@ -154,6 +154,7 @@ worker:
|
||||
consumer_listen_timeout: "seconds=30"
|
||||
task_max_retries: 20
|
||||
task_default_time_limit: "minutes=10"
|
||||
lock_purge_interval: "minutes=1"
|
||||
task_purge_interval: "days=1"
|
||||
task_expiration: "days=30"
|
||||
scheduler_interval: "seconds=60"
|
||||
|
||||
@@ -43,7 +43,9 @@ def structlog_configure():
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.TimeStamper(fmt="iso", utc=False),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.dict_tracebacks,
|
||||
structlog.processors.ExceptionRenderer(
|
||||
structlog.processors.ExceptionDictTransformer(show_locals=CONFIG.get_bool("debug"))
|
||||
),
|
||||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
||||
],
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
@@ -65,7 +67,14 @@ def get_logger_config():
|
||||
"json": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
"processor": structlog.processors.JSONRenderer(sort_keys=True),
|
||||
"foreign_pre_chain": LOG_PRE_CHAIN + [structlog.processors.dict_tracebacks],
|
||||
"foreign_pre_chain": LOG_PRE_CHAIN
|
||||
+ [
|
||||
structlog.processors.ExceptionRenderer(
|
||||
structlog.processors.ExceptionDictTransformer(
|
||||
show_locals=CONFIG.get_bool("debug")
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
"console": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from dramatiq.actor import Actor
|
||||
from dramatiq.results.errors import ResultFailure
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, CharField, ChoiceField
|
||||
@@ -110,9 +111,13 @@ class OutgoingSyncProviderStatusMixin:
|
||||
"override_dry_run": params.validated_data["override_dry_run"],
|
||||
"pk": params.validated_data["sync_object_id"],
|
||||
},
|
||||
retries=0,
|
||||
rel_obj=provider,
|
||||
)
|
||||
msg.get_result(block=True)
|
||||
try:
|
||||
msg.get_result(block=True)
|
||||
except ResultFailure:
|
||||
pass
|
||||
task: Task = msg.options["task"]
|
||||
task.refresh_from_db()
|
||||
return Response(SyncObjectResultSerializer(instance={"messages": task._messages}).data)
|
||||
|
||||
@@ -20,6 +20,7 @@ from authentik.lib.sync.outgoing.exceptions import (
|
||||
TransientSyncException,
|
||||
)
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
|
||||
from authentik.lib.utils.errors import exception_to_dict
|
||||
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
||||
from authentik.tasks.models import Task
|
||||
|
||||
@@ -164,16 +165,17 @@ class SyncTasks:
|
||||
except BadRequestSyncException as exc:
|
||||
self.logger.warning("failed to sync object", exc=exc, obj=obj)
|
||||
task.warning(
|
||||
f"Failed to sync {obj._meta.verbose_name} {str(obj)} due to error: {str(exc)}",
|
||||
f"Failed to sync {str(obj)} due to error: {str(exc)}",
|
||||
arguments=exc.args[1:],
|
||||
obj=sanitize_item(obj),
|
||||
exception=exception_to_dict(exc),
|
||||
)
|
||||
except TransientSyncException as exc:
|
||||
self.logger.warning("failed to sync object", exc=exc, user=obj)
|
||||
task.warning(
|
||||
f"Failed to sync {obj._meta.verbose_name} {str(obj)} due to "
|
||||
"transient error: {str(exc)}",
|
||||
f"Failed to sync {str(obj)} due to " f"transient error: {str(exc)}",
|
||||
obj=sanitize_item(obj),
|
||||
exception=exception_to_dict(exc),
|
||||
)
|
||||
except StopSync as exc:
|
||||
self.logger.warning("Stopping sync", exc=exc)
|
||||
|
||||
@@ -76,6 +76,7 @@ class OutpostConfig:
|
||||
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
||||
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
|
||||
kubernetes_ingress_class_name: str | None = field(default=None)
|
||||
kubernetes_ingress_path_type: str | None = field(default=None)
|
||||
kubernetes_httproute_annotations: dict[str, str] = field(default_factory=dict)
|
||||
kubernetes_httproute_parent_refs: list[dict[str, str]] = field(default_factory=list)
|
||||
kubernetes_service_type: str = field(default="ClusterIP")
|
||||
@@ -151,7 +152,7 @@ class OutpostServiceConnection(ScheduledModel, models.Model):
|
||||
|
||||
state = cache.get(self.state_key, None)
|
||||
if not state:
|
||||
outpost_service_connection_monitor.send_with_options(args=(self.pk), rel_obj=self)
|
||||
outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self)
|
||||
return OutpostServiceConnectionState("", False)
|
||||
return state
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ HIST_POLICIES_EXECUTION_TIME = Histogram(
|
||||
"binding_order",
|
||||
"binding_target_type",
|
||||
"binding_target_name",
|
||||
"object_pk",
|
||||
"object_type",
|
||||
"mode",
|
||||
],
|
||||
|
||||
@@ -86,7 +86,6 @@ class PolicyEngine:
|
||||
binding_order=binding.order,
|
||||
binding_target_type=binding.target_type,
|
||||
binding_target_name=binding.target_name,
|
||||
object_pk=str(self.request.obj.pk),
|
||||
object_type=class_to_path(self.request.obj.__class__),
|
||||
mode="cache_retrieve",
|
||||
).time():
|
||||
|
||||
@@ -131,7 +131,6 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
binding_order=self.binding.order,
|
||||
binding_target_type=self.binding.target_type,
|
||||
binding_target_name=self.binding.target_name,
|
||||
object_pk=str(self.request.obj.pk) if self.request.obj else "",
|
||||
object_type=class_to_path(self.request.obj.__class__) if self.request.obj else "",
|
||||
mode="execute_process",
|
||||
).time(),
|
||||
|
||||
@@ -11,7 +11,8 @@ def migrate_sessions(apps, schema_editor, model):
|
||||
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for obj in Model.objects.using(db_alias).all():
|
||||
objs = list(Model.objects.using(db_alias).select_related("old_session").all())
|
||||
for obj in objs:
|
||||
if not obj.old_session:
|
||||
continue
|
||||
obj.session = (
|
||||
|
||||
@@ -23,7 +23,12 @@ def user_session_deleted_oauth_backchannel_logout_and_tokens_removal(
|
||||
|
||||
backchannel_logout_notification_dispatch.send(
|
||||
revocations=[
|
||||
(token.provider_id, token.id_token.iss, token.session.user.uid)
|
||||
(
|
||||
token.provider_id,
|
||||
token.id_token.iss,
|
||||
token.id_token.sub,
|
||||
instance.session.session_key,
|
||||
)
|
||||
for token in access_tokens
|
||||
],
|
||||
)
|
||||
|
||||
@@ -14,13 +14,19 @@ LOGGER = get_logger()
|
||||
|
||||
|
||||
@actor(description=_("Send a back-channel logout request to the registered client"))
|
||||
def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None) -> bool:
|
||||
def send_backchannel_logout_request(
|
||||
provider_pk: int,
|
||||
iss: str,
|
||||
sub: str | None = None,
|
||||
session_key: str | None = None,
|
||||
) -> bool:
|
||||
"""Send a back-channel logout request to the registered client
|
||||
|
||||
Args:
|
||||
provider_pk: The OAuth2 provider's primary key
|
||||
iss: The issuer URL for the logout token
|
||||
sub: The subject identifier to include in the logout token
|
||||
session_key: The authentik session key to hash and include in the logout token
|
||||
|
||||
Returns:
|
||||
bool: True if the request was sent successfully, False otherwise
|
||||
@@ -33,11 +39,10 @@ def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None)
|
||||
return
|
||||
|
||||
# Generate the logout token
|
||||
logout_token = create_logout_token(iss, provider, None, sub)
|
||||
logout_token = create_logout_token(provider, iss, sub, session_key)
|
||||
|
||||
# Get the back-channel logout URI from the provider's dedicated backchannel_logout_uri field
|
||||
# Back-channel logout requires explicit configuration - no fallback to redirect URIs
|
||||
|
||||
backchannel_logout_uri = provider.backchannel_logout_uri
|
||||
if not backchannel_logout_uri:
|
||||
self.info("No back-channel logout URI found for provider")
|
||||
@@ -60,9 +65,9 @@ def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None)
|
||||
def backchannel_logout_notification_dispatch(revocations: list, **kwargs):
|
||||
"""Handle backchannel logout notifications dispatched via signal"""
|
||||
for revocation in revocations:
|
||||
provider_pk, iss, sub = revocation
|
||||
provider_pk, iss, sub, session_key = revocation
|
||||
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
|
||||
send_backchannel_logout_request.send_with_options(
|
||||
args=(provider_pk, iss, sub),
|
||||
args=(provider_pk, iss, sub, session_key),
|
||||
rel_obj=provider,
|
||||
)
|
||||
|
||||
@@ -4,17 +4,18 @@ import re
|
||||
import uuid
|
||||
from base64 import b64decode
|
||||
from binascii import Error
|
||||
from time import time
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.timezone import now
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.errors import BearerTokenError
|
||||
from authentik.providers.oauth2.id_token import hash_session_key
|
||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
|
||||
@@ -217,23 +218,25 @@ class HttpResponseRedirectScheme(HttpResponseRedirect):
|
||||
|
||||
|
||||
def create_logout_token(
|
||||
iss: str,
|
||||
provider: OAuth2Provider,
|
||||
session_key: str | None = None,
|
||||
iss: str,
|
||||
sub: str | None = None,
|
||||
session_key: str | None = None,
|
||||
) -> str:
|
||||
"""Create a logout token for Back-Channel Logout
|
||||
|
||||
As per https://openid.net/specs/openid-connect-backchannel-1_0.html
|
||||
"""
|
||||
|
||||
LOGGER.debug("Creating logout token", provider=provider, session_key=session_key, sub=sub)
|
||||
LOGGER.debug("Creating logout token", provider=provider, sub=sub)
|
||||
|
||||
_now = now()
|
||||
# Create the logout token payload
|
||||
payload = {
|
||||
"iss": str(iss),
|
||||
"aud": provider.client_id,
|
||||
"iat": int(time()),
|
||||
"iat": int(_now.timestamp()),
|
||||
"exp": int((_now + timedelta_from_string(provider.access_token_validity)).timestamp()),
|
||||
"jti": str(uuid.uuid4()),
|
||||
"events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {},
|
||||
|
||||
@@ -127,6 +127,9 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
and self.controller.outpost.config.kubernetes_ingress_secret_name
|
||||
):
|
||||
tls_hosts.append(external_host_name.hostname)
|
||||
path_type = "Prefix"
|
||||
if self.controller.outpost.config.kubernetes_ingress_path_type:
|
||||
path_type = self.controller.outpost.config.kubernetes_ingress_path_type
|
||||
if proxy_provider.mode in [
|
||||
ProxyMode.FORWARD_SINGLE,
|
||||
ProxyMode.FORWARD_DOMAIN,
|
||||
@@ -143,7 +146,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
),
|
||||
),
|
||||
path="/outpost.goauthentik.io",
|
||||
path_type="Prefix",
|
||||
path_type=path_type,
|
||||
)
|
||||
]
|
||||
),
|
||||
@@ -161,7 +164,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
|
||||
),
|
||||
),
|
||||
path="/",
|
||||
path_type="Prefix",
|
||||
path_type=path_type,
|
||||
)
|
||||
]
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ def migrate_sessions(apps, schema_editor):
|
||||
for token in ConnectionToken.objects.using(db_alias).all():
|
||||
token.session = (
|
||||
AuthenticatedSession.objects.using(db_alias)
|
||||
.filter(session_key=token.old_session.session_key)
|
||||
.filter(session__session_key=token.old_session.session_key)
|
||||
.first()
|
||||
)
|
||||
if token.session:
|
||||
|
||||
@@ -27,3 +27,8 @@ class SCIMRequestException(TransientSyncException):
|
||||
except ValidationError:
|
||||
pass
|
||||
return self._message
|
||||
|
||||
def __str__(self):
|
||||
if self._response:
|
||||
return self._response.text
|
||||
return super().__str__()
|
||||
|
||||
@@ -4,7 +4,6 @@ import importlib
|
||||
from collections import OrderedDict
|
||||
from hashlib import sha512
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
import orjson
|
||||
from sentry_sdk import set_tag
|
||||
@@ -368,6 +367,9 @@ DRAMATIQ = {
|
||||
"broker_class": "authentik.tasks.broker.Broker",
|
||||
"channel_prefix": "authentik",
|
||||
"task_model": "authentik.tasks.models.Task",
|
||||
"lock_purge_interval": timedelta_from_string(
|
||||
CONFIG.get("worker.lock_purge_interval")
|
||||
).total_seconds(),
|
||||
"task_purge_interval": timedelta_from_string(
|
||||
CONFIG.get("worker.task_purge_interval")
|
||||
).total_seconds(),
|
||||
@@ -424,7 +426,6 @@ DRAMATIQ = {
|
||||
(
|
||||
"authentik.tasks.middleware.MetricsMiddleware",
|
||||
{
|
||||
"multiproc_dir": str(Path(gettempdir()) / "authentik_prometheus_tmp"),
|
||||
"prefix": "authentik",
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any
|
||||
|
||||
from django.db.models import Q
|
||||
from ldap3 import SUBTREE
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.sources.ldap.models import LDAP_DISTINGUISHED_NAME, LDAP_UNIQUENESS, LDAPSource
|
||||
@@ -52,7 +53,8 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
for group in page_data:
|
||||
if self._source.lookup_groups_from_user:
|
||||
group_dn = group.get("dn", {})
|
||||
group_filter = f"({self._source.group_membership_field}={group_dn})"
|
||||
escaped_dn = escape_filter_chars(group_dn)
|
||||
group_filter = f"({self._source.group_membership_field}={escaped_dn})"
|
||||
group_members = self._source.connection().extend.standard.paged_search(
|
||||
search_base=self.base_dn_users,
|
||||
search_filter=group_filter,
|
||||
|
||||
@@ -4,6 +4,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.db.models import Q
|
||||
from django.test import TestCase
|
||||
from ldap3.core.exceptions import LDAPInvalidFilterError
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Group, User
|
||||
@@ -519,3 +521,89 @@ class LDAPSyncTests(TestCase):
|
||||
|
||||
self.assertFalse(User.objects.filter(username__startswith="not-in-the-source").exists())
|
||||
self.assertFalse(Group.objects.filter(name__startswith="not-in-the-source").exists())
|
||||
|
||||
def test_membership_sync_special_chars_in_group_dn(self):
|
||||
"""Test membership synchronization with special characters in group DN"""
|
||||
self.source.object_uniqueness_field = "uid"
|
||||
self.source.group_object_filter = "(objectClass=groupOfNames)"
|
||||
self.source.lookup_groups_from_user = True
|
||||
self.source.group_membership_field = "memberOf"
|
||||
|
||||
# Mock connection with group DN containing special characters
|
||||
mock_conn = MagicMock()
|
||||
|
||||
# Simulate group with special characters in DN: parentheses, backslashes, asterisks
|
||||
special_group_dn = "cn=test(group),ou=groups,dc=example,dc=com"
|
||||
backslash_group_dn = "cn=test\\group,ou=groups,dc=example,dc=com"
|
||||
asterisk_group_dn = "cn=test*group,ou=groups,dc=example,dc=com"
|
||||
|
||||
# Mock the paged_search method that would be called with the filter
|
||||
mock_standard = MagicMock()
|
||||
mock_conn.extend.standard = mock_standard
|
||||
|
||||
# Test case 1: Group DN with parentheses
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", return_value=mock_conn):
|
||||
membership_sync = MembershipLDAPSynchronizer(self.source, Task())
|
||||
|
||||
# Simulate group data with special characters in DN
|
||||
page_data = [{"dn": special_group_dn}]
|
||||
|
||||
# This should not raise LDAPInvalidFilterError anymore
|
||||
try:
|
||||
membership_sync.sync(page_data)
|
||||
# Verify that the filter was properly escaped
|
||||
# The call should have been made with escaped characters
|
||||
mock_standard.paged_search.assert_called()
|
||||
call_args = mock_standard.paged_search.call_args
|
||||
search_filter = call_args[1]["search_filter"]
|
||||
# The parentheses should be escaped as \28 and \29
|
||||
self.assertIn("\\28", search_filter) # Escaped (
|
||||
self.assertIn("\\29", search_filter) # Escaped )
|
||||
except LDAPInvalidFilterError:
|
||||
self.fail("LDAPInvalidFilterError should not be raised with escaped filter")
|
||||
|
||||
# Test case 2: Group DN with backslashes
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", return_value=mock_conn):
|
||||
membership_sync = MembershipLDAPSynchronizer(self.source, Task())
|
||||
page_data = [{"dn": backslash_group_dn}]
|
||||
|
||||
try:
|
||||
membership_sync.sync(page_data)
|
||||
call_args = mock_standard.paged_search.call_args
|
||||
search_filter = call_args[1]["search_filter"]
|
||||
# The backslash should be escaped as \5c
|
||||
self.assertIn("\\5c", search_filter) # Escaped \
|
||||
except LDAPInvalidFilterError:
|
||||
self.fail("LDAPInvalidFilterError should not be raised with escaped filter")
|
||||
|
||||
# Test case 3: Group DN with asterisks
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", return_value=mock_conn):
|
||||
membership_sync = MembershipLDAPSynchronizer(self.source, Task())
|
||||
page_data = [{"dn": asterisk_group_dn}]
|
||||
|
||||
try:
|
||||
membership_sync.sync(page_data)
|
||||
call_args = mock_standard.paged_search.call_args
|
||||
search_filter = call_args[1]["search_filter"]
|
||||
# The asterisk should be escaped as \2a
|
||||
self.assertIn("\\2a", search_filter) # Escaped *
|
||||
except LDAPInvalidFilterError:
|
||||
self.fail("LDAPInvalidFilterError should not be raised with escaped filter")
|
||||
|
||||
def test_escape_filter_chars_function(self):
|
||||
"""Test the escape_filter_chars function directly"""
|
||||
|
||||
# Test various special characters that need escaping
|
||||
test_cases = [
|
||||
("test(group)", "test\\28group\\29"), # parentheses
|
||||
("test\\group", "test\\5cgroup"), # backslash
|
||||
("test*group", "test\\2agroup"), # asterisk
|
||||
("test(*)group", "test\\28\\2a\\29group"), # multiple special chars
|
||||
("normalgroup", "normalgroup"), # no special chars
|
||||
("", ""), # empty string
|
||||
]
|
||||
|
||||
for input_str, expected in test_cases:
|
||||
with self.subTest(input_str=input_str):
|
||||
result = escape_filter_chars(input_str)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@@ -96,7 +96,11 @@ class EntraIDType(SourceType):
|
||||
}
|
||||
|
||||
def get_base_group_properties(self, source, group_id, **kwargs):
|
||||
raw_group = kwargs["info"]["raw_groups"][group_id]
|
||||
raw_groups = kwargs["info"]["raw_groups"]
|
||||
if group_id in raw_groups:
|
||||
name = raw_groups[group_id]["displayName"]
|
||||
else:
|
||||
name = group_id
|
||||
return {
|
||||
"name": raw_group["displayName"],
|
||||
"name": name,
|
||||
}
|
||||
|
||||
@@ -142,6 +142,11 @@ class AuthenticatorEmailStageView(ChallengeStageView):
|
||||
user = self.get_pending_user()
|
||||
|
||||
stage: AuthenticatorEmailStage = self.executor.current_stage
|
||||
# For the moment we only allow one email device per user
|
||||
if EmailDevice.objects.filter(Q(user=user), stage=stage.pk).exists():
|
||||
return self.executor.stage_invalid(
|
||||
_("The user already has an email address registered for MFA.")
|
||||
)
|
||||
if SESSION_KEY_EMAIL_DEVICE not in self.request.session:
|
||||
device = EmailDevice(user=user, confirmed=False, stage=stage, name="Email Device")
|
||||
valid_secs: int = timedelta_from_string(stage.token_expiry).total_seconds()
|
||||
|
||||
@@ -108,6 +108,17 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
)
|
||||
def test_stage_submit(self):
|
||||
"""Test stage email submission"""
|
||||
# test fail because of existing device
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
self.user,
|
||||
component="ak-stage-access-denied",
|
||||
)
|
||||
self.device.delete()
|
||||
# Initialize the flow
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
@@ -232,6 +243,7 @@ class TestAuthenticatorEmailStage(FlowTestCase):
|
||||
def test_challenge_generation(self):
|
||||
"""Test challenge generation"""
|
||||
# Test with masked email
|
||||
self.device.delete()
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
)
|
||||
|
||||
@@ -35,7 +35,12 @@ class Command(TenantCommand):
|
||||
template_context={},
|
||||
)
|
||||
try:
|
||||
send_mail(message.__dict__, stage.pk)
|
||||
if not stage.use_global_settings:
|
||||
message.from_email = stage.from_address
|
||||
|
||||
send_mail.send(message.__dict__, stage.pk).get_result(block=True)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Test email sent to {options['to']}"))
|
||||
finally:
|
||||
if delete_stage:
|
||||
stage.delete()
|
||||
|
||||
66
authentik/stages/email/tests/test_management_commands.py
Normal file
66
authentik/stages/email/tests/test_management_commands.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Test email management commands"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
|
||||
class TestEmailManagementCommands(TestCase):
|
||||
"""Test email management commands"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = create_test_admin_user()
|
||||
|
||||
def test_test_email_command_with_stage(self):
|
||||
"""Test test_email command with specified stage"""
|
||||
EmailStage.objects.create(
|
||||
name="test-stage",
|
||||
from_address="test@authentik.local",
|
||||
host="localhost",
|
||||
port=25,
|
||||
)
|
||||
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com", stage="test-stage")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik Test-Email")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
|
||||
def test_test_email_command_with_global_settings(self):
|
||||
"""Test test_email command with global settings"""
|
||||
# Mock the backend to use Django's locmem backend
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, "authentik Test-Email")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
|
||||
def test_test_email_command_invalid_stage(self):
|
||||
"""Test test_email command with invalid stage"""
|
||||
call_command("test_email", "test@example.com", stage="nonexistent")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
def test_test_email_command_with_custom_from(self):
|
||||
"""Test test_email command respects custom from address"""
|
||||
EmailStage.objects.create(
|
||||
name="test-stage",
|
||||
from_address="custom@authentik.local",
|
||||
host="localhost",
|
||||
port=25,
|
||||
)
|
||||
|
||||
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
|
||||
call_command("test_email", "test@example.com", stage="test-stage")
|
||||
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].from_email, "custom@authentik.local")
|
||||
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
|
||||
@@ -2,6 +2,8 @@ from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.utils.time import fqdn_rand
|
||||
from authentik.tasks.schedules.common import ScheduleSpec
|
||||
|
||||
PRIORITY_HIGH = 1000
|
||||
|
||||
|
||||
class AuthentikTasksConfig(ManagedAppConfig):
|
||||
name = "authentik.tasks"
|
||||
|
||||
@@ -14,6 +14,7 @@ from django_redis import get_redis_connection
|
||||
from dramatiq.broker import Broker
|
||||
from dramatiq.message import Message
|
||||
from dramatiq.middleware import Middleware
|
||||
from psycopg.errors import Error
|
||||
from redis.exceptions import RedisError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@@ -26,6 +27,7 @@ from authentik.tenants.utils import get_current_tenant
|
||||
|
||||
LOGGER = get_logger()
|
||||
HEALTHCHECK_LOGGER = get_logger("authentik.worker").bind()
|
||||
DB_ERRORS = (OperationalError, Error, RedisError)
|
||||
|
||||
|
||||
class TenantMiddleware(Middleware):
|
||||
@@ -175,7 +177,7 @@ class _healthcheck_handler(BaseHTTPRequestHandler):
|
||||
redis_conn = get_redis_connection()
|
||||
redis_conn.ping()
|
||||
self.send_response(200)
|
||||
except (OperationalError, RedisError): # pragma: no cover
|
||||
except DB_ERRORS: # pragma: no cover
|
||||
self.send_response(503)
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.send_header("Content-Length", "0")
|
||||
@@ -216,6 +218,14 @@ class WorkerStatusMiddleware(Middleware):
|
||||
hostname=socket.gethostname(),
|
||||
version=authentik_full_version(),
|
||||
)
|
||||
while True:
|
||||
try:
|
||||
WorkerStatusMiddleware.keep(status)
|
||||
except DB_ERRORS: # pragma: no cover
|
||||
sleep(10)
|
||||
pass
|
||||
|
||||
def keep(status: WorkerStatus):
|
||||
lock_id = f"goauthentik.io/worker/status/{status.pk}"
|
||||
with pglock.advisory(lock_id, side_effect=pglock.Raise):
|
||||
while True:
|
||||
|
||||
@@ -107,7 +107,6 @@ class ScheduleViewSet(
|
||||
"rel_obj_content_type__app_label",
|
||||
"rel_obj_content_type__model",
|
||||
"rel_obj_id",
|
||||
"description",
|
||||
)
|
||||
filterset_class = ScheduleFilter
|
||||
ordering = (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.8.0 Blueprint schema",
|
||||
"title": "authentik 2025.8.3 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
blueprints.goauthentik.io/system-bootstrap: "true"
|
||||
blueprints.goauthentik.io/system: "true"
|
||||
blueprints.goauthentik.io/description: |
|
||||
This blueprint configures the default admin user and group, and configures them for the [Automated install](https://goauthentik.io/docs/installation/automated-install).
|
||||
This blueprint configures the default admin user and group, and configures them for the [Automated install](https://docs.goauthentik.io/docs/install-config/automated-install?utm_source=bootstrap_blueprint).
|
||||
context:
|
||||
username: akadmin
|
||||
group_name: authentik Admins
|
||||
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.3}
|
||||
ports:
|
||||
- ${COMPOSE_PORT_HTTP:-9000}:9000
|
||||
- ${COMPOSE_PORT_HTTPS:-9443}:9443
|
||||
@@ -72,7 +72,7 @@ services:
|
||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.3}
|
||||
restart: unless-stopped
|
||||
user: root
|
||||
volumes:
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025.8.0
|
||||
2025.8.3
|
||||
@@ -2,6 +2,10 @@
|
||||
set -e -o pipefail
|
||||
MODE_FILE="${TMPDIR}/authentik-mode"
|
||||
|
||||
if [[ -z "${PROMETHEUS_MULTIPROC_DIR}" ]]; then
|
||||
export PROMETHEUS_MULTIPROC_DIR="${TMPDIR:-/tmp}/authentik_prometheus_tmp"
|
||||
fi
|
||||
|
||||
function log {
|
||||
printf '{"event": "%s", "level": "info", "logger": "bootstrap"}\n' "$@" >/dev/stderr
|
||||
}
|
||||
@@ -31,7 +35,7 @@ function check_if_root {
|
||||
GROUP="authentik:${GROUP_NAME}"
|
||||
fi
|
||||
# Fix permissions of certs and media
|
||||
chown -R authentik:authentik /media /certs
|
||||
chown -R authentik:authentik /media /certs "${PROMETHEUS_MULTIPROC_DIR}"
|
||||
chmod ug+rwx /media
|
||||
chmod ug+rx /certs
|
||||
exec chpst -u authentik:$GROUP env HOME=/authentik $1
|
||||
@@ -68,6 +72,8 @@ function prepare_debug {
|
||||
chown authentik:authentik /unittest.xml
|
||||
}
|
||||
|
||||
mkdir -p "${PROMETHEUS_MULTIPROC_DIR}"
|
||||
|
||||
if [[ "$(python -m authentik.lib.config debugger 2>/dev/null)" == "True" ]]; then
|
||||
prepare_debug
|
||||
fi
|
||||
|
||||
@@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.8.0
|
||||
Default: 2025.8.3
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
||||
@@ -33,15 +33,12 @@ wait_for_db()
|
||||
_tmp = Path(gettempdir())
|
||||
worker_class = "lifecycle.worker.DjangoUvicornWorker"
|
||||
worker_tmp_dir = str(_tmp.joinpath("authentik_gunicorn_tmp"))
|
||||
prometheus_tmp_dir = str(_tmp.joinpath("authentik_prometheus_tmp"))
|
||||
|
||||
os.makedirs(worker_tmp_dir, exist_ok=True)
|
||||
os.makedirs(prometheus_tmp_dir, exist_ok=True)
|
||||
|
||||
bind = f"unix://{str(_tmp.joinpath('authentik-core.sock'))}"
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
||||
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", prometheus_tmp_dir)
|
||||
|
||||
preload_app = True
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.8.0",
|
||||
"version": "2025.8.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.8.0",
|
||||
"version": "2025.8.3",
|
||||
"dependencies": {
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.8.0",
|
||||
"version": "2025.8.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -237,6 +237,9 @@ class _PostgresConsumer(Consumer):
|
||||
# Override because dramatiq doesn't allow us setting this manually
|
||||
self.timeout = Conf().worker["consumer_listen_timeout"]
|
||||
|
||||
self.lock_purge_interval = timezone.timedelta(seconds=Conf().lock_purge_interval)
|
||||
self.lock_purge_last_run = timezone.now()
|
||||
|
||||
self.task_purge_interval = timezone.timedelta(seconds=Conf().task_purge_interval)
|
||||
self.task_purge_last_run = timezone.now() - self.task_purge_interval
|
||||
|
||||
@@ -378,6 +381,8 @@ class _PostgresConsumer(Consumer):
|
||||
# Force creation of listen connection
|
||||
_ = self.listen_connection
|
||||
|
||||
self._purge_locks()
|
||||
|
||||
processing = len(self.in_processing)
|
||||
if processing >= self.prefetch:
|
||||
# Wait and don't consume the message, other worker will be faster
|
||||
@@ -415,24 +420,26 @@ class _PostgresConsumer(Consumer):
|
||||
)
|
||||
|
||||
# No message to process
|
||||
self._purge_locks()
|
||||
self._auto_purge()
|
||||
self._scheduler()
|
||||
|
||||
return None
|
||||
|
||||
def _purge_locks(self):
|
||||
if timezone.now() - self.lock_purge_last_run < self.lock_purge_interval:
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
message_id = self.unlock_queue.get(block=False)
|
||||
except Empty:
|
||||
return
|
||||
break
|
||||
self.logger.debug("Unlocking message", message_id=message_id)
|
||||
with self.connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT pg_advisory_unlock(%s)", (self._get_message_lock_id(message_id),)
|
||||
)
|
||||
self.unlock_queue.task_done()
|
||||
self.lock_purge_last_run = timezone.now()
|
||||
|
||||
def _auto_purge(self):
|
||||
if timezone.now() - self.task_purge_last_run < self.task_purge_interval:
|
||||
@@ -444,6 +451,7 @@ class _PostgresConsumer(Consumer):
|
||||
result_expiry__lte=timezone.now(),
|
||||
).delete()
|
||||
self.logger.info("Purged messages in all queues", count=count)
|
||||
self.task_purge_last_run = timezone.now()
|
||||
|
||||
def _scheduler(self):
|
||||
if not self.scheduler:
|
||||
@@ -451,6 +459,7 @@ class _PostgresConsumer(Consumer):
|
||||
if timezone.now() - self.scheduler_last_run < self.scheduler_interval:
|
||||
return
|
||||
self.scheduler.run()
|
||||
self.schedule_last_run = timezone.now()
|
||||
|
||||
@raise_connection_error
|
||||
def close(self):
|
||||
@@ -465,4 +474,7 @@ class _PostgresConsumer(Consumer):
|
||||
if self._listen_connection is not None:
|
||||
conn = self._listen_connection
|
||||
self._listen_connection = None
|
||||
conn.close()
|
||||
try:
|
||||
conn.close()
|
||||
except DatabaseError:
|
||||
pass
|
||||
|
||||
@@ -56,6 +56,10 @@ class Conf:
|
||||
def task_model(self) -> str:
|
||||
return self.conf["task_model"]
|
||||
|
||||
@property
|
||||
def lock_purge_interval(self) -> int:
|
||||
return self.conf.get("lock_purge_interval", 60)
|
||||
|
||||
@property
|
||||
def task_purge_interval(self) -> int:
|
||||
# 24 hours
|
||||
|
||||
@@ -26,7 +26,7 @@ class HTTPServer(BaseHTTPServer):
|
||||
self.socket.close()
|
||||
|
||||
host, port = self.server_address[:2]
|
||||
if host == "0.0.0.0": # nosec
|
||||
if host == "0.0.0.0" and socket.has_dualstack_ipv6(): # nosec
|
||||
host = "::" # nosec
|
||||
|
||||
# Strip IPv6 brackets
|
||||
@@ -36,7 +36,9 @@ class HTTPServer(BaseHTTPServer):
|
||||
self.server_address = (host, port)
|
||||
|
||||
self.address_family = (
|
||||
socket.AF_INET6 if isinstance(ip_address(host), IPv6Address) else socket.AF_INET
|
||||
socket.AF_INET6
|
||||
if socket.has_dualstack_ipv6() and isinstance(ip_address(host), IPv6Address)
|
||||
else socket.AF_INET
|
||||
)
|
||||
|
||||
self.socket = socket.create_server(
|
||||
@@ -141,7 +143,6 @@ class MetricsMiddleware(Middleware):
|
||||
def __init__(
|
||||
self,
|
||||
prefix: str,
|
||||
multiproc_dir: str,
|
||||
labels: list[str] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
@@ -151,9 +152,6 @@ class MetricsMiddleware(Middleware):
|
||||
self.delayed_messages = set()
|
||||
self.message_start_times = {}
|
||||
|
||||
os.makedirs(multiproc_dir, exist_ok=True)
|
||||
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", multiproc_dir)
|
||||
|
||||
@property
|
||||
def forks(self):
|
||||
from django_dramatiq_postgres.forks import worker_metrics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2025.8.0"
|
||||
version = "2025.8.3"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.13.*"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"dacite==1.9.2",
|
||||
"deepmerge==2.0",
|
||||
"defusedxml==0.7.1",
|
||||
"django==5.1.11",
|
||||
"django==5.1.12",
|
||||
"django-countries==7.6.1",
|
||||
"django-cte==2.0.0",
|
||||
"django-dramatiq-postgres",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2025.8.0
|
||||
version: 2025.8.3
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@@ -61879,6 +61879,7 @@ components:
|
||||
- object_pk
|
||||
UserPasswordSetRequest:
|
||||
type: object
|
||||
description: Payload to set a users' password directly
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
|
||||
16
uv.lock
generated
16
uv.lock
generated
@@ -159,7 +159,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authentik"
|
||||
version = "2025.8.0"
|
||||
version = "2025.8.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "argon2-cffi" },
|
||||
@@ -266,7 +266,7 @@ requires-dist = [
|
||||
{ name = "dacite", specifier = "==1.9.2" },
|
||||
{ name = "deepmerge", specifier = "==2.0" },
|
||||
{ name = "defusedxml", specifier = "==0.7.1" },
|
||||
{ name = "django", specifier = "==5.1.11" },
|
||||
{ name = "django", specifier = "==5.1.12" },
|
||||
{ name = "django-countries", specifier = "==7.6.1" },
|
||||
{ name = "django-cte", specifier = "==2.0.0" },
|
||||
{ name = "django-dramatiq-postgres", editable = "packages/django-dramatiq-postgres" },
|
||||
@@ -899,16 +899,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.1.11"
|
||||
version = "5.1.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/80/bf0f9b0aa434fca2b46fc6a31c39b08ea714b87a0a72a16566f053fb05a8/django-5.1.11.tar.gz", hash = "sha256:3bcdbd40e4d4623b5e04f59c28834323f3086df583058e65ebce99f9982385ce", size = 10734926, upload-time = "2025-06-10T10:12:48.229Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/99/a951d93a27a5bc59fb96edbcdbc03fb9bfac51177f1bc0110888de85af3f/django-5.1.12.tar.gz", hash = "sha256:8a8991b1ec052ef6a44fefd1ef336ab8daa221287bcb91a4a17d5e1abec5bbcc", size = 10737777, upload-time = "2025-09-03T13:09:45.855Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/91/2972ce330c6c0bd5b3200d4c2ad5cbf47eecff5243220c5a56444d3267a0/django-5.1.11-py3-none-any.whl", hash = "sha256:e48091f364007068728aca938e7450fbfe3f2217079bfd2b8af45122585acf64", size = 8277453, upload-time = "2025-06-10T10:12:42.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/1c/a9520c8263e980b0b9933c9b5ce8f22c9ddf007b062e4eb428b557ff0932/django-5.1.12-py3-none-any.whl", hash = "sha256:9eb695636cea3601b65690f1596993c042206729afb320ca0960b55f8ed4477b", size = 8277454, upload-time = "2025-09-03T13:09:30.997Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1499,15 +1499,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "4.2.0"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "hpack" },
|
||||
{ name = "hyperframe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682, upload-time = "2025-02-02T07:43:51.815Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957, upload-time = "2025-02-01T11:02:26.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2025.8.0",
|
||||
"version": "2025.8.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2025.8.0",
|
||||
"version": "2025.8.3",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/web",
|
||||
"version": "2025.8.0",
|
||||
"version": "2025.8.3",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -66,7 +66,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
quickActions: QuickAction[] = [
|
||||
[msg("Create a new application"), paramURL("/core/applications", { createWizard: true })],
|
||||
[msg("Check the logs"), paramURL("/events/log")],
|
||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
|
||||
[msg("Explore integrations"), "https://integrations.goauthentik.io/", true],
|
||||
[msg("Manage users"), paramURL("/identity/users")],
|
||||
[
|
||||
msg("Check the release notes"),
|
||||
|
||||
@@ -33,23 +33,7 @@ const DEFAULT_REPUTATION_UPPER_LIMIT = 5;
|
||||
|
||||
@customElement("ak-admin-settings-form")
|
||||
export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
//
|
||||
// Custom property accessors in Lit 2 require a manual call to requestUpdate(). See:
|
||||
// https://lit.dev/docs/v2/components/properties/#accessors-custom
|
||||
//
|
||||
set settings(value: Settings | undefined) {
|
||||
this._settings = value;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@property({ type: Object })
|
||||
get settings() {
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
private _settings?: Settings;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
public static styles: CSSResult[] = [
|
||||
...super.styles,
|
||||
PFList,
|
||||
css`
|
||||
@@ -59,25 +43,35 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ attribute: false })
|
||||
public settings!: Settings;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return msg("Successfully updated settings.");
|
||||
}
|
||||
|
||||
async send(data: SettingsRequest): Promise<Settings> {
|
||||
async send(settingsRequest: SettingsRequest): Promise<Settings> {
|
||||
settingsRequest.flags ??= this.settings.flags;
|
||||
|
||||
const result = await new AdminApi(DEFAULT_CONFIG).adminSettingsUpdate({
|
||||
settingsRequest: data,
|
||||
settingsRequest,
|
||||
});
|
||||
|
||||
this.dispatchEvent(new CustomEvent("ak-admin-setting-changed"));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
const { settings } = this;
|
||||
|
||||
return html`
|
||||
<ak-text-input
|
||||
name="avatars"
|
||||
label=${msg("Avatars")}
|
||||
value="${ifDefined(this._settings?.avatars)}"
|
||||
value="${ifDefined(settings.avatars)}"
|
||||
input-hint="code"
|
||||
required
|
||||
.bighelp=${html`
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
@@ -137,27 +131,26 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
)}
|
||||
</p>
|
||||
`}
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-switch-input
|
||||
name="defaultUserChangeName"
|
||||
label=${msg("Allow users to change name")}
|
||||
?checked="${this._settings?.defaultUserChangeName}"
|
||||
?checked=${settings.defaultUserChangeName}
|
||||
help=${msg("Enable the ability for users to change their name.")}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="defaultUserChangeEmail"
|
||||
label=${msg("Allow users to change email")}
|
||||
?checked="${this._settings?.defaultUserChangeEmail}"
|
||||
?checked=${settings.defaultUserChangeEmail}
|
||||
help=${msg("Enable the ability for users to change their email.")}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="defaultUserChangeUsername"
|
||||
label=${msg("Allow users to change username")}
|
||||
?checked="${this._settings?.defaultUserChangeUsername}"
|
||||
?checked=${settings.defaultUserChangeUsername}
|
||||
help=${msg("Enable the ability for users to change their username.")}
|
||||
>
|
||||
</ak-switch-input>
|
||||
@@ -166,7 +159,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
label=${msg("Event retention")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this._settings?.eventRetention)}"
|
||||
value="${ifDefined(settings.eventRetention)}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Duration after which events will be deleted from the database.")}
|
||||
</p>
|
||||
@@ -188,19 +181,19 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
label=${msg("Reputation: lower limit")}
|
||||
required
|
||||
name="reputationLowerLimit"
|
||||
value="${this._settings?.reputationLowerLimit ?? DEFAULT_REPUTATION_LOWER_LIMIT}"
|
||||
value="${settings.reputationLowerLimit ?? DEFAULT_REPUTATION_LOWER_LIMIT}"
|
||||
help=${msg("Reputation cannot decrease lower than this value. Zero or negative.")}
|
||||
></ak-number-input>
|
||||
<ak-number-input
|
||||
label=${msg("Reputation: upper limit")}
|
||||
required
|
||||
name="reputationUpperLimit"
|
||||
value="${this._settings?.reputationUpperLimit ?? DEFAULT_REPUTATION_UPPER_LIMIT}"
|
||||
value="${settings.reputationUpperLimit ?? DEFAULT_REPUTATION_UPPER_LIMIT}"
|
||||
help=${msg("Reputation cannot increase higher than this value. Zero or positive.")}
|
||||
></ak-number-input>
|
||||
<ak-form-element-horizontal label=${msg("Footer links")} name="footerLinks">
|
||||
<ak-array-input
|
||||
.items=${this._settings?.footerLinks ?? []}
|
||||
.items=${settings.footerLinks ?? []}
|
||||
.newItem=${() => ({ name: "", href: "" })}
|
||||
.row=${(f?: FooterLink) =>
|
||||
akFooterLinkInput({
|
||||
@@ -219,7 +212,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
<ak-switch-input
|
||||
name="gdprCompliance"
|
||||
label=${msg("GDPR compliance")}
|
||||
?checked="${this._settings?.gdprCompliance}"
|
||||
?checked=${settings.gdprCompliance}
|
||||
help=${msg(
|
||||
"When enabled, all the events caused by a user will be deleted upon the user's deletion.",
|
||||
)}
|
||||
@@ -228,14 +221,14 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
<ak-switch-input
|
||||
name="impersonation"
|
||||
label=${msg("Impersonation")}
|
||||
?checked="${this._settings?.impersonation}"
|
||||
?checked=${settings.impersonation}
|
||||
help=${msg("Globally enable/disable impersonation.")}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-switch-input
|
||||
name="impersonationRequireReason"
|
||||
label=${msg("Require reason for impersonation")}
|
||||
?checked="${this._settings?.impersonationRequireReason}"
|
||||
?checked=${settings.impersonationRequireReason}
|
||||
help=${msg("Require administrators to provide a reason for impersonating a user.")}
|
||||
>
|
||||
</ak-switch-input>
|
||||
@@ -244,7 +237,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
label=${msg("Default token duration")}
|
||||
input-hint="code"
|
||||
required
|
||||
value="${ifDefined(this._settings?.defaultTokenDuration)}"
|
||||
value="${ifDefined(settings.defaultTokenDuration)}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Default duration for generated tokens")}
|
||||
</p>
|
||||
@@ -255,13 +248,13 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
label=${msg("Default token length")}
|
||||
required
|
||||
name="defaultTokenLength"
|
||||
value="${this._settings?.defaultTokenLength ?? 60}"
|
||||
value="${settings.defaultTokenLength ?? 60}"
|
||||
help=${msg("Default length of generated tokens")}
|
||||
></ak-number-input>
|
||||
<ak-form-element-horizontal label=${msg("Flags")} name="flags" required>
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
value="${YAML.stringify(this._settings?.flags ?? {})}"
|
||||
value="${YAML.stringify(settings.flags ?? {})}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
|
||||
@@ -53,15 +53,15 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
||||
name="certificateData"
|
||||
input-hint="code"
|
||||
placeholder="-----BEGIN CERTIFICATE-----"
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
?required=${!this.instance}
|
||||
?revealed=${!this.instance}
|
||||
help=${msg("PEM-encoded Certificate data.")}
|
||||
></ak-secret-textarea-input>
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Private Key")}
|
||||
name="keyData"
|
||||
input-hint="code"
|
||||
?revealed=${this.instance === undefined}
|
||||
?revealed=${!this.instance}
|
||||
help=${msg(
|
||||
"Optional Private Key. If this is set, you can use this keypair for encryption.",
|
||||
)}
|
||||
|
||||
@@ -66,7 +66,7 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
|
||||
</ak-form-element-horizontal>
|
||||
<ak-secret-textarea-input
|
||||
name="key"
|
||||
?revealed=${this.instance === undefined}
|
||||
?revealed=${!this.instance}
|
||||
label=${msg("License key")}
|
||||
input-hint="code"
|
||||
>
|
||||
|
||||
@@ -259,7 +259,7 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
||||
<ak-secret-textarea-input
|
||||
name="syncKeytab"
|
||||
label=${msg("Sync keytab")}
|
||||
?revealed=${this.instance === undefined}
|
||||
?revealed=${!this.instance}
|
||||
help=${msg(
|
||||
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||
)}
|
||||
@@ -287,7 +287,7 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
||||
<ak-secret-textarea-input
|
||||
name="spnegoKeytab"
|
||||
label=${msg("SPNEGO keytab")}
|
||||
?revealed=${this.instance === undefined}
|
||||
?revealed=${!this.instance}
|
||||
help=${msg(
|
||||
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||
)}
|
||||
|
||||
@@ -442,8 +442,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
||||
name="consumerSecret"
|
||||
input-hint="code"
|
||||
help=${msg("Also known as Client Secret.")}
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
?required=${!this.instance}
|
||||
?revealed=${!this.instance}
|
||||
></ak-secret-textarea-input>
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||
<input
|
||||
@@ -530,9 +530,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -414,9 +414,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -574,9 +574,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-group label=${msg("Advanced settings")}>
|
||||
<div class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Policy engine mode")}
|
||||
required
|
||||
|
||||
@@ -121,7 +121,9 @@ export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string {
|
||||
|
||||
render(untrustedHTML, container);
|
||||
|
||||
const result = container.innerHTML;
|
||||
|
||||
const result = container.innerHTML
|
||||
// Remove all comments as they can interfere with the styles.
|
||||
.replaceAll("<!---->", "")
|
||||
.replaceAll(/<!--\?lit\$\d+\$-->/g, "");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -154,11 +154,17 @@ export class ListSelect extends AKElement implements IListSelect {
|
||||
return elementCount === 0 ? -1 : checkIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight the currently focused item.
|
||||
*
|
||||
* @todo
|
||||
* This doesn't quite work as intended, but this component will likely
|
||||
* be refined after the PatternFly upgrade.
|
||||
*/
|
||||
private highlightFocusedItem() {
|
||||
this.displayedElements.forEach((item) => {
|
||||
item.classList.remove("ak-highlight-item");
|
||||
item.removeAttribute("aria-selected");
|
||||
item.tabIndex = -1;
|
||||
});
|
||||
const currentElement = this.currentElement;
|
||||
if (!currentElement) {
|
||||
@@ -168,7 +174,6 @@ export class ListSelect extends AKElement implements IListSelect {
|
||||
// This is currently a radio emulation; "selected" is true here.
|
||||
// If this were a checkbox emulation (i.e. multi), "checked" would be appropriate.
|
||||
currentElement.setAttribute("aria-selected", "true");
|
||||
currentElement.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
}
|
||||
|
||||
@bound
|
||||
|
||||
@@ -9,7 +9,7 @@ import { html } from "lit";
|
||||
const ACTIONS: QuickAction[] = [
|
||||
["Create a new application", "/core/applications"],
|
||||
["Check the logs", "/events/log"],
|
||||
["Explore integrations", "https://goauthentik.io/integrations/", true],
|
||||
["Explore integrations", "https://integrations.goauthentik.io/", true],
|
||||
["Manage users", "/identity/users"],
|
||||
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
|
||||
];
|
||||
|
||||
@@ -11,7 +11,7 @@ import { html } from "lit";
|
||||
const ACTIONS: QuickAction[] = [
|
||||
["Create a new application", "/core/applications"],
|
||||
["Check the logs", "/events/log"],
|
||||
["Explore integrations", "https://goauthentik.io/integrations/", true],
|
||||
["Explore integrations", "https://integrations.goauthentik.io/", true],
|
||||
["Manage users", "/identity/users"],
|
||||
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
|
||||
];
|
||||
|
||||
@@ -18,7 +18,7 @@ import { instanceOfValidationError } from "@goauthentik/api";
|
||||
|
||||
import { snakeCase } from "change-case";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@@ -246,6 +246,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
return createFileMap<T>(this.shadowRoot?.querySelectorAll("ak-form-element-horizontal"));
|
||||
}
|
||||
|
||||
//#region Validation
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return !!this.form?.checkValidity?.();
|
||||
}
|
||||
@@ -261,6 +263,10 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
return reportValidityDeep(form);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Submission
|
||||
|
||||
/**
|
||||
* Convert the elements of the form to JSON.[4]
|
||||
*/
|
||||
@@ -273,6 +279,7 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
return serializeForm<T>(elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and send the form to the destination. The `send()` method must be overridden for
|
||||
* this to work. If processing the data results in an error, we catch the error, distribute
|
||||
@@ -310,6 +317,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
let errorMessage = pluckErrorDetail(error);
|
||||
let focused = false;
|
||||
|
||||
//#region Validation errors
|
||||
|
||||
if (instanceOfValidationError(parsedError)) {
|
||||
// assign all input-related errors to their elements
|
||||
const elements =
|
||||
@@ -346,6 +355,23 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
if (parsedError.nonFieldErrors) {
|
||||
this.nonFieldErrors = parsedError.nonFieldErrors;
|
||||
} else if (!focused) {
|
||||
// It's possible that the API has returned a field error that we're
|
||||
// not aware of. We can still show the error message, to at least
|
||||
// give the user some feedback.
|
||||
for (const [fieldName, fieldErrors] of Object.entries(parsedError)) {
|
||||
if (Array.isArray(fieldErrors)) {
|
||||
this.nonFieldErrors = [
|
||||
msg(str`${fieldName}: ${fieldErrors.join(", ")}`),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
"authentik/forms: API rejected the form submission due to an invalid field that doesn't appear to be in the form. This is likely a bug in authentik.",
|
||||
parsedError,
|
||||
);
|
||||
}
|
||||
|
||||
errorMessage = msg("Invalid update request.");
|
||||
@@ -357,6 +383,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
showMessage({
|
||||
message: errorMessage,
|
||||
level: MessageLevel.error,
|
||||
@@ -369,6 +397,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Render
|
||||
|
||||
public renderFormWrapper(): TemplateResult {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AKElement } from "#elements/Base";
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
|
||||
|
||||
@@ -18,24 +17,44 @@ export class FormStatic extends AKElement {
|
||||
static styles: CSSResult[] = [
|
||||
PFAvatar,
|
||||
css`
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
margin-block-start: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: var(--pf-global--spacer--xs);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
|
||||
.pf-c-avatar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.primary-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
max-width: 20rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
display: box;
|
||||
display: -webkit-box;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
box-orient: vertical;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.links {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -44,17 +63,22 @@ export class FormStatic extends AKElement {
|
||||
if (!this.user) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="form-control-static">
|
||||
<div class="avatar">
|
||||
<img
|
||||
class="pf-c-avatar"
|
||||
src="${ifDefined(this.userAvatar)}"
|
||||
alt="${msg("User's avatar")}"
|
||||
/>
|
||||
${this.user}
|
||||
<div class="primary-content">
|
||||
${this.userAvatar
|
||||
? html`<img
|
||||
class="pf-c-avatar"
|
||||
src=${this.userAvatar}
|
||||
alt=${msg("User's avatar")}
|
||||
/>`
|
||||
: nothing}
|
||||
<div class="username" aria-label=${msg("Username")}>${this.user}</div>
|
||||
</div>
|
||||
<div class="links">
|
||||
<slot name="link"></slot>
|
||||
</div>
|
||||
<slot name="link"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||
css`
|
||||
.icon-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-description i {
|
||||
font-size: 2em;
|
||||
|
||||
@@ -34,7 +34,7 @@ export class BaseDeviceStage<
|
||||
css`
|
||||
.pf-c-form__group.pf-m-action {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 1rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
|
||||
flex-direction: column;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ListenerController } from "#elements/utils/listenerController";
|
||||
import { randomId } from "#elements/utils/randomId";
|
||||
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import { CaptchaHandler, iframeTemplate } from "#flow/stages/captcha/shared";
|
||||
import { CaptchaHandler, CaptchaProvider, iframeTemplate } from "#flow/stages/captcha/shared";
|
||||
|
||||
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
|
||||
|
||||
@@ -109,7 +109,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
//#region State
|
||||
|
||||
@state()
|
||||
protected activeHandler: CaptchaHandler | null = null;
|
||||
protected activeHandler: CaptchaProvider | null = null;
|
||||
|
||||
@state()
|
||||
protected error: string | null = null;
|
||||
@@ -265,7 +265,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
|
||||
//#endregion
|
||||
|
||||
#handlers = new Map<string, CaptchaHandler>([
|
||||
/**
|
||||
* Mapping of captcha provider names to their respective JS API global.
|
||||
*
|
||||
* Note that this is a `Map` to ensure the preferred order of discovering provider globals.
|
||||
*/
|
||||
#handlers = new Map<CaptchaProvider, CaptchaHandler>([
|
||||
[
|
||||
"grecaptcha",
|
||||
{
|
||||
@@ -415,7 +420,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Listeners
|
||||
//#region Resizing
|
||||
|
||||
#loadListener = () => {
|
||||
const iframe = this.#iframeRef.value;
|
||||
@@ -423,17 +428,73 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
|
||||
if (!iframe || !contentDocument) return;
|
||||
|
||||
const resizeListener: ResizeObserverCallback = () => {
|
||||
if (!this.#iframeRef) return;
|
||||
let synchronizeHeight: () => void;
|
||||
|
||||
const target = contentDocument.getElementById("ak-container");
|
||||
if (this.activeHandler === CaptchaProvider.reCAPTCHA) {
|
||||
// reCAPTCHA's use of nested iframes prevents their internal resize observer from
|
||||
// reporting the correct height back to our iframe, so we have to do it ourselves.
|
||||
|
||||
if (!target) return;
|
||||
synchronizeHeight = () => {
|
||||
if (!this.#iframeRef) return;
|
||||
|
||||
this.iframeHeight = Math.round(target.clientHeight);
|
||||
};
|
||||
const target = contentDocument.getElementById("ak-container");
|
||||
|
||||
const resizeObserver = new ResizeObserver(resizeListener);
|
||||
if (!target) return;
|
||||
|
||||
const innerIFrame = contentDocument.querySelector<HTMLIFrameElement>(
|
||||
'iframe[style~="height:"]',
|
||||
);
|
||||
|
||||
const innerBottom = innerIFrame?.getBoundingClientRect().bottom ?? 0;
|
||||
|
||||
const actualHeight = Math.max(innerBottom, target.clientHeight);
|
||||
|
||||
this.iframeHeight = Math.round(actualHeight * 1.1);
|
||||
|
||||
if (innerIFrame?.parentElement) {
|
||||
innerIFrame.parentElement.style.height = `${actualHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
// We watch for any newly inserted iframes, as they may alter the height
|
||||
// of the parent iframe...
|
||||
const mutationObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
|
||||
for (const node of mutation.addedNodes as NodeListOf<HTMLElement>) {
|
||||
if (node.tagName !== "IFRAME") continue;
|
||||
|
||||
// And then resize the iframe to match the new size.
|
||||
//
|
||||
// This doesn't fix the issue entirely since the challenge frame
|
||||
// doesn't yet know the correct height, but at least the user can
|
||||
// try to load the challenge again with the correct height.
|
||||
|
||||
resizeObserver.observe(node as HTMLIFrameElement);
|
||||
|
||||
requestAnimationFrame(synchronizeHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mutationObserver.observe(contentDocument.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
} else {
|
||||
synchronizeHeight = () => {
|
||||
if (!this.#iframeRef) return;
|
||||
|
||||
const target = contentDocument.getElementById("ak-container");
|
||||
|
||||
if (!target) return;
|
||||
|
||||
this.iframeHeight = Math.round(target.clientHeight);
|
||||
};
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(synchronizeHeight);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
resizeObserver.observe(contentDocument.body);
|
||||
@@ -442,22 +503,26 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
});
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Loading
|
||||
|
||||
#scriptLoadListener = async (): Promise<void> => {
|
||||
console.debug("authentik/stages/captcha: script loaded");
|
||||
|
||||
this.error = null;
|
||||
this.#iframeLoaded = false;
|
||||
|
||||
for (const [name, handler] of this.#handlers) {
|
||||
for (const name of this.#handlers.keys()) {
|
||||
if (!Object.hasOwn(window, name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#run(handler);
|
||||
await this.#run(name);
|
||||
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
|
||||
|
||||
this.activeHandler = handler;
|
||||
this.activeHandler = name;
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
@@ -469,7 +534,9 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
}
|
||||
};
|
||||
|
||||
async #run(handler: CaptchaHandler) {
|
||||
async #run(captchaProvider: CaptchaProvider) {
|
||||
const handler = this.#handlers.get(captchaProvider)!;
|
||||
|
||||
if (this.challenge.interactive) {
|
||||
const iframe = this.#iframeRef.value;
|
||||
|
||||
@@ -478,18 +545,44 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
return;
|
||||
}
|
||||
|
||||
const { contentDocument } = iframe;
|
||||
|
||||
if (!contentDocument) {
|
||||
console.debug(
|
||||
`authentik/stages/captcha: No iframe content window found, skipping.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`authentik/stages/captcha: Rendering interactive.`);
|
||||
|
||||
const captchaElement = handler.interactive();
|
||||
const template = iframeTemplate(captchaElement, this.challenge.jsUrl);
|
||||
const template = iframeTemplate(captchaElement, {
|
||||
challengeURL: this.challenge.jsUrl,
|
||||
theme: this.activeTheme,
|
||||
});
|
||||
|
||||
URL.revokeObjectURL(this.#iframeSource);
|
||||
if (captchaProvider === CaptchaProvider.reCAPTCHA) {
|
||||
// reCAPTCHA's domain verification can't seem to penetrate the true origin
|
||||
// of the page when loaded from a blob URL, likely due to their double-nested
|
||||
// iframe structure.
|
||||
// We fallback to the deprecated `document.write` to get around this.
|
||||
this.#iframeSource = "about:blank";
|
||||
contentDocument.open();
|
||||
contentDocument.write(template);
|
||||
contentDocument.close();
|
||||
|
||||
const url = URL.createObjectURL(new Blob([template], { type: "text/html" }));
|
||||
// this.#loadListener();
|
||||
} else {
|
||||
URL.revokeObjectURL(this.#iframeSource);
|
||||
|
||||
this.#iframeSource = url;
|
||||
const url = URL.createObjectURL(new Blob([template], { type: "text/html" }));
|
||||
|
||||
iframe.src = url;
|
||||
this.#iframeSource = url;
|
||||
|
||||
iframe.src = url;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import type { ResolvedUITheme } from "#common/theme";
|
||||
|
||||
import { createDocumentTemplate } from "#elements/utils/iframe";
|
||||
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
/**
|
||||
* Mapping of captcha provider names to their respective JS API global.
|
||||
*/
|
||||
export const CaptchaProvider = {
|
||||
reCAPTCHA: "grecaptcha",
|
||||
hCaptcha: "hcaptcha",
|
||||
Turnstile: "turnstile",
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
export type CaptchaProvider = (typeof CaptchaProvider)[keyof typeof CaptchaProvider];
|
||||
|
||||
export interface CaptchaHandler {
|
||||
interactive(): TemplateResult;
|
||||
execute(): Promise<void>;
|
||||
@@ -9,6 +22,29 @@ export interface CaptchaHandler {
|
||||
refresh(): Promise<void>;
|
||||
}
|
||||
|
||||
const ThemeColor = {
|
||||
dark: "#18191a",
|
||||
light: "#ffffff",
|
||||
} as const satisfies Record<ResolvedUITheme, string>;
|
||||
|
||||
export function themeMeta(theme: ResolvedUITheme) {
|
||||
switch (theme) {
|
||||
case "dark":
|
||||
return html`
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content=${ThemeColor.dark} />
|
||||
`;
|
||||
case "light":
|
||||
return html` <meta name="color-scheme" content="light" />
|
||||
<meta name="theme-color" content=${ThemeColor.light} />`;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IFrameTemplateInit {
|
||||
challengeURL: string;
|
||||
theme: ResolvedUITheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* A container iframe for a hosted Captcha, with an event emitter to monitor
|
||||
* when the Captcha forces a resize.
|
||||
@@ -17,10 +53,17 @@ export interface CaptchaHandler {
|
||||
* margin, adding 2rem of height to our container adds padding and prevents scrollbars
|
||||
* or hidden rendering.
|
||||
*/
|
||||
export function iframeTemplate(children: TemplateResult, challengeURL: string): string {
|
||||
export function iframeTemplate(
|
||||
children: TemplateResult,
|
||||
{ challengeURL, theme }: IFrameTemplateInit,
|
||||
) {
|
||||
return createDocumentTemplate({
|
||||
head: html`<meta charset="UTF-8" />
|
||||
head: html`
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
${themeMeta(theme)}
|
||||
`,
|
||||
body: html`
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
@@ -43,6 +86,11 @@ export function iframeTemplate(children: TemplateResult, challengeURL: string):
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
background: ${ThemeColor[theme]};
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -58,8 +106,9 @@ export function iframeTemplate(children: TemplateResult, challengeURL: string):
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>`,
|
||||
body: html`${children}
|
||||
<script onload="loadListener()" src="${challengeURL}"></script> `,
|
||||
</style>
|
||||
${children}
|
||||
<script onload="loadListener()" src="${challengeURL}"></script>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ export class IdentificationStage extends BaseStage<
|
||||
PFButton,
|
||||
...AkRememberMeController.styles,
|
||||
css`
|
||||
.pf-c-form__group.pf-m-action {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* login page's icons */
|
||||
.pf-c-login__main-footer-links-item button {
|
||||
background-color: transparent;
|
||||
@@ -368,7 +374,9 @@ export class IdentificationStage extends BaseStage<
|
||||
|
||||
<div class="pf-c-form__group ${this.challenge.captchaStage ? "" : "pf-m-action"}">
|
||||
<button
|
||||
?disabled=${this.challenge.captchaStage && !this.captchaLoaded}
|
||||
?disabled=${this.challenge.captchaStage &&
|
||||
this.challenge.captchaStage.interactive &&
|
||||
!this.captchaLoaded}
|
||||
type="submit"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>
|
||||
|
||||
@@ -23,4 +23,4 @@ RUN npm run build
|
||||
|
||||
FROM docker.io/library/nginx:1.29.0
|
||||
|
||||
COPY --from=docs-builder /work/website/build /usr/share/nginx/html
|
||||
COPY --from=docs-builder /work/website/docs/build /usr/share/nginx/html
|
||||
|
||||
6
website/LICENSE
Normal file
6
website/LICENSE
Normal file
@@ -0,0 +1,6 @@
|
||||
Except where otherwise noted, the contents of this directory are licensed under
|
||||
the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0).
|
||||
|
||||
EXCEPTION: The contents of the `static/img/` directory (including logo and wordmark
|
||||
images) are copyrighted and not licensed under CC BY-SA. All rights reserved
|
||||
for those files.
|
||||
@@ -2,8 +2,11 @@
|
||||
* @file Docusaurus config.
|
||||
*
|
||||
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
|
||||
* @import { AKReleasesPluginOptions } from "@goauthentik/docusaurus-theme/releases/plugin"
|
||||
* @import * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
|
||||
* @import {Options as PresetOptions} from '@docusaurus/preset-classic';
|
||||
* @import { Options as RedirectsPluginOptions } from "@docusaurus/plugin-client-redirects";
|
||||
* @import { AKRedirectsPluginOptions } from "@goauthentik/docusaurus-theme/redirects/plugin"
|
||||
*/
|
||||
|
||||
import { cp } from "node:fs/promises";
|
||||
@@ -12,13 +15,22 @@ import { basename, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
|
||||
import { RewriteIndex } from "@goauthentik/docusaurus-theme/redirects";
|
||||
import { parse } from "@goauthentik/docusaurus-theme/redirects/node";
|
||||
import { prepareReleaseEnvironment } from "@goauthentik/docusaurus-theme/releases/node";
|
||||
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
const releaseEnvironment = prepareReleaseEnvironment();
|
||||
|
||||
const rootStaticDirectory = resolve(__dirname, "..", "static");
|
||||
const authentikModulePath = resolve(__dirname, "..", "..");
|
||||
const packageStaticDirectory = resolve(__dirname, "static");
|
||||
|
||||
const redirectsFile = resolve(packageStaticDirectory, "_redirects");
|
||||
const redirects = await parse(redirectsFile);
|
||||
const redirectsIndex = new RewriteIndex(redirects);
|
||||
|
||||
//#region Copy static files
|
||||
|
||||
@@ -67,6 +79,7 @@ export default createDocusaurusConfig({
|
||||
theme: {
|
||||
customCss: [require.resolve("@goauthentik/docusaurus-config/css/index.css")],
|
||||
},
|
||||
pages: false,
|
||||
docs: {
|
||||
routeBasePath: "/",
|
||||
path: ".",
|
||||
@@ -103,6 +116,13 @@ export default createDocusaurusConfig({
|
||||
//#region Plugins
|
||||
|
||||
plugins: [
|
||||
[
|
||||
"@goauthentik/docusaurus-theme/releases/plugin",
|
||||
/** @type {AKReleasesPluginOptions} */ ({
|
||||
docsDirectory: __dirname,
|
||||
environment: releaseEnvironment,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"docusaurus-plugin-openapi-docs",
|
||||
{
|
||||
@@ -121,6 +141,34 @@ export default createDocusaurusConfig({
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Inject redirects for later use during runtime,
|
||||
// such as navigating to non-existent page with the client-side router.
|
||||
|
||||
[
|
||||
"@goauthentik/docusaurus-theme/redirects/plugin",
|
||||
/** @type {AKRedirectsPluginOptions} */ ({
|
||||
redirects,
|
||||
}),
|
||||
],
|
||||
|
||||
// Create build-time redirects for later use in HTTP responses,
|
||||
// such as when navigating to a page for the first time.
|
||||
//
|
||||
// The existence of the _redirects file is also picked up by
|
||||
// Netlify's deployment, which will redirect to the correct URL, even
|
||||
// if the source is no longer present within the build output,
|
||||
// such as when a page is removed, renamed, or moved.
|
||||
[
|
||||
"@docusaurus/plugin-client-redirects",
|
||||
/** @type {RedirectsPluginOptions} */ ({
|
||||
createRedirects(existingPath) {
|
||||
const redirects = redirectsIndex.findAliases(existingPath);
|
||||
|
||||
return redirects;
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -11,7 +11,9 @@ import "./ensure-reference-sidebar.mjs";
|
||||
// @ts-ignore - Allows for project-wide type checking when partially building docs.
|
||||
import apiReference from "./reference/sidebar";
|
||||
|
||||
const DOCS_URL = process.env.DOCS_URL || "https://docs.goauthentik.io";
|
||||
import { prepareReleaseEnvironment } from "@goauthentik/docusaurus-theme/releases/node";
|
||||
|
||||
const releaseEnvironment = prepareReleaseEnvironment();
|
||||
|
||||
/**
|
||||
* @type {SidebarItemConfig}
|
||||
@@ -21,7 +23,7 @@ const sidebar = {
|
||||
{
|
||||
type: "link",
|
||||
label: "← Back to Developer Docs",
|
||||
href: new URL("/developer-docs", DOCS_URL).href,
|
||||
href: new URL("/developer-docs", releaseEnvironment.preReleaseOrigin).href,
|
||||
className: "navbar-sidebar__upwards",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import "./styles.css";
|
||||
|
||||
import { useCachedVersionPluginData } from "@goauthentik/docusaurus-theme/components/VersionPicker/utils.ts";
|
||||
|
||||
import isInternalUrl from "@docusaurus/isInternalUrl";
|
||||
import Link from "@docusaurus/Link";
|
||||
import { isActiveSidebarItem } from "@docusaurus/plugin-content-docs/client";
|
||||
@@ -7,16 +9,7 @@ import { ThemeClassNames } from "@docusaurus/theme-common";
|
||||
import type { Props } from "@theme/DocSidebarItem/Link";
|
||||
import IconExternalLink from "@theme/Icon/ExternalLink";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
const docsURL = new URL(process.env.DOCS_URL || "https://docs.goauthentik.io");
|
||||
function isInternalUrlOrDocsUrl(url: string) {
|
||||
if (isInternalUrl(url)) return true;
|
||||
|
||||
const inputURL = new URL(url);
|
||||
|
||||
return inputURL.origin === docsURL.origin;
|
||||
}
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
const DocSidebarItemLink: React.FC<Props> = ({
|
||||
item,
|
||||
@@ -29,7 +22,18 @@ const DocSidebarItemLink: React.FC<Props> = ({
|
||||
}) => {
|
||||
const { href, label, className, autoAddBaseUrl } = item;
|
||||
const isActive = isActiveSidebarItem(item, activePath);
|
||||
const internalLink = isInternalUrlOrDocsUrl(href);
|
||||
const versionPluginData = useCachedVersionPluginData();
|
||||
const apiReferenceOrigin = versionPluginData?.env.apiReferenceOrigin;
|
||||
|
||||
const internalLink = useMemo(() => {
|
||||
if (isInternalUrl(href)) return true;
|
||||
|
||||
if (!apiReferenceOrigin) return false;
|
||||
|
||||
const inputURL = new URL(href);
|
||||
|
||||
return inputURL.origin === apiReferenceOrigin;
|
||||
}, [href, apiReferenceOrigin]);
|
||||
|
||||
return (
|
||||
<li
|
||||
|
||||
3
website/api/static/_headers
Normal file
3
website/api/static/_headers
Normal file
@@ -0,0 +1,3 @@
|
||||
# Headers for static files
|
||||
/*
|
||||
X-Frame-Options: DENY
|
||||
11
website/api/static/_redirects
Normal file
11
website/api/static/_redirects
Normal file
@@ -0,0 +1,11 @@
|
||||
# @file Redirect rules for the Docusaurus site.
|
||||
#
|
||||
# See https://docs.netlify.com/manage/routing/redirects/overview/
|
||||
#
|
||||
# Note: The order of the rules defines the priority of the redirect.
|
||||
# i.e. The first rule that matches the URL will take precedence.
|
||||
|
||||
|
||||
#region api prefix
|
||||
/api/* /:splat 301!
|
||||
#endregion
|
||||
@@ -4,7 +4,7 @@ title: Applications
|
||||
|
||||
Applications, as defined in authentik, are used to configure and separate the authorization/access control and the appearance of a specific software application in the **My applications** page.
|
||||
|
||||
When a user logs into authentik, they see a list of the applications for which authentik is configured to provide authentication and authorization (the applications that that they are authorized to use).
|
||||
When a user logs into authentik, they see a list of the applications for which authentik is configured to provide authentication and authorization (the applications that they are authorized to use).
|
||||
|
||||
Applications are the "other half" of providers. They typically exist in a 1-to-1 relationship; each application needs a provider and every provider can be used with one application. Applications can, however, use specific, additional providers to augment the functionality of the main provider. For more information, see [Backchannel providers](./manage_apps.mdx#backchannel-providers).
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ To add an application to authentik and have it display on users' **My applicatio
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
|
||||
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.)**
|
||||
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** 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.
|
||||
@@ -73,7 +73,7 @@ return {
|
||||
|
||||
### Create an application entitlement
|
||||
|
||||
1. Open the Admin interface and navigate to **Applications -> Applications**.
|
||||
1. Open the Admin interface and navigate to **Applications > Applications**.
|
||||
2. Click the name of the application for which you want to create an entitlement.
|
||||
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.**
|
||||
|
||||
@@ -41,7 +41,7 @@ Starting with authentik 2022.8, flows will be exported as YAML, but JSON-based f
|
||||
To create a flow, follow these steps:
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Flows and Stages -> Flows**.
|
||||
2. In the Admin interface, navigate to **Flows and Stages > Flows**.
|
||||
3. Click **Create**, define the flow using the [configuration settings](#flow-configuration-options) described below, and then click **Finish**.
|
||||
|
||||
After creating the flow, you can then [bind specific stages](../stages/index.md#bind-a-stage-to-a-flow) to the flow and [bind policies](../../../customize/policies/working_with_policies.md) to the flow to further customize the user's log in and authentication process.
|
||||
|
||||
@@ -22,7 +22,7 @@ Starting with authentik 2025.2, for users with appropriate permissions to access
|
||||
|
||||
### Manually running a flow with the inspector
|
||||
|
||||
1. To access the inspector, open the Admin interface and navigate to **Flows and Stages -> Flows**.
|
||||
1. To access the inspector, open the Admin interface and navigate to **Flows and Stages > Flows**.
|
||||
|
||||
2. Select the specific flow that you want to inspect by clicking its name in the list.
|
||||
|
||||
|
||||
@@ -52,12 +52,12 @@ For detailed instructions, refer to Google documentation.
|
||||
### Set credentials for the service account
|
||||
|
||||
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**.
|
||||
2. Click the **Keys** tab at top of the page, the click **Add Key > Create new key**.
|
||||
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.
|
||||
5. Log in to the Admin Console, and then navigate to **Chrome browser -> Connectors**.
|
||||
5. Log in to the Admin Console, and then navigate to **Chrome browser > Connectors**.
|
||||
6. Click on **New Provider Configuration**.
|
||||
7. Under Universal Device Trust, click "Set up".
|
||||
8. Enter a name.
|
||||
@@ -68,7 +68,7 @@ For detailed instructions, refer to Google documentation.
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
|
||||
2. In the Admin interface, navigate to **Flows -> Stages**.
|
||||
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** box, define the following fields:
|
||||
- **Name**: define a descriptive name, such as "chrome-device-trust".
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
title: Email stage
|
||||
---
|
||||
|
||||
This stage can be used for email verification. authentik's background worker will send an email using the specified connection details. When an email can't be delivered, delivery is automatically retried periodically.
|
||||
This stage can be used for email verification. authentik's background worker will send an email using the specified connection details.
|
||||
|
||||

|
||||
When an email can't be delivered, authentik automatically retries periodically.
|
||||
|
||||
You can also configure rate-limiting for emails requested by users. See the [configure rate limiting](#configure-rate-limiting-for-emails) section for more information.
|
||||
|
||||
For information about creating a stage, refer to our [documentation](../#create-a-stage).
|
||||
|
||||
## Behaviour
|
||||
|
||||
By default, the email is sent to the currently pending user. To override this, you can set `email` in the plan's context to another email address, which will override the user's email address (the user won't be changed).
|
||||
|
||||
For example, create this expression policy and bind it to the email stage:
|
||||
For example, create this [expression policy](../../../../customize/policies/expression.mdx) and bind it to the email stage:
|
||||
|
||||
```python
|
||||
request.context["flow_plan"].context["email"] = "foo@bar.baz"
|
||||
@@ -21,6 +25,15 @@ request.context["flow_plan"].context["email"] = "foo@bar.baz"
|
||||
return True
|
||||
```
|
||||
|
||||
## Configure rate limiting for emails
|
||||
|
||||
You can configure the Email stage with _a maximum number of emails_ that can be sent within _a specified time period_.
|
||||
|
||||
To configure the rate limiting for recovery emails use these two fields when you create or edit an Email stage:
|
||||
|
||||
- **Account Recovery Max Attempts**: set the maximum number of emails to send.
|
||||
- **Account Recovery Cache Timeout**: specify the time window used to count recent recovery emails sent to the user (account recovery attempts).
|
||||
|
||||
## Custom Templates
|
||||
|
||||
You can also use custom email templates, to use your own design or layout.
|
||||
@@ -72,7 +85,7 @@ volumeMounts:
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
If you've add the line and created a file, and can't see if, check the worker logs using `docker compose logs -f worker` or `kubectl logs -f deployment/authentik-worker`.
|
||||
If you have added the line and created a file, and can't see it, check the worker logs using `docker compose logs -f worker` or `kubectl logs -f deployment/authentik-worker`.
|
||||
:::
|
||||
|
||||

|
||||
@@ -98,7 +111,7 @@ Templates are rendered using Django's templating engine. The following variables
|
||||
{% block content %}
|
||||
<tr>
|
||||
<td class="alert alert-success">
|
||||
{% blocktrans with username=user.username %} Hi {{ username }},
|
||||
{% blocktrans with username=user.username %} Hi {{ username }},
|
||||
{% endblocktrans %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -107,7 +120,7 @@ Templates are rendered using Django's templating engine. The following variables
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
{% trans 'You recently requested to change your password for you authentik account. Use the button below to set a new password.' %}
|
||||
{% trans 'You recently requested to change your password for you authentik account. Use the button below to set a new password.' %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -40,7 +40,7 @@ stage_3 --> done[["End of the flow"]]
|
||||
To create a stage, follow these steps:
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Flows and Stages -> Stages**.
|
||||
2. In the Admin interface, navigate to **Flows and Stages > Stages**.
|
||||
3. Click **Create**, define the flow using the configuration settings, and then click **Finish**.
|
||||
|
||||
After creating the stage, you can then [bind the stage to a flow](#bind-a-stage-to-a-flow) or [bind a policy to the stage](../../../customize/policies/working_with_policies.md) (the policy determines whether or not the stage will be implemented in the flow).
|
||||
@@ -50,7 +50,7 @@ After creating the stage, you can then [bind the stage to a flow](#bind-a-stage-
|
||||
To bind a stage to a flow, follow these steps:
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Flows and Stages -> Flows**.
|
||||
2. In the Admin interface, navigate to **Flows and Stages > Flows**.
|
||||
3. In the list of flows, click the name of the flow to which you want to bind one or more stages.
|
||||
4. On the Flow page, click the **Stage Bindings** tab at the top.
|
||||
5. Here, you can decide if you want to create a new stage and bind it to the flow (**Create and bind Stage**), or if you want to select an existing stage and bind it to the flow (**Bind existing stage**).
|
||||
@@ -62,7 +62,7 @@ You can use bindings to determine whether or not a stage is presented to a singl
|
||||
To bind a user or a group to a stage binding for a specific flow, follow these steps:
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Flows and Stages -> Flows**.
|
||||
2. In the Admin interface, navigate to **Flows and Stages > Flows**.
|
||||
3. In the list of flows, click the name of the flow to which you want to bind one or more stages.
|
||||
4. On the Flow page, click the **Stage Bindings** tab at the top.
|
||||
5. Locate the stage binding to which you want to bind a user or group, and then **click the caret (>) to expand the stage binding details.**
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: User delete stage
|
||||
title: User Delete stage
|
||||
---
|
||||
|
||||
:::danger
|
||||
This stage deletes the `pending_user` without any confirmation. You have to make sure the user is aware of this.
|
||||
:::
|
||||
|
||||
This stage is intended for an unenrollment flow. It deletes the currently pending user.
|
||||
The User Delete stage is intended for an unenrollment flow. It deletes the currently pending user.
|
||||
|
||||
The pending user is also removed from the current session.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: User login stage
|
||||
title: User Login stage
|
||||
---
|
||||
|
||||
This stage attaches a currently pending user to the current session.
|
||||
The User Login stage attaches a currently pending user to the current session.
|
||||
|
||||
It can be used after `user_write` during an enrollment flow, or after a `password` stage during an authentication flow.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: User logout stage
|
||||
title: User Logout stage
|
||||
---
|
||||
|
||||
Opposite stage of [User Login Stages](./user_login/index.md). It removes the user from the current session.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: User write stage
|
||||
title: User Write stage
|
||||
---
|
||||
|
||||
This stages writes data from the current flow context to a user.
|
||||
The User Write stage writes data from the current flow context to a user.
|
||||
|
||||
Newly created users can be created as inactive and can be assigned to a selected group.
|
||||
|
||||
### Dynamic groups
|
||||
|
||||
Starting with authentik 2022.5, users can be added to dynamic groups. To do so, simply set `groups` in the flow plan context before this stage is run, for example
|
||||
To add users to dynamic groups, set `groups` in the flow plan context before this stage is run. For example:
|
||||
|
||||
```python
|
||||
from authentik.core.models import Group
|
||||
@@ -22,6 +22,6 @@ return True
|
||||
|
||||
By default, this stage will create a new user when none is present in the flow context.
|
||||
|
||||
Starting with authentik 2022.12, the stage can by default not create new users to prevent users from creating new accounts without authorization.
|
||||
To prevent users from creating new accounts without authorization, you can configure the User Write stage to not automatically create new users.
|
||||
|
||||
Starting with authentik 2023.1, this option has been expanded to allow user creation, forbid it or force user creation.
|
||||
Alternatively, you can configure the stage to explicitly allow user creation, forbid it, or force user creation.
|
||||
|
||||
@@ -50,6 +50,7 @@ kubernetes_namespace: authentik
|
||||
kubernetes_ingress_annotations: {}
|
||||
# Name of the secret that is used for TLS connections, leave empty to disable TLS
|
||||
kubernetes_ingress_secret_name: authentik-outpost-tls
|
||||
# pathType to use on routes. Defaults to `Prefix`. Some ingress-nginx deployments need this to be set to `ImplementationSpecific`.
|
||||
# Service kind created, can be set to LoadBalancer for LDAP outposts for example
|
||||
kubernetes_service_type: ClusterIP
|
||||
# Disable any components of the kubernetes integration, can be any of
|
||||
|
||||
@@ -21,39 +21,37 @@ Any change made to the outpost's associated app or provider immediately triggers
|
||||
|
||||
## Create and configure an outpost
|
||||
|
||||
1. To create a new outpost, log in to authentik as an administrator, and open to the Admin interface.
|
||||
|
||||
2. Navigate to **Applications --> Outposts** and then click **Create**.
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Outposts** and then click **Create**.
|
||||
|
||||

|
||||
|
||||
3. Define the following values:
|
||||
3. Set the following values:
|
||||
- **Name**: define a name for the outpost.
|
||||
- **Type**: select the outpost type (Proxy, LDAP, Radius, RAC).
|
||||
- **Integration**: select either Docker or Kubernetes, or optionally select `----` and [manually deploy the outpost](#outpost-integrations).
|
||||
- **Applications**: select the applications that you want the outpost to serve.
|
||||
- **Advanced settings (optional)**: for further optional configuration settings, refer to [Configuration](#configuration) below.
|
||||
|
||||
- **Name**: a name for the new outpost
|
||||
- **Type**: select the provider type (Proxy, LDAP, Radius, RAC)
|
||||
- **Integration** (_optional_): select either your [Docker or Kubernetes connection](#more-about-outpost-integrations)
|
||||
- **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 box.
|
||||
4. Click **Create**.
|
||||
|
||||
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.
|
||||
|
||||
### More about outpost integrations
|
||||
## Outpost integrations
|
||||
|
||||
authentik can manage the deployment, updating, and general lifecycle of an outpost. To communicate with the underlying platforms on which the outpost is deployed, authentik has several built-in integrations.
|
||||
|
||||
- If you've deployed authentik on Docker Compose, authentik automatically creates an integration for the local docker socket (See [Docker](./integrations/docker.md)).
|
||||
- If you've deployed authentik on Kubernetes, with `kubernetesIntegration` set to true (default), authentik automatically creates an integrations for the local Kubernetes Cluster (see [Kubernetes](./integrations/kubernetes.md)).
|
||||
|
||||
To deploy an outpost with these integrations, select them during the creation of an outpost. A background task is started, which creates the container/deployment. The outpost deployment can be monitored from the **Dashboards -> System Tasks** page in the Admin interface.
|
||||
To deploy an outpost with these integrations, select them during the creation of an outpost. A background task is started, which creates the container/deployment. The outpost deployment can be monitored from the **Dashboards > System Tasks** page in the Admin interface.
|
||||
|
||||
To deploy an outpost manually, see:
|
||||
|
||||
- [Kubernetes](./manual-deploy-kubernetes.md)
|
||||
- [Docker Compose](./manual-deploy-docker-compose.md)
|
||||
|
||||
### Configuration
|
||||
## Configuration
|
||||
|
||||
Outposts fetch their configuration from authentik. Below are all the options you can set, and how they influence the outpost.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ As detailed in the steps below, when you add an Entra ID provider in authentik y
|
||||
### Create the Entra ID provider in authentik
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Applications -> Providers**.
|
||||
2. In the Admin interface, navigate to **Applications > Providers**.
|
||||
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".
|
||||
@@ -43,7 +43,7 @@ As detailed in the steps below, when you add an Entra ID provider in authentik y
|
||||
### Create an Entra ID application in authentik
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Applications -> Applications**.
|
||||
2. In the Admin interface, navigate to **Applications > Applications**.
|
||||
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.
|
||||
|
||||
@@ -17,7 +17,7 @@ When adding the Google Workspace provider in authentik, you must define the **Ba
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
|
||||
2. In the Admin interface, navigate to **Applications -> Providers**.
|
||||
2. In the Admin interface, navigate to **Applications > Providers**.
|
||||
|
||||
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".
|
||||
@@ -42,7 +42,7 @@ When adding the Google Workspace provider in authentik, you must define the **Ba
|
||||
### Create a Google Workspace application in authentik
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Applications -> Applications**.
|
||||
2. In the Admin interface, navigate to **Applications > Applications**.
|
||||
:::info
|
||||
If you have also configured Google Workspace to log in using authentik following this [ integration guide](/integrations/cloud-providers/google), 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.
|
||||
:::
|
||||
|
||||
@@ -40,13 +40,13 @@ For detailed instructions, refer to Google documentation.
|
||||
### Set credentials for the service account
|
||||
|
||||
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**.
|
||||
2. Click the **Keys** tab at top of the page, the click **Add Key > Create new key**.
|
||||
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.
|
||||
5. Copy the **Client ID** (under **Domain-wide delegation**), and then click **View Google Workspace Admin Console**.
|
||||
6. Log in to the Admin Console, and then navigate to **Security -> Access and data control -> API controls**.
|
||||
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** 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:
|
||||
@@ -59,7 +59,7 @@ For detailed instructions, refer to Google documentation.
|
||||
|
||||
The Delegated Subject email address is a required field when creating the provider in authentik.
|
||||
|
||||
1. Open to the main Admin console page, and navigate to **Directory -> Users**.
|
||||
1. Open to the main Admin console page, and navigate to **Directory > Users**.
|
||||
2. You can either select an existing user's email address or **Add new user** and define the user and email address to use as the Delegated Subject.
|
||||
3. Save this email address to enter into authentik when you are creating the Google Workspace provider.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Create an LDAP provider
|
||||
|
||||
### Create Service account
|
||||
|
||||
1. Create a new user account to bind with under _Directory_ -> _Users_ -> _Create_, in this example called `ldapservice`.
|
||||
1. Create a new user account to bind with under **Directory > Users > Create**, in this example called `ldapservice`.
|
||||
|
||||
Note the DN of this user will be `cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io`
|
||||
|
||||
@@ -16,22 +16,22 @@ Note: The `default-authentication-flow` validates MFA by default, and currently
|
||||
|
||||
#### Create Custom Stages
|
||||
|
||||
1. Create a new identification stage. _Flows & Stage_ -> _Stages_ -> _Create_
|
||||
1. Create a new identification stage. **Flows & Stages > Stages > Create**
|
||||

|
||||
2. Name it `ldap-identification-stage`. Select User fields Username and Email (and UPN if it is relevant to your setup).
|
||||

|
||||
3. Create a new password stage. _Flows & Stage_ -> _Stages_ -> _Create_
|
||||
3. Create a new password stage. **Flows & Stages > Stages > Create**
|
||||

|
||||
4. Name it `ldap-authentication-password`. Leave the defaults for Backends.
|
||||

|
||||
5. Create a new user login stage. _Flows & Stage_ -> _Stages_ -> _Create_
|
||||
5. Create a new user login stage. **Flows & Stages > Stages > Create**
|
||||

|
||||
6. Name it `ldap-authentication-login`.
|
||||

|
||||
|
||||
#### Create Custom Flow
|
||||
|
||||
1. Create a new authentication flow under _Flows & Stage_ -> _Flows_ -> _Create_, and name it `ldap-authentication-flow`
|
||||
1. Create a new authentication flow under **Flows & Stages > Flows > Create**, and name it `ldap-authentication-flow`
|
||||

|
||||
2. Click the newly created flow and choose _Stage Bindings_.
|
||||

|
||||
@@ -46,20 +46,20 @@ 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 provider_ and name it `LDAP`.
|
||||
1. Create the LDAP Application under **Applications > Applications > Create With provider** and name it `LDAP`.
|
||||

|
||||

|
||||
|
||||
### Assign LDAP permissions
|
||||
|
||||
1. Navigate to the LDAP Provider under _Applications_ -> _Providers_ -> `Provider for LDAP`.
|
||||
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 typing in its username. Select the _Search full LDAP directory_ permission and click _Assign_
|
||||
|
||||
### Create LDAP Outpost
|
||||
|
||||
1. Create (or update) the LDAP Outpost under _Applications_ -> _Outposts_ -> _Create_. Set the Type to `LDAP` and choose the `LDAP` application created in the previous step.
|
||||
1. Create (or update) the LDAP Outpost under **Applications > Outposts > Create**. Set the Type to `LDAP` and choose the `LDAP` application created in the previous step.
|
||||

|
||||
|
||||
:::info
|
||||
@@ -91,5 +91,5 @@ ldapsearch \
|
||||
```
|
||||
|
||||
:::info
|
||||
This query will log the first successful attempt in an event in the _Events_ -> _Logs_ area, further successful logins from the same user are not logged as they are cached in the outpost.
|
||||
This query will log the first successful attempt in an event in the **Events > Logs** area, further successful logins from the same user are not logged as they are cached in the outpost.
|
||||
:::
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
title: Create an OAuth2 provider
|
||||
---
|
||||
|
||||
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**.)
|
||||
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **Create with provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to solely create the provider by navigating to **Applications** > **Providers** and clicking **Create**.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
|
||||
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** box, define the application details, and then click **Next**.
|
||||
|
||||
4. Select the **Provider Type** of **OAuth2/OIDC**, and then click **Next**.
|
||||
|
||||
5. On the **Configure OAuth2/OpenId Provider** page, provide the configuration settings and then click **Submit** to create and save both the application and the provider.
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
|
||||
3. On the **New application** page, define the application settings, and then click **Next**.
|
||||
4. Select **OAuth2/OIDC** as the **Provider Type**, and then click **Next**.
|
||||
5. On the **Configure OAuth2/OpenId Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
|
||||
|
||||
:::info
|
||||
Optionally, configure the provider to have the `offline_access` scope mapping. Starting with authentik 2024.2, by default applications only receive an access token. To receive a refresh token, both applications and authentik must be configured to request the `offline_access` scope. Do this in the Scope mapping area on the **Configure OAuth2/OpenId Provider** page.
|
||||
Optionally, configure the provider with the `offline_access` scope mapping. By default, applications only receive an access token. To receive a refresh token, applications and authentik must be configured to request the `offline_access` scope. Do this in the Scope mapping area on the **Configure OAuth2/OpenId Provider** page.
|
||||
:::
|
||||
|
||||
@@ -6,7 +6,7 @@ title: Header authentication
|
||||
|
||||
### Send HTTP Basic authentication
|
||||
|
||||
Proxy providers have the option to _Send HTTP-Basic Authentication_ to the upstream authentication. When the option in the provider is enabled, two attributes must be specified. These attributes are the keys of values which can be saved on a user or group level that contain the credentials.
|
||||
Proxy providers have the option to _Send HTTP-Basic Authentication_ to the upstream application. When the option in the provider is enabled, two attributes must be specified. These attributes are the keys of values which can be saved on a user or group level that contain the credentials.
|
||||
|
||||
For example, with _HTTP-Basic Username Key_ set to `app_username` and _HTTP-Basic Password Key_ set to `app_password`, these attributes would have to be set either on a user or a group the user is member of:
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ For overview information, see the [RAC provider](./index.md) documentation. You
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts;start=22" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The RAC provider requires the deployment of the [RAC Outpost](../../outposts/index.mdx).
|
||||
|
||||
## Overview workflow to create an RAC provider
|
||||
|
||||
The typical workflow to create and configure a RAC provider is:
|
||||
@@ -19,6 +15,7 @@ The typical workflow to create and configure a RAC provider is:
|
||||
1. Create an application and provider.
|
||||
2. Create property mappings (that define the access credentials to each remote machine).
|
||||
3. Create an endpoint for each remote machine you want to connect to.
|
||||
4. Create an RAC outpost to service the provider.
|
||||
|
||||
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
|
||||
|
||||
@@ -35,8 +32,8 @@ The first step is to create the RAC application and provider pair.
|
||||
Next, you need to add property mappings for each remote machine you want to access. Property mappings allow you to pass information to external applications, and with RAC they are used to pass the host name, IP address, and access credentials of the remote machine.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Customization > Property Mappings** and click **Create**.
|
||||
- **Select Type**: RAC Property Mappings
|
||||
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
|
||||
- **Select Type**: `RAC Provider Property Mapping`
|
||||
- **Create RAC Property Mapping**:
|
||||
- **Name**s: define a name for the property mapping, perhaps include the type of connection (RDP, SSH, VNC)
|
||||
- **General settings**:
|
||||
@@ -54,10 +51,10 @@ Next, you need to add property mappings for each remote machine you want to acce
|
||||
|
||||
### Create endpoints for the provider
|
||||
|
||||
Finally, you need to create an endpoint for each remote machine. Endpoints are defined within providers; connections between the remote machine and authentik are enabled through communication between the provider's endpoint and the remote machine.
|
||||
Then, you need to create an endpoint for each remote machine. Endpoints are defined within providers; connections between the remote machine and authentik are enabled through communication between the provider's endpoint and the remote machine.
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications > Providers**.
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the **Edit** button on the RAC provider that you previously created.
|
||||
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
|
||||
- **Name**: define a name for the endpoint, perhaps include the type of connection (RDP, SSH, VNC).
|
||||
@@ -69,6 +66,21 @@ Finally, you need to create an endpoint for each remote machine. Endpoints are d
|
||||
|
||||
5. Click **Create**.
|
||||
|
||||
### Create an RAC outpost
|
||||
|
||||
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Applications** > **Outposts**.
|
||||
3. Click **Create** and set the following values:
|
||||
- **Name**: define a name for the outpost.
|
||||
- **Type**: `RAC`
|
||||
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
|
||||
- **Applications**: select the RAC application that you previously created.
|
||||
- **Advanced settings (optional)**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
|
||||
|
||||
4. Click Create to save your new outpost.
|
||||
|
||||
## Access the remote machine
|
||||
|
||||
To verify your configuration and then access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page click the **Remote Access** application and authentik then connects you to a secure session on the remote machine, in your web browser.
|
||||
|
||||
@@ -14,8 +14,8 @@ SSH private keys can be configured via several methods:
|
||||
2. Navigate to **Applications** > **Providers**.
|
||||
3. Click the **Edit** icon on the RAC provider that requires public key authentication.
|
||||
4. In the **Settings** codebox enter the private key of the endpoint, for example:
|
||||
```python
|
||||
private-key:
|
||||
```yaml
|
||||
private-key: |
|
||||
-----BEGIN SSH PRIVATE KEY-----
|
||||
SAMPLEgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
|
||||
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
|
||||
@@ -28,6 +28,10 @@ SSH private keys can be configured via several methods:
|
||||
```
|
||||
5. Click **Update**.
|
||||
|
||||
:::note
|
||||
The pipe character (`|`) is required to preserve linebreaks in the YAML text. See the [YAML spec](https://yaml.org/spec/1.2.2/#literal-style) for more information.
|
||||
:::
|
||||
|
||||
## Apply a private key to an RAC endpoint
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
@@ -35,8 +39,8 @@ SSH private keys can be configured via several methods:
|
||||
3. Click the name of the RAC provider that the endpoint belongs to.
|
||||
4. Under **Endpoints**- click on the **Edit** icon next to the endpoint that requires public key authentication.
|
||||
5. Under **Advanced settings**, in the **Settings** codebox enter the private key of the endpoint:
|
||||
```python
|
||||
private-key:
|
||||
```yaml
|
||||
private-key: |
|
||||
-----BEGIN SSH PRIVATE KEY-----
|
||||
SAMPLEgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
|
||||
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
|
||||
@@ -49,6 +53,10 @@ SSH private keys can be configured via several methods:
|
||||
```
|
||||
6. Click **Update**.
|
||||
|
||||
:::note
|
||||
The pipe character (`|`) is required to preserve linebreaks in the YAML text. See the [YAML spec](https://yaml.org/spec/1.2.2/#literal-style) for more information.
|
||||
:::
|
||||
|
||||
## Apply a private key to an RAC property mapping
|
||||
|
||||
1. Log in to authentik as an administrator, and open the authentik Admin interface.
|
||||
@@ -97,5 +105,5 @@ SSH private keys can be configured via several methods:
|
||||
7. Click **Update**.
|
||||
|
||||
:::note
|
||||
For group attributes, the following expression can be used `request.user.group_attributes(request.http_request)`
|
||||
For group attributes, the following expression can be used `request.user.group_attributes(request.http_request)`.
|
||||
:::
|
||||
|
||||
@@ -16,7 +16,7 @@ The workflow to implement an SSF provider as a [backchannel provider](../../appl
|
||||
|
||||
## Create the SSF provider
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications -> Providers**.
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
@@ -28,7 +28,7 @@ The workflow to implement an SSF provider as a [backchannel provider](../../appl
|
||||
|
||||
## Create the OIDC provider
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications -> Providers**.
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
@@ -38,7 +38,7 @@ The workflow to implement an SSF provider as a [backchannel provider](../../appl
|
||||
|
||||
## Create the application
|
||||
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications -> Applications**.
|
||||
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Applications**.
|
||||
|
||||
2. Click **Create**.
|
||||
|
||||
|
||||
@@ -4,10 +4,6 @@ title: Customize your instance
|
||||
|
||||
You can customize the behaviour, look, and available resources for your authentik instance. For more information refer to each of the topics below:
|
||||
|
||||
- [Policies](./policies/working_with_policies.md)
|
||||
- Interfaces:
|
||||
- [Flow interface](./interfaces/flow)
|
||||
- [User interface](./interfaces/user)
|
||||
- [Admin interface](./interfaces/admin)
|
||||
- [Blueprints](./blueprints/index.mdx)
|
||||
- [Branding](./branding.md)
|
||||
import DocCardList from "@theme/DocCardList";
|
||||
|
||||
<DocCardList />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user