Compare commits
1 Commits
enterprise
...
admin/add-
| Author | SHA1 | Date | |
|---|---|---|---|
| bb4602745e |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@ -35,7 +35,7 @@ runs:
|
||||
run: |
|
||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||
docker compose -f .github/actions/setup/docker-compose.yml up -d
|
||||
poetry sync
|
||||
poetry install --sync
|
||||
cd web && npm ci
|
||||
- name: Generate config
|
||||
shell: poetry run python {0}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
"""User API Views"""
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from hashlib import sha256
|
||||
from json import loads
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import AnonymousUser, Permission
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
from django.db.models.functions import ExtractHour
|
||||
@ -84,6 +85,7 @@ from authentik.flows.models import FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||
from authentik.lib.avatars import get_avatar
|
||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.models import get_permission_choices
|
||||
from authentik.stages.email.models import EmailStage
|
||||
@ -446,15 +448,19 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def _create_recovery_link(self) -> tuple[str, Token]:
|
||||
def _create_recovery_link(self, expires: datetime) -> tuple[str, Token]:
|
||||
"""Create a recovery link (when the current brand has a recovery flow set),
|
||||
that can either be shown to an admin or sent to the user directly"""
|
||||
brand: Brand = self.request._request.brand
|
||||
# Check that there is a recovery flow, if not return an error
|
||||
flow = brand.flow_recovery
|
||||
if not flow:
|
||||
raise ValidationError({"non_field_errors": "No recovery flow set."})
|
||||
raise ValidationError(
|
||||
{"non_field_errors": [_("Recovery flow is not set for this brand.")]}
|
||||
)
|
||||
# Mimic an unauthenticated user navigating the recovery flow
|
||||
user: User = self.get_object()
|
||||
self.request._request.user = AnonymousUser()
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
try:
|
||||
@ -466,16 +472,16 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
raise ValidationError(
|
||||
{"non_field_errors": "Recovery flow not applicable to user"}
|
||||
{"non_field_errors": [_("Recovery flow is not applicable to this user.")]}
|
||||
) from None
|
||||
token, __ = FlowToken.objects.update_or_create(
|
||||
identifier=f"{user.uid}-password-reset",
|
||||
defaults={
|
||||
"user": user,
|
||||
"flow": flow,
|
||||
"_plan": FlowToken.pickle(plan),
|
||||
},
|
||||
token = FlowToken.objects.create(
|
||||
identifier=f"{user.uid}-password-reset-{sha256(str(datetime.now()).encode('UTF-8')).hexdigest()[:8]}",
|
||||
user=user,
|
||||
flow=flow,
|
||||
_plan=FlowToken.pickle(plan),
|
||||
expires=expires,
|
||||
)
|
||||
|
||||
querystring = urlencode({QS_KEY_TOKEN: token.key})
|
||||
link = self.request.build_absolute_uri(
|
||||
reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||
@ -610,61 +616,68 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="email_stage",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="token_duration",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
],
|
||||
responses={
|
||||
"200": LinkSerializer(many=False),
|
||||
},
|
||||
request=None,
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
def recovery(self, request: Request, pk: int) -> Response:
|
||||
def recovery_link(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
link, _ = self._create_recovery_link()
|
||||
return Response({"link": link})
|
||||
token_duration = request.query_params.get("token_duration", "")
|
||||
timedelta_string_validator(token_duration)
|
||||
expires = now() + timedelta_from_string(token_duration)
|
||||
link, token = self._create_recovery_link(expires)
|
||||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="email_stage",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
if email_stage := request.query_params.get("email_stage"):
|
||||
for_user: User = self.get_object()
|
||||
if for_user.email == "":
|
||||
LOGGER.debug("User doesn't have an email address")
|
||||
raise ValidationError(
|
||||
{"non_field_errors": [_("User does not have an email address set.")]}
|
||||
)
|
||||
|
||||
# Lookup the email stage to assure the current user can access it
|
||||
stages = get_objects_for_user(
|
||||
request.user, "authentik_stages_email.view_emailstage"
|
||||
).filter(pk=email_stage)
|
||||
if not stages.exists():
|
||||
if stages := EmailStage.objects.filter(pk=email_stage).exists():
|
||||
raise ValidationError(
|
||||
{"non_field_errors": [_("User has no permissions to this Email stage.")]}
|
||||
)
|
||||
else:
|
||||
raise ValidationError(
|
||||
{"non_field_errors": [_("The given Email stage does not exist.")]}
|
||||
)
|
||||
email_stage: EmailStage = stages.first()
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(email_stage.subject),
|
||||
to=[(for_user.name, for_user.email)],
|
||||
template_name=email_stage.template,
|
||||
language=for_user.locale(request),
|
||||
template_context={
|
||||
"url": link,
|
||||
"user": for_user,
|
||||
"expires": token.expires,
|
||||
},
|
||||
)
|
||||
],
|
||||
responses={
|
||||
"204": OpenApiResponse(description="Successfully sent recover email"),
|
||||
},
|
||||
request=None,
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
def recovery_email(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
for_user: User = self.get_object()
|
||||
if for_user.email == "":
|
||||
LOGGER.debug("User doesn't have an email address")
|
||||
raise ValidationError({"non_field_errors": "User does not have an email address set."})
|
||||
link, token = self._create_recovery_link()
|
||||
# Lookup the email stage to assure the current user can access it
|
||||
stages = get_objects_for_user(
|
||||
request.user, "authentik_stages_email.view_emailstage"
|
||||
).filter(pk=request.query_params.get("email_stage"))
|
||||
if not stages.exists():
|
||||
LOGGER.debug("Email stage does not exist/user has no permissions")
|
||||
raise ValidationError({"non_field_errors": "Email stage does not exist."})
|
||||
email_stage: EmailStage = stages.first()
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(email_stage.subject),
|
||||
to=[(for_user.name, for_user.email)],
|
||||
template_name=email_stage.template,
|
||||
language=for_user.locale(request),
|
||||
template_context={
|
||||
"url": link,
|
||||
"user": for_user,
|
||||
"expires": token.expires,
|
||||
},
|
||||
)
|
||||
send_mails(email_stage, message)
|
||||
return Response(status=204)
|
||||
send_mails(email_stage, message)
|
||||
|
||||
return Response({"link": link})
|
||||
|
||||
@permission_required("authentik_core.impersonate")
|
||||
@extend_schema(
|
||||
|
||||
@ -35,7 +35,8 @@ from authentik.flows.planner import (
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.utils import delete_none_values
|
||||
@ -46,9 +47,8 @@ from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
||||
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
|
||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
||||
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
||||
|
||||
|
||||
class MessageStage(StageView):
|
||||
@ -219,28 +219,28 @@ class SourceFlowManager:
|
||||
}
|
||||
)
|
||||
flow_context.update(self.policy_context)
|
||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||
plan = token.plan
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||
plan.context.update(flow_context)
|
||||
for stage in self.get_stages_to_append(flow):
|
||||
plan.append_stage(stage)
|
||||
if stages:
|
||||
for stage in stages:
|
||||
plan.append_stage(stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
flow_slug = token.flow.slug
|
||||
token.delete()
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
self.request.GET,
|
||||
flow_slug=flow_slug,
|
||||
)
|
||||
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
|
||||
|
||||
if not flow:
|
||||
# We only check for the flow token here if we don't have a flow, otherwise we rely on
|
||||
# SESSION_KEY_SOURCE_FLOW_STAGES to delegate the usage of this token and dynamically add
|
||||
# stages that deal with this token to return to another flow
|
||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||
self._logger.info(
|
||||
"Replacing source flow with overridden flow", flow=token.flow.slug
|
||||
)
|
||||
plan = token.plan
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||
plan.context.update(flow_context)
|
||||
for stage in self.get_stages_to_append(flow):
|
||||
plan.append_stage(stage)
|
||||
if stages:
|
||||
for stage in stages:
|
||||
plan.append_stage(stage)
|
||||
redirect = plan.to_redirect(self.request, token.flow)
|
||||
token.delete()
|
||||
return redirect
|
||||
return bad_request_message(
|
||||
self.request,
|
||||
_("Configured flow does not exist."),
|
||||
@ -259,8 +259,6 @@ class SourceFlowManager:
|
||||
if stages:
|
||||
for stage in stages:
|
||||
plan.append_stage(stage)
|
||||
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
|
||||
plan.append_stage(stage)
|
||||
return plan.to_redirect(self.request, flow)
|
||||
|
||||
def handle_auth(
|
||||
@ -297,8 +295,6 @@ class SourceFlowManager:
|
||||
# When request isn't authenticated we jump straight to auth
|
||||
if not self.request.user.is_authenticated:
|
||||
return self.handle_auth(connection)
|
||||
# When an override flow token exists we actually still use a flow for link
|
||||
# to continue the existing flow we came from
|
||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||
return self._prepare_flow(None, connection)
|
||||
connection.save()
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
"""Reporting app config"""
|
||||
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class AuthentikEnterpriseReporting(EnterpriseConfig):
|
||||
"""authentik enterprise reporting app config"""
|
||||
|
||||
name = "authentik.enterprise.reporting"
|
||||
label = "authentik_reporting"
|
||||
verbose_name = "authentik Enterprise.Reporting"
|
||||
default = True
|
||||
@ -1,22 +0,0 @@
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.enterprise.reporting.models import Report
|
||||
|
||||
|
||||
class ReportExecutor:
|
||||
"""Execute a report"""
|
||||
|
||||
def __init__(self, report: Report) -> None:
|
||||
self.report = report
|
||||
self.logger = get_logger().bind(report=self.report)
|
||||
|
||||
def execute(self):
|
||||
# 1. Run through policies bound to report itself
|
||||
# 2. Get all bound components by running through ReportComponentBinding,
|
||||
# while evaluating policies bound to each
|
||||
# 3. render the actual components
|
||||
# 4. Store the final data...somewhere??
|
||||
# 5. Optionally render PDF via chromedriver (special frontend that uses API)
|
||||
# (not required for MVP)
|
||||
# 6. Send out link to CSV/PDF or attach to email via delivery
|
||||
pass
|
||||
@ -1,131 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-18 21:47
|
||||
|
||||
import authentik.lib.models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0007_event_authentik_e_action_9a9dd9_idx_and_more"),
|
||||
("authentik_policies", "0011_policybinding_failure_result_and_more"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ReportComponent",
|
||||
fields=[
|
||||
(
|
||||
"widget_uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Report Component",
|
||||
"verbose_name_plural": "Report Components",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Report",
|
||||
fields=[
|
||||
(
|
||||
"policybindingmodel_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_policies.policybindingmodel",
|
||||
),
|
||||
),
|
||||
("name", models.TextField()),
|
||||
("schedule", models.TextField()),
|
||||
("output_type", models.TextField(choices=[("csv", "Csv"), ("pdf", "Pdf")])),
|
||||
(
|
||||
"delivery",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_events.notificationtransport",
|
||||
),
|
||||
),
|
||||
(
|
||||
"run_as",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Report",
|
||||
"verbose_name_plural": "Reports",
|
||||
},
|
||||
bases=("authentik_policies.policybindingmodel", models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ReportComponentBinding",
|
||||
fields=[
|
||||
(
|
||||
"policybindingmodel_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
to="authentik_policies.policybindingmodel",
|
||||
),
|
||||
),
|
||||
(
|
||||
"binding_uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
||||
),
|
||||
),
|
||||
("enabled", models.BooleanField(default=True)),
|
||||
("layout_x", models.PositiveIntegerField(default=0)),
|
||||
("layout_y", models.PositiveIntegerField(default=0)),
|
||||
(
|
||||
"target",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="authentik_reporting.report"
|
||||
),
|
||||
),
|
||||
(
|
||||
"widget",
|
||||
authentik.lib.models.InheritanceForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="authentik_reporting.reportcomponent",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Report Component Binding",
|
||||
"verbose_name_plural": "Report Component Bindings",
|
||||
"unique_together": {("target", "widget")},
|
||||
},
|
||||
bases=("authentik_policies.policybindingmodel", models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="report",
|
||||
name="components",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="bindings",
|
||||
through="authentik_reporting.ReportComponentBinding",
|
||||
to="authentik_reporting.reportcomponent",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,87 +0,0 @@
|
||||
"""Reporting models"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from celery.schedules import crontab
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.events.models import NotificationTransport
|
||||
from authentik.lib.models import InheritanceForeignKey, SerializerModel
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
|
||||
|
||||
class OutputType(models.TextChoices):
|
||||
"""Different choices in which a report can be 'rendered'"""
|
||||
|
||||
csv = "csv"
|
||||
pdf = "pdf"
|
||||
|
||||
|
||||
class Report(SerializerModel, PolicyBindingModel):
|
||||
"""A report with a defined list of components, which can run on a schedule"""
|
||||
|
||||
name = models.TextField()
|
||||
|
||||
schedule = models.TextField()
|
||||
|
||||
# User under which permissions the queries are run,
|
||||
# when no user is selected the report is inactive
|
||||
run_as = models.ForeignKey(
|
||||
"authentik_core.user", on_delete=models.SET_DEFAULT, default=None, null=True
|
||||
)
|
||||
components = models.ManyToManyField(
|
||||
"ReportComponent", through="ReportComponentBinding", related_name="bindings", blank=True
|
||||
)
|
||||
output_type = models.TextField(choices=OutputType.choices)
|
||||
# Use notification transport to send report result (either link for webhook based?
|
||||
# maybe send full csv?) or fully rendered PDF via Email
|
||||
# when no transport is selected, reports are not sent anywhere but can be retrieved in authentik
|
||||
delivery = models.ForeignKey(
|
||||
NotificationTransport, on_delete=models.SET_DEFAULT, default=None, null=True
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def get_celery_schedule(self) -> crontab:
|
||||
return crontab(*self.schedule.split())
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Report")
|
||||
verbose_name_plural = _("Reports")
|
||||
|
||||
|
||||
class ReportComponentBinding(SerializerModel, PolicyBindingModel):
|
||||
"""Binding of a component to a report"""
|
||||
|
||||
binding_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
layout_x = models.PositiveIntegerField(default=0)
|
||||
layout_y = models.PositiveIntegerField(default=0)
|
||||
|
||||
target = models.ForeignKey("Report", on_delete=models.CASCADE)
|
||||
widget = InheritanceForeignKey("ReportComponent", on_delete=models.CASCADE, related_name="+")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Binding from {self.report.name} to {self.widget}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Report Component Binding")
|
||||
verbose_name_plural = _("Report Component Bindings")
|
||||
unique_together = ("target", "widget")
|
||||
|
||||
|
||||
class ReportComponent(SerializerModel):
|
||||
"""An individual component of a report, a query or graph, etc"""
|
||||
|
||||
widget_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return super().__str__()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Report Component")
|
||||
verbose_name_plural = _("Report Components")
|
||||
@ -1,38 +0,0 @@
|
||||
from json import dumps
|
||||
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
||||
|
||||
from authentik.enterprise.reporting.models import Report
|
||||
|
||||
|
||||
@receiver(post_save, sender=Report)
|
||||
def report_post_save(sender, instance: Report, **_):
|
||||
if instance.schedule == "":
|
||||
return
|
||||
schedule = CrontabSchedule.from_schedule(instance.get_celery_schedule())
|
||||
schedule.save()
|
||||
PeriodicTask.objects.update_or_create(
|
||||
name=str(instance.pk),
|
||||
defaults={
|
||||
"crontab": schedule,
|
||||
"task": "authentik.enterprise.reporting.tasks.process_report",
|
||||
"queue": "authentik_reporting",
|
||||
"description": f"Report {instance.name}",
|
||||
"kwargs": dumps(
|
||||
{
|
||||
"report_uuid": str(instance.pk),
|
||||
}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Report)
|
||||
def report_pre_delete(sender, instance: Report, **_):
|
||||
if instance.schedule == "":
|
||||
return
|
||||
PeriodicTask.objects.filter(name=str(instance.pk)).delete()
|
||||
# Cleanup schedules without any tasks
|
||||
CrontabSchedule.objects.filter(periodictask__isnull=True).delete()
|
||||
@ -1,11 +0,0 @@
|
||||
from authentik.enterprise.reporting.executor import ReportExecutor
|
||||
from authentik.enterprise.reporting.models import Report
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def process_report(report_uuid: str):
|
||||
report = Report.objects.filter(pk=report_uuid).first()
|
||||
if not report or not report.run_as:
|
||||
return
|
||||
ReportExecutor(report).execute()
|
||||
@ -17,7 +17,6 @@ TENANT_APPS = [
|
||||
"authentik.enterprise.providers.google_workspace",
|
||||
"authentik.enterprise.providers.microsoft_entra",
|
||||
"authentik.enterprise.providers.ssf",
|
||||
"authentik.enterprise.reporting",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
"authentik.enterprise.stages.source",
|
||||
]
|
||||
|
||||
@ -9,16 +9,13 @@ from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.models import Source, User
|
||||
from authentik.core.sources.flow_manager import (
|
||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
|
||||
SESSION_KEY_SOURCE_FLOW_STAGES,
|
||||
)
|
||||
from authentik.core.sources.flow_manager import SESSION_KEY_OVERRIDE_FLOW_TOKEN
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.enterprise.stages.source.models import SourceStage
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||
from authentik.flows.models import FlowToken, in_memory_stage
|
||||
from authentik.flows.models import FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
|
||||
from authentik.flows.stage import ChallengeStageView, StageView
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
|
||||
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
|
||||
@ -52,7 +49,6 @@ class SourceStageView(ChallengeStageView):
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
resume_token = self.create_flow_token()
|
||||
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
|
||||
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
|
||||
return self.login_button.challenge
|
||||
|
||||
def create_flow_token(self) -> FlowToken:
|
||||
@ -81,19 +77,3 @@ class SourceStageView(ChallengeStageView):
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
return self.executor.stage_ok()
|
||||
|
||||
|
||||
class SourceStageFinal(StageView):
|
||||
"""Dynamic stage injected in the source flow manager. This is injected in the
|
||||
flow the source flow manager picks (authentication or enrollment), and will run at the end.
|
||||
This stage uses the override flow token to resume execution of the initial flow the
|
||||
source stage is bound to."""
|
||||
|
||||
def dispatch(self):
|
||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||
plan = token.plan
|
||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||
response = plan.to_redirect(self.request, token.flow)
|
||||
token.delete()
|
||||
return response
|
||||
|
||||
@ -36,6 +36,15 @@ class FlowAuthenticationRequirement(models.TextChoices):
|
||||
REQUIRE_REDIRECT = "require_redirect"
|
||||
REQUIRE_OUTPOST = "require_outpost"
|
||||
|
||||
@property
|
||||
def possibly_unauthenticated(self) -> bool:
|
||||
"""Check if unauthenticated users can run this flow. Flows like this may require additional
|
||||
hardening."""
|
||||
return self in [
|
||||
FlowAuthenticationRequirement.NONE,
|
||||
FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED,
|
||||
]
|
||||
|
||||
|
||||
class NotConfiguredAction(models.TextChoices):
|
||||
"""Decides how the FlowExecutor should proceed when a stage isn't configured"""
|
||||
|
||||
@ -64,8 +64,6 @@ debugger: false
|
||||
log_level: info
|
||||
|
||||
session_storage: cache
|
||||
sessions:
|
||||
unauthenticated_age: days=1
|
||||
|
||||
error_reporting:
|
||||
enabled: false
|
||||
|
||||
@ -31,7 +31,7 @@ def timedelta_string_validator(value: str):
|
||||
|
||||
|
||||
def timedelta_from_string(expr: str) -> datetime.timedelta:
|
||||
"""Convert a string with the format of 'hours=1;minute=3;seconds=5' to a
|
||||
"""Convert a string with the format of 'hours=1;minutes=3;seconds=5' to a
|
||||
`datetime.timedelta` Object with hours = 1, minutes = 3, seconds = 5"""
|
||||
kwargs = {}
|
||||
for duration_pair in expr.split(";"):
|
||||
|
||||
@ -128,7 +128,7 @@ class GeoIPPolicy(Policy):
|
||||
(geoip_data["lat"], geoip_data["long"]),
|
||||
)
|
||||
if self.check_history_distance and dist.km >= (
|
||||
self.history_max_distance_km + self.distance_tolerance_km
|
||||
self.history_max_distance_km - self.distance_tolerance_km
|
||||
):
|
||||
return PolicyResult(
|
||||
False, _("Distance from previous authentication is larger than threshold.")
|
||||
@ -139,7 +139,7 @@ class GeoIPPolicy(Policy):
|
||||
# clamped to be at least 1 hour
|
||||
rel_time_hours = max(int((_now - previous_login.created).total_seconds() / 3600), 1)
|
||||
if self.check_impossible_travel and dist.km >= (
|
||||
(MAX_DISTANCE_HOUR_KM * rel_time_hours) + self.distance_tolerance_km
|
||||
(MAX_DISTANCE_HOUR_KM * rel_time_hours) - self.distance_tolerance_km
|
||||
):
|
||||
return PolicyResult(False, _("Distance is further than possible."))
|
||||
return PolicyResult(True)
|
||||
|
||||
@ -148,10 +148,10 @@ class PasswordPolicy(Policy):
|
||||
user_inputs.append(request.user.email)
|
||||
if request.http_request:
|
||||
user_inputs.append(request.http_request.brand.branding_title)
|
||||
# Only calculate result for the first 72 characters, as with over 100 char
|
||||
# Only calculate result for the first 100 characters, as with over 100 char
|
||||
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
||||
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
||||
results = zxcvbn(password[:72], user_inputs)
|
||||
results = zxcvbn(password[:100], user_inputs)
|
||||
LOGGER.debug("password failed", check="zxcvbn", score=results["score"])
|
||||
result = PolicyResult(results["score"] > self.zxcvbn_score_threshold)
|
||||
if not result.passing:
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models import Q, QuerySet
|
||||
from django_filters.filters import ModelChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
@ -18,6 +18,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.blueprints.v1.importer import excluded_models
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.validators import RequiredTogetherValidator
|
||||
@ -105,13 +106,13 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
return (
|
||||
Permission.objects.all()
|
||||
.select_related("content_type")
|
||||
.filter(
|
||||
content_type__app_label__startswith="authentik",
|
||||
query = Q()
|
||||
for model in excluded_models():
|
||||
query |= Q(
|
||||
content_type__app_label=model._meta.app_label,
|
||||
content_type__model=model._meta.model_name,
|
||||
)
|
||||
)
|
||||
return Permission.objects.all().select_related("content_type").exclude(query)
|
||||
|
||||
|
||||
class PermissionAssignSerializer(PassiveSerializer):
|
||||
|
||||
@ -16,7 +16,6 @@ from authentik.lib.config import CONFIG, django_db_config, redis_url
|
||||
from authentik.lib.logging import get_logger_config, structlog_configure
|
||||
from authentik.lib.sentry import sentry_init
|
||||
from authentik.lib.utils.reflection import get_env
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
|
||||
BASE_DIR = Path(__file__).absolute().parent.parent.parent
|
||||
@ -125,7 +124,6 @@ TENANT_APPS = [
|
||||
"authentik.brands",
|
||||
"authentik.blueprints",
|
||||
"guardian",
|
||||
"django_celery_beat",
|
||||
]
|
||||
|
||||
TENANT_MODEL = "authentik_tenants.Tenant"
|
||||
@ -244,9 +242,6 @@ SESSION_CACHE_ALIAS = "default"
|
||||
# Configured via custom SessionMiddleware
|
||||
# SESSION_COOKIE_SAMESITE = "None"
|
||||
# SESSION_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_AGE = timedelta_from_string(
|
||||
CONFIG.get("sessions.unauthenticated_age", "days=1")
|
||||
).total_seconds()
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
|
||||
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from requests import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
|
||||
@ -22,35 +21,10 @@ class AzureADOAuthRedirect(OAuthRedirect):
|
||||
}
|
||||
|
||||
|
||||
class AzureADClient(UserprofileHeaderAuthClient):
|
||||
"""Fetch AzureAD group information"""
|
||||
|
||||
def get_profile_info(self, token):
|
||||
profile_data = super().get_profile_info(token)
|
||||
if "https://graph.microsoft.com/GroupMember.Read.All" not in self.source.additional_scopes:
|
||||
return profile_data
|
||||
group_response = self.session.request(
|
||||
"get",
|
||||
"https://graph.microsoft.com/v1.0/me/memberOf",
|
||||
headers={"Authorization": f"{token['token_type']} {token['access_token']}"},
|
||||
)
|
||||
try:
|
||||
group_response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning(
|
||||
"Unable to fetch user profile",
|
||||
exc=exc,
|
||||
response=exc.response.text if exc.response else str(exc),
|
||||
)
|
||||
return None
|
||||
profile_data["raw_groups"] = group_response.json()
|
||||
return profile_data
|
||||
|
||||
|
||||
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
|
||||
"""AzureAD OAuth2 Callback"""
|
||||
|
||||
client_class = AzureADClient
|
||||
client_class = UserprofileHeaderAuthClient
|
||||
|
||||
def get_user_id(self, info: dict[str, str]) -> str:
|
||||
# Default try to get `id` for the Graph API endpoint
|
||||
@ -79,24 +53,8 @@ class AzureADType(SourceType):
|
||||
|
||||
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
|
||||
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
|
||||
# Format group info
|
||||
groups = []
|
||||
group_id_dict = {}
|
||||
for group in info.get("raw_groups", {}).get("value", []):
|
||||
if group["@odata.type"] != "#microsoft.graph.group":
|
||||
continue
|
||||
groups.append(group["id"])
|
||||
group_id_dict[group["id"]] = group
|
||||
info["raw_groups"] = group_id_dict
|
||||
return {
|
||||
"username": info.get("userPrincipalName"),
|
||||
"email": mail,
|
||||
"name": info.get("displayName"),
|
||||
"groups": groups,
|
||||
}
|
||||
|
||||
def get_base_group_properties(self, source, group_id, **kwargs):
|
||||
raw_group = kwargs["info"]["raw_groups"][group_id]
|
||||
return {
|
||||
"name": raw_group["displayName"],
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ from rest_framework.serializers import ValidationError
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse
|
||||
from authentik.flows.exceptions import StageInvalidException
|
||||
from authentik.flows.models import FlowDesignation, FlowToken
|
||||
from authentik.flows.models import FlowAuthenticationRequirement, FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
|
||||
@ -97,14 +97,27 @@ class EmailStageView(ChallengeStageView):
|
||||
"""Helper function that sends the actual email. Implies that you've
|
||||
already checked that there is a pending user."""
|
||||
pending_user = self.get_pending_user()
|
||||
if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
|
||||
# Pending user does not have a primary key, and we're in a recovery flow,
|
||||
# which means the user entered an invalid identifier, so we pretend to send the
|
||||
# email, to not disclose if the user exists
|
||||
return
|
||||
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
|
||||
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, pending_user.email)
|
||||
if FlowAuthenticationRequirement(
|
||||
self.executor.flow.authentication
|
||||
).possibly_unauthenticated:
|
||||
# In possibly unauthenticated flows, do not disclose whether user or their email exists
|
||||
# to prevent enumeration attacks
|
||||
if not pending_user.pk:
|
||||
self.logger.debug(
|
||||
"User object does not exist. Email not sent.", pending_user=pending_user
|
||||
)
|
||||
return
|
||||
if not email:
|
||||
self.logger.debug(
|
||||
"No recipient email address could be determined. Email not sent.",
|
||||
pending_user=pending_user,
|
||||
)
|
||||
return
|
||||
if not email:
|
||||
email = pending_user.email
|
||||
raise StageInvalidException(
|
||||
"No recipient email address could be determined. Email not sent."
|
||||
)
|
||||
current_stage: EmailStage = self.executor.current_stage
|
||||
token = self.get_token()
|
||||
# Send mail to user
|
||||
@ -133,7 +146,9 @@ class EmailStageView(ChallengeStageView):
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
# Check if the user came back from the email link to verify
|
||||
restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None)
|
||||
restore_token: FlowToken | None = self.executor.plan.context.get(
|
||||
PLAN_CONTEXT_IS_RESTORED, None
|
||||
)
|
||||
user = self.get_pending_user()
|
||||
if restore_token:
|
||||
if restore_token.user != user:
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
"""Tenant-aware Celery beat scheduler"""
|
||||
|
||||
from django_celery_beat.schedulers import DatabaseScheduler, ModelEntry
|
||||
from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry, TenantAwareSchedulerMixin
|
||||
from tenant_schemas_celery.scheduler import (
|
||||
TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler,
|
||||
)
|
||||
from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry
|
||||
|
||||
|
||||
class SchedulerEntry(ModelEntry, TenantAwareScheduleEntry):
|
||||
pass
|
||||
|
||||
|
||||
class TenantAwarePersistentScheduler(TenantAwareSchedulerMixin, DatabaseScheduler):
|
||||
class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
|
||||
"""Tenant-aware Celery beat scheduler"""
|
||||
|
||||
Entry = SchedulerEntry
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls):
|
||||
return super().get_queryset().filter(ready=True)
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/debug"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||
@ -25,8 +24,7 @@ Required environment variables:
|
||||
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Long: helpMessage,
|
||||
Version: constants.FullVersion(),
|
||||
Long: helpMessage,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetFormatter(&log.JSONFormatter{
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/debug"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||
@ -28,8 +27,7 @@ Optionally, you can set these:
|
||||
- AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST`
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Long: helpMessage,
|
||||
Version: constants.FullVersion(),
|
||||
Long: helpMessage,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetFormatter(&log.JSONFormatter{
|
||||
|
||||
@ -9,7 +9,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/debug"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||
@ -24,8 +23,7 @@ Required environment variables:
|
||||
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Long: helpMessage,
|
||||
Version: constants.FullVersion(),
|
||||
Long: helpMessage,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetFormatter(&log.JSONFormatter{
|
||||
|
||||
@ -9,7 +9,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/debug"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/ak/healthcheck"
|
||||
@ -24,8 +23,7 @@ Required environment variables:
|
||||
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Long: helpMessage,
|
||||
Version: constants.FullVersion(),
|
||||
Long: helpMessage,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetFormatter(&log.JSONFormatter{
|
||||
|
||||
10
go.mod
10
go.mod
@ -22,14 +22,14 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.8.0
|
||||
github.com/prometheus/client_golang v1.21.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/sethvargo/go-envconfig v1.1.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2024123.7
|
||||
goauthentik.io/api/v3 v3.2024123.6
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.26.0
|
||||
golang.org/x/sync v0.11.0
|
||||
@ -62,14 +62,14 @@ require (
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
@ -79,6 +79,6 @@ require (
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
20
go.sum
20
go.sum
@ -207,8 +207,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@ -239,13 +239,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2024123.7 h1:vjmEnxXTHGFylJ9kTBFNYy4kcTrUM2hSIt3ja8gNVAY=
|
||||
goauthentik.io/api/v3 v3.2024123.7/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024123.6 h1:AGOCa7Fc/9eONCPEW4sEhTiyEBvxN57Lfqz1zm6Gy98=
|
||||
goauthentik.io/api/v3 v3.2024123.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -595,8 +595,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
10
lifecycle/aws/package-lock.json
generated
10
lifecycle/aws/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1000.2",
|
||||
"aws-cdk": "^2.179.0",
|
||||
"cross-env": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -17,16 +17,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1000.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1000.2.tgz",
|
||||
"integrity": "sha512-QsXqJhGWjHNqP7etgE3sHOTiDBXItmSKdFKgsm1qPMBabCMyFfmWZnEeUxfZ4sMaIoxvLpr3sqoWSNeLuUk4sg==",
|
||||
"version": "2.179.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.179.0.tgz",
|
||||
"integrity": "sha512-aA2+8S2g4UBQHkUEt0mYd16VLt/ucR+QfyUJi34LDKRAhOCNDjPCZ4z9z/JEDyuni0BdzsYA55pnpDN9tMULpA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"cdk": "bin/cdk"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
"node": ">= 14.15.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1000.2",
|
||||
"aws-cdk": "^2.179.0",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
390
poetry.lock
generated
390
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -92,7 +92,6 @@ dacite = "*"
|
||||
deepmerge = "*"
|
||||
defusedxml = "*"
|
||||
django = "*"
|
||||
django-celery-beat = "*"
|
||||
django-countries = "*"
|
||||
django-cte = "*"
|
||||
django-filter = "*"
|
||||
|
||||
48
schema.yml
48
schema.yml
@ -6095,17 +6095,26 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/users/{id}/recovery/:
|
||||
/core/users/{id}/recovery_link/:
|
||||
post:
|
||||
operationId: core_users_recovery_create
|
||||
operationId: core_users_recovery_link_create
|
||||
description: Create a temporary link that a user can use to recover their accounts
|
||||
parameters:
|
||||
- in: query
|
||||
name: email_stage
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User.
|
||||
required: true
|
||||
- in: query
|
||||
name: token_duration
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
@ -6129,41 +6138,6 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/users/{id}/recovery_email/:
|
||||
post:
|
||||
operationId: core_users_recovery_email_create
|
||||
description: Create a temporary link that a user can use to recover their accounts
|
||||
parameters:
|
||||
- in: query
|
||||
name: email_stage
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: Successfully sent recover email
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/users/{id}/set_password/:
|
||||
post:
|
||||
operationId: core_users_set_password_create
|
||||
|
||||
@ -4,7 +4,7 @@ This package provides a generated API Client for [authentik](https://goauthentik
|
||||
|
||||
### Building
|
||||
|
||||
See https://docs.goauthentik.io/docs/developer-docs/api/making-schema-changes#building-the-web-client
|
||||
See https://docs.goauthentik.io/docs/developer-docs/making-schema-changes
|
||||
|
||||
### Consuming
|
||||
|
||||
|
||||
@ -88,11 +88,7 @@ const baseArgs = {
|
||||
treeShaking: true,
|
||||
external: ["*.woff", "*.woff2"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
loader: {
|
||||
".css": "text",
|
||||
".md": "text",
|
||||
".mdx": "text",
|
||||
},
|
||||
loader: { ".css": "text", ".md": "text" },
|
||||
define: definitions,
|
||||
format: "esm",
|
||||
logOverride: {
|
||||
|
||||
@ -22,7 +22,7 @@ import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@ -126,7 +126,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
);
|
||||
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
${this.instance ? nothing : html`<ak-alert level="pf-m-info">${alertMsg}</ak-alert>`}
|
||||
<ak-alert level="pf-m-info">${alertMsg}</ak-alert>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(this.instance?.name)}
|
||||
|
||||
@ -2,11 +2,14 @@ import "@goauthentik/admin/users/ServiceAccountForm";
|
||||
import "@goauthentik/admin/users/UserActiveForm";
|
||||
import "@goauthentik/admin/users/UserForm";
|
||||
import "@goauthentik/admin/users/UserImpersonateForm";
|
||||
import {
|
||||
renderRecoveryEmailRequest,
|
||||
renderRecoveryLinkRequest,
|
||||
} from "@goauthentik/admin/users/UserListPage";
|
||||
import "@goauthentik/admin/users/UserPasswordForm";
|
||||
import "@goauthentik/admin/users/UserResetEmailForm";
|
||||
import "@goauthentik/admin/users/UserRecoveryLinkForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import { MessageLevel } from "@goauthentik/common/messages";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
@ -21,7 +24,6 @@ import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
|
||||
@ -37,14 +39,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreUsersListTypeEnum,
|
||||
Group,
|
||||
ResponseError,
|
||||
SessionUser,
|
||||
User,
|
||||
} from "@goauthentik/api";
|
||||
import { CoreApi, CoreUsersListTypeEnum, Group, SessionUser, User } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-user-related-add")
|
||||
export class RelatedUserAdd extends Form<{ users: number[] }> {
|
||||
@ -301,60 +296,11 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
${msg("Set password")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${this.brand?.flowRecovery
|
||||
${this.brand.flowRecovery
|
||||
? html`
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersRecoveryCreate({
|
||||
id: item.pk,
|
||||
})
|
||||
.then((rec) => {
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: msg(
|
||||
"Successfully generated recovery link",
|
||||
),
|
||||
description: rec.link,
|
||||
});
|
||||
})
|
||||
.catch((ex: ResponseError) => {
|
||||
ex.response.json().then(() => {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: msg(
|
||||
"No recovery flow is configured.",
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
${msg("Copy recovery link")}
|
||||
</ak-action-button>
|
||||
${renderRecoveryLinkRequest(item)}
|
||||
${item.email
|
||||
? html`<ak-forms-modal
|
||||
.closeAfterSuccessfulSubmit=${false}
|
||||
>
|
||||
<span slot="submit">
|
||||
${msg("Send link")}
|
||||
</span>
|
||||
<span slot="header">
|
||||
${msg("Send recovery link to user")}
|
||||
</span>
|
||||
<ak-user-reset-email-form
|
||||
slot="form"
|
||||
.user=${item}
|
||||
>
|
||||
</ak-user-reset-email-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary"
|
||||
>
|
||||
${msg("Email recovery link")}
|
||||
</button>
|
||||
</ak-forms-modal>`
|
||||
? renderRecoveryEmailRequest(item)
|
||||
: html`<span
|
||||
>${msg(
|
||||
"Recovery link cannot be emailed, user has no email address saved.",
|
||||
@ -363,7 +309,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
|
||||
`
|
||||
: html` <p>
|
||||
${msg(
|
||||
"To let a user directly reset a their password, configure a recovery flow on the currently active brand.",
|
||||
"To let a user directly reset their password, configure a recovery flow on the currently active brand.",
|
||||
)}
|
||||
</p>`}
|
||||
</div>
|
||||
|
||||
@ -105,22 +105,6 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Maximum distance")}
|
||||
name="historyMaxDistanceKm"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Maximum distance a login attempt is allowed from in kilometers.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Distance tolerance")}
|
||||
name="distanceToleranceKm"
|
||||
@ -149,6 +133,27 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
|
||||
${msg("Amount of previous login events to check against.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Maximum distance")}
|
||||
name="historyMaxDistanceKm"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Maximum distance a login attempt is allowed from in kilometers.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Distance settings (Impossible travel)")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal name="checkImpossibleTravel">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
|
||||
@ -4,7 +4,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import renderDescriptionList from "@goauthentik/components/DescriptionList";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import MDProviderOAuth2 from "@goauthentik/docs/add-secure-apps/providers/oauth2/index.mdx";
|
||||
import MDProviderOAuth2 from "@goauthentik/docs/add-secure-apps/providers/oauth2/index.md";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
|
||||
@ -13,7 +13,7 @@ import MDNginxStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy
|
||||
import MDTraefikCompose from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_compose.md";
|
||||
import MDTraefikIngress from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_ingress.md";
|
||||
import MDTraefikStandalone from "@goauthentik/docs/add-secure-apps/providers/proxy/_traefik_standalone.md";
|
||||
import MDHeaderAuthentication from "@goauthentik/docs/add-secure-apps/providers/proxy/header_authentication.mdx";
|
||||
import MDHeaderAuthentication from "@goauthentik/docs/add-secure-apps/providers/proxy/header_authentication.md";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
|
||||
@ -4,11 +4,10 @@ import "@goauthentik/admin/users/UserActiveForm";
|
||||
import "@goauthentik/admin/users/UserForm";
|
||||
import "@goauthentik/admin/users/UserImpersonateForm";
|
||||
import "@goauthentik/admin/users/UserPasswordForm";
|
||||
import "@goauthentik/admin/users/UserResetEmailForm";
|
||||
import "@goauthentik/admin/users/UserRecoveryLinkForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import { userTypeToLabel } from "@goauthentik/common/labels";
|
||||
import { MessageLevel } from "@goauthentik/common/messages";
|
||||
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import { getRelativeTime } from "@goauthentik/common/utils";
|
||||
@ -23,12 +22,10 @@ import "@goauthentik/elements/TreeView";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import { TablePage } from "@goauthentik/elements/table/TablePage";
|
||||
import { writeToClipboard } from "@goauthentik/elements/utils/writeToClipboard";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@ -39,40 +36,24 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
import { CoreApi, ResponseError, SessionUser, User, UserPath } from "@goauthentik/api";
|
||||
import { CoreApi, SessionUser, User, UserPath } from "@goauthentik/api";
|
||||
|
||||
export const requestRecoveryLink = (user: User) =>
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersRecoveryCreate({
|
||||
id: user.pk,
|
||||
})
|
||||
.then((rec) =>
|
||||
writeToClipboard(rec.link).then((wroteToClipboard) =>
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
message: rec.link,
|
||||
description: wroteToClipboard
|
||||
? msg("A copy of this recovery link has been placed in your clipboard")
|
||||
: "",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.catch((ex: ResponseError) =>
|
||||
ex.response.json().then(() =>
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: msg(
|
||||
"The current brand must have a recovery flow configured to use a recovery link",
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
export const renderRecoveryLinkRequest = (user: User) =>
|
||||
html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-link-recovery-request">
|
||||
<span slot="submit"> ${msg("Create link")} </span>
|
||||
<span slot="header"> ${msg("Create recovery link")} </span>
|
||||
<ak-user-recovery-link-form slot="form" .user=${user}> </ak-user-recovery-link-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Create recovery link")}
|
||||
</button>
|
||||
</ak-forms-modal>`;
|
||||
|
||||
export const renderRecoveryEmailRequest = (user: User) =>
|
||||
html`<ak-forms-modal .closeAfterSuccessfulSubmit=${false} id="ak-email-recovery-request">
|
||||
<span slot="submit"> ${msg("Send link")} </span>
|
||||
<span slot="header"> ${msg("Send recovery link to user")} </span>
|
||||
<ak-user-reset-email-form slot="form" .user=${user}> </ak-user-reset-email-form>
|
||||
<ak-user-recovery-link-form slot="form" .user=${user} .withEmailStage=${true}>
|
||||
</ak-user-recovery-link-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Email recovery link")}
|
||||
</button>
|
||||
@ -362,12 +343,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
||||
</ak-forms-modal>
|
||||
${this.brand.flowRecovery
|
||||
? html`
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => requestRecoveryLink(item)}
|
||||
>
|
||||
${msg("Create recovery link")}
|
||||
</ak-action-button>
|
||||
${renderRecoveryLinkRequest(item)}
|
||||
${item.email
|
||||
? renderRecoveryEmailRequest(item)
|
||||
: html`<span
|
||||
|
||||
104
web/src/admin/users/UserRecoveryLinkForm.ts
Normal file
104
web/src/admin/users/UserRecoveryLinkForm.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import { writeToClipboard } from "@goauthentik/elements/utils/writeToClipboard";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreUsersRecoveryLinkCreateRequest,
|
||||
Link,
|
||||
Stage,
|
||||
StagesAllListRequest,
|
||||
StagesApi,
|
||||
User,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-user-recovery-link-form")
|
||||
export class UserRecoveryLinkForm extends Form<CoreUsersRecoveryLinkCreateRequest> {
|
||||
@property({ attribute: false })
|
||||
user!: User;
|
||||
|
||||
@property({ type: Boolean })
|
||||
withEmailStage = false;
|
||||
|
||||
async send(data: CoreUsersRecoveryLinkCreateRequest): Promise<Link> {
|
||||
data.id = this.user.pk;
|
||||
const response = await new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryLinkCreate(data);
|
||||
|
||||
if (this.withEmailStage) {
|
||||
this.successMessage = msg("Successfully sent email.");
|
||||
} else {
|
||||
const wroteToClipboard = await writeToClipboard(response.link);
|
||||
if (wroteToClipboard) {
|
||||
this.successMessage = msg(
|
||||
`A copy of this recovery link has been placed in your clipboard: ${response.link}`,
|
||||
);
|
||||
} else {
|
||||
this.successMessage = msg(
|
||||
`authentik does not have access to your clipboard, please copy the recovery link manually: ${response.link}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
renderEmailStageInput(): TemplateResult {
|
||||
if (!this.withEmailStage) return html``;
|
||||
return html`
|
||||
<ak-form-element-horizontal name="emailStage" label=${msg("Email stage")} required>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
||||
const args: StagesAllListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
|
||||
return stages.results;
|
||||
}}
|
||||
.groupBy=${(items: Stage[]) => {
|
||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||
}}
|
||||
.renderElement=${(stage: Stage): string => {
|
||||
return stage.name;
|
||||
}}
|
||||
.value=${(stage: Stage | undefined): string | undefined => {
|
||||
return stage?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
${this.renderEmailStageInput()}
|
||||
<ak-text-input
|
||||
name="tokenDuration"
|
||||
label=${msg("Token duration")}
|
||||
required
|
||||
value="days=1"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Duration for generated token")}
|
||||
</p>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-user-recovery-link-form": UserRecoveryLinkForm;
|
||||
}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { groupBy } from "@goauthentik/common/utils";
|
||||
import { Form } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreUsersRecoveryEmailCreateRequest,
|
||||
Stage,
|
||||
StagesAllListRequest,
|
||||
StagesApi,
|
||||
User,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-user-reset-email-form")
|
||||
export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailCreateRequest> {
|
||||
@property({ attribute: false })
|
||||
user!: User;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return msg("Successfully sent email.");
|
||||
}
|
||||
|
||||
async send(data: CoreUsersRecoveryEmailCreateRequest): Promise<void> {
|
||||
data.id = this.user.pk;
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailCreate(data);
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${msg("Email stage")}
|
||||
?required=${true}
|
||||
name="emailStage"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
||||
const args: StagesAllListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
|
||||
return stages.results;
|
||||
}}
|
||||
.groupBy=${(items: Stage[]) => {
|
||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||
}}
|
||||
.renderElement=${(stage: Stage): string => {
|
||||
return stage.name;
|
||||
}}
|
||||
.value=${(stage: Stage | undefined): string | undefined => {
|
||||
return stage?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-user-reset-email-form": UserResetEmailForm;
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ import "@goauthentik/admin/users/UserForm";
|
||||
import "@goauthentik/admin/users/UserImpersonateForm";
|
||||
import {
|
||||
renderRecoveryEmailRequest,
|
||||
requestRecoveryLink,
|
||||
renderRecoveryLinkRequest,
|
||||
} from "@goauthentik/admin/users/UserListPage";
|
||||
import "@goauthentik/admin/users/UserPasswordForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
@ -110,11 +110,8 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
|
||||
.ak-button-collection > * {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
#reset-password-button {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#ak-email-recovery-request,
|
||||
#ak-link-recovery-request .pf-c-button,
|
||||
#update-password-request .pf-c-button,
|
||||
#ak-email-recovery-request .pf-c-button {
|
||||
margin: 0;
|
||||
@ -248,18 +245,7 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-action-button
|
||||
id="reset-password-button"
|
||||
class="pf-m-secondary pf-m-block"
|
||||
.apiRequest=${() => requestRecoveryLink(user)}
|
||||
>
|
||||
<pf-tooltip
|
||||
position="top"
|
||||
content=${msg("Create a link for this user to reset their password")}
|
||||
>
|
||||
${msg("Create Recovery Link")}
|
||||
</pf-tooltip>
|
||||
</ak-action-button>
|
||||
${renderRecoveryLinkRequest(user)}
|
||||
${user.email ? renderRecoveryEmailRequest(user) : nothing}
|
||||
</div> `;
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ export interface GlobalAuthentik {
|
||||
build: string;
|
||||
api: {
|
||||
base: string;
|
||||
relBase: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -28,7 +27,6 @@ export function globalAK(): GlobalAuthentik {
|
||||
ak.brand = CurrentBrandFromJSON(ak.brand);
|
||||
ak.config = ConfigFromJSON(ak.config);
|
||||
}
|
||||
const apiBase = new URL(process.env.AK_API_BASE_PATH || window.location.origin);
|
||||
if (!ak) {
|
||||
return {
|
||||
config: ConfigFromJSON({
|
||||
@ -41,8 +39,7 @@ export function globalAK(): GlobalAuthentik {
|
||||
versionSubdomain: "",
|
||||
build: "",
|
||||
api: {
|
||||
base: apiBase.toString(),
|
||||
relBase: apiBase.pathname,
|
||||
base: process.env.AK_API_BASE_PATH || window.location.origin,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -45,8 +45,6 @@ html > form > input {
|
||||
left: -2000px;
|
||||
}
|
||||
|
||||
/*#region Icons*/
|
||||
|
||||
.pf-icon {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
@ -56,18 +54,6 @@ html > form > input {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pf-c-form-control {
|
||||
--pf-c-form-control--m-caps-lock--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23aaabac' viewBox='0 0 56 56' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 20.7812 37.6211 L 35.2421 37.6211 C 38.5233 37.6211 40.2577 35.6992 40.2577 32.6055 L 40.2577 28.4570 L 49.1404 28.4570 C 51.0859 28.4570 52.6329 27.3086 52.6329 25.5039 C 52.6329 24.4024 52.0703 23.5351 51.0158 22.6211 L 30.9062 4.8789 C 29.9452 4.0351 29.0546 3.4727 27.9999 3.4727 C 26.9687 3.4727 26.0780 4.0351 25.1171 4.8789 L 4.9843 22.6445 C 3.8828 23.6055 3.3671 24.4024 3.3671 25.5039 C 3.3671 27.3086 4.9140 28.4570 6.8828 28.4570 L 15.7421 28.4570 L 15.7421 32.6055 C 15.7421 35.6992 17.4999 37.6211 20.7812 37.6211 Z M 21.1562 34.0820 C 20.2655 34.0820 19.6562 33.4961 19.6562 32.6055 L 19.6562 25.7149 C 19.6562 25.1524 19.4452 24.9180 18.8828 24.9180 L 8.6640 24.9180 C 8.4999 24.9180 8.4296 24.8476 8.4296 24.7305 C 8.4296 24.6367 8.4530 24.5430 8.5702 24.4492 L 27.5077 7.9961 C 27.7187 7.8086 27.8359 7.7383 27.9999 7.7383 C 28.1640 7.7383 28.3046 7.8086 28.4921 7.9961 L 47.4532 24.4492 C 47.5703 24.5430 47.5939 24.6367 47.5939 24.7305 C 47.5939 24.8476 47.4998 24.9180 47.3356 24.9180 L 37.1406 24.9180 C 36.5780 24.9180 36.3671 25.1524 36.3671 25.7149 L 36.3671 32.6055 C 36.3671 33.4727 35.7109 34.0820 34.8671 34.0820 Z M 19.7733 52.5273 L 36.0624 52.5273 C 38.7577 52.5273 40.3046 51.0273 40.3046 48.3086 L 40.3046 44.9336 C 40.3046 42.2148 38.7577 40.6680 36.0624 40.6680 L 19.7733 40.6680 C 17.0546 40.6680 15.5077 42.2383 15.5077 44.9336 L 15.5077 48.3086 C 15.5077 51.0039 17.0546 52.5273 19.7733 52.5273 Z M 20.3124 49.2227 C 19.4921 49.2227 19.0468 48.8008 19.0468 47.9805 L 19.0468 45.2617 C 19.0468 44.4414 19.4921 43.9727 20.3124 43.9727 L 35.5233 43.9727 C 36.3202 43.9727 36.7655 44.4414 36.7655 45.2617 L 36.7655 47.9805 C 36.7655 48.8008 36.3202 49.2227 35.5233 49.2227 Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.pf-c-form-control.pf-m-icon.pf-m-caps-lock {
|
||||
--pf-c-form-control--m-icon--BackgroundUrl: var(
|
||||
--pf-c-form-control--m-caps-lock--BackgroundUrl
|
||||
);
|
||||
}
|
||||
|
||||
/*#endregion*/
|
||||
|
||||
.pf-c-page__header {
|
||||
z-index: 0;
|
||||
background-color: var(--ak-dark-background-light);
|
||||
|
||||
@ -3,7 +3,6 @@ import type { AbstractConstructor } from "@goauthentik/elements/types.js";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import type { LitElement } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
|
||||
import type { CurrentBrand } from "@goauthentik/api";
|
||||
|
||||
@ -13,7 +12,6 @@ export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
|
||||
) {
|
||||
abstract class WithBrandProvider extends superclass {
|
||||
@consume({ context: authentikBrandContext, subscribe })
|
||||
@state()
|
||||
public brand!: CurrentBrand;
|
||||
}
|
||||
return WithBrandProvider;
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Utilities for DOM element interaction, focus management, and event handling.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Recursively check if the target element or any of its children are active (i.e. "focused").
|
||||
*
|
||||
* @param targetElement The element to check if it is active.
|
||||
* @param containerElement The container element to check if the target element is active within.
|
||||
*/
|
||||
export function isActiveElement(
|
||||
targetElement: Element | null,
|
||||
containerElement: Element | null,
|
||||
): boolean {
|
||||
// Does the container element even exist?
|
||||
if (!containerElement) return false;
|
||||
|
||||
// Does the container element have a shadow root?
|
||||
if (!("shadowRoot" in containerElement)) return false;
|
||||
if (containerElement.shadowRoot === null) return false;
|
||||
|
||||
// Is the target element the active element?
|
||||
if (containerElement.shadowRoot.activeElement === targetElement) return true;
|
||||
|
||||
// Let's check the children of the container element...
|
||||
return isActiveElement(containerElement.shadowRoot.activeElement, containerElement);
|
||||
}
|
||||
@ -1,93 +1,36 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base.js";
|
||||
import { bound } from "@goauthentik/elements/decorators/bound";
|
||||
import "@goauthentik/elements/forms/FormElement";
|
||||
import { isActiveElement } from "@goauthentik/elements/utils/focus";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { Ref, createRef, ref } from "lit/directives/ref.js";
|
||||
import { html, nothing, render } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
/**
|
||||
* A configuration object for the visibility states of the password input.
|
||||
*/
|
||||
interface VisibilityProps {
|
||||
icon: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum-like object for the visibility states of the password input.
|
||||
*/
|
||||
const Visibility = {
|
||||
Reveal: {
|
||||
icon: "fa-eye",
|
||||
label: msg("Show password"),
|
||||
},
|
||||
Mask: {
|
||||
icon: "fa-eye-slash",
|
||||
label: msg("Hide password"),
|
||||
},
|
||||
} as const satisfies Record<string, VisibilityProps>;
|
||||
|
||||
@customElement("ak-flow-input-password")
|
||||
export class InputPassword extends AKElement {
|
||||
static get styles() {
|
||||
return [PFBase, PFInputGroup, PFFormControl, PFButton];
|
||||
}
|
||||
|
||||
//#region Properties
|
||||
|
||||
/**
|
||||
* The ID of the input field.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, attribute: "input-id" })
|
||||
inputId = "ak-stage-password-input";
|
||||
|
||||
/**
|
||||
* The name of the input field.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
name = "password";
|
||||
|
||||
/**
|
||||
* The label for the input field.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
label = msg("Password");
|
||||
|
||||
/**
|
||||
* The placeholder text for the input field.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
placeholder = msg("Please enter your password");
|
||||
|
||||
/**
|
||||
* The initial value of the input field.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, attribute: "prefill" })
|
||||
initialValue = "";
|
||||
passwordPrefill = "";
|
||||
|
||||
/**
|
||||
* The errors for the input field.
|
||||
*/
|
||||
@property({ type: Object })
|
||||
errors: Record<string, string> = {};
|
||||
|
||||
@ -98,215 +41,113 @@ export class InputPassword extends AKElement {
|
||||
@property({ type: String })
|
||||
invalid?: string;
|
||||
|
||||
/**
|
||||
* Whether to allow the user to toggle the visibility of the password.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "allow-show-password" })
|
||||
allowShowPassword = false;
|
||||
|
||||
/**
|
||||
* Whether the password is currently visible.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "password-visible" })
|
||||
passwordVisible = false;
|
||||
|
||||
/**
|
||||
* Automatically grab focus after rendering.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "grab-focus" })
|
||||
grabFocus = false;
|
||||
|
||||
//#endregion
|
||||
timer?: number;
|
||||
|
||||
//#region Refs
|
||||
input?: HTMLInputElement;
|
||||
|
||||
inputRef: Ref<HTMLInputElement> = createRef();
|
||||
|
||||
toggleVisibilityRef: Ref<HTMLButtonElement> = createRef();
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region State
|
||||
|
||||
/**
|
||||
* Whether the caps lock key is enabled.
|
||||
*/
|
||||
@state()
|
||||
capsLock = false;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Listeners
|
||||
|
||||
/**
|
||||
* Toggle the visibility of the password field.
|
||||
*
|
||||
* Directly affects the DOM, so no `.requestUpdate()` required. Effect is immediately visible.
|
||||
*
|
||||
* @param event The event that triggered the visibility toggle.
|
||||
*/
|
||||
@bound
|
||||
togglePasswordVisibility(event?: PointerEvent) {
|
||||
event?.stopPropagation();
|
||||
event?.preventDefault();
|
||||
|
||||
const input = this.inputRef.value;
|
||||
|
||||
if (!input) {
|
||||
console.warn("ak-flow-password-input: unable to identify input field");
|
||||
|
||||
return;
|
||||
cleanup(): void {
|
||||
if (this.timer) {
|
||||
console.debug("authentik/stages/password: cleared focus timer");
|
||||
window.clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
|
||||
input.type = input.type === "password" ? "text" : "password";
|
||||
|
||||
this.syncVisibilityToggle(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for key events, synchronizing the caps lock indicators.
|
||||
*/
|
||||
@bound
|
||||
capsLockListener(event: KeyboardEvent) {
|
||||
this.capsLock = event.getModifierState("CapsLock");
|
||||
}
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
/**
|
||||
* Interval ID for the focus observer.
|
||||
*
|
||||
* @see {@linkcode observeInputFocus}
|
||||
*/
|
||||
inputFocusIntervalID?: ReturnType<typeof setInterval>;
|
||||
|
||||
/**
|
||||
* Periodically attempt to focus the input field until it is focused.
|
||||
*
|
||||
* This is some-what of a crude way to get autofocus, but in most cases
|
||||
* the `autofocus` attribute isn't enough, due to timing within shadow doms and such.
|
||||
*/
|
||||
observeInputFocus(): void {
|
||||
this.inputFocusIntervalID = setInterval(() => {
|
||||
const input = this.inputRef.value;
|
||||
|
||||
if (!input) return;
|
||||
|
||||
if (isActiveElement(input, document.activeElement)) {
|
||||
console.debug("authentik/stages/password: cleared focus observer");
|
||||
clearInterval(this.inputFocusIntervalID);
|
||||
}
|
||||
|
||||
input.focus();
|
||||
}, 10);
|
||||
|
||||
console.debug("authentik/stages/password: started focus observer");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.observeInputFocus();
|
||||
|
||||
addEventListener("keydown", this.capsLockListener);
|
||||
addEventListener("keyup", this.capsLockListener);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
clearInterval(this.inputFocusIntervalID);
|
||||
|
||||
super.disconnectedCallback();
|
||||
|
||||
removeEventListener("keydown", this.capsLockListener);
|
||||
removeEventListener("keyup", this.capsLockListener);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Render
|
||||
|
||||
/**
|
||||
* Create the render root for the password input.
|
||||
*
|
||||
* Must support both older browsers and shadyDom; we'll keep using this in-line,
|
||||
* but it'll still be in the scope of the parent element, not an independent shadowDOM.
|
||||
*/
|
||||
// Must support both older browsers and shadyDom; we'll keep using this in-line, but it'll still
|
||||
// be in the scope of the parent element, not an independent shadowDOM.
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the password visibility toggle button.
|
||||
*
|
||||
* In the unlikely event that we want to make "show password" the _default_ behavior,
|
||||
* this effect handler is broken out into its own method.
|
||||
*
|
||||
* The current behavior in the main {@linkcode render} method assumes the field is of type "password."
|
||||
*
|
||||
* To have this effect, er, take effect, call it in an {@linkcode updated} method.
|
||||
*
|
||||
* @param input The password field to render the visibility features for.
|
||||
*/
|
||||
syncVisibilityToggle(input: HTMLInputElement | undefined = this.inputRef.value): void {
|
||||
if (!input) return;
|
||||
// State is saved in the DOM, and read from the DOM. Directly affects the DOM,
|
||||
// so no `.requestUpdate()` required. Effect is immediately visible.
|
||||
togglePasswordVisibility(ev: PointerEvent) {
|
||||
const passwordField = this.renderRoot.querySelector(`#${this.inputId}`) as HTMLInputElement;
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const toggleElement = this.toggleVisibilityRef.value;
|
||||
if (!passwordField) {
|
||||
throw new Error("ak-flow-password-input: unable to identify input field");
|
||||
}
|
||||
|
||||
if (!toggleElement) return;
|
||||
passwordField.type = passwordField.type === "password" ? "text" : "password";
|
||||
this.renderPasswordVisibilityFeatures(passwordField);
|
||||
}
|
||||
|
||||
const masked = input.type === "password";
|
||||
|
||||
toggleElement.setAttribute(
|
||||
// In the unlikely event that we want to make "show password" the _default_ behavior, this
|
||||
// effect handler is broken out into its own method. The current behavior in the main
|
||||
// `.render()` method assumes the field is of type "password." To have this effect, er, take
|
||||
// effect, call it in an `.updated()` method.
|
||||
renderPasswordVisibilityFeatures(passwordField: HTMLInputElement) {
|
||||
const toggleId = `#${this.inputId}-visibility-toggle`;
|
||||
const visibilityToggle = this.renderRoot.querySelector(toggleId) as HTMLButtonElement;
|
||||
if (!visibilityToggle) {
|
||||
return;
|
||||
}
|
||||
const show = passwordField.type === "password";
|
||||
visibilityToggle?.setAttribute(
|
||||
"aria-label",
|
||||
msg(masked ? Visibility.Reveal.label : Visibility.Mask.label),
|
||||
show ? msg("Show password") : msg("Hide password"),
|
||||
);
|
||||
visibilityToggle?.querySelector("i")?.remove();
|
||||
render(
|
||||
show
|
||||
? html`<i class="fas fa-eye" aria-hidden="true"></i>`
|
||||
: html`<i class="fas fa-eye-slash" aria-hidden="true"></i>`,
|
||||
visibilityToggle,
|
||||
);
|
||||
|
||||
const iconElement = toggleElement.querySelector("i")!;
|
||||
|
||||
iconElement.classList.remove(Visibility.Mask.icon, Visibility.Reveal.icon);
|
||||
iconElement.classList.add(masked ? Visibility.Reveal.icon : Visibility.Mask.icon);
|
||||
}
|
||||
|
||||
renderVisibilityToggle() {
|
||||
if (!this.allowShowPassword) return nothing;
|
||||
renderInput(): HTMLInputElement {
|
||||
this.input = document.createElement("input");
|
||||
this.input.id = `${this.inputId}`;
|
||||
this.input.type = "password";
|
||||
this.input.name = this.name;
|
||||
this.input.placeholder = this.placeholder;
|
||||
this.input.autofocus = this.grabFocus;
|
||||
this.input.autocomplete = "current-password";
|
||||
this.input.classList.add("pf-c-form-control");
|
||||
this.input.required = true;
|
||||
this.input.value = this.passwordPrefill ?? "";
|
||||
if (this.invalid) {
|
||||
this.input.setAttribute("aria-invalid", this.invalid);
|
||||
}
|
||||
// This is somewhat of a crude way to get autofocus, but in most cases the `autofocus` attribute
|
||||
// isn't enough, due to timing within shadow doms and such.
|
||||
|
||||
const { label, icon } = this.passwordVisible ? Visibility.Mask : Visibility.Reveal;
|
||||
|
||||
return html`<button
|
||||
${ref(this.toggleVisibilityRef)}
|
||||
aria-label=${msg(label)}
|
||||
@click=${this.togglePasswordVisibility}
|
||||
class="pf-c-button pf-m-control"
|
||||
type="button"
|
||||
>
|
||||
<i class="fas ${icon}" aria-hidden="true"></i>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
renderHelperText() {
|
||||
if (!this.capsLock) return nothing;
|
||||
|
||||
return html`<div
|
||||
class="pf-c-form__helper-text"
|
||||
id="helper-text-form-caps-lock-helper"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="pf-c-helper-text">
|
||||
<div class="pf-c-helper-text__item pf-m-warning">
|
||||
<span class="pf-c-helper-text__item-icon">
|
||||
<i class="fas fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
||||
<span class="pf-c-helper-text__item-text">${msg("Caps Lock is enabled.")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
if (this.grabFocus) {
|
||||
this.timer = window.setInterval(() => {
|
||||
if (!this.input) {
|
||||
return;
|
||||
}
|
||||
// Because activeElement behaves differently with shadow dom
|
||||
// we need to recursively check
|
||||
const rootEl = document.activeElement;
|
||||
const isActive = (el: Element | null): boolean => {
|
||||
if (!rootEl) return false;
|
||||
if (!("shadowRoot" in rootEl)) return false;
|
||||
if (rootEl.shadowRoot === null) return false;
|
||||
if (rootEl.shadowRoot.activeElement === el) return true;
|
||||
return isActive(rootEl.shadowRoot.activeElement);
|
||||
};
|
||||
if (isActive(this.input)) {
|
||||
this.cleanup();
|
||||
}
|
||||
this.input.focus();
|
||||
}, 10);
|
||||
console.debug("authentik/stages/password: started focus timer");
|
||||
}
|
||||
return this.input;
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -316,34 +157,22 @@ export class InputPassword extends AKElement {
|
||||
class="pf-c-form__group"
|
||||
.errors=${this.errors}
|
||||
>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-input-group">
|
||||
<input
|
||||
type=${this.passwordVisible ? "text" : "password"}
|
||||
id=${this.inputId}
|
||||
name=${this.name}
|
||||
placeholder=${this.placeholder}
|
||||
autocomplete="current-password"
|
||||
class="${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-icon": true,
|
||||
"pf-m-caps-lock": this.capsLock,
|
||||
})}"
|
||||
required
|
||||
aria-invalid=${ifDefined(this.invalid)}
|
||||
value=${this.initialValue}
|
||||
${ref(this.inputRef)}
|
||||
/>
|
||||
|
||||
${this.renderVisibilityToggle()}
|
||||
</div>
|
||||
|
||||
${this.renderHelperText()}
|
||||
<div class="pf-c-input-group">
|
||||
${this.renderInput()}
|
||||
${this.allowShowPassword
|
||||
? html` <button
|
||||
id="${this.inputId}-visibility-toggle"
|
||||
class="pf-c-button pf-m-control ak-stage-password-toggle-visibility"
|
||||
type="button"
|
||||
aria-label=${msg("Show password")}
|
||||
@click=${(ev: PointerEvent) => this.togglePasswordVisibility(ev)}
|
||||
>
|
||||
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ak-form-element>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@ -161,7 +161,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
get captchaDocumentContainer(): HTMLDivElement {
|
||||
get captchaDocumentContainer() {
|
||||
if (this._captchaDocumentContainer) {
|
||||
return this._captchaDocumentContainer;
|
||||
}
|
||||
@ -170,7 +170,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
return this._captchaDocumentContainer;
|
||||
}
|
||||
|
||||
get captchaFrame(): HTMLIFrameElement {
|
||||
get captchaFrame() {
|
||||
if (this._captchaFrame) {
|
||||
return this._captchaFrame;
|
||||
}
|
||||
@ -326,7 +326,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (!(changedProperties.has("challenge") && this.challenge !== undefined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
6
web/src/global.d.ts
vendored
6
web/src/global.d.ts
vendored
@ -6,12 +6,6 @@ declare module "*.md" {
|
||||
const filename: string;
|
||||
}
|
||||
|
||||
declare module "*.mdx" {
|
||||
const html: string;
|
||||
const metadata: { [key: string]: string };
|
||||
const filename: string;
|
||||
}
|
||||
|
||||
declare namespace Intl {
|
||||
class ListFormat {
|
||||
constructor(locale: string, args: { [key: string]: string });
|
||||
|
||||
@ -99,19 +99,6 @@ export class LibraryApplication extends AKElement {
|
||||
if (this.application?.launchUrl === "goauthentik.io://providers/rac/launch") {
|
||||
return html`<ak-library-rac-endpoint-launch .app=${this.application}>
|
||||
</ak-library-rac-endpoint-launch>
|
||||
<div class="pf-c-card__header">
|
||||
<a
|
||||
@click=${() => {
|
||||
this.racEndpointLaunch?.onClick();
|
||||
}}
|
||||
>
|
||||
<ak-app-icon
|
||||
size=${PFSize.Large}
|
||||
name=${this.application.name}
|
||||
icon=${ifDefined(this.application.metaIcon || undefined)}
|
||||
></ak-app-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__title">
|
||||
<a
|
||||
@click=${() => {
|
||||
@ -122,25 +109,13 @@ export class LibraryApplication extends AKElement {
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
return html`<div class="pf-c-card__header">
|
||||
<a
|
||||
href="${ifDefined(this.application.launchUrl ?? "")}"
|
||||
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
|
||||
>
|
||||
<ak-app-icon
|
||||
size=${PFSize.Large}
|
||||
name=${this.application.name}
|
||||
icon=${ifDefined(this.application.metaIcon || undefined)}
|
||||
></ak-app-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__title">
|
||||
<a
|
||||
href="${ifDefined(this.application.launchUrl ?? "")}"
|
||||
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
|
||||
>${this.application.name}</a
|
||||
>
|
||||
</div>`;
|
||||
return html`<div class="pf-c-card__title">
|
||||
<a
|
||||
href="${ifDefined(this.application.launchUrl ?? "")}"
|
||||
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
|
||||
>${this.application.name}</a
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
@ -160,6 +135,18 @@ export class LibraryApplication extends AKElement {
|
||||
class="pf-c-card pf-m-hoverable pf-m-compact ${classMap(classes)}"
|
||||
style=${styleMap(styles)}
|
||||
>
|
||||
<div class="pf-c-card__header">
|
||||
<a
|
||||
href="${ifDefined(this.application.launchUrl ?? "")}"
|
||||
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
|
||||
>
|
||||
<ak-app-icon
|
||||
size=${PFSize.Large}
|
||||
name=${this.application.name}
|
||||
icon=${ifDefined(this.application.metaIcon || undefined)}
|
||||
></ak-app-icon>
|
||||
</a>
|
||||
</div>
|
||||
${this.renderLaunch()}
|
||||
<div class="expander"></div>
|
||||
${expandable ? this.renderExpansion(this.application) : nothing}
|
||||
|
||||
@ -32,7 +32,7 @@ export class UserSettingsPassword extends AKElement {
|
||||
<div class="pf-c-card__body">
|
||||
<a
|
||||
href="${ifDefined(this.configureUrl)}${AndNext(
|
||||
`${globalAK().api.relBase}if/user/#/settings;${JSON.stringify({ page: "page-details" })}`,
|
||||
`${globalAK().api.base}if/user/#/settings;${JSON.stringify({ page: "page-details" })}`,
|
||||
)}"
|
||||
class="pf-c-button pf-m-primary"
|
||||
>
|
||||
|
||||
@ -10,7 +10,7 @@ import { StageHost } from "@goauthentik/flow/stages/base";
|
||||
import "@goauthentik/user/user-settings/details/stages/prompt/PromptStage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
@ -83,14 +83,12 @@ export class UserSettingsFlowExecutor
|
||||
});
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("brand") && this.brand) {
|
||||
this.flowSlug = this.brand?.flowUserSettings;
|
||||
if (!this.flowSlug) {
|
||||
return;
|
||||
}
|
||||
this.nextChallenge();
|
||||
firstUpdated(): void {
|
||||
this.flowSlug = this.brand?.flowUserSettings;
|
||||
if (!this.flowSlug) {
|
||||
return;
|
||||
}
|
||||
this.nextChallenge();
|
||||
}
|
||||
|
||||
async nextChallenge(): Promise<void> {
|
||||
@ -163,7 +161,7 @@ export class UserSettingsFlowExecutor
|
||||
// Flow has finished, so let's load while in the background we can restart the flow
|
||||
this.loading = true;
|
||||
console.debug("authentik/user/flows: redirect to '/', restarting flow.");
|
||||
this.nextChallenge();
|
||||
this.firstUpdated();
|
||||
this.globalRefresh();
|
||||
showMessage({
|
||||
level: MessageLevel.success,
|
||||
|
||||
@ -74,7 +74,7 @@ export class MFADevicesPage extends Table<Device> {
|
||||
return html`<li>
|
||||
<a
|
||||
href="${ifDefined(stage.configureUrl)}${AndNext(
|
||||
`${globalAK().api.relBase}if/user/#/settings;${JSON.stringify({
|
||||
`${globalAK().api.base}if/user/#/settings;${JSON.stringify({
|
||||
page: "page-mfa",
|
||||
})}`,
|
||||
)}"
|
||||
|
||||
@ -6,12 +6,12 @@ Applications, as defined in authentik, are used to configure and separate the au
|
||||
|
||||
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).
|
||||
|
||||
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).
|
||||
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.md#backchannel-providers).
|
||||
|
||||
Furthermore, the [RAC (Remote Access Control)](../providers/rac/index.md) feature uses a single application and a single provider, but multiple "endpoints". An endpoint defines each remote machine.
|
||||
|
||||
:::info
|
||||
For information about creating and managing applications, refer to [Manage applications](./manage_apps.mdx).
|
||||
For information about creating and managing applications, refer to [Manage applications](./manage_apps.md).
|
||||
:::
|
||||
|
||||
## Appearance
|
||||
|
||||
@ -10,15 +10,7 @@ Learn how to add new applications from our video or follow the instructions belo
|
||||
|
||||
### Video
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/broUAWrIWDI;start=22"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/broUAWrIWDI;start=22" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||
|
||||
### Instructions
|
||||
|
||||
@ -53,12 +45,8 @@ When multiple policies/groups/users are attached, you can configure the _Policy
|
||||
|
||||
## Application Entitlements
|
||||
|
||||
<p className="badge-group">
|
||||
|
||||
:ak-version[2024.12]
|
||||
:ak-preview
|
||||
|
||||
</p>
|
||||
<span class="badge badge--preview">Preview</span>
|
||||
<span class="badge badge--version">authentik 2024.12+</span>
|
||||
|
||||
Application entitlements can be used through authentik to manage authorization within an application (what areas of the app users or groups can access). Entitlements are scoped to a single application and can be bound to multiple users and/or groups (binding policies is not currently supported), giving them access to the entitlement. An application can either check for the name of the entitlement (via the `entitlements` scope), or via attributes stored in entitlements.
|
||||
|
||||
@ -24,7 +24,7 @@ A _policy binding_ connects a specific policy to a flow or to a stage. With the
|
||||
|
||||
You can also bind groups and users to another component (a policy, a stage, a flow, etc.). For example, you can create a binding for a specific group, and then [bind that to a stage binding](../stages/index.md#bind-users-and-groups-to-a-flows-stage-binding), with the result that everyone in that group now will see that stage (and any policies bound to that stage) as part of their flow. Or more specifically, and going one step deeper, you can also _bind a binding to a binding_.
|
||||
|
||||
Bindings are also used for [Application Entitlements](../../applications/manage_apps.mdx#application-entitlements), where you can bind specific users or groups to an application as a way to manage who has access to the application.
|
||||
Bindings are also used for [Application Entitlements](../../applications/manage_apps.md#application-entitlements), where you can bind specific users or groups to an application as a way to manage who has access to the application.
|
||||
|
||||
It's important to remember that bindings are instantiated objects themselves, and conceptually can be considered as a "connector" between two components. This is why you might read about "binding a binding", because technically, a binding is "spliced" into another binding, in order to intercept and enforce the criteria defined in the second binding.
|
||||
|
||||
|
||||
@ -8,6 +8,6 @@ For instructions to create a binding, refer to the documentation for the specifi
|
||||
|
||||
- [Bind a stage to a flow](../stages/index.md#bind-a-stage-to-a-flow)
|
||||
- [Bind a policy to a flow or stage](../../../customize/policies/working_with_policies#bind-a-policy-to-a-flow-or-stage)
|
||||
- [Bind users or groups to a specific application with an Application Entitlement](../../applications/manage_apps.mdx#application-entitlements)
|
||||
- [Bind a policy to a specific application when you create a new app using the Wizard](../../applications/manage_apps.mdx#instructions)
|
||||
- [Bind users or groups to a specific application with an Application Entitlement](../../applications/manage_apps.md#application-entitlements)
|
||||
- [Bind a policy to a specific application when you create a new app using the Wizard](../../applications/manage_apps.md#instructions)
|
||||
- [Bind users and groups to a stage binding, to define whether or not that stage is shown](../stages/index.md#bind-users-and-groups-to-a-flows-stage-binding)
|
||||
|
||||
@ -24,11 +24,11 @@ Keys prefixed with `goauthentik.io` are used internally by authentik and are sub
|
||||
|
||||
### Common keys
|
||||
|
||||
#### `pending_user` ([User object](../../../../users-sources/user/user_ref.mdx#object-properties))
|
||||
#### `pending_user` ([User object](../../../../users-sources/user/user_ref.md#object-properties))
|
||||
|
||||
`pending_user` is used by multiple stages. In the context of most flow executions, it represents the data of the user that is executing the flow. This value is not set automatically, it is set via the [Identification stage](../../stages/identification/index.mdx).
|
||||
`pending_user` is used by multiple stages. In the context of most flow executions, it represents the data of the user that is executing the flow. This value is not set automatically, it is set via the [Identification stage](../../stages/identification/index.md).
|
||||
|
||||
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx) and others will use this value if it is set, and fallback to the request's users when possible.
|
||||
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.md) and others will use this value if it is set, and fallback to the request's users when possible.
|
||||
|
||||
#### `prompt_data` (Dictionary)
|
||||
|
||||
@ -62,7 +62,7 @@ When an unauthenticated user attempts to access a secured resource, they are red
|
||||
|
||||
When a user authenticates/enrolls via an external source, this will be set to the source they are using.
|
||||
|
||||
#### `outpost` (dictionary):ak-version[2024.10]
|
||||
#### `outpost` (dictionary) <span class="badge badge--version">authentik 2024.10+</span>
|
||||
|
||||
When a flow is executed by an Outpost (for example the [LDAP](../../../providers/ldap/index.md) or [RADIUS](../../../providers/radius/index.mdx)), this will be set to a dictionary containing the Outpost instance under the key `"instance"`.
|
||||
|
||||
@ -76,7 +76,7 @@ This key is set to `True` when the flow is executed from an "SSO" context. For e
|
||||
|
||||
This key is set when a flow execution is continued from a token. This happens for example when an [Email stage](../../stages/email/index.mdx) is used and the user clicks on the link within the email. The token object contains the key that was used to restore the flow execution.
|
||||
|
||||
#### `is_redirected` (Flow object):ak-version[2024.12]
|
||||
#### `is_redirected` (Flow object) <span class="badge badge--version">authentik 2024.12+</span>
|
||||
|
||||
This key is set when the current flow was reached through a [Redirect stage](../../stages/redirect/index.md) in Flow mode.
|
||||
|
||||
@ -98,7 +98,7 @@ URL that the form will be submitted to.
|
||||
|
||||
Key-value pairs of the data that is included in the form and will be submitted to `url`.
|
||||
|
||||
#### Captcha stage:ak-version[2024.6]
|
||||
#### Captcha stage <span class="badge badge--version">authentik 2024.6+</span>
|
||||
|
||||
##### `captcha` (dictionary)
|
||||
|
||||
@ -118,7 +118,7 @@ An optional list of all permissions that will be given to the application by gra
|
||||
|
||||
#### Deny stage
|
||||
|
||||
##### `deny_message` (string)
|
||||
##### `deny_message` (string) <span class="badge badge--version">authentik 2023.10+</span>
|
||||
|
||||
Optionally overwrite the deny message shown, has a higher priority than the message configured in the stage.
|
||||
|
||||
@ -134,7 +134,7 @@ If set, this must be a list of group objects and not group names.
|
||||
|
||||
Path the `pending_user` will be written to. If not set in the flow, falls back to the value set in the user_write stage, and otherwise to the `users` path.
|
||||
|
||||
##### `user_type` (string)
|
||||
##### `user_type` (string) <span class="badge badge--version">authentik 2023.10+</span>
|
||||
|
||||
Type the `pending_user` will be created as. Must be one of `internal`, `external` or `service_account`.
|
||||
|
||||
@ -146,7 +146,7 @@ Set by the [Password stage](../../stages/password/index.md) after successfully a
|
||||
|
||||
##### `auth_method` (string)
|
||||
|
||||
Set by the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx), the [OAuth2 Provider](../../../providers/oauth2/index.mdx), and the API authentication depending on which method was used to authenticate.
|
||||
Set by the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.md), the [OAuth2 Provider](../../../providers/oauth2/index.md), and the API authentication depending on which method was used to authenticate.
|
||||
|
||||
Possible options:
|
||||
|
||||
@ -155,7 +155,7 @@ Possible options:
|
||||
- `ldap` (Authenticated via LDAP bind from an LDAP source)
|
||||
- `auth_mfa` (Authentication via MFA device without password)
|
||||
- `auth_webauthn_pwl` (Passwordless authentication via WebAuthn)
|
||||
- `jwt` ([M2M](../../../providers/oauth2/client_credentials.mdx) authentication via an existing JWT)
|
||||
- `jwt` ([M2M](../../../providers/oauth2/client_credentials.md) authentication via an existing JWT)
|
||||
|
||||
##### `auth_method_args` (dictionary)
|
||||
|
||||
@ -198,7 +198,7 @@ If _Show matched user_ is disabled, this key will be set to the user identifier
|
||||
|
||||
#### Redirect stage
|
||||
|
||||
##### `redirect_stage_target` (string):ak-version[2024.12]
|
||||
##### `redirect_stage_target` (string) <span class="badge badge--version">authentik 2024.12+</span>
|
||||
|
||||
[Set this key](../../../../customize/policies/expression/managing_flow_context_keys.md) in an Expression Policy to override [Redirect stage](../../stages/redirect/index.md) to force it to redirect to a certain URL or flow. This is useful when a flow requires that the redirection target be decided dynamically.
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
title: Example policy snippets for flows
|
||||
---
|
||||
|
||||
### Redirect current flow to another URL
|
||||
### Redirect current flow to another URL <span class="badge badge--version">authentik 2022.7+</span>
|
||||
|
||||
```python
|
||||
plan = request.context.get("flow_plan")
|
||||
@ -6,6 +6,6 @@ The headless flow executor is used by clients that don't have access to the web
|
||||
|
||||
The following stages are supported:
|
||||
|
||||
- [**Identification stage**](../../stages/identification/index.mdx)
|
||||
- [**Identification stage**](../../stages/identification/index.md)
|
||||
- [**Password stage**](../../stages/password/index.md)
|
||||
- [**Authenticator Validation Stage**](../../stages/authenticator_validate/index.mdx)
|
||||
- [**Authenticator Validation Stage**](../../stages/authenticator_validate/index.md)
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
---
|
||||
title: Simplified flow executor
|
||||
authentik_version: "2024.6.1"
|
||||
---
|
||||
|
||||
<span class="badge badge--version">authentik 2024.6.1+</span>
|
||||
|
||||
A simplified web-based flow executor that authentik automatically uses for older browsers that do not support modern web technologies.
|
||||
|
||||
Currently this flow executor is automatically used for the following browsers:
|
||||
@ -12,14 +13,14 @@ Currently this flow executor is automatically used for the following browsers:
|
||||
|
||||
The following stages are supported:
|
||||
|
||||
- [**Identification stage**](../../stages/identification/index.mdx)
|
||||
- [**Identification stage**](../../stages/identification/index.md)
|
||||
|
||||
:::info
|
||||
Only user identifier and user identifier + password stage configurations are supported; sources and passwordless configurations are not supported.
|
||||
:::
|
||||
|
||||
- [**Password stage**](../../stages/password/index.md)
|
||||
- [**Authenticator Validation Stage**](../../stages/authenticator_validate/index.mdx)
|
||||
- [**Authenticator Validation Stage**](../../stages/authenticator_validate/index.md)
|
||||
|
||||
Compared to the [default flow executor](./if-flow.md), this flow executor does _not_ support the following features:
|
||||
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
title: User settings
|
||||
---
|
||||
|
||||
<span class="badge badge--version">authentik 2023.3+</span>
|
||||
|
||||
---
|
||||
|
||||
The user interface (/if/user/) uses a specialized flow executor to allow individual users to customize their profile. A user's profile consists of key/value fields, so this executor only supports Prompt or User Write stages. If the configured flow contains another stage, a button will be shown to open the default executor.
|
||||
|
||||
Because the stages in a flow can change during its execution, be awre that configuring this executor to use any stage type other than Prompt or User Write will automatically trigger a redirect to the standard executor.
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
- **Invalidation**: designates a default flow to be used to invalidate a session. Use `default-invalidation-flow` for invalidation from authentik itself, or use `default-provider-invalidation-flow` to invalidate when the session of an application ends. When you use the `default-invalidation-flow` as a global invalidation flow, it should contain a [**User Logout**](../../stages/user_logout.md) stage. When you use the `default-provider-invalidation-flow` (supported with OIDC, SAML, Proxy, and RAC providers), you can configure this default flow to present users log-off options such as "log out of the app but remain logged in to authentik" or "return to the **My Applications** page", or "log out completely". (Alternatively, you can create a custom invalidation flow, with a branded background image.)
|
||||
|
||||
- **Recovery**: designates a flow for recovery. This flow normally contains an [**Identification**](../../stages/identification/index.mdx) stage to find the user. It can also contain any amount of verification stages, such as [**Email**](../../stages/email/index.mdx) or [**CAPTCHA**](../../stages/captcha/index.md). Afterwards, use the [**Prompt**](../../stages/prompt/index.md) stage to ask the user for a new password and the [**User Write**](../../stages/user_write.md) stage to update the password.
|
||||
- **Recovery**: designates a flow for recovery. This flow normally contains an [**Identification**](../../stages/identification/index.md) stage to find the user. It can also contain any amount of verification stages, such as [**Email**](../../stages/email/index.mdx) or [**CAPTCHA**](../../stages/captcha/index.md). Afterwards, use the [**Prompt**](../../stages/prompt/index.md) stage to ask the user for a new password and the [**User Write**](../../stages/user_write.md) stage to update the password.
|
||||
|
||||
- **Stage configuration**: designates a flow for general setup. This designation doesn't have any constraints in what you can do. For example, by default this designation is used to configure authenticators, like change a password and set up TOTP.
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ When these stages are successfully completed, authentik logs in the user.
|
||||
|
||||
By default, policies are evaluated dynamically, right before the stage (to which a policy is bound) is presented to the user. This flexibility allows the login process to continue, change, or stop, based on the success or failure of each policy.
|
||||
|
||||
This default behaviour can be altered by enabling the **Evaluate when flow is planned** option on the stage binding. With this setting a _flow plan_ containing all stages is generated upon flow execution. This means that all attached policies are evaluated upon execution. For more information about flow plans, read our [flow context documentation](./context/index.mdx).
|
||||
This default behaviour can be altered by enabling the **Evaluate when flow is planned** option on the stage binding. With this setting a _flow plan_ containing all stages is generated upon flow execution. This means that all attached policies are evaluated upon execution. For more information about flow plans, read our [flow context documentation](./context/index.md).
|
||||
|
||||
## Permissions
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
title: Flow Inspector
|
||||
---
|
||||
|
||||
The flow inspector, introduced in 2021.10, allows administrators to visually determine how custom flows work, inspect the current [flow context](./context/index.mdx), and investigate issues.
|
||||
The flow inspector, introduced in 2021.10, allows administrators to visually determine how custom flows work, inspect the current [flow context](./context/index.md), and investigate issues.
|
||||
|
||||
As shown in the screenshot below, the flow inspector displays next to the selected flow (in this case, "Change Password"), with [information](#flow-inspector-details) about that specific flow and flow context.
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ Copy all of the integration key, secret key and API hostname, and paste them in
|
||||
|
||||
Devices created reference the stage they were created with, since the API credentials are needed to authenticate. This also means when the stage is deleted, all devices are removed.
|
||||
|
||||
## Importing users
|
||||
## Importing users <span class="badge badge--version">authentik 2022.9+</span>
|
||||
|
||||
:::info
|
||||
Due to the way the Duo API works, authentik can only automatically import existing Duo users when a Duo MFA or higher license is active.
|
||||
@ -20,7 +20,7 @@ To import a device, open the Stages list in the authentik Admin interface. On th
|
||||
|
||||
The Duo username can be found by navigating to your Duo Admin dashboard and selecting _Users_ in the sidebar. Optionally if you have multiple users with the same username, you can click on a User and copy their ID from the URL, and use that to import the device.
|
||||
|
||||
### Older versions
|
||||
### Older versions <span class="badge badge--version">authentik 2021.9.1+</span>
|
||||
|
||||
You can call the `/api/v3/stages/authenticator/duo/{stage_uuid}/import_devices/` endpoint ([see here](https://goauthentik.io/api/#post-/stages/authenticator/duo/-stage_uuid-/import_devices/)) using the following parameters:
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
---
|
||||
title: Endpoint Authenticator Google Device Trust Connector Stage
|
||||
authentik_version: "2024.10"
|
||||
authentik_preview: true
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
<span class="badge badge--preview">Preview</span>
|
||||
<span class="badge badge--version">authentik 2024.10+</span>
|
||||
|
||||
---
|
||||
|
||||
With this stage, authentik can validate users' Chrome browsers and ensure that users' devices are compliant and up-to-date.
|
||||
@ -15,11 +18,10 @@ This stage only works with Google Chrome, as it relies on the [Chrome Verified A
|
||||
|
||||
The main steps to set up your Google workspace are as follows:
|
||||
|
||||
- [Configuration](#configuration)
|
||||
- [Create a Google cloud project](#create-a-google-cloud-project)
|
||||
- [Create a service account](#create-a-service-account)
|
||||
- [Set credentials for the service account](#set-credentials-for-the-service-account)
|
||||
- [Create the stage](#create-the-stage)
|
||||
1. [Create your Google Cloud Project](#create-a-google-cloud-project)
|
||||
2. [Create a service account](#create-a-service-account)
|
||||
3. [Set credentials for the service account](#set-credentials-for-the-service-account)
|
||||
4. [Define access and scope in the Admin Console](#set-credentials-for-the-service-account)
|
||||
|
||||
For detailed instructions, refer to Google documentation.
|
||||
|
||||
@ -74,4 +76,4 @@ For detailed instructions, refer to Google documentation.
|
||||
|
||||
4. Click **Finish**.
|
||||
|
||||
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the users' browser.
|
||||
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.md), this stage only verifies the users' browser.
|
||||
|
||||
@ -46,9 +46,9 @@ return {
|
||||
}
|
||||
```
|
||||
|
||||
## Verify only
|
||||
## Verify only <span class="badge badge--version">authentik 2022.6+</span>
|
||||
|
||||
To only verify the validity of a users' phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
|
||||
To only verify the validity of a users' phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.md) stage.
|
||||
|
||||
## Limiting phone numbers
|
||||
|
||||
@ -4,11 +4,11 @@ title: Authenticator validation stage
|
||||
|
||||
This stage validates an already configured Authenticator Device. This device has to be configured using any of the other authenticator stages:
|
||||
|
||||
- [Duo authenticator stage](../authenticator_duo/index.mdx)
|
||||
- [SMS authenticator stage](../authenticator_sms/index.mdx)
|
||||
- [Duo authenticator stage](../authenticator_duo/index.md)
|
||||
- [SMS authenticator stage](../authenticator_sms/index.md)
|
||||
- [Static authenticator stage](../authenticator_static/index.md)
|
||||
- [TOTP authenticator stage](../authenticator_totp/index.md)
|
||||
- [WebAuthn authenticator stage](../authenticator_webauthn/index.mdx)
|
||||
- [WebAuthn authenticator stage](../authenticator_webauthn/index.md)
|
||||
|
||||
You can select which type of device classes are allowed.
|
||||
|
||||
@ -23,11 +23,11 @@ Keep in mind that when using Code-based devices (TOTP, Static and SMS), values l
|
||||
|
||||
### Options
|
||||
|
||||
#### Less-frequent validation
|
||||
#### Less-frequent validation <span class="badge badge--version">authentik 2022.5.1+</span>
|
||||
|
||||
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the users devices within the selected classes are checked.
|
||||
|
||||
#### Passwordless authentication
|
||||
#### Passwordless authentication <span class="badge badge--version">authentik 2021.12.4+</span>
|
||||
|
||||
:::caution
|
||||
Firefox has some known issues regarding TouchID (see https://bugzilla.mozilla.org/show_bug.cgi?id=1536482)
|
||||
@ -68,7 +68,7 @@ Logins which used Passwordless authentication have the _auth_method_ context var
|
||||
}
|
||||
```
|
||||
|
||||
#### WebAuthn Device type restrictions:ak-version[2024.4]
|
||||
#### WebAuthn Device type restrictions <span class="badge badge--version">authentik 2024.4+</span>
|
||||
|
||||
Optionally restrict which WebAuthn device types can be used to authenticate.
|
||||
|
||||
@ -12,13 +12,13 @@ Configure if authentik should require, prefer or discourage user verification fo
|
||||
|
||||
#### Resident key requirement
|
||||
|
||||
Configure if the created authenticator is stored in the encrypted memory on the device or in persistent memory. When configuring [passwordless login](../identification/index.mdx#passwordless-flow), this should be set to either _Preferred_ or _Required_, otherwise the authenticator cannot be used for passwordless authentication.
|
||||
Configure if the created authenticator is stored in the encrypted memory on the device or in persistent memory. When configuring [passwordless login](../identification/index.md#passwordless-flow), this should be set to either _Preferred_ or _Required_, otherwise the authenticator cannot be used for passwordless authentication.
|
||||
|
||||
#### Authenticator Attachment
|
||||
|
||||
Configure if authentik will require either a removable device (like a YubiKey, Google Titan, etc) or a non-removable device (like Windows Hello, TouchID or password managers), or not send a requirement.
|
||||
|
||||
#### Device type restrictions:ak-version[2024.4]
|
||||
#### Device type restrictions <span class="badge badge--version">authentik 2024.4+</span>
|
||||
|
||||
Optionally restrict the types of devices allowed to be enrolled. This option can be used to ensure users are only able to enroll FIPS-compliant devices for example.
|
||||
|
||||
@ -30,13 +30,13 @@ To run a CAPTCHA process in the background while the user is entering their iden
|
||||
|
||||
These fields specify if and which flows are linked on the form. The enrollment flow is linked as `Need an account? Sign up.`, and the recovery flow is linked as `Forgot username or password?`.
|
||||
|
||||
## Pretend user exists:ak-version[2024.2]
|
||||
## Pretend user exists <span class="badge badge--version">authentik 2024.2+</span>
|
||||
|
||||
When enabled, any user identifier will be accepted as valid (as long as they match the correct format, i.e. when [User fields](#user-fields) is set to only allow Emails, then the identifier still needs to be an Email). The stage will succeed and the flow will continue to the next stage. Stages like the [Password stage](../password/index.md) and [Email stage](../email/index.mdx) are aware of this "pretend" user and will behave the same as if the user would exist.
|
||||
|
||||
## Source settings
|
||||
|
||||
Some sources (like the [OAuth Source](../../../../users-sources/sources/protocols/oauth/index.mdx) and [SAML Source](../../../../users-sources/sources/protocols/saml/index.md)) require user interaction. To make these sources available to users, they can be selected in the Identification stage settings, which will show them below the selected [user field](#user-fields).
|
||||
Some sources (like the [OAuth Source](../../../../users-sources/sources/protocols/oauth/index.md) and [SAML Source](../../../../users-sources/sources/protocols/saml/index.md)) require user interaction. To make these sources available to users, they can be selected in the Identification stage settings, which will show them below the selected [user field](#user-fields).
|
||||
|
||||
By default, sources are only shown with their icon, which can be changed with the _Show sources' labels_ option.
|
||||
|
||||
@ -50,7 +50,7 @@ Starting with authentik 2023.5, when no user fields are selected and only one so
|
||||
|
||||
### Passwordless flow
|
||||
|
||||
See [Passwordless authentication](../authenticator_validate/index.mdx#passwordless-authentication).
|
||||
See [Passwordless authentication](../authenticator_validate/index.md#passwordless-authentication-authentik-2021124).
|
||||
|
||||
### Enrollment flow
|
||||
|
||||
@ -6,7 +6,7 @@ This is a generic password prompt which authenticates the current `pending_user`
|
||||
|
||||
## Passwordless login
|
||||
|
||||
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the users device, which is documented here.
|
||||
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.md#passwordless-authentication-authentik-2021124) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the users device, which is documented here.
|
||||
|
||||
Depending on what kind of device you want to require the user to have:
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
---
|
||||
title: Redirect stage
|
||||
authentik_version: "2024.12"
|
||||
---
|
||||
|
||||
<span class="badge badge--version">authentik 2024.12+</span>
|
||||
|
||||
---
|
||||
|
||||
This stage's main purpose is to redirect the user to a new Flow while keeping flow context. For convenience, it can also redirect the user to a static URL.
|
||||
@ -13,6 +16,6 @@ When the user reaches this stage, they are redirected to a static URL.
|
||||
|
||||
### Flow mode
|
||||
|
||||
When the user reaches this stage, they are redirected to a specified flow, retaining all [flow context](../../flow/context/index.mdx).
|
||||
When the user reaches this stage, they are redirected to a specified flow, retaining all [flow context](../../flow/context).
|
||||
|
||||
Optionally, untoggle the "Keep flow context" switch. If this is untoggled, all flow context is cleared with the exception of the [is_redirected](../../flow/context#is_redirected-flow-object) key.
|
||||
Optionally, untoggle the "Keep flow context" switch. If this is untoggled, all flow context is cleared with the exception of the [is_redirected](../../flow/context#is_redirected-flow-object-authentik-202412) key.
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
---
|
||||
title: Source stage
|
||||
authentik_version: "2024.4"
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
The source stage injects an [OAuth](../../../../users-sources/sources/protocols/oauth/index.mdx) or [SAML](../../../../users-sources/sources/protocols/saml/index.md) Source into the flow execution. This allows for additional user verification, or to dynamically access different sources for different user identifiers (username, email address, etc).
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
<span class="badge badge--version">authentik 2024.4+</span>
|
||||
|
||||
---
|
||||
|
||||
The source stage injects an [OAuth](../../../../users-sources/sources/protocols/oauth/index.md) or [SAML](../../../../users-sources/sources/protocols/saml/index.md) Source into the flow execution. This allows for additional user verification, or to dynamically access different sources for different user identifiers (username, email address, etc).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@ -41,13 +44,13 @@ This stage can be used to leverage an external OAuth/SAML identity provider.
|
||||
|
||||
For example, you can authenticate users by routing them through a custom device-health solution.
|
||||
|
||||
Another use case is to route users to authenticate with your legacy (Okta, etc) IdP and then use the returned identity and attributes within authentik as part of an authorization flow, for example as part of an IdP migration. For authentication/enrollment this is also possible with an [OAuth](../../../../users-sources/sources/protocols/oauth/index.mdx)/[SAML](../../../../users-sources/sources/protocols/saml/index.md) source by itself.
|
||||
Another use case is to route users to authenticate with your legacy (Okta, etc) IdP and then use the returned identity and attributes within authentik as part of an authorization flow, for example as part of an IdP migration. For authentication/enrollment this is also possible with an [OAuth](../../../../users-sources/sources/protocols/oauth/index.md)/[SAML](../../../../users-sources/sources/protocols/saml/index.md) source by itself.
|
||||
|
||||
### Options
|
||||
|
||||
#### Source
|
||||
|
||||
The source the user is redirected to. Must be a web-based source, such as [OAuth](../../../../users-sources/sources/protocols/oauth/index.mdx) or [SAML](../../../../users-sources/sources/protocols/saml/index.md). Sources like [LDAP](../../../../users-sources/sources/protocols/ldap/index.md) are _not_ compatible.
|
||||
The source the user is redirected to. Must be a web-based source, such as [OAuth](../../../../users-sources/sources/protocols/oauth/index.md) or [SAML](../../../../users-sources/sources/protocols/saml/index.md). Sources like [LDAP](../../../../users-sources/sources/protocols/ldap/index.md) are _not_ compatible.
|
||||
|
||||
#### Resume timeout
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
---
|
||||
title: Add an Entra ID provider
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
|
||||
---
|
||||
|
||||
For more information about using an Entra ID provider, see the [Overview](./index.md) documentation.
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
---
|
||||
title: Microsoft Entra ID provider
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
|
||||
---
|
||||
|
||||
With the Microsoft Entra ID provider, authentik serves as the single source of truth for all users and groups. Configuring Entra ID as a provider allows for auto-discovery of user and group accounts, on-going synchronization of user data such as email address, name, and status, and integrated data mapping of field names and values.
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
---
|
||||
title: Configure Entra ID
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
|
||||
---
|
||||
|
||||
The configuration of your Microsoft Entra ID environment must be completed before you [add the new provider](./add-entra-provider.md) in authentik.
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
---
|
||||
title: Create a Google Workspace provider
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
|
||||
---
|
||||
|
||||
For more information about using a Google Workspace provider, see the [Overview](./index.md) documentation.
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
---
|
||||
title: Google Workspace provider
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
|
||||
---
|
||||
|
||||
With the Google Workspace provider, authentik serves as the single source of truth for all users and groups, when using Google products like Gmail.
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
---
|
||||
title: Configure Google Workspace
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
|
||||
---
|
||||
|
||||
The configuration and set up of your Google Workspace must be completed before you [add the new provider](./add-gws-provider.md) in authentik.
|
||||
|
||||
@ -9,9 +9,9 @@ A Provider is an authentication method, a service that is used by authentik to a
|
||||
|
||||
Providers are the "other half" of [applications](../applications/index.md). 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 use additional providers to augment the functionality of the main provider. For more information, see [Backchannel providers](../applications/manage_apps.mdx#backchannel-providers).
|
||||
Applications can use additional providers to augment the functionality of the main provider. For more information, see [Backchannel providers](../applications/manage_apps.md#backchannel-providers).
|
||||
|
||||
You can create a new provider in the Admin interface, or you can use the [Application wizard](../applications/manage_apps.mdx#instructions) to create a new application and its provider at the same time.
|
||||
You can create a new provider in the Admin interface, or you can use the [Application wizard](../applications/manage_apps.md#instructions) to create a new application and its provider at the same time.
|
||||
|
||||
When you create certain types of providers, you need to select specific [flows](../flows-stages/flow/index.md) to apply to users who access authentik via the provider. To learn more, refer to our [default flow documentation](../flows-stages/flow/examples/default_flows.md).
|
||||
|
||||
|
||||
@ -78,9 +78,9 @@ All bind modes rely on flows.
|
||||
|
||||
The following stages are supported:
|
||||
|
||||
- [Identification](../../flows-stages/stages/identification/index.mdx)
|
||||
- [Identification](../../flows-stages/stages/identification/index.md)
|
||||
- [Password](../../flows-stages/stages/password/index.md)
|
||||
- [Authenticator validation](../../flows-stages/stages/authenticator_validate/index.mdx)
|
||||
- [Authenticator validation](../../flows-stages/stages/authenticator_validate/index.md)
|
||||
|
||||
Note: Authenticator validation currently only supports DUO, TOTP and static authenticators.
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ In addition to that, with authentik 2024.4 it is also possible to pass the confi
|
||||
|
||||
### JWT-authentication
|
||||
|
||||
#### Externally issued JWTs
|
||||
#### Externally issued JWTs <span class="badge badge--version">authentik 2022.4+</span>
|
||||
|
||||
You can authenticate and get a token using an existing JWT. For readability we will refer to the JWT issued by the external issuer/platform as input JWT, and the resulting JWT from authentik as the output JWT.
|
||||
|
||||
@ -59,7 +59,7 @@ To dynamically limit access based on the claims of the tokens, you can use _[Exp
|
||||
return request.context["oauth_jwt"]["iss"] == "https://my.issuer"
|
||||
```
|
||||
|
||||
#### authentik-issued JWTs:ak-version[2024.12]
|
||||
#### authentik-issued JWTs <span class="badge badge--version">authentik 2024.12+</span>
|
||||
|
||||
To allow federation between providers, modify the provider settings of the application (whose token will be used for authentication) to select the provider of the application to which you want to federate.
|
||||
|
||||
@ -112,7 +112,7 @@ The Hybrid Flow is an OpenID Connect flow that incorporates traits of both the I
|
||||
|
||||
The client credentials flow and grant types are typically implemented for server-to-server scenarios, when code in a web application invokes a web API.
|
||||
|
||||
For more information, see [Machine-to-machine authentication](./client_credentials.mdx).
|
||||
For more information, see [Machine-to-machine authentication](./client_credentials.md).
|
||||
|
||||
### 3. Device code
|
||||
|
||||
@ -176,6 +176,6 @@ When a _Signing Key_ is selected in the provider, the JWT will be signed asymmet
|
||||
|
||||
When no _Signing Key_ is selected, the JWT will be signed symmetrically with the _Client secret_ of the provider, which can be seen in the provider settings.
|
||||
|
||||
### Encryption:ak-version[2024.10]
|
||||
### Encryption <span class="badge badge--version">authentik 2024.10+</span>
|
||||
|
||||
authentik can also encrypt JWTs (turning them into JWEs) it issues by selecting an _Encryption Key_ in the provider. When selected, all JWTs will be encrypted symmetrically using the selected certificate. authentik uses the `RSA-OAEP-256` algorithm with the `A256CBC-HS512` encryption method.
|
||||
@ -6,7 +6,7 @@ The property mapping should return a value that is expected by the provider. Sup
|
||||
|
||||
## Available Functions
|
||||
|
||||
import Functions from "../../../expressions/_functions.mdx";
|
||||
import Functions from "../../../expressions/_functions.md";
|
||||
|
||||
<Functions />
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
Use the following configuration:
|
||||
|
||||
```apacheconf
|
||||
```
|
||||
app.company {
|
||||
# directive execution order is only as stated if enclosed with route.
|
||||
route {
|
||||
@ -26,7 +26,7 @@ app.company {
|
||||
|
||||
If you're trying to proxy to an upstream over HTTPS, you need to set the `Host` header to the value they expect for it to work correctly.
|
||||
|
||||
```conf
|
||||
```
|
||||
reverse_proxy /outpost.goauthentik.io/* https://outpost.company {
|
||||
header_up Host {http.reverse_proxy.upstream.hostport}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
```nginx
|
||||
```
|
||||
# Increase buffer size for large headers
|
||||
# This is needed only if you get 'upstream sent too big header while reading response
|
||||
# header from upstream' error when trying to access an application protected by goauthentik
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
```nginx
|
||||
```
|
||||
# Upgrade WebSocket if requested, otherwise use keepalive
|
||||
map $http_upgrade $connection_upgrade_keepalive {
|
||||
default upgrade;
|
||||
|
||||
@ -25,9 +25,9 @@ By default, when _Intercept header authentication_ is enabled, authentik will in
|
||||
|
||||
If the proxied application requires usage of the "Authorization" header, the setting should be disabled. When this setting is disabled, authentik will still attempt to interpret the "Authorization" header, and fall back to the default behaviour if it can't.
|
||||
|
||||
### Receiving HTTP Basic authentication
|
||||
### Receiving HTTP Basic authentication <span class="badge badge--version">authentik 2023.1+</span>
|
||||
|
||||
Proxy providers can receive HTTP basic authentication credentials. The password is expected to be an _App password_, as the credentials are used internally with the [OAuth2 machine-to-machine authentication flow](../oauth2/client_credentials.mdx).
|
||||
Proxy providers can receive HTTP basic authentication credentials. The password is expected to be an _App password_, as the credentials are used internally with the [OAuth2 machine-to-machine authentication flow](../oauth2/client_credentials.md).
|
||||
|
||||
Access control is done with the policies bound to the application being accessed.
|
||||
|
||||
@ -39,9 +39,9 @@ It is **strongly** recommended that the client sending requests with HTTP-Basic
|
||||
|
||||
Starting with authentik 2023.2, logging in with the reserved username `goauthentik.io/token` will behave as if a bearer token was used. All the same options as below apply. This is to allow token-based authentication for applications which might only support basic authentication.
|
||||
|
||||
### Receiving HTTP Bearer authentication
|
||||
### Receiving HTTP Bearer authentication <span class="badge badge--version">authentik 2023.1+</span>
|
||||
|
||||
Proxy providers can receive HTTP bearer authentication credentials. The token is expected to be a JWT token issued for the proxy provider. This is described [here](../oauth2/client_credentials.mdx), using the _client_id_ value shown in the admin interface. Both static and JWT authentication methods are supported.
|
||||
Proxy providers can receive HTTP bearer authentication credentials. The token is expected to be a JWT token issued for the proxy provider. This is described [here](../oauth2/client_credentials.md), using the _client_id_ value shown in the admin interface. Both static and JWT authentication methods are supported.
|
||||
|
||||
Access control is done with the policies bound to the application being accessed.
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
---
|
||||
title: Caddy
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
import Placeholders from "./__placeholders.md";
|
||||
import CaddyStandalone from "./_caddy_standalone.md";
|
||||
|
||||
# Caddy <span class="badge badge--version">authentik 2022.8+</span>
|
||||
|
||||
The configuration template shown below apply to both single-application and domain-level forward auth.
|
||||
|
||||
import Placeholders from "./__placeholders.md";
|
||||
|
||||
<Placeholders />
|
||||
|
||||
<Tabs
|
||||
@ -18,6 +21,8 @@ The configuration template shown below apply to both single-application and doma
|
||||
]}>
|
||||
<TabItem value="caddy-standalone">
|
||||
|
||||
import CaddyStandalone from "./_caddy_standalone.md";
|
||||
|
||||
<CaddyStandalone />
|
||||
|
||||
</TabItem>
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
---
|
||||
title: Envoy
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
import Placeholders from "./__placeholders.md";
|
||||
import EnvoyIstio from "./_envoy_istio.md";
|
||||
|
||||
# Envoy
|
||||
# Envoy <span class="badge badge--version">authentik 2022.6+</span>
|
||||
|
||||
The configuration template shown below apply to both single-application and domain-level forward auth.
|
||||
|
||||
@ -15,18 +14,20 @@ The configuration template shown below apply to both single-application and doma
|
||||
If you are using Istio and Kubernetes, use the port number that is exposed for your cluster.
|
||||
:::
|
||||
|
||||
import Placeholders from "./__placeholders.md";
|
||||
|
||||
<Placeholders />
|
||||
|
||||
<Tabs
|
||||
defaultValue="envoy-istio"
|
||||
values={[
|
||||
{
|
||||
label: "Envoy (Istio)",
|
||||
value: "envoy-istio",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TabItem value="envoy-istio">
|
||||
<EnvoyIstio />
|
||||
</TabItem>
|
||||
defaultValue="envoy-istio"
|
||||
values={[
|
||||
{label: 'Envoy (Istio)', value: 'envoy-istio'},
|
||||
]}>
|
||||
<TabItem value="envoy-istio">
|
||||
|
||||
import EnvoyIstio from "./_envoy_istio.md";
|
||||
|
||||
<EnvoyIstio />
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
@ -26,7 +26,7 @@ The first step is to create the RAC app and provider.
|
||||
|
||||
2. In the Admin interface, navigate to **Applications -> Applications**.
|
||||
|
||||
3. Click **Create with Wizard**. Follow the [instructions](../../applications/manage_apps.mdx#instructions) to create your RAC application and provider.
|
||||
3. Click **Create with Wizard**. Follow the [instructions](../../applications/manage_apps.md#instructions) to create your RAC application and provider.
|
||||
|
||||
### Step 2. Create RAC property mapping
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
---
|
||||
title: Remote Access Control (RAC) Provider
|
||||
authentik_enterprise: true
|
||||
---
|
||||
|
||||
<span class="badge badge--primary">Enterprise</span>
|
||||
|
||||
---
|
||||
|
||||
:::info
|
||||
|
||||
@ -18,9 +18,9 @@ Authentication requests against the Radius Server use a flow in the background.
|
||||
|
||||
The following stages are supported:
|
||||
|
||||
- [Identification](../../flows-stages/stages/identification/index.mdx)
|
||||
- [Identification](../../flows-stages/stages/identification/index.md)
|
||||
- [Password](../../flows-stages/stages/password/index.md)
|
||||
- [Authenticator validation](../../flows-stages/stages/authenticator_validate/index.mdx)
|
||||
- [Authenticator validation](../../flows-stages/stages/authenticator_validate/index.md)
|
||||
|
||||
Note: Authenticator validation currently only supports DUO, TOTP, and static authenticators.
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
title: Export
|
||||
---
|
||||
|
||||
## Global export
|
||||
## Global export <span class="badge badge--version">authentik 2022.8.2+</span>
|
||||
|
||||
To migrate existing configurations to blueprints, run `ak export_blueprint` within any authentik Worker container. This will output a blueprint for most currently created objects. Some objects will not be exported as they might have dependencies on other things.
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
title: Blueprints
|
||||
---
|
||||
|
||||
<span class="badge badge--version">authentik 2022.8+</span>
|
||||
|
||||
---
|
||||
|
||||
Blueprints offer a new way to template, automate and distribute authentik configuration. Blueprints can be used to automatically configure instances, manage config as code without any external tools, and to distribute application configs.
|
||||
|
||||
## Types
|
||||
@ -54,7 +58,7 @@ To push a blueprint to an OCI-compatible registry, [ORAS](https://oras.land/) ca
|
||||
oras push ghcr.io/<username>/blueprint/<blueprint name>:latest <yaml file>:application/vnd.goauthentik.blueprint.v1+yaml
|
||||
```
|
||||
|
||||
## Storage - Internal
|
||||
## Storage - Internal <span class="badge badge--version">authentik 2023.1+</span>
|
||||
|
||||
Blueprints can be stored in authentik's database, which allows blueprints to be managed via external configuration management tools like Terraform.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user