Merge branch 'main' into benchmarks

This commit is contained in:
Marc 'risson' Schmitt
2024-04-11 19:11:27 +02:00
59 changed files with 1038 additions and 187 deletions

View File

@ -84,7 +84,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Python dependencies
FROM docker.io/python:3.12.2-slim-bookworm AS python-deps
FROM docker.io/python:3.12.3-slim-bookworm AS python-deps
WORKDIR /ak-root/poetry
@ -110,7 +110,7 @@ RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
poetry install --only=main --no-ansi --no-interaction --no-root"
# Stage 6: Run
FROM docker.io/python:3.12.2-slim-bookworm AS final-image
FROM docker.io/python:3.12.3-slim-bookworm AS final-image
ARG GIT_BUILD_HASH
ARG VERSION

View File

@ -20,9 +20,18 @@ from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents
from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token,
TokenIntents,
User,
default_token_duration,
token_expires_from_timedelta,
)
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict
from authentik.lib.utils.time import timedelta_from_string
from authentik.rbac.decorators import permission_required
@ -49,6 +58,30 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
attrs.setdefault("intent", TokenIntents.INTENT_API)
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
raise ValidationError({"intent": f"Invalid intent {attrs.get('intent')}"})
if attrs.get("intent") == TokenIntents.INTENT_APP_PASSWORD:
# user IS in attrs
user: User = attrs.get("user")
max_token_lifetime = user.group_attributes(request).get(
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
)
max_token_lifetime_dt = default_token_duration()
if max_token_lifetime is not None:
try:
max_token_lifetime_dt = timedelta_from_string(max_token_lifetime)
except ValueError:
max_token_lifetime_dt = default_token_duration()
if "expires" in attrs and attrs.get("expires") > token_expires_from_timedelta(
max_token_lifetime_dt
):
raise ValidationError(
{"expires": f"Token expires exceeds maximum lifetime ({max_token_lifetime})."}
)
elif attrs.get("intent") == TokenIntents.INTENT_API:
# For API tokens, expires cannot be overridden
attrs["expires"] = default_token_duration()
return attrs
class Meta:

View File

@ -5,6 +5,7 @@ from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.core.models
from authentik.lib.generators import generate_id
def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
@ -16,6 +17,10 @@ def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
token.save()
def default_token_key():
return generate_id(60)
class Migration(migrations.Migration):
replaces = [
("authentik_core", "0012_auto_20201003_1737"),
@ -62,7 +67,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name="token",
name="key",
field=models.TextField(default=authentik.core.models.default_token_key),
field=models.TextField(default=default_token_key),
),
migrations.AlterUniqueTogether(
name="token",

View File

@ -0,0 +1,31 @@
# Generated by Django 5.0.2 on 2024-02-29 10:15
from django.db import migrations, models
import authentik.core.models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0033_alter_user_options"),
("authentik_tenants", "0002_tenant_default_token_duration_and_more"),
]
operations = [
migrations.AlterField(
model_name="authenticatedsession",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
migrations.AlterField(
model_name="token",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
migrations.AlterField(
model_name="token",
name="key",
field=models.TextField(default=authentik.core.models.default_token_key),
),
]

View File

@ -1,6 +1,6 @@
"""authentik core models"""
from datetime import timedelta
from datetime import datetime, timedelta
from hashlib import sha256
from typing import Any, Optional, Self
from uuid import uuid4
@ -25,15 +25,16 @@ from authentik.blueprints.models import ManagedModel
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
from authentik.lib.models import (
CreatedUpdatedModel,
DomainlessFormattedURLValidator,
SerializerModel,
)
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import PolicyBindingModel
from authentik.tenants.utils import get_unique_identifier
from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGTH
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
@ -42,13 +43,13 @@ USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME = "goauthentik.io/user/token-maximum-lifetime" # nosec
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
# used_by API that allows models to specify if they shadow an object
# for example the proxy provider which is built on top of an oauth provider
@ -59,16 +60,33 @@ options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
)
def default_token_duration():
def default_token_duration() -> datetime:
"""Default duration a Token is valid"""
return now() + timedelta(minutes=30)
current_tenant = get_current_tenant()
token_duration = (
current_tenant.default_token_duration
if hasattr(current_tenant, "default_token_duration")
else DEFAULT_TOKEN_DURATION
)
return now() + timedelta_from_string(token_duration)
def default_token_key():
def token_expires_from_timedelta(dt: timedelta) -> datetime:
"""Return a `datetime.datetime` object with the duration of the Token"""
return now() + dt
def default_token_key() -> str:
"""Default token key"""
current_tenant = get_current_tenant()
token_length = (
current_tenant.default_token_length
if hasattr(current_tenant, "default_token_length")
else DEFAULT_TOKEN_LENGTH
)
# We use generate_id since the chars in the key should be easy
# to use in Emails (for verification) and URLs (for recovery)
return generate_id(CONFIG.get_int("default_token_length"))
return generate_id(token_length)
class UserTypes(models.TextChoices):
@ -627,7 +645,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
class ExpiringModel(models.Model):
"""Base Model which can expire, and is automatically cleaned up."""
expires = models.DateTimeField(default=default_token_duration)
expires = models.DateTimeField(default=None, null=True)
expiring = models.BooleanField(default=True)
class Meta:

View File

@ -10,7 +10,14 @@ from django.dispatch import receiver
from django.http.request import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider, User
from authentik.core.models import (
Application,
AuthenticatedSession,
BackchannelProvider,
ExpiringModel,
User,
default_token_duration,
)
# Arguments: user: User, password: str
password_changed = Signal()
@ -61,3 +68,12 @@ def backchannel_provider_pre_save(sender: type[Model], instance: Model, **_):
if not isinstance(instance, BackchannelProvider):
return
instance.is_backchannel = True
@receiver(pre_save)
def expiring_model_pre_save(sender: type[Model], instance: Model, **_):
"""Ensure expires is set on ExpiringModels that are set to expire"""
if not issubclass(sender, ExpiringModel):
return
if instance.expiring and instance.expires is None:
instance.expires = default_token_duration()

View File

@ -1,5 +1,6 @@
"""Test token API"""
from datetime import datetime, timedelta
from json import loads
from django.urls.base import reverse
@ -7,7 +8,13 @@ from guardian.shortcuts import get_anonymous_user
from rest_framework.test import APITestCase
from authentik.core.api.tokens import TokenSerializer
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token,
TokenIntents,
User,
)
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
@ -76,6 +83,77 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, False)
def test_token_create_expiring(self):
"""Test token creation endpoint"""
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = True
self.user.save()
response = self.client.post(
reverse("authentik_api:token-list"), {"identifier": "test-token"}
)
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier="test-token")
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
def test_token_create_expiring_custom_ok(self):
"""Test token creation endpoint"""
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = True
self.user.attributes[USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME] = "hours=2"
self.user.save()
expires = datetime.now() + timedelta(hours=1)
response = self.client.post(
reverse("authentik_api:token-list"),
{
"identifier": "test-token",
"expires": expires,
"intent": TokenIntents.INTENT_APP_PASSWORD,
},
)
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier="test-token")
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_APP_PASSWORD)
self.assertEqual(token.expiring, True)
self.assertEqual(token.expires.timestamp(), expires.timestamp())
def test_token_create_expiring_custom_nok(self):
"""Test token creation endpoint"""
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = True
self.user.attributes[USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME] = "hours=2"
self.user.save()
expires = datetime.now() + timedelta(hours=3)
response = self.client.post(
reverse("authentik_api:token-list"),
{
"identifier": "test-token",
"expires": expires,
"intent": TokenIntents.INTENT_APP_PASSWORD,
},
)
self.assertEqual(response.status_code, 400)
def test_token_create_expiring_custom_api(self):
"""Test token creation endpoint"""
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = True
self.user.attributes[USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME] = "hours=2"
self.user.save()
expires = datetime.now() + timedelta(seconds=3)
response = self.client.post(
reverse("authentik_api:token-list"),
{
"identifier": "test-token",
"expires": expires,
"intent": TokenIntents.INTENT_API,
},
)
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier="test-token")
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
def test_list(self):
"""Test Token List (Test normal authentication)"""
Token.objects.all().delete()

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-02-29 10:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_rac", "0001_squashed_0003_alter_connectiontoken_options_and_more"),
]
operations = [
migrations.AlterField(
model_name="connectiontoken",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.0.2 on 2024-02-29 10:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_events",
"0004_systemtask_squashed_0005_remove_systemtask_finish_timestamp_and_more",
),
]
operations = [
migrations.AlterField(
model_name="systemtask",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
]

View File

@ -110,7 +110,6 @@ events:
asn: "/geoip/GeoLite2-ASN.mmdb"
cert_discovery_dir: /certs
default_token_length: 60
tenants:
enabled: false

View File

@ -8,6 +8,7 @@ from django.http import HttpRequest
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from authentik.core.models import default_token_duration
from authentik.events.signals import get_login_event
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.constants import (
@ -87,7 +88,9 @@ class IDToken:
) -> "IDToken":
"""Create ID Token"""
id_token = IDToken(provider, token, **kwargs)
id_token.exp = int(token.expires.timestamp())
id_token.exp = int(
(token.expires if token.expires is not None else default_token_duration()).timestamp()
)
id_token.iss = provider.get_issuer(request)
id_token.aud = provider.client_id
id_token.claims = {}

View File

@ -0,0 +1,36 @@
# Generated by Django 5.0.2 on 2024-02-29 10:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_oauth2",
"0017_accesstoken_session_id_authorizationcode_session_id_and_more",
),
]
operations = [
migrations.AlterField(
model_name="accesstoken",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
migrations.AlterField(
model_name="authorizationcode",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
migrations.AlterField(
model_name="devicetoken",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
migrations.AlterField(
model_name="refreshtoken",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
]

View File

@ -130,7 +130,13 @@ class OAuthSourceSerializer(SourceSerializer):
"oidc_jwks_url",
"oidc_jwks",
]
extra_kwargs = {"consumer_secret": {"write_only": True}}
extra_kwargs = {
"consumer_secret": {"write_only": True},
"request_token_url": {"allow_blank": True},
"authorization_url": {"allow_blank": True},
"access_token_url": {"allow_blank": True},
"profile_url": {"allow_blank": True},
}
class OAuthSourceFilter(FilterSet):

View File

@ -7,11 +7,16 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.flows.models import NotConfiguredAction
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer
class AuthenticatorValidateStageSerializer(StageSerializer):
"""AuthenticatorValidateStage Serializer"""
webauthn_allowed_device_types_obj = WebAuthnDeviceTypeSerializer(
source="webauthn_allowed_device_types", many=True, read_only=True
)
def validate_not_configured_action(self, value):
"""Ensure that a configuration stage is set when not_configured_action is configure"""
configuration_stages = self.initial_data.get("configuration_stages", None)
@ -31,6 +36,8 @@ class AuthenticatorValidateStageSerializer(StageSerializer):
"configuration_stages",
"last_auth_threshold",
"webauthn_user_verification",
"webauthn_allowed_device_types",
"webauthn_allowed_device_types_obj",
]

View File

@ -14,8 +14,9 @@ from structlog.stdlib import get_logger
from webauthn import options_to_json
from webauthn.authentication.generate_authentication_options import generate_authentication_options
from webauthn.authentication.verify_authentication_response import verify_authentication_response
from webauthn.helpers import parse_authentication_credential_json
from webauthn.helpers.base64url_to_bytes import base64url_to_bytes
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from webauthn.helpers.exceptions import InvalidAuthenticationResponse, InvalidJSONStructure
from webauthn.helpers.structs import UserVerificationRequirement
from authentik.core.api.utils import JSONDictField, PassiveSerializer
@ -131,23 +132,40 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
"""Validate WebAuthn Challenge"""
request = stage_view.request
challenge = request.session.get(SESSION_KEY_WEBAUTHN_CHALLENGE)
credential_id = data.get("id")
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
try:
credential = parse_authentication_credential_json(data)
except InvalidJSONStructure as exc:
LOGGER.warning("Invalid WebAuthn challenge response", exc=exc)
raise ValidationError("Invalid device", "invalid") from None
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
device = WebAuthnDevice.objects.filter(credential_id=credential.id).first()
if not device:
raise ValidationError("Invalid device")
raise ValidationError("Invalid device", "invalid")
# We can only check the device's user if the user we're given isn't anonymous
# as this validation is also used for password-less login where webauthn is the very first
# step done by a user. Only if this validation happens at a later stage we can check
# that the device belongs to the user
if not user.is_anonymous and device.user != user:
raise ValidationError("Invalid device")
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
raise ValidationError("Invalid device", "invalid")
# When a device_type was set when creating the device (2024.4+), and we have a limitation,
# make sure the device type is allowed.
if (
device.device_type
and stage.webauthn_allowed_device_types.exists()
and not stage.webauthn_allowed_device_types.filter(pk=device.device_type.pk).exists()
):
raise ValidationError(
_(
"Invalid device type. Contact your {brand} administrator for help.".format(
brand=stage_view.request.brand.branding_title
)
),
"invalid",
)
try:
authentication_verification = verify_authentication_response(
credential=data,
credential=credential,
expected_challenge=challenge,
expected_rp_id=get_rp_id(request),
expected_origin=get_origin(request),

View File

@ -0,0 +1,27 @@
# Generated by Django 5.0.3 on 2024-04-08 18:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_stages_authenticator_validate",
"0012_authenticatorvalidatestage_webauthn_user_verification",
),
(
"authentik_stages_authenticator_webauthn",
"0010_webauthndevicetype_authenticatorwebauthnstage_and_more",
),
]
operations = [
migrations.AddField(
model_name="authenticatorvalidatestage",
name="webauthn_allowed_device_types",
field=models.ManyToManyField(
blank=True, to="authentik_stages_authenticator_webauthn.webauthndevicetype"
),
),
]

View File

@ -71,6 +71,9 @@ class AuthenticatorValidateStage(Stage):
choices=UserVerification.choices,
default=UserVerification.PREFERRED,
)
webauthn_allowed_device_types = models.ManyToManyField(
"authentik_stages_authenticator_webauthn.WebAuthnDeviceType", blank=True
)
@property
def serializer(self) -> type[BaseSerializer]:

View File

@ -5,6 +5,7 @@ from hashlib import sha256
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext_lazy as _
from jwt import PyJWTError, decode, encode
from rest_framework.fields import CharField, IntegerField, ListField, UUIDField
from rest_framework.serializers import ValidationError
@ -176,15 +177,30 @@ class AuthenticatorValidateStageView(ChallengeStageView):
threshold = timedelta_from_string(stage.last_auth_threshold)
allowed_devices = []
has_webauthn_filters_set = stage.webauthn_allowed_device_types.exists()
for device in user_devices:
device_class = device.__class__.__name__.lower().replace("device", "")
if device_class not in stage.device_classes:
self.logger.debug("device class not allowed", device_class=device_class)
continue
if isinstance(device, SMSDevice) and device.is_hashed:
self.logger.debug("Hashed SMS device, skipping")
self.logger.debug("Hashed SMS device, skipping", device=device)
continue
allowed_devices.append(device)
# Ignore WebAuthn devices which are not in the allowed types
if (
isinstance(device, WebAuthnDevice)
and device.device_type
and has_webauthn_filters_set
):
if not stage.webauthn_allowed_device_types.filter(
pk=device.device_type.pk
).exists():
self.logger.debug(
"WebAuthn device type not allowed", device=device, type=device.device_type
)
continue
# Ensure only one challenge per device class
# WebAuthn does another device loop to find all WebAuthn devices
if device_class in seen_classes:
@ -251,7 +267,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
return self.executor.stage_ok()
if stage.not_configured_action == NotConfiguredAction.DENY:
self.logger.debug("Authenticator not configured, denying")
return self.executor.stage_invalid()
return self.executor.stage_invalid(_("No (allowed) MFA authenticator configured."))
if stage.not_configured_action == NotConfiguredAction.CONFIGURE:
self.logger.debug("Authenticator not configured, forcing configure")
return self.prepare_stages(user)

View File

@ -26,8 +26,16 @@ from authentik.stages.authenticator_validate.stage import (
PLAN_CONTEXT_DEVICE_CHALLENGES,
AuthenticatorValidateStageView,
)
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
from authentik.stages.authenticator_webauthn.models import (
UserVerification,
WebAuthnDevice,
WebAuthnDeviceType,
)
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.tasks import (
webauthn_aaguid_import,
webauthn_mds_import,
)
from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.user_login.models import UserLoginStage
@ -120,7 +128,56 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
{}, StageView(FlowExecutorView(current_stage=stage), request=request), self.user
)
def test_get_challenge(self):
def test_device_challenge_webauthn_restricted(self):
"""Test webauthn (getting device challenges with a webauthn
device that is not allowed due to aaguid restrictions)"""
webauthn_mds_import(force=True)
webauthn_aaguid_import()
request = get_request("/")
request.user = self.user
WebAuthnDevice.objects.create(
user=self.user,
public_key=bytes_to_base64url(b"qwerqwerqre"),
credential_id=bytes_to_base64url(b"foobarbaz"),
sign_count=0,
rp_id=generate_id(),
device_type=WebAuthnDeviceType.objects.get(
aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a"
),
)
flow = create_test_flow()
stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
last_auth_threshold="milliseconds=0",
not_configured_action=NotConfiguredAction.DENY,
device_classes=[DeviceClasses.WEBAUTHN],
webauthn_user_verification=UserVerification.PREFERRED,
)
stage.webauthn_allowed_device_types.set(
WebAuthnDeviceType.objects.filter(
description="Android Authenticator with SafetyNet Attestation"
)
)
session = self.client.session
plan = FlowPlan(flow_pk=flow.pk.hex)
plan.append_stage(stage)
plan.append_stage(UserLoginStage(name=generate_id()))
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session[SESSION_KEY_PLAN] = plan
session.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertStageResponse(
response,
flow,
component="ak-stage-access-denied",
error_message="No (allowed) MFA authenticator configured.",
)
def test_raw_get_challenge(self):
"""Test webauthn"""
request = get_request("/")
request.user = self.user
@ -190,17 +247,21 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
},
)
def test_validate_challenge(self):
"""Test webauthn"""
def test_validate_challenge_unrestricted(self):
"""Test webauthn authentication (unrestricted webauthn device)"""
webauthn_mds_import(force=True)
webauthn_aaguid_import()
device = WebAuthnDevice.objects.create(
user=self.user,
public_key=(
"pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J"
"H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU"
"pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo"
),
credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
sign_count=4,
credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
sign_count=2,
rp_id=generate_id(),
device_type=WebAuthnDeviceType.objects.get(
aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a"
),
)
flow = create_test_flow()
stage = AuthenticatorValidateStage.objects.create(
@ -222,7 +283,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
)
session.save()
@ -230,24 +291,23 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
data={
"webauthn": {
"id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
"rawId": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
"id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
"rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
"type": "public-key",
"assertionClientExtensions": "{}",
"response": {
"clientDataJSON": (
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2WlhvN"
"Wx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGczQW"
"dPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWx"
"ob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2Jl"
"X2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2Fpb"
"nN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ=="
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN"
"mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV"
"k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1"
"jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj"
"cm9zc09yaWdpbiI6ZmFsc2V9"
),
"signature": (
"MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI"
"AiAqtOK6mIZPk62kZN0OzFsHfuvu_RlOl7zlqSNzDdz_Ag=="
"MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl"
"rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ=="
),
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==",
"userHandle": None,
},
},
@ -261,6 +321,96 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
def test_validate_challenge_restricted(self):
"""Test webauthn authentication (restricted device type, failure)"""
webauthn_mds_import(force=True)
webauthn_aaguid_import()
device = WebAuthnDevice.objects.create(
user=self.user,
public_key=(
"pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo"
),
credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
sign_count=2,
rp_id=generate_id(),
device_type=WebAuthnDeviceType.objects.get(
aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a"
),
)
flow = create_test_flow()
stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.WEBAUTHN],
)
stage.webauthn_allowed_device_types.set(
WebAuthnDeviceType.objects.filter(
description="Android Authenticator with SafetyNet Attestation"
)
)
session = self.client.session
plan = FlowPlan(flow_pk=flow.pk.hex)
plan.append_stage(stage)
plan.append_stage(UserLoginStage(name=generate_id()))
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
{
"device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk,
"challenge": {},
}
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
)
session.save()
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
data={
"webauthn": {
"id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
"rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
"type": "public-key",
"assertionClientExtensions": "{}",
"response": {
"clientDataJSON": (
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN"
"mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV"
"k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1"
"jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj"
"cm9zc09yaWdpbiI6ZmFsc2V9"
),
"signature": (
"MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl"
"rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ=="
),
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==",
"userHandle": None,
},
}
},
SERVER_NAME="localhost",
SERVER_PORT="9000",
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
flow,
component="ak-stage-authenticator-validate",
response_errors={
"webauthn": [
{
"string": (
"Invalid device type. Contact your authentik administrator for help."
),
"code": "invalid",
}
]
},
)
def test_validate_challenge_userless(self):
"""Test webauthn"""
device = WebAuthnDevice.objects.create(

View File

@ -126,6 +126,10 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
if authenticator_attachment:
authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment))
attestation = AttestationConveyancePreference.DIRECT
if stage.device_type_restrictions.exists():
attestation = AttestationConveyancePreference.ENTERPRISE
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request),
rp_name=self.request.brand.branding_title,
@ -137,7 +141,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
user_verification=UserVerificationRequirement(str(stage.user_verification)),
authenticator_attachment=authenticator_attachment,
),
attestation=AttestationConveyancePreference.DIRECT,
attestation=attestation,
)
self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-02-29 10:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_consent", "0005_alter_consentstage_mode"),
]
operations = [
migrations.AlterField(
model_name="userconsent",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-02-29 10:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_invitation", "0007_invitation_flow"),
]
operations = [
migrations.AlterField(
model_name="invitation",
name="expires",
field=models.DateTimeField(default=None, null=True),
),
]

View File

@ -23,6 +23,8 @@ class SettingsSerializer(ModelSerializer):
"footer_links",
"gdpr_compliance",
"impersonation",
"default_token_duration",
"default_token_length",
]

View File

@ -0,0 +1,35 @@
# Generated by Django 5.0.2 on 2024-02-20 08:26
import django.core.validators
from django.db import migrations, models
import authentik.lib.utils.time
from authentik.lib.config import CONFIG
class Migration(migrations.Migration):
dependencies = [
("authentik_tenants", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="tenant",
name="default_token_duration",
field=models.TextField(
default=CONFIG.get("default_token_duration", "minutes=30"),
help_text="Default token duration",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
migrations.AddField(
model_name="tenant",
name="default_token_length",
field=models.PositiveIntegerField(
default=CONFIG.get_int("default_token_length", 60),
help_text="Default token length",
validators=[django.core.validators.MinValueValidator(1)],
),
),
]

View File

@ -5,6 +5,7 @@ from uuid import uuid4
from django.apps import apps
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.db.utils import IntegrityError
from django.dispatch import receiver
@ -22,6 +23,9 @@ LOGGER = get_logger()
VALID_SCHEMA_NAME = re.compile(r"^t_[a-z0-9]{1,61}$")
DEFAULT_TOKEN_DURATION = "minutes=30" # nosec
DEFAULT_TOKEN_LENGTH = 60
def _validate_schema_name(name):
if not VALID_SCHEMA_NAME.match(name):
@ -81,6 +85,16 @@ class Tenant(TenantMixin, SerializerModel):
impersonation = models.BooleanField(
help_text=_("Globally enable/disable impersonation."), default=True
)
default_token_duration = models.TextField(
help_text=_("Default token duration"),
default=DEFAULT_TOKEN_DURATION,
validators=[timedelta_string_validator],
)
default_token_length = models.PositiveIntegerField(
help_text=_("Default token length"),
default=DEFAULT_TOKEN_LENGTH,
validators=[MinValueValidator(1)],
)
def save(self, *args, **kwargs):
if self.schema_name == "template":

View File

@ -4547,7 +4547,6 @@
"null"
],
"maxLength": 255,
"minLength": 1,
"title": "Request Token URL",
"description": "URL used to request the initial token. This URL is only required for OAuth 1."
},
@ -4557,7 +4556,6 @@
"null"
],
"maxLength": 255,
"minLength": 1,
"title": "Authorization URL",
"description": "URL the user is redirect to to conest the flow."
},
@ -4567,7 +4565,6 @@
"null"
],
"maxLength": 255,
"minLength": 1,
"title": "Access Token URL",
"description": "URL used by authentik to retrieve tokens."
},
@ -4577,7 +4574,6 @@
"null"
],
"maxLength": 255,
"minLength": 1,
"title": "Profile URL",
"description": "URL used by authentik to get user information."
},
@ -5641,6 +5637,14 @@
],
"title": "Webauthn user verification",
"description": "Enforce user verification for WebAuthn devices."
},
"webauthn_allowed_device_types": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Webauthn allowed device types"
}
},
"required": []
@ -5782,7 +5786,8 @@
"device_type_restrictions": {
"type": "array",
"items": {
"type": "integer"
"type": "string",
"format": "uuid"
},
"title": "Device type restrictions"
}
@ -6028,7 +6033,10 @@
"type": "object",
"properties": {
"expires": {
"type": "string",
"type": [
"string",
"null"
],
"format": "date-time",
"title": "Expires"
},
@ -6797,7 +6805,10 @@
"title": "Name"
},
"expires": {
"type": "string",
"type": [
"string",
"null"
],
"format": "date-time",
"title": "Expires"
},
@ -7988,7 +7999,10 @@
"title": "Description"
},
"expires": {
"type": "string",
"type": [
"string",
"null"
],
"format": "date-time",
"title": "Expires"
},

2
go.mod
View File

@ -30,7 +30,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024022.7
goauthentik.io/api/v3 v3.2024022.8
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.19.0
golang.org/x/sync v0.7.0

4
go.sum
View File

@ -294,8 +294,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.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024022.7 h1:VR9OmcZvTzPSjit2Dx2EoHrLc9v9XRyjPXNpnGISWWM=
goauthentik.io/api/v3 v3.2024022.7/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024022.8 h1:bHKUZgQXf4/qjL4VkITc/HK0pjMtX9X5Dlob8yTg7K4=
goauthentik.io/api/v3 v3.2024022.8/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=

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-01 23:02+0000\n"
"POT-Creation-Date: 2024-04-09 00:08+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -2287,6 +2287,19 @@ msgstr "WebAuthn 设备"
msgid "WebAuthn Devices"
msgstr "WebAuthn 设备"
#: authentik/stages/authenticator_webauthn/models.py
msgid "WebAuthn Device type"
msgstr "WebAuthn 设备类型"
#: authentik/stages/authenticator_webauthn/models.py
msgid "WebAuthn Device types"
msgstr "WebAuthn 设备类型"
#: authentik/stages/authenticator_webauthn/stage.py
#, python-brace-format
msgid "Invalid device type. Contact your {brand} administrator for help."
msgstr "无效的设备类型。请联系您的 {brand} 管理员获得帮助。"
#: authentik/stages/captcha/models.py
msgid "Public key, acquired your captcha Provider."
msgstr "公钥,从您的验证码提供商处取得。"

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-01 23:02+0000\n"
"POT-Creation-Date: 2024-04-09 00:08+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -2287,6 +2287,19 @@ msgstr "WebAuthn 设备"
msgid "WebAuthn Devices"
msgstr "WebAuthn 设备"
#: authentik/stages/authenticator_webauthn/models.py
msgid "WebAuthn Device type"
msgstr "WebAuthn 设备类型"
#: authentik/stages/authenticator_webauthn/models.py
msgid "WebAuthn Device types"
msgstr "WebAuthn 设备类型"
#: authentik/stages/authenticator_webauthn/stage.py
#, python-brace-format
msgid "Invalid device type. Contact your {brand} administrator for help."
msgstr "无效的设备类型。请联系您的 {brand} 管理员获得帮助。"
#: authentik/stages/captcha/models.py
msgid "Public key, acquired your captcha Provider."
msgstr "公钥,从您的验证码提供商处取得。"

6
poetry.lock generated
View File

@ -3585,13 +3585,13 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
[[package]]
name = "sentry-sdk"
version = "1.44.1"
version = "1.45.0"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = "*"
files = [
{file = "sentry-sdk-1.44.1.tar.gz", hash = "sha256:24e6a53eeabffd2f95d952aa35ca52f0f4201d17f820ac9d3ff7244c665aaf68"},
{file = "sentry_sdk-1.44.1-py2.py3-none-any.whl", hash = "sha256:5f75eb91d8ab6037c754a87b8501cc581b2827e923682f593bed3539ce5b3999"},
{file = "sentry-sdk-1.45.0.tar.gz", hash = "sha256:509aa9678c0512344ca886281766c2e538682f8acfa50fd8d405f8c417ad0625"},
{file = "sentry_sdk-1.45.0-py2.py3-none-any.whl", hash = "sha256:1ce29e30240cc289a027011103a8c83885b15ef2f316a60bcc7c5300afa144f1"},
]
[package.dependencies]

View File

@ -29976,6 +29976,7 @@ components:
expires:
type: string
format: date-time
nullable: true
required:
- asn
- current
@ -30619,6 +30620,16 @@ components:
allOf:
- $ref: '#/components/schemas/UserVerificationEnum'
description: Enforce user verification for WebAuthn devices.
webauthn_allowed_device_types:
type: array
items:
type: string
format: uuid
webauthn_allowed_device_types_obj:
type: array
items:
$ref: '#/components/schemas/WebAuthnDeviceType'
readOnly: true
required:
- component
- meta_model_name
@ -30626,6 +30637,7 @@ components:
- pk
- verbose_name
- verbose_name_plural
- webauthn_allowed_device_types_obj
AuthenticatorValidateStageRequest:
type: object
description: AuthenticatorValidateStage Serializer
@ -30661,6 +30673,11 @@ components:
allOf:
- $ref: '#/components/schemas/UserVerificationEnum'
description: Enforce user verification for WebAuthn devices.
webauthn_allowed_device_types:
type: array
items:
type: string
format: uuid
required:
- name
AuthenticatorValidationChallenge:
@ -32674,6 +32691,7 @@ components:
expires:
type: string
format: date-time
nullable: true
scope:
type: array
items:
@ -33774,6 +33792,7 @@ components:
expires:
type: string
format: date-time
nullable: true
fixed_data:
type: object
additionalProperties: {}
@ -33810,6 +33829,7 @@ components:
expires:
type: string
format: date-time
nullable: true
fixed_data:
type: object
additionalProperties: {}
@ -35551,26 +35571,22 @@ components:
request_token_url:
type: string
nullable: true
minLength: 1
description: URL used to request the initial token. This URL is only required
for OAuth 1.
maxLength: 255
authorization_url:
type: string
nullable: true
minLength: 1
description: URL the user is redirect to to conest the flow.
maxLength: 255
access_token_url:
type: string
nullable: true
minLength: 1
description: URL used by authentik to retrieve tokens.
maxLength: 255
profile_url:
type: string
nullable: true
minLength: 1
description: URL used by authentik to get user information.
maxLength: 255
consumer_key:
@ -37536,6 +37552,11 @@ components:
allOf:
- $ref: '#/components/schemas/UserVerificationEnum'
description: Enforce user verification for WebAuthn devices.
webauthn_allowed_device_types:
type: array
items:
type: string
format: uuid
PatchedAuthenticatorWebAuthnStageRequest:
type: object
description: AuthenticatorWebAuthnStage Serializer
@ -38125,6 +38146,7 @@ components:
expires:
type: string
format: date-time
nullable: true
fixed_data:
type: object
additionalProperties: {}
@ -38563,26 +38585,22 @@ components:
request_token_url:
type: string
nullable: true
minLength: 1
description: URL used to request the initial token. This URL is only required
for OAuth 1.
maxLength: 255
authorization_url:
type: string
nullable: true
minLength: 1
description: URL the user is redirect to to conest the flow.
maxLength: 255
access_token_url:
type: string
nullable: true
minLength: 1
description: URL used by authentik to retrieve tokens.
maxLength: 255
profile_url:
type: string
nullable: true
minLength: 1
description: URL used by authentik to get user information.
maxLength: 255
consumer_key:
@ -39429,6 +39447,15 @@ components:
impersonation:
type: boolean
description: Globally enable/disable impersonation.
default_token_duration:
type: string
minLength: 1
description: Default token duration
default_token_length:
type: integer
maximum: 2147483647
minimum: 1
description: Default token length
PatchedSourceStageRequest:
type: object
description: SourceStage Serializer
@ -39506,6 +39533,7 @@ components:
expires:
type: string
format: date-time
nullable: true
expiring:
type: boolean
PatchedUserDeleteStageRequest:
@ -42458,6 +42486,14 @@ components:
impersonation:
type: boolean
description: Globally enable/disable impersonation.
default_token_duration:
type: string
description: Default token duration
default_token_length:
type: integer
maximum: 2147483647
minimum: 1
description: Default token length
SettingsRequest:
type: object
description: Settings Serializer
@ -42489,6 +42525,15 @@ components:
impersonation:
type: boolean
description: Globally enable/disable impersonation.
default_token_duration:
type: string
minLength: 1
description: Default token duration
default_token_length:
type: integer
maximum: 2147483647
minimum: 1
description: Default token length
SeverityEnum:
enum:
- notice
@ -43156,6 +43201,7 @@ components:
expires:
type: string
format: date-time
nullable: true
expiring:
type: boolean
required:
@ -43181,6 +43227,7 @@ components:
expires:
type: string
format: date-time
nullable: true
scope:
type: array
items:
@ -43225,6 +43272,7 @@ components:
expires:
type: string
format: date-time
nullable: true
expiring:
type: boolean
required:
@ -43463,6 +43511,7 @@ components:
expires:
type: string
format: date-time
nullable: true
expiring:
type: boolean
user:

View File

@ -6,7 +6,7 @@
"": {
"name": "@goauthentik/web-tests",
"dependencies": {
"chromedriver": "^123.0.2"
"chromedriver": "^123.0.3"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
@ -22,7 +22,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.4",
"typescript": "^5.4.5",
"wdio-wait-for": "^3.0.11"
},
"engines": {
@ -2084,9 +2084,9 @@
}
},
"node_modules/chromedriver": {
"version": "123.0.2",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.2.tgz",
"integrity": "sha512-Kx0r/IGULm7eciaUtX/OKaFbdBdHRDSguiV1Q4zuQncz11gvymDdMtELa7ppk+kTL5113NLPud92nuIMNTRhww==",
"version": "123.0.3",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.3.tgz",
"integrity": "sha512-35IeTqDLcVR0htF9nD/Lh+g24EG088WHVKXBXiFyWq+2lelnoM0B3tKTBiUEjLng0GnELI4QyQPFK7i97Fz1fQ==",
"hasInstallScript": true,
"dependencies": {
"@testim/chrome-version": "^1.1.4",
@ -8620,9 +8620,9 @@
}
},
"node_modules/typescript": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",

View File

@ -16,7 +16,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.4.4",
"typescript": "^5.4.5",
"wdio-wait-for": "^3.0.11"
},
"scripts": {
@ -32,6 +32,6 @@
"node": ">=20"
},
"dependencies": {
"chromedriver": "^123.0.2"
"chromedriver": "^123.0.3"
}
}

16
web/package-lock.json generated
View File

@ -17,7 +17,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.2.2-1712571709",
"@goauthentik/api": "^2024.2.2-1712833826",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.0",
"@lit/localize": "^0.12.1",
@ -101,7 +101,7 @@
"ts-lit-plugin": "^2.0.2",
"tslib": "^2.6.2",
"turnstile-types": "^1.2.0",
"typescript": "^5.4.4",
"typescript": "^5.4.5",
"vite-tsconfig-paths": "^4.3.2"
},
"engines": {
@ -2840,9 +2840,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2024.2.2-1712571709",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.2.2-1712571709.tgz",
"integrity": "sha512-+uS+d13aCDC7W3bZk8j3RnqvDq8iivXnP98GHFEoB9pUuMJ1LK7sgJwr2JHmJe5KiFMl0oxycY8VutsBnYmjog=="
"version": "2024.2.2-1712833826",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.2.2-1712833826.tgz",
"integrity": "sha512-0DmJ/GqGvj2mQ3IuA8YBERbj/E1o1HzS+wYIAbZzGAydocKH9g7Hn9h47eDU2lTgUu1N1DDOvYkXAsdqCFVwDQ=="
},
"node_modules/@hcaptcha/types": {
"version": "1.0.3",
@ -17129,9 +17129,9 @@
}
},
"node_modules/typescript": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",

View File

@ -38,7 +38,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.2.2-1712571709",
"@goauthentik/api": "^2024.2.2-1712833826",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.0",
"@lit/localize": "^0.12.1",
@ -122,7 +122,7 @@
"ts-lit-plugin": "^2.0.2",
"tslib": "^2.6.2",
"turnstile-types": "^1.2.0",
"typescript": "^5.4.4",
"typescript": "^5.4.5",
"vite-tsconfig-paths": "^4.3.2"
},
"optionalDependencies": {

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/CodeMirror";
@ -192,6 +193,24 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
help=${msg("Globally enable/disable impersonation.")}
>
</ak-switch-input>
<ak-text-input
name="defaultTokenDuration"
label=${msg("Default token duration")}
required
value="${ifDefined(this._settings?.defaultTokenDuration)}"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Default duration for generated tokens")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-number-input
label=${msg("Default token length")}
required
name="defaultTokenLength"
value="${first(this._settings?.defaultTokenLength, 60)}"
help=${msg("Default length of generated tokens")}
></ak-number-input>
`;
}
}

View File

@ -575,7 +575,11 @@ export class SAMLProviderViewPage extends AKElement {
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ul class="pf-c-list"></ul>
<ul class="pf-c-list">
${attr.Value.map((value) => {
return html` <li><pre>${value}</pre></li> `;
})}
</ul>
</div>
</dd>
</div>`;

View File

@ -108,7 +108,6 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authorization URL")}
?required=${true}
name="authorizationUrl"
>
<input
@ -119,17 +118,12 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
"",
)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("URL the user is redirect to to consent the authorization.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Access token URL")}
?required=${true}
name="accessTokenUrl"
>
<ak-form-element-horizontal label=${msg("Access token URL")} name="accessTokenUrl">
<input
type="text"
value="${first(
@ -138,17 +132,12 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
"",
)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("URL used by authentik to retrieve tokens.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Profile URL")}
?required=${true}
name="profileUrl"
>
<ak-form-element-horizontal label=${msg("Profile URL")} name="profileUrl">
<input
type="text"
value="${first(
@ -157,7 +146,6 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
"",
)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("URL used by authentik to get user information.")}

View File

@ -1,5 +1,9 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -71,7 +75,8 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
[DeviceClassesEnum.Sms, msg("SMS-based Authenticators")],
];
return html` <span>
return html`
<span>
${msg(
"Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.",
)}
@ -119,7 +124,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
/>
<p class="pf-c-form__helper-text">
${msg(
"If any of the devices user of the types selected above have been used within this duration, this stage will be skipped.",
"If the user has successfully authenticated with a device in the classes listed above within this configured duration, this stage will be skipped.",
)}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
@ -166,33 +171,6 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("WebAuthn User verification")}
?required=${true}
name="webauthnUserVerification"
>
<ak-radio
.options=${[
{
label: msg("User verification must occur."),
value: UserVerificationEnum.Required,
default: true,
},
{
label: msg(
"User verification is preferred if available, but not required.",
),
value: UserVerificationEnum.Preferred,
},
{
label: msg("User verification should not occur."),
value: UserVerificationEnum.Discouraged,
},
]}
.value=${this.instance?.webauthnUserVerification}
>
</ak-radio>
</ak-form-element-horizontal>
${this.showConfigurationStages
? html`
<ak-form-element-horizontal
@ -228,6 +206,77 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
`
: html``}
</div>
</ak-form-group>`;
</ak-form-group>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("WebAuthn-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("WebAuthn User verification")}
?required=${true}
name="webauthnUserVerification"
>
<ak-radio
.options=${[
{
label: msg("User verification must occur."),
value: UserVerificationEnum.Required,
default: true,
},
{
label: msg(
"User verification is preferred if available, but not required.",
),
value: UserVerificationEnum.Preferred,
},
{
label: msg("User verification should not occur."),
value: UserVerificationEnum.Discouraged,
},
]}
.value=${this.instance?.webauthnUserVerification}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("WebAuthn Device type restrictions")}
name="webauthnAllowedDeviceTypes"
>
<ak-dual-select-provider
.provider=${(page: number, search?: string): Promise<DataProvision> => {
return new StagesApi(DEFAULT_CONFIG)
.stagesAuthenticatorWebauthnDeviceTypesList({
page: page,
search: search,
})
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(deviceTypeRestrictionPair),
};
});
}}
.selected=${(this.instance?.webauthnAllowedDeviceTypesObj ?? []).map(
deviceTypeRestrictionPair,
)}
available-label="${msg("Available Device types")}"
selected-label="${msg("Selected Device types")}"
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed.",
)}
</p>
<ak-alert ?inline=${true}>
${
/* TODO: Remove this after 2024.6..or maybe later? */
msg(
"This restriction only applies to devices created in authentik 2024.4 or later.",
)
}
</ak-alert>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}
}

View File

@ -1,9 +1,7 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import {
DataProvision,
DualSelectPair,
} from "@goauthentik/authentik/elements/ak-dual-select/types";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DataProvision } from "@goauthentik/authentik/elements/ak-dual-select/types";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
@ -25,23 +23,12 @@ import {
ResidentKeyRequirementEnum,
StagesApi,
UserVerificationEnum,
WebAuthnDeviceType,
} from "@goauthentik/api";
@customElement("ak-stage-authenticator-webauthn-form")
export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorWebAuthnStage> {
deviceTypeRestrictionPair(item: WebAuthnDeviceType): DualSelectPair {
const label = item.description ? item.description : item.aaguid;
return [
item.aaguid,
html`<div class="selection-main">${label}</div>
<div class="selection-desc">${item.aaguid}</div>`,
label,
];
}
loadInstance(pk: string): Promise<AuthenticatorWebAuthnStage> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnRetrieve({
async loadInstance(pk: string): Promise<AuthenticatorWebAuthnStage> {
return await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnRetrieve({
stageUuid: pk,
});
}
@ -194,14 +181,12 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(
this.deviceTypeRestrictionPair,
),
options: results.results.map(deviceTypeRestrictionPair),
};
});
}}
.selected=${(this.instance?.deviceTypeRestrictionsObj ?? []).map(
this.deviceTypeRestrictionPair,
deviceTypeRestrictionPair,
)}
available-label="${msg("Available Device types")}"
selected-label="${msg("Selected Device types")}"

View File

@ -0,0 +1,15 @@
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import { html } from "lit";
import { WebAuthnDeviceType } from "@goauthentik/api";
export function deviceTypeRestrictionPair(item: WebAuthnDeviceType): DualSelectPair {
const label = item.description ? item.description : item.aaguid;
return [
item.aaguid,
html`<div class="selection-main">${label}</div>
<div class="selection-desc">${item.aaguid}</div>`,
label,
];
}

View File

@ -55,8 +55,6 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
private pagination?: Pagination;
selectedMap: WeakMap<DataProvider, DualSelectPair[]> = new WeakMap();
constructor() {
super();
setTimeout(() => this.fetch(1), 0);
@ -72,16 +70,14 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("searchDelay")) {
this.doSearch = debounce(this.doSearch.bind(this), this.searchDelay);
this.doSearch = debounce(
AkDualSelectProvider.prototype.doSearch.bind(this),
this.searchDelay,
);
}
if (changedProperties.has("provider")) {
this.pagination = undefined;
const previousProvider = changedProperties.get("provider");
if (previousProvider) {
this.selectedMap.set(previousProvider, this.selected);
this.selected = this.selectedMap.get(this.provider) ?? [];
}
this.fetch();
}
}

View File

@ -28,6 +28,7 @@ export class UserTokenForm extends ModelForm<Token, string> {
async send(data: Token): Promise<Token> {
if (this.instance) {
data.intent = this.instance.intent;
return new CoreApi(DEFAULT_CONFIG).coreTokensUpdate({
identifier: this.instance.identifier,
tokenRequest: data,
@ -41,6 +42,14 @@ export class UserTokenForm extends ModelForm<Token, string> {
}
renderForm(): TemplateResult {
const now = new Date();
const expiringDate = this.instance?.expires
? new Date(
this.instance.expires.getTime() -
this.instance.expires.getTimezoneOffset() * 60000,
)
: new Date(now.getTime() + 30 * 60000 - now.getTimezoneOffset() * 60000);
return html` <ak-form-element-horizontal
label=${msg("Identifier")}
?required=${true}
@ -59,6 +68,16 @@ export class UserTokenForm extends ModelForm<Token, string> {
value="${ifDefined(this.instance?.description)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>`;
</ak-form-element-horizontal>
${this.intent == IntentEnum.AppPassword
? html`<ak-form-element-horizontal label=${msg("Expiring")} name="expires">
<input
type="datetime-local"
value="${expiringDate.toISOString().slice(0, -8)}"
min="${now.toISOString().slice(0, -8)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>`
: html``}`;
}
}

View File

@ -160,7 +160,11 @@ export class UserTokenList extends Table<Token> {
<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Token")} </span>
<ak-user-token-form slot="form" .instancePk=${item.identifier}>
<ak-user-token-form
intent=${item.intent ?? IntentEnum.Api}
slot="form"
.instancePk=${item.identifier}
>
</ak-user-token-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>

View File

@ -8526,15 +8526,19 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="sa64fb483becc9c2c">
<source>Device type restrictions</source>
<target>设备类型限制</target>
</trans-unit>
<trans-unit id="sbb928551c84cd63f">
<source>Available Device types</source>
<target>可用设备类型</target>
</trans-unit>
<trans-unit id="s6446c35d6b411e53">
<source>Selected Device types</source>
<target>已选设备类型</target>
</trans-unit>
<trans-unit id="s1f4df216b56de4ac">
<source>Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed.</source>
<target>可选的 WebAuthn 可用设备类型限制。如果未选择设备类型,则允许所有设备。</target>
</trans-unit>
</body>
</file>

View File

@ -8523,6 +8523,22 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sc7d071fb5cc1f6bf">
<source>A selection is required</source>
<target>需要进行选择</target>
</trans-unit>
<trans-unit id="sa64fb483becc9c2c">
<source>Device type restrictions</source>
<target>设备类型限制</target>
</trans-unit>
<trans-unit id="sbb928551c84cd63f">
<source>Available Device types</source>
<target>可用设备类型</target>
</trans-unit>
<trans-unit id="s6446c35d6b411e53">
<source>Selected Device types</source>
<target>已选设备类型</target>
</trans-unit>
<trans-unit id="s1f4df216b56de4ac">
<source>Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed.</source>
<target>可选的 WebAuthn 可用设备类型限制。如果未选择设备类型,则允许所有设备。</target>
</trans-unit>
</body>
</file>

View File

@ -12,7 +12,7 @@ For example, a standard login flow would consist of the following stages:
Upon flow execution, a plan containing all stages is generated. This means that all attached policies are evaluated upon execution. This behaviour can be altered by enabling the **Evaluate when stage is run** option on the binding.
To determine which flow is linked, authentik searches all flows with the required designation and chooses the first instance the current user has access to.
The determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../core/brands.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
## Permissions
@ -42,7 +42,7 @@ The authentication flow should always contain a [**User Login**](stages/user_log
This designates a flow to be used to invalidate a session.
This stage should always contain a [**User Logout**](stages/user_logout.md) stage, which resets the current session.
This flow should always contain a [**User Logout**](stages/user_logout.md) stage, which resets the current session.
#### Enrollment
@ -68,3 +68,13 @@ Flows can be imported and exported to share with other people, the community and
Download our [Example flows](./examples/flows.md) and then import them into your authentik instance.
Starting with authentik 2022.8, flows will be exported as YAML, but JSON-based flows can still be imported.
## Behavior settings
### Compatibility mode
The compatibility mode increases compatibility with password managers. Password managers like [1Password](https://1password.com/) for example don't need this setting to be enabled, when accessing the flow from a desktop browser. However accessing the flow from a mobile device might necessitate this setting to be enabled.
The technical reasons for this settings' existence is due to the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
When the compatibility mode is enabled, authentik uses a polyfill which emulates the Shadow DOM APIs without actually using the feature, and instead a traditional DOM is rendered. This increases support for password managers, especially on mobile devices.

View File

@ -72,3 +72,15 @@ Logins which used Passwordless authentication have the _auth_method_ context var
}
}
```
### `WebAuthn Device type restrictions`
:::info
Requires authentik 2024.4
:::
Optionally restrict which WebAuthn device types can be used to authenticate.
When no restriction is set, all WebAuthn devices a user has registered are allowed.
These restrictions only apply to WebAuthn devices created with authentik 2024.4 or later.

View File

@ -272,14 +272,6 @@ Disable the inbuilt update-checker. Defaults to `false`.
- Kubeconfig
- Existence of a docker socket
### `AUTHENTIK_DEFAULT_TOKEN_LENGTH`
:::info
Requires authentik 2022.4.1
:::
Configure the length of generated tokens. Defaults to 60.
### `AUTHENTIK_LDAP__TASK_TIMEOUT_HOURS`
:::info

View File

@ -4,7 +4,7 @@ title: Ensure unique email addresses
Due to the database design of authentik, email addresses are by default not required to be unique. This behavior can however be changed by policies.
The snippet below can as the expression in policies both with enrollment flows, where the policy should be bound to any stage before the [User write](../../flow/stages/user_write.md) stage, or it can be used with the [Prompt stage](../../flow/stages/prompt/index.md).
The snippet below can be used as the expression in policies both with enrollment flows, where the policy should be bound to any stage before the [User write](../../flow/stages/user_write.md) stage, or with the [Prompt stage](../../flow/stages/prompt/index.md).
```python
from authentik.core.models import User

View File

@ -0,0 +1,60 @@
---
title: Release next
slug: /releases/next
---
<!-- ## Breaking changes -->
## Breaking changes
### Manual action is required
### Manual action may be required
- **Configuration options migrated to the Admin interface**
The following config options have been moved from the config file and can now be set using the Admin interface (under **System** -> **Settings**) or the API:
- `AUTHENTIK_DEFAULT_TOKEN_LENGTH`
When upgrading to 2024.next, the currently configured options will be automatically migrated to the database, and can be removed from the `.env` or helm values file afterwards.
## New features
- Configurable app password token expiring
Thanks @jmdilly for contributing this feature!
Admins can now configure the default token duration (which defaults to `minutes=30`) in the admin interface as specified above. This value can also be overridden per-user with the `goauthentik.io/user/token-maximum-lifetime` attribute.
## Upgrading
This release does not introduce any new requirements.
### docker-compose
To upgrade, download the new docker-compose file and update the Docker stack with the new version, using these commands:
```
wget -O docker-compose.yml https://goauthentik.io/version/xxxx.x/docker-compose.yml
docker compose up -d
```
The `-O` flag retains the downloaded file's name, overwriting any existing local file with the same name.
### Kubernetes
Upgrade the Helm Chart to the new version, using the following commands:
```shell
helm repo update
helm upgrade authentik authentik/authentik -f values.yaml --version ^xxxx.x
```
## Minor changes/fixes
<!-- _Insert the output of `make gen-changelog` here_ -->
## API Changes
<!-- _Insert output of `make gen-diff` here_ -->

View File

@ -4,6 +4,8 @@ title: Manage users
The following topics are for the basic management of users: how to create, modify, delete or deactivate users, and using a recovery email.
[Policies](../../policies/index.md) can be used to further manage how users are authenticated. For example, by default authentik does not require email addresses be unique, but you can use a policy to [enforce unique email addresses](../../policies/working_with_policies/unique_email.md).
### Create a user
> If you want to automate user creation, you can do that either by [invitations](./invitations.md), [`user_write` stage](../../flow/stages/user_write), or [using the API](/developer-docs/api/browser).

View File

@ -70,6 +70,14 @@ Optional flag, when set to false, Tokens created by the user will not expire.
Only applies when the token creation is triggered by the user with this attribute set. Additionally, the flag does not apply to superusers.
### `goauthentik.io/user/token-maximum-lifetime`:
Optional flag, when set, defines the maximum lifetime of user-created tokens. Defaults to the system setting if not set.
Only applies when `goauthentik.io/user/token-expires` set to true.
Format is string of format `days=10;hours=1;minute=3;seconds=5`.
### `goauthentik.io/user/debug`:
See [Troubleshooting access problems](../../troubleshooting/access), when set, the user gets a more detailed explanation of access decisions.

View File

@ -20,7 +20,7 @@ The following placeholders will be used:
Create an OAuth2/OpenID provider with the following parameters:
- **Client Type**: `Confidential`
- Scopes: OpenID, Email and Profile
- **Scopes**: OpenID, Email and Profile
- **Signing Key**: Select any available key
Note the Client ID and Client Secret values for the provider.

View File

@ -138,6 +138,9 @@ Add a new provider using the `+` button and set the following values:
You need to enable the "Use group provisioning" checkmark to be able to write to this field
:::
- Use unique user ID: If you only have one provider you can uncheck this if you prefer.
:::tip
To avoid your group assignment being a hash value, deselect **Use unique user ID**.
:::
At this stage you should be able to login with SSO.

View File

@ -35,7 +35,7 @@
"@docusaurus/types": "3.2.1",
"@types/react": "^18.2.75",
"prettier": "3.2.5",
"typescript": "~5.4.4"
"typescript": "~5.4.5"
},
"engines": {
"node": ">=20"
@ -15806,9 +15806,9 @@
}
},
"node_modules/typescript": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@ -54,7 +54,7 @@
"@docusaurus/types": "3.2.1",
"@types/react": "^18.2.75",
"prettier": "3.2.5",
"typescript": "~5.4.4"
"typescript": "~5.4.5"
},
"engines": {
"node": ">=20"