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 uuid import uuid4
|
||||||
|
|
||||||
from deepmerge import always_merger
|
from deepmerge import always_merger
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
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.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
from authentik.root.install_id import get_install_id
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||||
|
@ -217,7 +217,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||||
@property
|
@property
|
||||||
def uid(self) -> str:
|
def uid(self) -> str:
|
||||||
"""Generate a globally unique UID, based on the user ID and the hashed secret key"""
|
"""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:
|
def locale(self, request: Optional[HttpRequest] = None) -> str:
|
||||||
"""Get the locale the user has configured"""
|
"""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.models import Flow
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
|
||||||
|
from authentik.root.install_id import get_install_id
|
||||||
|
|
||||||
|
|
||||||
class FlowInspectorPlanSerializer(PassiveSerializer):
|
class FlowInspectorPlanSerializer(PassiveSerializer):
|
||||||
|
@ -51,7 +52,7 @@ class FlowInspectorPlanSerializer(PassiveSerializer):
|
||||||
"""Get a unique session ID"""
|
"""Get a unique session ID"""
|
||||||
request: Request = self.context["request"]
|
request: Request = self.context["request"]
|
||||||
return sha256(
|
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()
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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"""
|
"""Dynamically set SameSite depending if the upstream connection is TLS or not"""
|
||||||
|
from functools import lru_cache
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
from time import time
|
from time import time
|
||||||
from timeit import default_timer
|
from timeit import default_timer
|
||||||
|
@ -16,10 +17,16 @@ from jwt import PyJWTError, decode, encode
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
|
from authentik.root.install_id import get_install_id
|
||||||
|
|
||||||
LOGGER = get_logger("authentik.asgi")
|
LOGGER = get_logger("authentik.asgi")
|
||||||
ACR_AUTHENTIK_SESSION = "goauthentik.io/core/default"
|
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):
|
class SessionMiddleware(UpstreamSessionMiddleware):
|
||||||
|
@ -47,7 +54,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
||||||
# for testing setups, where the session is directly set
|
# for testing setups, where the session is directly set
|
||||||
session_key = key if settings.TEST else None
|
session_key = key if settings.TEST else None
|
||||||
try:
|
try:
|
||||||
session_payload = decode(key, SIGNING_HASH, algorithms=["HS256"])
|
session_payload = decode(key, get_signing_hash(), algorithms=["HS256"])
|
||||||
session_key = session_payload["sid"]
|
session_key = session_payload["sid"]
|
||||||
except (KeyError, PyJWTError):
|
except (KeyError, PyJWTError):
|
||||||
pass
|
pass
|
||||||
|
@ -114,7 +121,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
||||||
}
|
}
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
payload["sub"] = request.user.uid
|
payload["sub"] = request.user.uid
|
||||||
value = encode(payload=payload, key=SIGNING_HASH)
|
value = encode(payload=payload, key=get_signing_hash())
|
||||||
if settings.TEST:
|
if settings.TEST:
|
||||||
value = request.session.session_key
|
value = request.session.session_key
|
||||||
response.set_cookie(
|
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.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
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_sms.models import SMSDevice
|
||||||
from authentik.stages.authenticator_validate.challenge import (
|
from authentik.stages.authenticator_validate.challenge import (
|
||||||
DeviceChallenge,
|
DeviceChallenge,
|
||||||
|
@ -316,7 +317,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||||
def cookie_jwt_key(self) -> str:
|
def cookie_jwt_key(self) -> str:
|
||||||
"""Signing key for MFA Cookie for this stage"""
|
"""Signing key for MFA Cookie for this stage"""
|
||||||
return sha256(
|
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()
|
).hexdigest()
|
||||||
|
|
||||||
def check_mfa_cookie(self, allowed_devices: list[Device]):
|
def check_mfa_cookie(self, allowed_devices: list[Device]):
|
||||||
|
|
|
@ -3,7 +3,6 @@ from datetime import datetime, timedelta
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from django_otp.oath import TOTP
|
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.tests import FlowTestCase
|
||||||
from authentik.flows.views.executor import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.root.install_id import get_install_id
|
||||||
from authentik.stages.authenticator_validate.challenge import (
|
from authentik.stages.authenticator_validate.challenge import (
|
||||||
get_challenge_for_device,
|
get_challenge_for_device,
|
||||||
validate_challenge_code,
|
validate_challenge_code,
|
||||||
|
@ -194,7 +194,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
"stage": stage.pk.hex + generate_id(),
|
"stage": stage.pk.hex + generate_id(),
|
||||||
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
|
"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(
|
response = self.client.post(
|
||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
|
@ -233,7 +233,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
"stage": stage.pk.hex,
|
"stage": stage.pk.hex,
|
||||||
"exp": (datetime.now() + timedelta(days=3)).timestamp(),
|
"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(
|
response = self.client.post(
|
||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||||
|
@ -272,7 +272,7 @@ class AuthenticatorValidateStageTOTPTests(FlowTestCase):
|
||||||
"stage": stage.pk.hex,
|
"stage": stage.pk.hex,
|
||||||
"exp": (datetime.now() - timedelta(days=3)).timestamp(),
|
"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(
|
response = self.client.post(
|
||||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
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.config import CONFIG
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.lib.utils.reflection import get_env
|
from authentik.lib.utils.reflection import get_env
|
||||||
|
from authentik.root.install_id import get_install_id
|
||||||
from lifecycle.worker import DjangoUvicornWorker
|
from lifecycle.worker import DjangoUvicornWorker
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -148,9 +149,7 @@ if not CONFIG.y_bool("disable_startup_analytics", False):
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
headers={
|
headers={
|
||||||
"User-Agent": sha512(str(CONFIG.y("secret_key")).encode("ascii")).hexdigest()[
|
"User-Agent": sha512(get_install_id().encode("ascii")).hexdigest()[:16],
|
||||||
:16
|
|
||||||
],
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
timeout=5,
|
timeout=5,
|
||||||
|
|
|
@ -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 New Issue