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:
committed by
GitHub
parent
f78adab9d1
commit
02709e4ede
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>`
|
||||||
|
|||||||
Reference in New Issue
Block a user