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:
		| @ -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 New Issue
	
	Block a user
	 Jens L
					Jens L