Merge branch 'main' into benchmarks
This commit is contained in:
@ -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"
|
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||||
|
|
||||||
# Stage 5: Python dependencies
|
# 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
|
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"
|
poetry install --only=main --no-ansi --no-interaction --no-root"
|
||||||
|
|
||||||
# Stage 6: Run
|
# 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 GIT_BUILD_HASH
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
@ -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.used_by import UsedByMixin
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
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.models import Event, EventAction
|
||||||
from authentik.events.utils import model_to_dict
|
from authentik.events.utils import model_to_dict
|
||||||
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +58,30 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
|||||||
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||||
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||||
raise ValidationError({"intent": f"Invalid intent {attrs.get('intent')}"})
|
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
|
return attrs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -5,6 +5,7 @@ from django.db import migrations, models
|
|||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
import authentik.core.models
|
import authentik.core.models
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
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()
|
token.save()
|
||||||
|
|
||||||
|
|
||||||
|
def default_token_key():
|
||||||
|
return generate_id(60)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
replaces = [
|
replaces = [
|
||||||
("authentik_core", "0012_auto_20201003_1737"),
|
("authentik_core", "0012_auto_20201003_1737"),
|
||||||
@ -62,7 +67,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="token",
|
model_name="token",
|
||||||
name="key",
|
name="key",
|
||||||
field=models.TextField(default=authentik.core.models.default_token_key),
|
field=models.TextField(default=default_token_key),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name="token",
|
name="token",
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -1,6 +1,6 @@
|
|||||||
"""authentik core models"""
|
"""authentik core models"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Any, Optional, Self
|
from typing import Any, Optional, Self
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@ -25,15 +25,16 @@ from authentik.blueprints.models import ManagedModel
|
|||||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||||
from authentik.lib.avatars import get_avatar
|
from authentik.lib.avatars import get_avatar
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.models import (
|
from authentik.lib.models import (
|
||||||
CreatedUpdatedModel,
|
CreatedUpdatedModel,
|
||||||
DomainlessFormattedURLValidator,
|
DomainlessFormattedURLValidator,
|
||||||
SerializerModel,
|
SerializerModel,
|
||||||
)
|
)
|
||||||
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.policies.models import PolicyBindingModel
|
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()
|
LOGGER = get_logger()
|
||||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
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_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
||||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
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_USERNAME = "goauthentik.io/user/can-change-username"
|
||||||
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
|
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
|
||||||
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
||||||
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
||||||
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
||||||
|
|
||||||
|
|
||||||
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
|
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
|
||||||
# used_by API that allows models to specify if they shadow an object
|
# 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
|
# 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"""
|
"""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"""
|
"""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
|
# We use generate_id since the chars in the key should be easy
|
||||||
# to use in Emails (for verification) and URLs (for recovery)
|
# 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):
|
class UserTypes(models.TextChoices):
|
||||||
@ -627,7 +645,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
|||||||
class ExpiringModel(models.Model):
|
class ExpiringModel(models.Model):
|
||||||
"""Base Model which can expire, and is automatically cleaned up."""
|
"""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)
|
expiring = models.BooleanField(default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -10,7 +10,14 @@ from django.dispatch import receiver
|
|||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
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
|
# Arguments: user: User, password: str
|
||||||
password_changed = Signal()
|
password_changed = Signal()
|
||||||
@ -61,3 +68,12 @@ def backchannel_provider_pre_save(sender: type[Model], instance: Model, **_):
|
|||||||
if not isinstance(instance, BackchannelProvider):
|
if not isinstance(instance, BackchannelProvider):
|
||||||
return
|
return
|
||||||
instance.is_backchannel = True
|
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()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Test token API"""
|
"""Test token API"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
@ -7,7 +8,13 @@ from guardian.shortcuts import get_anonymous_user
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.api.tokens import TokenSerializer
|
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.core.tests.utils import create_test_admin_user
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
@ -76,6 +83,77 @@ class TestTokenAPI(APITestCase):
|
|||||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||||
self.assertEqual(token.expiring, False)
|
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):
|
def test_list(self):
|
||||||
"""Test Token List (Test normal authentication)"""
|
"""Test Token List (Test normal authentication)"""
|
||||||
Token.objects.all().delete()
|
Token.objects.all().delete()
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
21
authentik/events/migrations/0006_alter_systemtask_expires.py
Normal file
21
authentik/events/migrations/0006_alter_systemtask_expires.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -110,7 +110,6 @@ events:
|
|||||||
asn: "/geoip/GeoLite2-ASN.mmdb"
|
asn: "/geoip/GeoLite2-ASN.mmdb"
|
||||||
|
|
||||||
cert_discovery_dir: /certs
|
cert_discovery_dir: /certs
|
||||||
default_token_length: 60
|
|
||||||
|
|
||||||
tenants:
|
tenants:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -8,6 +8,7 @@ from django.http import HttpRequest
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.events.signals import get_login_event
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.constants import (
|
from authentik.providers.oauth2.constants import (
|
||||||
@ -87,7 +88,9 @@ class IDToken:
|
|||||||
) -> "IDToken":
|
) -> "IDToken":
|
||||||
"""Create ID Token"""
|
"""Create ID Token"""
|
||||||
id_token = IDToken(provider, token, **kwargs)
|
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.iss = provider.get_issuer(request)
|
||||||
id_token.aud = provider.client_id
|
id_token.aud = provider.client_id
|
||||||
id_token.claims = {}
|
id_token.claims = {}
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -130,7 +130,13 @@ class OAuthSourceSerializer(SourceSerializer):
|
|||||||
"oidc_jwks_url",
|
"oidc_jwks_url",
|
||||||
"oidc_jwks",
|
"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):
|
class OAuthSourceFilter(FilterSet):
|
||||||
|
@ -7,11 +7,16 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.flows.api.stages import StageSerializer
|
from authentik.flows.api.stages import StageSerializer
|
||||||
from authentik.flows.models import NotConfiguredAction
|
from authentik.flows.models import NotConfiguredAction
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||||
|
from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorValidateStageSerializer(StageSerializer):
|
class AuthenticatorValidateStageSerializer(StageSerializer):
|
||||||
"""AuthenticatorValidateStage Serializer"""
|
"""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):
|
def validate_not_configured_action(self, value):
|
||||||
"""Ensure that a configuration stage is set when not_configured_action is configure"""
|
"""Ensure that a configuration stage is set when not_configured_action is configure"""
|
||||||
configuration_stages = self.initial_data.get("configuration_stages", None)
|
configuration_stages = self.initial_data.get("configuration_stages", None)
|
||||||
@ -31,6 +36,8 @@ class AuthenticatorValidateStageSerializer(StageSerializer):
|
|||||||
"configuration_stages",
|
"configuration_stages",
|
||||||
"last_auth_threshold",
|
"last_auth_threshold",
|
||||||
"webauthn_user_verification",
|
"webauthn_user_verification",
|
||||||
|
"webauthn_allowed_device_types",
|
||||||
|
"webauthn_allowed_device_types_obj",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,8 +14,9 @@ from structlog.stdlib import get_logger
|
|||||||
from webauthn import options_to_json
|
from webauthn import options_to_json
|
||||||
from webauthn.authentication.generate_authentication_options import generate_authentication_options
|
from webauthn.authentication.generate_authentication_options import generate_authentication_options
|
||||||
from webauthn.authentication.verify_authentication_response import verify_authentication_response
|
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.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 webauthn.helpers.structs import UserVerificationRequirement
|
||||||
|
|
||||||
from authentik.core.api.utils import JSONDictField, PassiveSerializer
|
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"""
|
"""Validate WebAuthn Challenge"""
|
||||||
request = stage_view.request
|
request = stage_view.request
|
||||||
challenge = request.session.get(SESSION_KEY_WEBAUTHN_CHALLENGE)
|
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:
|
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
|
# 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
|
# 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
|
# step done by a user. Only if this validation happens at a later stage we can check
|
||||||
# that the device belongs to the user
|
# that the device belongs to the user
|
||||||
if not user.is_anonymous and device.user != user:
|
if not user.is_anonymous and device.user != user:
|
||||||
raise ValidationError("Invalid device")
|
raise ValidationError("Invalid device", "invalid")
|
||||||
|
# When a device_type was set when creating the device (2024.4+), and we have a limitation,
|
||||||
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
|
# 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:
|
try:
|
||||||
authentication_verification = verify_authentication_response(
|
authentication_verification = verify_authentication_response(
|
||||||
credential=data,
|
credential=credential,
|
||||||
expected_challenge=challenge,
|
expected_challenge=challenge,
|
||||||
expected_rp_id=get_rp_id(request),
|
expected_rp_id=get_rp_id(request),
|
||||||
expected_origin=get_origin(request),
|
expected_origin=get_origin(request),
|
||||||
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -71,6 +71,9 @@ class AuthenticatorValidateStage(Stage):
|
|||||||
choices=UserVerification.choices,
|
choices=UserVerification.choices,
|
||||||
default=UserVerification.PREFERRED,
|
default=UserVerification.PREFERRED,
|
||||||
)
|
)
|
||||||
|
webauthn_allowed_device_types = models.ManyToManyField(
|
||||||
|
"authentik_stages_authenticator_webauthn.WebAuthnDeviceType", blank=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[BaseSerializer]:
|
def serializer(self) -> type[BaseSerializer]:
|
||||||
|
@ -5,6 +5,7 @@ from hashlib import sha256
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from jwt import PyJWTError, decode, encode
|
from jwt import PyJWTError, decode, encode
|
||||||
from rest_framework.fields import CharField, IntegerField, ListField, UUIDField
|
from rest_framework.fields import CharField, IntegerField, ListField, UUIDField
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
@ -176,15 +177,30 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
threshold = timedelta_from_string(stage.last_auth_threshold)
|
threshold = timedelta_from_string(stage.last_auth_threshold)
|
||||||
allowed_devices = []
|
allowed_devices = []
|
||||||
|
|
||||||
|
has_webauthn_filters_set = stage.webauthn_allowed_device_types.exists()
|
||||||
|
|
||||||
for device in user_devices:
|
for device in user_devices:
|
||||||
device_class = device.__class__.__name__.lower().replace("device", "")
|
device_class = device.__class__.__name__.lower().replace("device", "")
|
||||||
if device_class not in stage.device_classes:
|
if device_class not in stage.device_classes:
|
||||||
self.logger.debug("device class not allowed", device_class=device_class)
|
self.logger.debug("device class not allowed", device_class=device_class)
|
||||||
continue
|
continue
|
||||||
if isinstance(device, SMSDevice) and device.is_hashed:
|
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
|
continue
|
||||||
allowed_devices.append(device)
|
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
|
# Ensure only one challenge per device class
|
||||||
# WebAuthn does another device loop to find all WebAuthn devices
|
# WebAuthn does another device loop to find all WebAuthn devices
|
||||||
if device_class in seen_classes:
|
if device_class in seen_classes:
|
||||||
@ -251,7 +267,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
if stage.not_configured_action == NotConfiguredAction.DENY:
|
if stage.not_configured_action == NotConfiguredAction.DENY:
|
||||||
self.logger.debug("Authenticator not configured, denying")
|
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:
|
if stage.not_configured_action == NotConfiguredAction.CONFIGURE:
|
||||||
self.logger.debug("Authenticator not configured, forcing configure")
|
self.logger.debug("Authenticator not configured, forcing configure")
|
||||||
return self.prepare_stages(user)
|
return self.prepare_stages(user)
|
||||||
|
@ -26,8 +26,16 @@ from authentik.stages.authenticator_validate.stage import (
|
|||||||
PLAN_CONTEXT_DEVICE_CHALLENGES,
|
PLAN_CONTEXT_DEVICE_CHALLENGES,
|
||||||
AuthenticatorValidateStageView,
|
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.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.identification.models import IdentificationStage, UserFields
|
||||||
from authentik.stages.user_login.models import UserLoginStage
|
from authentik.stages.user_login.models import UserLoginStage
|
||||||
|
|
||||||
@ -120,7 +128,56 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
{}, StageView(FlowExecutorView(current_stage=stage), request=request), self.user
|
{}, 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"""
|
"""Test webauthn"""
|
||||||
request = get_request("/")
|
request = get_request("/")
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
@ -190,17 +247,21 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_validate_challenge(self):
|
def test_validate_challenge_unrestricted(self):
|
||||||
"""Test webauthn"""
|
"""Test webauthn authentication (unrestricted webauthn device)"""
|
||||||
|
webauthn_mds_import(force=True)
|
||||||
|
webauthn_aaguid_import()
|
||||||
device = WebAuthnDevice.objects.create(
|
device = WebAuthnDevice.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
public_key=(
|
public_key=(
|
||||||
"pQECAyYgASFYIGsBLkklToCQkT7qJT_bJYN1sEc1oJdbnmoOc43i0J"
|
"pQECAyYgASFYIF-N4GvQJdTJMAmTOxFX9_boL00zBiSrP0DY9xvJl_FFIlggnyZloVSVofdJNTLMeMdjQHgW2Rzmd5_Xt5AWtNztcdo"
|
||||||
"H6IlggLTXytuhzFVYYAK4PQNj8_coGrbbzSfUxdiPAcZTQCyU"
|
|
||||||
),
|
),
|
||||||
credential_id="QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
|
credential_id="X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
|
||||||
sign_count=4,
|
sign_count=2,
|
||||||
rp_id=generate_id(),
|
rp_id=generate_id(),
|
||||||
|
device_type=WebAuthnDeviceType.objects.get(
|
||||||
|
aaguid="2fc0579f-8113-47ea-b116-bb5a8db9202a"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
flow = create_test_flow()
|
flow = create_test_flow()
|
||||||
stage = AuthenticatorValidateStage.objects.create(
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
@ -222,7 +283,7 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
]
|
]
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
"aCC6ak_DP45xMH1qyxzUM5iC2xc4QthQb09v7m4qDBmY8FvWvhxFzSuFlDYQmclrh5fWS5q0TPxgJGF4vimcFQ"
|
||||||
)
|
)
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
@ -230,24 +291,23 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
data={
|
data={
|
||||||
"webauthn": {
|
"webauthn": {
|
||||||
"id": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
|
"id": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
|
||||||
"rawId": "QKZ97ASJAOIDyipAs6mKUxDUZgDrWrbAsUb5leL7-oU",
|
"rawId": "X43ga9Al1MkwCZM7EXD1r8Sxj7aXnNsuR013XM7he4kZ-GS9TaA-u3i36wsswjPm",
|
||||||
"type": "public-key",
|
"type": "public-key",
|
||||||
"assertionClientExtensions": "{}",
|
"assertionClientExtensions": "{}",
|
||||||
"response": {
|
"response": {
|
||||||
"clientDataJSON": (
|
"clientDataJSON": (
|
||||||
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZzk4STUxbVF2WlhvN"
|
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYUNDN"
|
||||||
"Wx4TGZockQyemZvbGhaYkxSeUNncWtrWWFwMWp3U2FKMTNCZ3VvSldDRjlfTGczQW"
|
"mFrX0RQNDV4TUgxcXl4elVNNWlDMnhjNFF0aFFiMDl2N200cURCbV"
|
||||||
"dPNFdoLUJxYTU1NkpFMjBvS3NZYmw2UkEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWx"
|
"k4RnZXdmh4RnpTdUZsRFlRbWNscmg1ZldTNXEwVFB4Z0pHRjR2aW1"
|
||||||
"ob3N0OjkwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2Jl"
|
"jRlEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJj"
|
||||||
"X2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2Fpb"
|
"cm9zc09yaWdpbiI6ZmFsc2V9"
|
||||||
"nN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ=="
|
|
||||||
),
|
),
|
||||||
"signature": (
|
"signature": (
|
||||||
"MEQCIFNlrHf9ablJAalXLWkrqvHB8oIu8kwvRpH3X3rbJVpI"
|
"MEQCIAHQCGfE_PX1z6mBDaXUNqK_NrllhXylNOmETUD3Khv9AiBTl"
|
||||||
"AiAqtOK6mIZPk62kZN0OzFsHfuvu_RlOl7zlqSNzDdz_Ag=="
|
"rX3GDRj5OaOfTToOwUwAhtd74tu0T6DZAVHPb_hlQ=="
|
||||||
),
|
),
|
||||||
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==",
|
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABg==",
|
||||||
"userHandle": None,
|
"userHandle": None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -261,6 +321,96 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
)
|
)
|
||||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
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):
|
def test_validate_challenge_userless(self):
|
||||||
"""Test webauthn"""
|
"""Test webauthn"""
|
||||||
device = WebAuthnDevice.objects.create(
|
device = WebAuthnDevice.objects.create(
|
||||||
|
@ -126,6 +126,10 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||||||
if authenticator_attachment:
|
if authenticator_attachment:
|
||||||
authenticator_attachment = AuthenticatorAttachment(str(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(
|
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
|
||||||
rp_id=get_rp_id(self.request),
|
rp_id=get_rp_id(self.request),
|
||||||
rp_name=self.request.brand.branding_title,
|
rp_name=self.request.brand.branding_title,
|
||||||
@ -137,7 +141,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||||||
user_verification=UserVerificationRequirement(str(stage.user_verification)),
|
user_verification=UserVerificationRequirement(str(stage.user_verification)),
|
||||||
authenticator_attachment=authenticator_attachment,
|
authenticator_attachment=authenticator_attachment,
|
||||||
),
|
),
|
||||||
attestation=AttestationConveyancePreference.DIRECT,
|
attestation=attestation,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge
|
self.request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = registration_options.challenge
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -23,6 +23,8 @@ class SettingsSerializer(ModelSerializer):
|
|||||||
"footer_links",
|
"footer_links",
|
||||||
"gdpr_compliance",
|
"gdpr_compliance",
|
||||||
"impersonation",
|
"impersonation",
|
||||||
|
"default_token_duration",
|
||||||
|
"default_token_length",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -5,6 +5,7 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@ -22,6 +23,9 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
VALID_SCHEMA_NAME = re.compile(r"^t_[a-z0-9]{1,61}$")
|
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):
|
def _validate_schema_name(name):
|
||||||
if not VALID_SCHEMA_NAME.match(name):
|
if not VALID_SCHEMA_NAME.match(name):
|
||||||
@ -81,6 +85,16 @@ class Tenant(TenantMixin, SerializerModel):
|
|||||||
impersonation = models.BooleanField(
|
impersonation = models.BooleanField(
|
||||||
help_text=_("Globally enable/disable impersonation."), default=True
|
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):
|
def save(self, *args, **kwargs):
|
||||||
if self.schema_name == "template":
|
if self.schema_name == "template":
|
||||||
|
@ -4547,7 +4547,6 @@
|
|||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"maxLength": 255,
|
"maxLength": 255,
|
||||||
"minLength": 1,
|
|
||||||
"title": "Request Token URL",
|
"title": "Request Token URL",
|
||||||
"description": "URL used to request the initial token. This URL is only required for OAuth 1."
|
"description": "URL used to request the initial token. This URL is only required for OAuth 1."
|
||||||
},
|
},
|
||||||
@ -4557,7 +4556,6 @@
|
|||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"maxLength": 255,
|
"maxLength": 255,
|
||||||
"minLength": 1,
|
|
||||||
"title": "Authorization URL",
|
"title": "Authorization URL",
|
||||||
"description": "URL the user is redirect to to conest the flow."
|
"description": "URL the user is redirect to to conest the flow."
|
||||||
},
|
},
|
||||||
@ -4567,7 +4565,6 @@
|
|||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"maxLength": 255,
|
"maxLength": 255,
|
||||||
"minLength": 1,
|
|
||||||
"title": "Access Token URL",
|
"title": "Access Token URL",
|
||||||
"description": "URL used by authentik to retrieve tokens."
|
"description": "URL used by authentik to retrieve tokens."
|
||||||
},
|
},
|
||||||
@ -4577,7 +4574,6 @@
|
|||||||
"null"
|
"null"
|
||||||
],
|
],
|
||||||
"maxLength": 255,
|
"maxLength": 255,
|
||||||
"minLength": 1,
|
|
||||||
"title": "Profile URL",
|
"title": "Profile URL",
|
||||||
"description": "URL used by authentik to get user information."
|
"description": "URL used by authentik to get user information."
|
||||||
},
|
},
|
||||||
@ -5641,6 +5637,14 @@
|
|||||||
],
|
],
|
||||||
"title": "Webauthn user verification",
|
"title": "Webauthn user verification",
|
||||||
"description": "Enforce user verification for WebAuthn devices."
|
"description": "Enforce user verification for WebAuthn devices."
|
||||||
|
},
|
||||||
|
"webauthn_allowed_device_types": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"title": "Webauthn allowed device types"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
@ -5782,7 +5786,8 @@
|
|||||||
"device_type_restrictions": {
|
"device_type_restrictions": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "integer"
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
},
|
},
|
||||||
"title": "Device type restrictions"
|
"title": "Device type restrictions"
|
||||||
}
|
}
|
||||||
@ -6028,7 +6033,10 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"expires": {
|
"expires": {
|
||||||
"type": "string",
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"title": "Expires"
|
"title": "Expires"
|
||||||
},
|
},
|
||||||
@ -6797,7 +6805,10 @@
|
|||||||
"title": "Name"
|
"title": "Name"
|
||||||
},
|
},
|
||||||
"expires": {
|
"expires": {
|
||||||
"type": "string",
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"title": "Expires"
|
"title": "Expires"
|
||||||
},
|
},
|
||||||
@ -7988,7 +7999,10 @@
|
|||||||
"title": "Description"
|
"title": "Description"
|
||||||
},
|
},
|
||||||
"expires": {
|
"expires": {
|
||||||
"type": "string",
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"title": "Expires"
|
"title": "Expires"
|
||||||
},
|
},
|
||||||
|
2
go.mod
2
go.mod
@ -30,7 +30,7 @@ require (
|
|||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/wwt/guac v1.3.2
|
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/exp v0.0.0-20230210204819-062eb4c674ab
|
||||||
golang.org/x/oauth2 v0.19.0
|
golang.org/x/oauth2 v0.19.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.7.0
|
||||||
|
4
go.sum
4
go.sum
@ -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.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 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
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.8 h1:bHKUZgQXf4/qjL4VkITc/HK0pjMtX9X5Dlob8yTg7K4=
|
||||||
goauthentik.io/api/v3 v3.2024022.7/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
@ -14,7 +14,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2024\n"
|
"Last-Translator: deluxghost, 2024\n"
|
||||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||||
@ -2287,6 +2287,19 @@ msgstr "WebAuthn 设备"
|
|||||||
msgid "WebAuthn Devices"
|
msgid "WebAuthn Devices"
|
||||||
msgstr "WebAuthn 设备"
|
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
|
#: authentik/stages/captcha/models.py
|
||||||
msgid "Public key, acquired your captcha Provider."
|
msgid "Public key, acquired your captcha Provider."
|
||||||
msgstr "公钥,从您的验证码提供商处取得。"
|
msgstr "公钥,从您的验证码提供商处取得。"
|
||||||
|
@ -14,7 +14,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||||
"Last-Translator: deluxghost, 2024\n"
|
"Last-Translator: deluxghost, 2024\n"
|
||||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||||
@ -2287,6 +2287,19 @@ msgstr "WebAuthn 设备"
|
|||||||
msgid "WebAuthn Devices"
|
msgid "WebAuthn Devices"
|
||||||
msgstr "WebAuthn 设备"
|
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
|
#: authentik/stages/captcha/models.py
|
||||||
msgid "Public key, acquired your captcha Provider."
|
msgid "Public key, acquired your captcha Provider."
|
||||||
msgstr "公钥,从您的验证码提供商处取得。"
|
msgstr "公钥,从您的验证码提供商处取得。"
|
||||||
|
6
poetry.lock
generated
6
poetry.lock
generated
@ -3585,13 +3585,13 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sentry-sdk"
|
name = "sentry-sdk"
|
||||||
version = "1.44.1"
|
version = "1.45.0"
|
||||||
description = "Python client for Sentry (https://sentry.io)"
|
description = "Python client for Sentry (https://sentry.io)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "sentry-sdk-1.44.1.tar.gz", hash = "sha256:24e6a53eeabffd2f95d952aa35ca52f0f4201d17f820ac9d3ff7244c665aaf68"},
|
{file = "sentry-sdk-1.45.0.tar.gz", hash = "sha256:509aa9678c0512344ca886281766c2e538682f8acfa50fd8d405f8c417ad0625"},
|
||||||
{file = "sentry_sdk-1.44.1-py2.py3-none-any.whl", hash = "sha256:5f75eb91d8ab6037c754a87b8501cc581b2827e923682f593bed3539ce5b3999"},
|
{file = "sentry_sdk-1.45.0-py2.py3-none-any.whl", hash = "sha256:1ce29e30240cc289a027011103a8c83885b15ef2f316a60bcc7c5300afa144f1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
65
schema.yml
65
schema.yml
@ -29976,6 +29976,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- asn
|
- asn
|
||||||
- current
|
- current
|
||||||
@ -30619,6 +30620,16 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/UserVerificationEnum'
|
- $ref: '#/components/schemas/UserVerificationEnum'
|
||||||
description: Enforce user verification for WebAuthn devices.
|
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:
|
required:
|
||||||
- component
|
- component
|
||||||
- meta_model_name
|
- meta_model_name
|
||||||
@ -30626,6 +30637,7 @@ components:
|
|||||||
- pk
|
- pk
|
||||||
- verbose_name
|
- verbose_name
|
||||||
- verbose_name_plural
|
- verbose_name_plural
|
||||||
|
- webauthn_allowed_device_types_obj
|
||||||
AuthenticatorValidateStageRequest:
|
AuthenticatorValidateStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: AuthenticatorValidateStage Serializer
|
description: AuthenticatorValidateStage Serializer
|
||||||
@ -30661,6 +30673,11 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/UserVerificationEnum'
|
- $ref: '#/components/schemas/UserVerificationEnum'
|
||||||
description: Enforce user verification for WebAuthn devices.
|
description: Enforce user verification for WebAuthn devices.
|
||||||
|
webauthn_allowed_device_types:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
AuthenticatorValidationChallenge:
|
AuthenticatorValidationChallenge:
|
||||||
@ -32674,6 +32691,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
scope:
|
scope:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -33774,6 +33792,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
fixed_data:
|
fixed_data:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
@ -33810,6 +33829,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
fixed_data:
|
fixed_data:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
@ -35551,26 +35571,22 @@ components:
|
|||||||
request_token_url:
|
request_token_url:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
|
||||||
description: URL used to request the initial token. This URL is only required
|
description: URL used to request the initial token. This URL is only required
|
||||||
for OAuth 1.
|
for OAuth 1.
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
authorization_url:
|
authorization_url:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
|
||||||
description: URL the user is redirect to to conest the flow.
|
description: URL the user is redirect to to conest the flow.
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
access_token_url:
|
access_token_url:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
|
||||||
description: URL used by authentik to retrieve tokens.
|
description: URL used by authentik to retrieve tokens.
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
profile_url:
|
profile_url:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
|
||||||
description: URL used by authentik to get user information.
|
description: URL used by authentik to get user information.
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
consumer_key:
|
consumer_key:
|
||||||
@ -37536,6 +37552,11 @@ components:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/UserVerificationEnum'
|
- $ref: '#/components/schemas/UserVerificationEnum'
|
||||||
description: Enforce user verification for WebAuthn devices.
|
description: Enforce user verification for WebAuthn devices.
|
||||||
|
webauthn_allowed_device_types:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
PatchedAuthenticatorWebAuthnStageRequest:
|
PatchedAuthenticatorWebAuthnStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: AuthenticatorWebAuthnStage Serializer
|
description: AuthenticatorWebAuthnStage Serializer
|
||||||
@ -38125,6 +38146,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
fixed_data:
|
fixed_data:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
@ -38563,26 +38585,22 @@ components:
|
|||||||
request_token_url:
|
request_token_url:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
|
||||||
description: URL used to request the initial token. This URL is only required
|
description: URL used to request the initial token. This URL is only required
|
||||||
for OAuth 1.
|
for OAuth 1.
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
authorization_url:
|
authorization_url:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
|
||||||
description: URL the user is redirect to to conest the flow.
|
description: URL the user is redirect to to conest the flow.
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
access_token_url:
|
access_token_url:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
|
||||||
description: URL used by authentik to retrieve tokens.
|
description: URL used by authentik to retrieve tokens.
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
profile_url:
|
profile_url:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
minLength: 1
|
|
||||||
description: URL used by authentik to get user information.
|
description: URL used by authentik to get user information.
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
consumer_key:
|
consumer_key:
|
||||||
@ -39429,6 +39447,15 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
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:
|
PatchedSourceStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: SourceStage Serializer
|
description: SourceStage Serializer
|
||||||
@ -39506,6 +39533,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
expiring:
|
expiring:
|
||||||
type: boolean
|
type: boolean
|
||||||
PatchedUserDeleteStageRequest:
|
PatchedUserDeleteStageRequest:
|
||||||
@ -42458,6 +42486,14 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
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:
|
SettingsRequest:
|
||||||
type: object
|
type: object
|
||||||
description: Settings Serializer
|
description: Settings Serializer
|
||||||
@ -42489,6 +42525,15 @@ components:
|
|||||||
impersonation:
|
impersonation:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Globally enable/disable impersonation.
|
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:
|
SeverityEnum:
|
||||||
enum:
|
enum:
|
||||||
- notice
|
- notice
|
||||||
@ -43156,6 +43201,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
expiring:
|
expiring:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
@ -43181,6 +43227,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
scope:
|
scope:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -43225,6 +43272,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
expiring:
|
expiring:
|
||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
@ -43463,6 +43511,7 @@ components:
|
|||||||
expires:
|
expires:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
nullable: true
|
||||||
expiring:
|
expiring:
|
||||||
type: boolean
|
type: boolean
|
||||||
user:
|
user:
|
||||||
|
16
tests/wdio/package-lock.json
generated
16
tests/wdio/package-lock.json
generated
@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "@goauthentik/web-tests",
|
"name": "@goauthentik/web-tests",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chromedriver": "^123.0.2"
|
"chromedriver": "^123.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.4",
|
"typescript": "^5.4.5",
|
||||||
"wdio-wait-for": "^3.0.11"
|
"wdio-wait-for": "^3.0.11"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -2084,9 +2084,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chromedriver": {
|
"node_modules/chromedriver": {
|
||||||
"version": "123.0.2",
|
"version": "123.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.3.tgz",
|
||||||
"integrity": "sha512-Kx0r/IGULm7eciaUtX/OKaFbdBdHRDSguiV1Q4zuQncz11gvymDdMtELa7ppk+kTL5113NLPud92nuIMNTRhww==",
|
"integrity": "sha512-35IeTqDLcVR0htF9nD/Lh+g24EG088WHVKXBXiFyWq+2lelnoM0B3tKTBiUEjLng0GnELI4QyQPFK7i97Fz1fQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testim/chrome-version": "^1.1.4",
|
"@testim/chrome-version": "^1.1.4",
|
||||||
@ -8620,9 +8620,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.4",
|
"version": "5.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||||
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.4",
|
"typescript": "^5.4.5",
|
||||||
"wdio-wait-for": "^3.0.11"
|
"wdio-wait-for": "^3.0.11"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -32,6 +32,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chromedriver": "^123.0.2"
|
"chromedriver": "^123.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
web/package-lock.json
generated
16
web/package-lock.json
generated
@ -17,7 +17,7 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.5.5",
|
"@formatjs/intl-listformat": "^7.5.5",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@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-labs/task": "^3.1.0",
|
||||||
"@lit/context": "^1.1.0",
|
"@lit/context": "^1.1.0",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
@ -101,7 +101,7 @@
|
|||||||
"ts-lit-plugin": "^2.0.2",
|
"ts-lit-plugin": "^2.0.2",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"turnstile-types": "^1.2.0",
|
"turnstile-types": "^1.2.0",
|
||||||
"typescript": "^5.4.4",
|
"typescript": "^5.4.5",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -2840,9 +2840,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@goauthentik/api": {
|
"node_modules/@goauthentik/api": {
|
||||||
"version": "2024.2.2-1712571709",
|
"version": "2024.2.2-1712833826",
|
||||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.2.2-1712571709.tgz",
|
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.2.2-1712833826.tgz",
|
||||||
"integrity": "sha512-+uS+d13aCDC7W3bZk8j3RnqvDq8iivXnP98GHFEoB9pUuMJ1LK7sgJwr2JHmJe5KiFMl0oxycY8VutsBnYmjog=="
|
"integrity": "sha512-0DmJ/GqGvj2mQ3IuA8YBERbj/E1o1HzS+wYIAbZzGAydocKH9g7Hn9h47eDU2lTgUu1N1DDOvYkXAsdqCFVwDQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@hcaptcha/types": {
|
"node_modules/@hcaptcha/types": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
@ -17129,9 +17129,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.4",
|
"version": "5.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||||
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@formatjs/intl-listformat": "^7.5.5",
|
"@formatjs/intl-listformat": "^7.5.5",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
"@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-labs/task": "^3.1.0",
|
||||||
"@lit/context": "^1.1.0",
|
"@lit/context": "^1.1.0",
|
||||||
"@lit/localize": "^0.12.1",
|
"@lit/localize": "^0.12.1",
|
||||||
@ -122,7 +122,7 @@
|
|||||||
"ts-lit-plugin": "^2.0.2",
|
"ts-lit-plugin": "^2.0.2",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"turnstile-types": "^1.2.0",
|
"turnstile-types": "^1.2.0",
|
||||||
"typescript": "^5.4.4",
|
"typescript": "^5.4.5",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
|
import "@goauthentik/components/ak-number-input";
|
||||||
import "@goauthentik/components/ak-switch-input";
|
import "@goauthentik/components/ak-switch-input";
|
||||||
import "@goauthentik/components/ak-text-input";
|
import "@goauthentik/components/ak-text-input";
|
||||||
import "@goauthentik/elements/CodeMirror";
|
import "@goauthentik/elements/CodeMirror";
|
||||||
@ -192,6 +193,24 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
|||||||
help=${msg("Globally enable/disable impersonation.")}
|
help=${msg("Globally enable/disable impersonation.")}
|
||||||
>
|
>
|
||||||
</ak-switch-input>
|
</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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -575,7 +575,11 @@ export class SAMLProviderViewPage extends AKElement {
|
|||||||
</dt>
|
</dt>
|
||||||
<dd class="pf-c-description-list__description">
|
<dd class="pf-c-description-list__description">
|
||||||
<div class="pf-c-description-list__text">
|
<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>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
@ -108,7 +108,6 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
|||||||
<div slot="body" class="pf-c-form">
|
<div slot="body" class="pf-c-form">
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Authorization URL")}
|
label=${msg("Authorization URL")}
|
||||||
?required=${true}
|
|
||||||
name="authorizationUrl"
|
name="authorizationUrl"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -119,17 +118,12 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
|||||||
"",
|
"",
|
||||||
)}"
|
)}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${msg("URL the user is redirect to to consent the authorization.")}
|
${msg("URL the user is redirect to to consent the authorization.")}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal label=${msg("Access token URL")} name="accessTokenUrl">
|
||||||
label=${msg("Access token URL")}
|
|
||||||
?required=${true}
|
|
||||||
name="accessTokenUrl"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value="${first(
|
value="${first(
|
||||||
@ -138,17 +132,12 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
|||||||
"",
|
"",
|
||||||
)}"
|
)}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${msg("URL used by authentik to retrieve tokens.")}
|
${msg("URL used by authentik to retrieve tokens.")}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal label=${msg("Profile URL")} name="profileUrl">
|
||||||
label=${msg("Profile URL")}
|
|
||||||
?required=${true}
|
|
||||||
name="profileUrl"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value="${first(
|
value="${first(
|
||||||
@ -157,7 +146,6 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
|||||||
"",
|
"",
|
||||||
)}"
|
)}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${msg("URL used by authentik to get user information.")}
|
${msg("URL used by authentik to get user information.")}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
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 { 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/FormGroup";
|
||||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||||
import "@goauthentik/elements/forms/Radio";
|
import "@goauthentik/elements/forms/Radio";
|
||||||
@ -71,7 +75,8 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
|
|||||||
[DeviceClassesEnum.Sms, msg("SMS-based Authenticators")],
|
[DeviceClassesEnum.Sms, msg("SMS-based Authenticators")],
|
||||||
];
|
];
|
||||||
|
|
||||||
return html` <span>
|
return html`
|
||||||
|
<span>
|
||||||
${msg(
|
${msg(
|
||||||
"Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.",
|
"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">
|
<p class="pf-c-form__helper-text">
|
||||||
${msg(
|
${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>
|
</p>
|
||||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
||||||
@ -166,33 +171,6 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</ak-form-element-horizontal>
|
</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
|
${this.showConfigurationStages
|
||||||
? html`
|
? html`
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
@ -228,6 +206,77 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
|
|||||||
`
|
`
|
||||||
: html``}
|
: html``}
|
||||||
</div>
|
</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>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||||
import {
|
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
|
||||||
DataProvision,
|
import { DataProvision } from "@goauthentik/authentik/elements/ak-dual-select/types";
|
||||||
DualSelectPair,
|
|
||||||
} from "@goauthentik/authentik/elements/ak-dual-select/types";
|
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { first } from "@goauthentik/common/utils";
|
import { first } from "@goauthentik/common/utils";
|
||||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
|
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
|
||||||
@ -25,23 +23,12 @@ import {
|
|||||||
ResidentKeyRequirementEnum,
|
ResidentKeyRequirementEnum,
|
||||||
StagesApi,
|
StagesApi,
|
||||||
UserVerificationEnum,
|
UserVerificationEnum,
|
||||||
WebAuthnDeviceType,
|
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-webauthn-form")
|
@customElement("ak-stage-authenticator-webauthn-form")
|
||||||
export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorWebAuthnStage> {
|
export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorWebAuthnStage> {
|
||||||
deviceTypeRestrictionPair(item: WebAuthnDeviceType): DualSelectPair {
|
async loadInstance(pk: string): Promise<AuthenticatorWebAuthnStage> {
|
||||||
const label = item.description ? item.description : item.aaguid;
|
return await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnRetrieve({
|
||||||
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({
|
|
||||||
stageUuid: pk,
|
stageUuid: pk,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -194,14 +181,12 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
|
|||||||
.then((results) => {
|
.then((results) => {
|
||||||
return {
|
return {
|
||||||
pagination: results.pagination,
|
pagination: results.pagination,
|
||||||
options: results.results.map(
|
options: results.results.map(deviceTypeRestrictionPair),
|
||||||
this.deviceTypeRestrictionPair,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
.selected=${(this.instance?.deviceTypeRestrictionsObj ?? []).map(
|
.selected=${(this.instance?.deviceTypeRestrictionsObj ?? []).map(
|
||||||
this.deviceTypeRestrictionPair,
|
deviceTypeRestrictionPair,
|
||||||
)}
|
)}
|
||||||
available-label="${msg("Available Device types")}"
|
available-label="${msg("Available Device types")}"
|
||||||
selected-label="${msg("Selected Device types")}"
|
selected-label="${msg("Selected Device types")}"
|
||||||
|
15
web/src/admin/stages/authenticator_webauthn/utils.ts
Normal file
15
web/src/admin/stages/authenticator_webauthn/utils.ts
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
@ -55,8 +55,6 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
|||||||
|
|
||||||
private pagination?: Pagination;
|
private pagination?: Pagination;
|
||||||
|
|
||||||
selectedMap: WeakMap<DataProvider, DualSelectPair[]> = new WeakMap();
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
setTimeout(() => this.fetch(1), 0);
|
setTimeout(() => this.fetch(1), 0);
|
||||||
@ -72,16 +70,14 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
|
|||||||
|
|
||||||
willUpdate(changedProperties: PropertyValues<this>) {
|
willUpdate(changedProperties: PropertyValues<this>) {
|
||||||
if (changedProperties.has("searchDelay")) {
|
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")) {
|
if (changedProperties.has("provider")) {
|
||||||
this.pagination = undefined;
|
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();
|
this.fetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ export class UserTokenForm extends ModelForm<Token, string> {
|
|||||||
|
|
||||||
async send(data: Token): Promise<Token> {
|
async send(data: Token): Promise<Token> {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
|
data.intent = this.instance.intent;
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreTokensUpdate({
|
return new CoreApi(DEFAULT_CONFIG).coreTokensUpdate({
|
||||||
identifier: this.instance.identifier,
|
identifier: this.instance.identifier,
|
||||||
tokenRequest: data,
|
tokenRequest: data,
|
||||||
@ -41,6 +42,14 @@ export class UserTokenForm extends ModelForm<Token, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
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
|
return html` <ak-form-element-horizontal
|
||||||
label=${msg("Identifier")}
|
label=${msg("Identifier")}
|
||||||
?required=${true}
|
?required=${true}
|
||||||
@ -59,6 +68,16 @@ export class UserTokenForm extends ModelForm<Token, string> {
|
|||||||
value="${ifDefined(this.instance?.description)}"
|
value="${ifDefined(this.instance?.description)}"
|
||||||
class="pf-c-form-control"
|
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``}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,11 @@ export class UserTokenList extends Table<Token> {
|
|||||||
<ak-forms-modal>
|
<ak-forms-modal>
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
<span slot="header"> ${msg("Update Token")} </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>
|
</ak-user-token-form>
|
||||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||||
|
@ -8526,15 +8526,19 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sa64fb483becc9c2c">
|
<trans-unit id="sa64fb483becc9c2c">
|
||||||
<source>Device type restrictions</source>
|
<source>Device type restrictions</source>
|
||||||
|
<target>设备类型限制</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="sbb928551c84cd63f">
|
<trans-unit id="sbb928551c84cd63f">
|
||||||
<source>Available Device types</source>
|
<source>Available Device types</source>
|
||||||
|
<target>可用设备类型</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s6446c35d6b411e53">
|
<trans-unit id="s6446c35d6b411e53">
|
||||||
<source>Selected Device types</source>
|
<source>Selected Device types</source>
|
||||||
|
<target>已选设备类型</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="s1f4df216b56de4ac">
|
<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>
|
<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>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
|
@ -8523,6 +8523,22 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||||||
<trans-unit id="sc7d071fb5cc1f6bf">
|
<trans-unit id="sc7d071fb5cc1f6bf">
|
||||||
<source>A selection is required</source>
|
<source>A selection is required</source>
|
||||||
<target>需要进行选择</target>
|
<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>
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</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.
|
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
|
## 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 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
|
#### 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.
|
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.
|
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.
|
||||||
|
@ -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.
|
||||||
|
@ -272,14 +272,6 @@ Disable the inbuilt update-checker. Defaults to `false`.
|
|||||||
- Kubeconfig
|
- Kubeconfig
|
||||||
- Existence of a docker socket
|
- 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`
|
### `AUTHENTIK_LDAP__TASK_TIMEOUT_HOURS`
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
@ -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.
|
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
|
```python
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
60
website/docs/releases/2024/next.md
Normal file
60
website/docs/releases/2024/next.md
Normal 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_ -->
|
@ -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.
|
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
|
### 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).
|
> 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).
|
||||||
|
@ -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.
|
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`:
|
### `goauthentik.io/user/debug`:
|
||||||
|
|
||||||
See [Troubleshooting access problems](../../troubleshooting/access), when set, the user gets a more detailed explanation of access decisions.
|
See [Troubleshooting access problems](../../troubleshooting/access), when set, the user gets a more detailed explanation of access decisions.
|
||||||
|
@ -20,7 +20,7 @@ The following placeholders will be used:
|
|||||||
Create an OAuth2/OpenID provider with the following parameters:
|
Create an OAuth2/OpenID provider with the following parameters:
|
||||||
|
|
||||||
- **Client Type**: `Confidential`
|
- **Client Type**: `Confidential`
|
||||||
- Scopes: OpenID, Email and Profile
|
- **Scopes**: OpenID, Email and Profile
|
||||||
- **Signing Key**: Select any available key
|
- **Signing Key**: Select any available key
|
||||||
|
|
||||||
Note the Client ID and Client Secret values for the provider.
|
Note the Client ID and Client Secret values for the provider.
|
||||||
|
@ -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
|
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.
|
- 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.
|
At this stage you should be able to login with SSO.
|
||||||
|
|
||||||
|
8
website/package-lock.json
generated
8
website/package-lock.json
generated
@ -35,7 +35,7 @@
|
|||||||
"@docusaurus/types": "3.2.1",
|
"@docusaurus/types": "3.2.1",
|
||||||
"@types/react": "^18.2.75",
|
"@types/react": "^18.2.75",
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
"typescript": "~5.4.4"
|
"typescript": "~5.4.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
@ -15806,9 +15806,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.4",
|
"version": "5.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||||
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
"@docusaurus/types": "3.2.1",
|
"@docusaurus/types": "3.2.1",
|
||||||
"@types/react": "^18.2.75",
|
"@types/react": "^18.2.75",
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
"typescript": "~5.4.4"
|
"typescript": "~5.4.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
Reference in New Issue
Block a user