root: add install ID (#5717)
* root: add install ID Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add fallback when no migrations table exists Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
e3e1fbad3f
commit
0d0bb1a559
|
@ -5,7 +5,6 @@ from typing import Any, Optional
|
|||
from uuid import uuid4
|
||||
|
||||
from deepmerge import always_merger
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
|
@ -33,6 +32,7 @@ from authentik.lib.models import (
|
|||
)
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.root.install_id import get_install_id
|
||||
|
||||
LOGGER = get_logger()
|
||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||
|
@ -217,7 +217,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||
@property
|
||||
def uid(self) -> str:
|
||||
"""Generate a globally unique UID, based on the user ID and the hashed secret key"""
|
||||
return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
|
||||
return sha256(f"{self.id}-{get_install_id()}".encode("ascii")).hexdigest()
|
||||
|
||||
def locale(self, request: Optional[HttpRequest] = None) -> str:
|
||||
"""Get the locale the user has configured"""
|
||||
|
|
|
@ -23,6 +23,7 @@ from authentik.flows.api.bindings import FlowStageBindingSerializer
|
|||
from authentik.flows.models import Flow
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
|
||||
from authentik.root.install_id import get_install_id
|
||||
|
||||
|
||||
class FlowInspectorPlanSerializer(PassiveSerializer):
|
||||
|
@ -51,7 +52,7 @@ class FlowInspectorPlanSerializer(PassiveSerializer):
|
|||
"""Get a unique session ID"""
|
||||
request: Request = self.context["request"]
|
||||
return sha256(
|
||||
f"{request._request.session.session_key}-{settings.SECRET_KEY}".encode("ascii")
|
||||
f"{request._request.session.session_key}-{get_install_id()}".encode("ascii")
|
||||
).hexdigest()
|
||||
|
||||
|
||||
|
|
17
authentik/root/install_id.py
Normal file
17
authentik/root/install_id.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
"""install ID"""
|
||||
from functools import lru_cache
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_install_id() -> str:
|
||||
"""Get install ID of this instance. The method is cached as the install ID is
|
||||
not expected to change"""
|
||||
if settings.TEST:
|
||||
return str(uuid4())
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT id FROM authentik_install_id LIMIT 1;")
|
||||
return cursor.fetchone()[0]
|
|
@ -1,4 +1,5 @@
|
|||
"""Dynamically set SameSite depending if the upstream connection is TLS or not"""
|
||||
from functools import lru_cache
|
||||
from hashlib import sha512
|
||||
from time import time
|
||||
from timeit import default_timer
|
||||
|
@ -16,10 +17,16 @@ from jwt import PyJWTError, decode, encode
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.root.install_id import get_install_id
|
||||
|
||||
LOGGER = get_logger("authentik.asgi")
|
||||
ACR_AUTHENTIK_SESSION = "goauthentik.io/core/default"
|
||||
SIGNING_HASH = sha512(settings.SECRET_KEY.encode()).hexdigest()
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_signing_hash():
|
||||
"""Get cookie JWT signing hash"""
|
||||
return sha512(get_install_id().encode()).hexdigest()
|
||||
|
||||
|
||||
class SessionMiddleware(UpstreamSessionMiddleware):
|
||||
|
@ -47,7 +54,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
|||
# for testing setups, where the session is directly set
|
||||
session_key = key if settings.TEST else None
|
||||
try:
|
||||
session_payload = decode(key, SIGNING_HASH, algorithms=["HS256"])
|
||||
session_payload = decode(key, get_signing_hash(), algorithms=["HS256"])
|
||||
session_key = session_payload["sid"]
|
||||
except (KeyError, PyJWTError):
|
||||
pass
|
||||
|
@ -114,7 +121,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
|||
}
|
||||
if request.user.is_authenticated:
|
||||
payload["sub"] = request.user.uid
|
||||
value = encode(payload=payload, key=SIGNING_HASH)
|
||||
value = encode(payload=payload, key=get_signing_hash())
|
||||
if settings.TEST:
|
||||
value = request.session.session_key
|
||||
response.set_cookie(
|
||||
|
|
|
@ -20,6 +20,7 @@ from authentik.flows.models import FlowDesignation, NotConfiguredAction, Stage
|
|||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.root.install_id import get_install_id
|
||||
from authentik.stages.authenticator_sms.models import SMSDevice
|
||||
from authentik.stages.authenticator_validate.challenge import (
|
||||
DeviceChallenge,
|
||||
|
@ -316,7 +317,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
def cookie_jwt_key(self) -> str:
|
||||
"""Signing key for MFA Cookie for this stage"""
|
||||
return sha256(
|
||||
f"{settings.SECRET_KEY}:{self.executor.current_stage.pk.hex}".encode("ascii")
|
||||
f"{get_install_id()}:{self.executor.current_stage.pk.hex}".encode("ascii")
|
||||
).hexdigest()
|
||||
|
||||
def check_mfa_cookie(self, allowed_devices: list[Device]):
|
||||
|
|
|
@ -3,7 +3,6 @@ from datetime import datetime, timedelta
|
|||
from hashlib import sha256
|
||||
from time import sleep
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls.base import reverse
|
||||
from django_otp.oath import TOTP
|
||||
|
@ -17,6 +16,7 @@ from authentik.flows.stage import StageView
|
|||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import FlowExecutorView
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.root.install_id import get_install_id
|
||||
from authentik.stages.authenticator_validate.challenge import (
|
||||
get_challenge_for_device,
|
||||
validate_challenge_code,
|
||||
|
@ -194,7 +194,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
|||
"stage": stage.pk.hex + generate_id(),
|
||||
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
|
||||
},
|
||||
key=sha256(f"{settings.SECRET_KEY}:{stage.pk.hex}".encode("ascii")).hexdigest(),
|
||||
key=sha256(f"{get_install_id()}:{stage.pk.hex}".encode("ascii")).hexdigest(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
|
@ -233,7 +233,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
|||
"stage": stage.pk.hex,
|
||||
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
|
||||
},
|
||||
key=sha256(f"{settings.SECRET_KEY}:{stage.pk.hex}".encode("ascii")).hexdigest(),
|
||||
key=sha256(f"{get_install_id()}:{stage.pk.hex}".encode("ascii")).hexdigest(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
|
@ -272,7 +272,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
|||
"stage": stage.pk.hex,
|
||||
"exp": (datetime.now() - timedelta(days=3)).timestamp(),
|
||||
},
|
||||
key=sha256(f"{settings.SECRET_KEY}:{stage.pk.hex}".encode("ascii")).hexdigest(),
|
||||
key=sha256(f"{get_install_id()}:{stage.pk.hex}".encode("ascii")).hexdigest(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
|
|
|
@ -15,6 +15,7 @@ from authentik import get_full_version
|
|||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.lib.utils.reflection import get_env
|
||||
from authentik.root.install_id import get_install_id
|
||||
from lifecycle.worker import DjangoUvicornWorker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -148,9 +149,7 @@ if not CONFIG.y_bool("disable_startup_analytics", False):
|
|||
),
|
||||
},
|
||||
headers={
|
||||
"User-Agent": sha512(str(CONFIG.y("secret_key")).encode("ascii")).hexdigest()[
|
||||
:16
|
||||
],
|
||||
"User-Agent": sha512(get_install_id().encode("ascii")).hexdigest()[:16],
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=5,
|
||||
|
|
45
lifecycle/system_migrations/install_id.py
Normal file
45
lifecycle/system_migrations/install_id.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# flake8: noqa
|
||||
from uuid import uuid4
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
from lifecycle.migrate import BaseMigration
|
||||
|
||||
SQL_STATEMENT = """BEGIN TRANSACTION;
|
||||
CREATE TABLE IF NOT EXISTS authentik_install_id (
|
||||
id TEXT NOT NULL
|
||||
);
|
||||
COMMIT;"""
|
||||
|
||||
|
||||
class Migration(BaseMigration):
|
||||
def needs_migration(self) -> bool:
|
||||
self.cur.execute(
|
||||
"select * from information_schema.tables where table_name = 'authentik_install_id';"
|
||||
)
|
||||
return not bool(self.cur.rowcount)
|
||||
|
||||
def upgrade(self, migrate=False):
|
||||
self.cur.execute(SQL_STATEMENT)
|
||||
self.con.commit()
|
||||
if migrate:
|
||||
# If we already have migrations in the database, assume we're upgrading an existing install
|
||||
# and set the install id to the secret key
|
||||
self.cur.execute(
|
||||
"INSERT INTO authentik_install_id (id) VALUES (%s)", (CONFIG.y("secret_key"),)
|
||||
)
|
||||
else:
|
||||
# Otherwise assume a new install, generate an install ID based on a UUID
|
||||
install_id = str(uuid4())
|
||||
self.cur.execute("INSERT INTO authentik_install_id (id) VALUES (%s)", (install_id,))
|
||||
self.con.commit()
|
||||
|
||||
def run(self):
|
||||
self.cur.execute(
|
||||
"select * from information_schema.tables where table_name = 'django_migrations';"
|
||||
)
|
||||
if not bool(self.cur.rowcount):
|
||||
# No django_migrations table, so generate a new id
|
||||
return self.upgrade(migrate=False)
|
||||
self.cur.execute("select count(*) from django_migrations;")
|
||||
migrations = self.cur.fetchone()[0]
|
||||
return self.upgrade(migrate=migrations > 0)
|
Reference in a new issue