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 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() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										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""" | """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, | ||||||
|  | |||||||
							
								
								
									
										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