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