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:
Jean-Michel DILLY
2024-04-11 13:05:05 +02:00
committed by GitHub
parent 40c672f246
commit a70363bd95
24 changed files with 520 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
},

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.