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:
Jens L 2023-05-22 17:24:12 +02:00 committed by GitHub
parent e3e1fbad3f
commit 0d0bb1a559
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 84 additions and 14 deletions

View file

@ -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"""

View file

@ -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()

View 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]

View file

@ -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(

View file

@ -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]):

View file

@ -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}),

View file

@ -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,

View 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)