core: add user settable token durations (#7410)
* core: add support for user settable token duration * web: add support for user settable token duration * website: add documentation for user settable token duration * core : fix locales * web: fix tokenIntent when updating * core: fix linting * website: Update website/docs/user-group-role/user/user_ref.md Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jean-Michel DILLY <48059109+jmdilly@users.noreply.github.com> * make token duration system-wide configurable Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * small fixup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * migrate token configs to tenants Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add release notes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * make website Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint-fix Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix migrations Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * nosec Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint-fix Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix migrations for real this time Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * trying with no model using default_token_key Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint-fix Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix save Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint-fix Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * use signal instead of overriding save Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> --------- Signed-off-by: Jean-Michel DILLY <48059109+jmdilly@users.noreply.github.com> Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:

committed by
GitHub

parent
40c672f246
commit
a70363bd95
@ -20,9 +20,18 @@ from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
default_token_duration,
|
||||
token_expires_from_timedelta,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import model_to_dict
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
|
||||
@ -49,6 +58,30 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||
raise ValidationError({"intent": f"Invalid intent {attrs.get('intent')}"})
|
||||
|
||||
if attrs.get("intent") == TokenIntents.INTENT_APP_PASSWORD:
|
||||
# user IS in attrs
|
||||
user: User = attrs.get("user")
|
||||
max_token_lifetime = user.group_attributes(request).get(
|
||||
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
|
||||
)
|
||||
max_token_lifetime_dt = default_token_duration()
|
||||
if max_token_lifetime is not None:
|
||||
try:
|
||||
max_token_lifetime_dt = timedelta_from_string(max_token_lifetime)
|
||||
except ValueError:
|
||||
max_token_lifetime_dt = default_token_duration()
|
||||
|
||||
if "expires" in attrs and attrs.get("expires") > token_expires_from_timedelta(
|
||||
max_token_lifetime_dt
|
||||
):
|
||||
raise ValidationError(
|
||||
{"expires": f"Token expires exceeds maximum lifetime ({max_token_lifetime})."}
|
||||
)
|
||||
elif attrs.get("intent") == TokenIntents.INTENT_API:
|
||||
# For API tokens, expires cannot be overridden
|
||||
attrs["expires"] = default_token_duration()
|
||||
|
||||
return attrs
|
||||
|
||||
class Meta:
|
||||
|
@ -5,6 +5,7 @@ from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.core.models
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
@ -16,6 +17,10 @@ def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
token.save()
|
||||
|
||||
|
||||
def default_token_key():
|
||||
return generate_id(60)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [
|
||||
("authentik_core", "0012_auto_20201003_1737"),
|
||||
@ -62,7 +67,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="key",
|
||||
field=models.TextField(default=authentik.core.models.default_token_key),
|
||||
field=models.TextField(default=default_token_key),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="token",
|
||||
|
@ -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"""
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from hashlib import sha256
|
||||
from typing import Any, Optional, Self
|
||||
from uuid import uuid4
|
||||
@ -25,15 +25,16 @@ from authentik.blueprints.models import ManagedModel
|
||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.lib.avatars import get_avatar
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.models import (
|
||||
CreatedUpdatedModel,
|
||||
DomainlessFormattedURLValidator,
|
||||
SerializerModel,
|
||||
)
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.tenants.utils import get_unique_identifier
|
||||
from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGTH
|
||||
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
|
||||
|
||||
LOGGER = get_logger()
|
||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||
@ -42,13 +43,13 @@ USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
|
||||
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
||||
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME = "goauthentik.io/user/token-maximum-lifetime" # nosec
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
|
||||
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
||||
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
||||
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
||||
|
||||
|
||||
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
|
||||
# used_by API that allows models to specify if they shadow an object
|
||||
# for example the proxy provider which is built on top of an oauth provider
|
||||
@ -59,16 +60,33 @@ options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
|
||||
)
|
||||
|
||||
|
||||
def default_token_duration():
|
||||
def default_token_duration() -> datetime:
|
||||
"""Default duration a Token is valid"""
|
||||
return now() + timedelta(minutes=30)
|
||||
current_tenant = get_current_tenant()
|
||||
token_duration = (
|
||||
current_tenant.default_token_duration
|
||||
if hasattr(current_tenant, "default_token_duration")
|
||||
else DEFAULT_TOKEN_DURATION
|
||||
)
|
||||
return now() + timedelta_from_string(token_duration)
|
||||
|
||||
|
||||
def default_token_key():
|
||||
def token_expires_from_timedelta(dt: timedelta) -> datetime:
|
||||
"""Return a `datetime.datetime` object with the duration of the Token"""
|
||||
return now() + dt
|
||||
|
||||
|
||||
def default_token_key() -> str:
|
||||
"""Default token key"""
|
||||
current_tenant = get_current_tenant()
|
||||
token_length = (
|
||||
current_tenant.default_token_length
|
||||
if hasattr(current_tenant, "default_token_length")
|
||||
else DEFAULT_TOKEN_LENGTH
|
||||
)
|
||||
# We use generate_id since the chars in the key should be easy
|
||||
# to use in Emails (for verification) and URLs (for recovery)
|
||||
return generate_id(CONFIG.get_int("default_token_length"))
|
||||
return generate_id(token_length)
|
||||
|
||||
|
||||
class UserTypes(models.TextChoices):
|
||||
@ -627,7 +645,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||
class ExpiringModel(models.Model):
|
||||
"""Base Model which can expire, and is automatically cleaned up."""
|
||||
|
||||
expires = models.DateTimeField(default=default_token_duration)
|
||||
expires = models.DateTimeField(default=None, null=True)
|
||||
expiring = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -10,7 +10,14 @@ from django.dispatch import receiver
|
||||
from django.http.request import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider, User
|
||||
from authentik.core.models import (
|
||||
Application,
|
||||
AuthenticatedSession,
|
||||
BackchannelProvider,
|
||||
ExpiringModel,
|
||||
User,
|
||||
default_token_duration,
|
||||
)
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
@ -61,3 +68,12 @@ def backchannel_provider_pre_save(sender: type[Model], instance: Model, **_):
|
||||
if not isinstance(instance, BackchannelProvider):
|
||||
return
|
||||
instance.is_backchannel = True
|
||||
|
||||
|
||||
@receiver(pre_save)
|
||||
def expiring_model_pre_save(sender: type[Model], instance: Model, **_):
|
||||
"""Ensure expires is set on ExpiringModels that are set to expire"""
|
||||
if not issubclass(sender, ExpiringModel):
|
||||
return
|
||||
if instance.expiring and instance.expires is None:
|
||||
instance.expires = default_token_duration()
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test token API"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from json import loads
|
||||
|
||||
from django.urls.base import reverse
|
||||
@ -7,7 +8,13 @@ from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.api.tokens import TokenSerializer
|
||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
)
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
@ -76,6 +83,77 @@ class TestTokenAPI(APITestCase):
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, False)
|
||||
|
||||
def test_token_create_expiring(self):
|
||||
"""Test token creation endpoint"""
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = True
|
||||
self.user.save()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-list"), {"identifier": "test-token"}
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
token = Token.objects.get(identifier="test-token")
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, True)
|
||||
|
||||
def test_token_create_expiring_custom_ok(self):
|
||||
"""Test token creation endpoint"""
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = True
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME] = "hours=2"
|
||||
self.user.save()
|
||||
expires = datetime.now() + timedelta(hours=1)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-list"),
|
||||
{
|
||||
"identifier": "test-token",
|
||||
"expires": expires,
|
||||
"intent": TokenIntents.INTENT_APP_PASSWORD,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
token = Token.objects.get(identifier="test-token")
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_APP_PASSWORD)
|
||||
self.assertEqual(token.expiring, True)
|
||||
self.assertEqual(token.expires.timestamp(), expires.timestamp())
|
||||
|
||||
def test_token_create_expiring_custom_nok(self):
|
||||
"""Test token creation endpoint"""
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = True
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME] = "hours=2"
|
||||
self.user.save()
|
||||
expires = datetime.now() + timedelta(hours=3)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-list"),
|
||||
{
|
||||
"identifier": "test-token",
|
||||
"expires": expires,
|
||||
"intent": TokenIntents.INTENT_APP_PASSWORD,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_token_create_expiring_custom_api(self):
|
||||
"""Test token creation endpoint"""
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = True
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME] = "hours=2"
|
||||
self.user.save()
|
||||
expires = datetime.now() + timedelta(seconds=3)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-list"),
|
||||
{
|
||||
"identifier": "test-token",
|
||||
"expires": expires,
|
||||
"intent": TokenIntents.INTENT_API,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
token = Token.objects.get(identifier="test-token")
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, True)
|
||||
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
|
||||
|
||||
def test_list(self):
|
||||
"""Test Token List (Test normal authentication)"""
|
||||
Token.objects.all().delete()
|
||||
|
@ -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"
|
||||
|
||||
cert_discovery_dir: /certs
|
||||
default_token_length: 60
|
||||
|
||||
tenants:
|
||||
enabled: false
|
||||
|
@ -8,6 +8,7 @@ from django.http import HttpRequest
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.core.models import default_token_duration
|
||||
from authentik.events.signals import get_login_event
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.constants import (
|
||||
@ -87,7 +88,9 @@ class IDToken:
|
||||
) -> "IDToken":
|
||||
"""Create ID Token"""
|
||||
id_token = IDToken(provider, token, **kwargs)
|
||||
id_token.exp = int(token.expires.timestamp())
|
||||
id_token.exp = int(
|
||||
(token.expires if token.expires is not None else default_token_duration()).timestamp()
|
||||
)
|
||||
id_token.iss = provider.get_issuer(request)
|
||||
id_token.aud = provider.client_id
|
||||
id_token.claims = {}
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -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",
|
||||
"gdpr_compliance",
|
||||
"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.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.utils import IntegrityError
|
||||
from django.dispatch import receiver
|
||||
@ -22,6 +23,9 @@ LOGGER = get_logger()
|
||||
|
||||
VALID_SCHEMA_NAME = re.compile(r"^t_[a-z0-9]{1,61}$")
|
||||
|
||||
DEFAULT_TOKEN_DURATION = "minutes=30" # nosec
|
||||
DEFAULT_TOKEN_LENGTH = 60
|
||||
|
||||
|
||||
def _validate_schema_name(name):
|
||||
if not VALID_SCHEMA_NAME.match(name):
|
||||
@ -81,6 +85,16 @@ class Tenant(TenantMixin, SerializerModel):
|
||||
impersonation = models.BooleanField(
|
||||
help_text=_("Globally enable/disable impersonation."), default=True
|
||||
)
|
||||
default_token_duration = models.TextField(
|
||||
help_text=_("Default token duration"),
|
||||
default=DEFAULT_TOKEN_DURATION,
|
||||
validators=[timedelta_string_validator],
|
||||
)
|
||||
default_token_length = models.PositiveIntegerField(
|
||||
help_text=_("Default token length"),
|
||||
default=DEFAULT_TOKEN_LENGTH,
|
||||
validators=[MinValueValidator(1)],
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.schema_name == "template":
|
||||
|
@ -6025,7 +6025,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expires": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "date-time",
|
||||
"title": "Expires"
|
||||
},
|
||||
@ -6794,7 +6797,10 @@
|
||||
"title": "Name"
|
||||
},
|
||||
"expires": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "date-time",
|
||||
"title": "Expires"
|
||||
},
|
||||
@ -7985,7 +7991,10 @@
|
||||
"title": "Description"
|
||||
},
|
||||
"expires": {
|
||||
"type": "string",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "date-time",
|
||||
"title": "Expires"
|
||||
},
|
||||
|
36
schema.yml
36
schema.yml
@ -29976,6 +29976,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
required:
|
||||
- asn
|
||||
- current
|
||||
@ -32674,6 +32675,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
scope:
|
||||
type: array
|
||||
items:
|
||||
@ -33774,6 +33776,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
fixed_data:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
@ -33810,6 +33813,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
fixed_data:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
@ -38121,6 +38125,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
fixed_data:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
@ -39421,6 +39426,15 @@ components:
|
||||
impersonation:
|
||||
type: boolean
|
||||
description: Globally enable/disable impersonation.
|
||||
default_token_duration:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Default token duration
|
||||
default_token_length:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 1
|
||||
description: Default token length
|
||||
PatchedSourceStageRequest:
|
||||
type: object
|
||||
description: SourceStage Serializer
|
||||
@ -39498,6 +39512,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
expiring:
|
||||
type: boolean
|
||||
PatchedUserDeleteStageRequest:
|
||||
@ -42450,6 +42465,14 @@ components:
|
||||
impersonation:
|
||||
type: boolean
|
||||
description: Globally enable/disable impersonation.
|
||||
default_token_duration:
|
||||
type: string
|
||||
description: Default token duration
|
||||
default_token_length:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 1
|
||||
description: Default token length
|
||||
SettingsRequest:
|
||||
type: object
|
||||
description: Settings Serializer
|
||||
@ -42481,6 +42504,15 @@ components:
|
||||
impersonation:
|
||||
type: boolean
|
||||
description: Globally enable/disable impersonation.
|
||||
default_token_duration:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: Default token duration
|
||||
default_token_length:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 1
|
||||
description: Default token length
|
||||
SeverityEnum:
|
||||
enum:
|
||||
- notice
|
||||
@ -43148,6 +43180,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
expiring:
|
||||
type: boolean
|
||||
required:
|
||||
@ -43173,6 +43206,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
scope:
|
||||
type: array
|
||||
items:
|
||||
@ -43217,6 +43251,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
expiring:
|
||||
type: boolean
|
||||
required:
|
||||
@ -43455,6 +43490,7 @@ components:
|
||||
expires:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
expiring:
|
||||
type: boolean
|
||||
user:
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
@ -192,6 +193,24 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
|
||||
help=${msg("Globally enable/disable impersonation.")}
|
||||
>
|
||||
</ak-switch-input>
|
||||
<ak-text-input
|
||||
name="defaultTokenDuration"
|
||||
label=${msg("Default token duration")}
|
||||
required
|
||||
value="${ifDefined(this._settings?.defaultTokenDuration)}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Default duration for generated tokens")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-number-input
|
||||
label=${msg("Default token length")}
|
||||
required
|
||||
name="defaultTokenLength"
|
||||
value="${first(this._settings?.defaultTokenLength, 60)}"
|
||||
help=${msg("Default length of generated tokens")}
|
||||
></ak-number-input>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ export class UserTokenForm extends ModelForm<Token, string> {
|
||||
|
||||
async send(data: Token): Promise<Token> {
|
||||
if (this.instance) {
|
||||
data.intent = this.instance.intent;
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTokensUpdate({
|
||||
identifier: this.instance.identifier,
|
||||
tokenRequest: data,
|
||||
@ -41,6 +42,14 @@ export class UserTokenForm extends ModelForm<Token, string> {
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
const now = new Date();
|
||||
const expiringDate = this.instance?.expires
|
||||
? new Date(
|
||||
this.instance.expires.getTime() -
|
||||
this.instance.expires.getTimezoneOffset() * 60000,
|
||||
)
|
||||
: new Date(now.getTime() + 30 * 60000 - now.getTimezoneOffset() * 60000);
|
||||
|
||||
return html` <ak-form-element-horizontal
|
||||
label=${msg("Identifier")}
|
||||
?required=${true}
|
||||
@ -59,6 +68,16 @@ export class UserTokenForm extends ModelForm<Token, string> {
|
||||
value="${ifDefined(this.instance?.description)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>`;
|
||||
</ak-form-element-horizontal>
|
||||
${this.intent == IntentEnum.AppPassword
|
||||
? html`<ak-form-element-horizontal label=${msg("Expiring")} name="expires">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value="${expiringDate.toISOString().slice(0, -8)}"
|
||||
min="${now.toISOString().slice(0, -8)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>`
|
||||
: html``}`;
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +160,11 @@ export class UserTokenList extends Table<Token> {
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update Token")} </span>
|
||||
<ak-user-token-form slot="form" .instancePk=${item.identifier}>
|
||||
<ak-user-token-form
|
||||
intent=${item.intent ?? IntentEnum.Api}
|
||||
slot="form"
|
||||
.instancePk=${item.identifier}
|
||||
>
|
||||
</ak-user-token-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
|
@ -272,14 +272,6 @@ Disable the inbuilt update-checker. Defaults to `false`.
|
||||
- Kubeconfig
|
||||
- Existence of a docker socket
|
||||
|
||||
### `AUTHENTIK_DEFAULT_TOKEN_LENGTH`
|
||||
|
||||
:::info
|
||||
Requires authentik 2022.4.1
|
||||
:::
|
||||
|
||||
Configure the length of generated tokens. Defaults to 60.
|
||||
|
||||
### `AUTHENTIK_LDAP__TASK_TIMEOUT_HOURS`
|
||||
|
||||
:::info
|
||||
|
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_ -->
|
@ -70,6 +70,14 @@ Optional flag, when set to false, Tokens created by the user will not expire.
|
||||
|
||||
Only applies when the token creation is triggered by the user with this attribute set. Additionally, the flag does not apply to superusers.
|
||||
|
||||
### `goauthentik.io/user/token-maximum-lifetime`:
|
||||
|
||||
Optional flag, when set, defines the maximum lifetime of user-created tokens. Defaults to the system setting if not set.
|
||||
|
||||
Only applies when `goauthentik.io/user/token-expires` set to true.
|
||||
|
||||
Format is string of format `days=10;hours=1;minute=3;seconds=5`.
|
||||
|
||||
### `goauthentik.io/user/debug`:
|
||||
|
||||
See [Troubleshooting access problems](../../troubleshooting/access), when set, the user gets a more detailed explanation of access decisions.
|
||||
|
Reference in New Issue
Block a user