diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index 5fb185cacb..43637f10d7 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -2,6 +2,7 @@ from typing import Any +from django.utils.timezone import now from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer from guardian.shortcuts import assign_perm, get_anonymous_user @@ -27,7 +28,6 @@ from authentik.core.models import ( TokenIntents, User, default_token_duration, - token_expires_from_timedelta, ) from authentik.events.models import Event, EventAction from authentik.events.utils import model_to_dict @@ -68,15 +68,17 @@ class TokenSerializer(ManagedSerializer, ModelSerializer): 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) + max_token_lifetime_dt = now() + timedelta_from_string(max_token_lifetime) except ValueError: - max_token_lifetime_dt = default_token_duration() + pass - if "expires" in attrs and attrs.get("expires") > token_expires_from_timedelta( - max_token_lifetime_dt - ): + if "expires" in attrs and attrs.get("expires") > max_token_lifetime_dt: raise ValidationError( - {"expires": f"Token expires exceeds maximum lifetime ({max_token_lifetime})."} + { + "expires": ( + f"Token expires exceeds maximum lifetime ({max_token_lifetime_dt} UTC)." + ) + } ) elif attrs.get("intent") == TokenIntents.INTENT_API: # For API tokens, expires cannot be overridden diff --git a/authentik/core/models.py b/authentik/core/models.py index 651490fccd..5b1e4076c7 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -1,6 +1,6 @@ """authentik core models""" -from datetime import datetime, timedelta +from datetime import datetime from hashlib import sha256 from typing import Any, Optional, Self from uuid import uuid4 @@ -68,11 +68,6 @@ def default_token_duration() -> datetime: return now() + timedelta_from_string(token_duration) -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() diff --git a/authentik/tenants/models.py b/authentik/tenants/models.py index 4dbf8684d8..df44159c37 100644 --- a/authentik/tenants/models.py +++ b/authentik/tenants/models.py @@ -23,7 +23,7 @@ LOGGER = get_logger() VALID_SCHEMA_NAME = re.compile(r"^t_[a-z0-9]{1,61}$") -DEFAULT_TOKEN_DURATION = "minutes=30" # nosec +DEFAULT_TOKEN_DURATION = "days=1" # nosec DEFAULT_TOKEN_LENGTH = 60 diff --git a/web/src/common/utils.ts b/web/src/common/utils.ts index ff4fe512b3..7c5dfff92a 100644 --- a/web/src/common/utils.ts +++ b/web/src/common/utils.ts @@ -111,6 +111,21 @@ export function dateTimeLocal(date: Date): string { return `${parts[0]}:${parts[1]}`; } +export function dateToUTC(date: Date): Date { + // Sigh...so our API is UTC/can take TZ info in the ISO format as it should. + // datetime-local fields (which is almost the only date-time input we use) + // can return its value as a UTC timestamp...however the generated API client + // _requires_ a Date object, only to then convert it to an ISO string anyways + // JS Dates don't include timezone info in the ISO string, so that just sends + // the local time as UTC...which is wrong + // Instead we have to do this, convert the given date to a UTC timestamp, + // then subtract the timezone offset to create an "invalid" date (correct time&date) + // but it still "thinks" it's in local TZ + const timestamp = date.getTime(); + const offset = -1 * (new Date().getTimezoneOffset() * 60000); + return new Date(timestamp - offset); +} + // Lit is extremely well-typed with regard to CSS, and Storybook's `build` does not currently have a // coherent way of importing CSS-as-text into CSSStyleSheet. It works well when Storybook is running // in `dev,` but in `build` it fails. Storied components will have to map their textual CSS imports diff --git a/web/src/elements/forms/Form.ts b/web/src/elements/forms/Form.ts index 220df8183f..0530f499bb 100644 --- a/web/src/elements/forms/Form.ts +++ b/web/src/elements/forms/Form.ts @@ -1,6 +1,6 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { MessageLevel } from "@goauthentik/common/messages"; -import { camelToSnake, convertToSlug } from "@goauthentik/common/utils"; +import { camelToSnake, convertToSlug, dateToUTC } from "@goauthentik/common/utils"; import { AKElement } from "@goauthentik/elements/Base"; import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement"; import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; @@ -104,7 +104,7 @@ export function serializeForm( inputElement.tagName.toLowerCase() === "input" && inputElement.type === "datetime-local" ) { - assignValue(inputElement, new Date(inputElement.valueAsNumber), json); + assignValue(inputElement, dateToUTC(new Date(inputElement.valueAsNumber)), json); } else if ( inputElement.tagName.toLowerCase() === "input" && "type" in inputElement.dataset && @@ -112,7 +112,7 @@ export function serializeForm( ) { // Workaround for Firefox <93, since 92 and older don't support // datetime-local fields - assignValue(inputElement, new Date(inputElement.value), json); + assignValue(inputElement, dateToUTC(new Date(inputElement.value)), json); } else if ( inputElement.tagName.toLowerCase() === "input" && inputElement.type === "checkbox" diff --git a/web/src/user/user-settings/tokens/UserTokenForm.ts b/web/src/user/user-settings/tokens/UserTokenForm.ts index b3d2088b6f..eb94821616 100644 --- a/web/src/user/user-settings/tokens/UserTokenForm.ts +++ b/web/src/user/user-settings/tokens/UserTokenForm.ts @@ -1,3 +1,4 @@ +import { dateTimeLocal } from "@goauthentik/authentik/common/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import "@goauthentik/elements/forms/HorizontalFormElement"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; @@ -44,11 +45,8 @@ export class UserTokenForm extends ModelForm { 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); + ? new Date(this.instance.expires.getTime()) + : new Date(now.getTime() + 30 * 60000); return html` { ? html` `