core: fix logic for token expiration (cherry-pick #9426) (#9428)

core: fix logic for token expiration (#9426)

* core: fix logic for token expiration



* bump default token expiration



* fix frontend



* fix



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
This commit is contained in:
gcp-cherry-pick-bot[bot]
2024-04-25 16:05:30 +02:00
committed by GitHub
parent f78adab9d1
commit 02709e4ede
6 changed files with 34 additions and 24 deletions

View File

@ -2,6 +2,7 @@
from typing import Any from typing import Any
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user from guardian.shortcuts import assign_perm, get_anonymous_user
@ -27,7 +28,6 @@ from authentik.core.models import (
TokenIntents, TokenIntents,
User, User,
default_token_duration, 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
@ -68,15 +68,17 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
max_token_lifetime_dt = default_token_duration() max_token_lifetime_dt = default_token_duration()
if max_token_lifetime is not None: if max_token_lifetime is not None:
try: try:
max_token_lifetime_dt = timedelta_from_string(max_token_lifetime) max_token_lifetime_dt = now() + timedelta_from_string(max_token_lifetime)
except ValueError: except ValueError:
max_token_lifetime_dt = default_token_duration() pass
if "expires" in attrs and attrs.get("expires") > token_expires_from_timedelta( if "expires" in attrs and attrs.get("expires") > max_token_lifetime_dt:
max_token_lifetime_dt
):
raise ValidationError( 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: elif attrs.get("intent") == TokenIntents.INTENT_API:
# For API tokens, expires cannot be overridden # For API tokens, expires cannot be overridden

View File

@ -1,6 +1,6 @@
"""authentik core models""" """authentik core models"""
from datetime import datetime, timedelta from datetime import datetime
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
@ -68,11 +68,6 @@ def default_token_duration() -> datetime:
return now() + timedelta_from_string(token_duration) 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: def default_token_key() -> str:
"""Default token key""" """Default token key"""
current_tenant = get_current_tenant() current_tenant = get_current_tenant()

View File

@ -23,7 +23,7 @@ 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_DURATION = "days=1" # nosec
DEFAULT_TOKEN_LENGTH = 60 DEFAULT_TOKEN_LENGTH = 60

View File

@ -111,6 +111,21 @@ export function dateTimeLocal(date: Date): string {
return `${parts[0]}:${parts[1]}`; 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 // 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 // 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 // in `dev,` but in `build` it fails. Storied components will have to map their textual CSS imports

View File

@ -1,6 +1,6 @@
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { MessageLevel } from "@goauthentik/common/messages"; 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 { AKElement } from "@goauthentik/elements/Base";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement"; import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
@ -104,7 +104,7 @@ export function serializeForm<T extends KeyUnknown>(
inputElement.tagName.toLowerCase() === "input" && inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "datetime-local" inputElement.type === "datetime-local"
) { ) {
assignValue(inputElement, new Date(inputElement.valueAsNumber), json); assignValue(inputElement, dateToUTC(new Date(inputElement.valueAsNumber)), json);
} else if ( } else if (
inputElement.tagName.toLowerCase() === "input" && inputElement.tagName.toLowerCase() === "input" &&
"type" in inputElement.dataset && "type" in inputElement.dataset &&
@ -112,7 +112,7 @@ export function serializeForm<T extends KeyUnknown>(
) { ) {
// Workaround for Firefox <93, since 92 and older don't support // Workaround for Firefox <93, since 92 and older don't support
// datetime-local fields // datetime-local fields
assignValue(inputElement, new Date(inputElement.value), json); assignValue(inputElement, dateToUTC(new Date(inputElement.value)), json);
} else if ( } else if (
inputElement.tagName.toLowerCase() === "input" && inputElement.tagName.toLowerCase() === "input" &&
inputElement.type === "checkbox" inputElement.type === "checkbox"

View File

@ -1,3 +1,4 @@
import { dateTimeLocal } from "@goauthentik/authentik/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -44,11 +45,8 @@ export class UserTokenForm extends ModelForm<Token, string> {
renderForm(): TemplateResult { renderForm(): TemplateResult {
const now = new Date(); const now = new Date();
const expiringDate = this.instance?.expires const expiringDate = this.instance?.expires
? new Date( ? new Date(this.instance.expires.getTime())
this.instance.expires.getTime() - : new Date(now.getTime() + 30 * 60000);
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")}
@ -73,8 +71,8 @@ export class UserTokenForm extends ModelForm<Token, string> {
? html`<ak-form-element-horizontal label=${msg("Expiring")} name="expires"> ? html`<ak-form-element-horizontal label=${msg("Expiring")} name="expires">
<input <input
type="datetime-local" type="datetime-local"
value="${expiringDate.toISOString().slice(0, -8)}" value="${dateTimeLocal(expiringDate)}"
min="${now.toISOString().slice(0, -8)}" min="${dateTimeLocal(now)}"
class="pf-c-form-control" class="pf-c-form-control"
/> />
</ak-form-element-horizontal>` </ak-form-element-horizontal>`