authentik.root.middleware

Dynamically set SameSite depending if the upstream connection is TLS or not

  1"""Dynamically set SameSite depending if the upstream connection is TLS or not"""
  2
  3from collections.abc import Callable
  4from hashlib import sha512
  5from ipaddress import ip_address
  6from time import perf_counter, time
  7from typing import Any
  8
  9from channels.exceptions import DenyConnection
 10from django.conf import settings
 11from django.contrib.sessions.backends.base import UpdateError
 12from django.contrib.sessions.exceptions import SessionInterrupted
 13from django.contrib.sessions.middleware import SessionMiddleware as UpstreamSessionMiddleware
 14from django.http.request import HttpRequest
 15from django.http.response import HttpResponse, HttpResponseServerError
 16from django.middleware.csrf import CSRF_SESSION_KEY
 17from django.middleware.csrf import CsrfViewMiddleware as UpstreamCsrfViewMiddleware
 18from django.utils.cache import patch_vary_headers
 19from django.utils.http import http_date
 20from jwt import PyJWTError, decode, encode
 21from sentry_sdk import Scope
 22from structlog.stdlib import get_logger
 23
 24from authentik.core.models import Token, TokenIntents, User, UserTypes
 25from authentik.lib.config import CONFIG
 26
 27LOGGER = get_logger("authentik.asgi")
 28ACR_AUTHENTIK_SESSION = "goauthentik.io/core/default"
 29SIGNING_HASH = sha512(settings.SECRET_KEY.encode()).hexdigest()
 30
 31
 32class SessionMiddleware(UpstreamSessionMiddleware):
 33    """Dynamically set SameSite depending if the upstream connection is TLS or not"""
 34
 35    @staticmethod
 36    def is_secure(request: HttpRequest) -> bool:
 37        """Check if request is TLS'd or localhost"""
 38        if request.is_secure():
 39            return True
 40        host, _, _ = request.get_host().partition(":")
 41        if host == "localhost" and settings.DEBUG:
 42            # Since go does not consider localhost with http a secure origin
 43            # we can't set the secure flag.
 44            user_agent = request.META.get("HTTP_USER_AGENT", "")
 45            if user_agent.startswith("goauthentik.io/outpost/") or (
 46                "safari" in user_agent.lower() and "chrome" not in user_agent.lower()
 47            ):
 48                return False
 49            return True
 50        return False
 51
 52    @staticmethod
 53    def decode_session_key(key: str | None) -> str | None:
 54        """Decode raw session cookie, and parse JWT"""
 55        # We need to support the standard django format of just a session key
 56        # for testing setups, where the session is directly set
 57        session_key = key if settings.TEST else None
 58        try:
 59            session_payload = decode(key, SIGNING_HASH, algorithms=["HS256"])
 60            session_key = session_payload["sid"]
 61        except KeyError, PyJWTError:
 62            pass
 63        return session_key
 64
 65    @staticmethod
 66    def encode_session(session_key: str, user: User):
 67        payload = {
 68            "sid": session_key,
 69            "iss": "authentik",
 70            "sub": "anonymous",
 71            "authenticated": user.is_authenticated,
 72            "acr": ACR_AUTHENTIK_SESSION,
 73        }
 74        if user.is_authenticated:
 75            payload["sub"] = user.uid
 76        value = encode(payload=payload, key=SIGNING_HASH)
 77        if settings.TEST:
 78            value = session_key
 79        return value
 80
 81    def process_request(self, request: HttpRequest):
 82        raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
 83        session_key = SessionMiddleware.decode_session_key(raw_session)
 84        request.session = self.SessionStore(
 85            session_key,
 86            last_ip=ClientIPMiddleware.get_client_ip(request),
 87            last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
 88        )
 89
 90    def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
 91        """
 92        If request.session was modified, or if the configuration is to save the
 93        session every time, save the changes and set a session cookie or delete
 94        the session cookie if the session has been emptied.
 95        """
 96        try:
 97            accessed = request.session.accessed
 98            modified = request.session.modified
 99            empty = request.session.is_empty()
100        except AttributeError:
101            return response
102        # Set SameSite based on whether or not the request is secure
103        secure = SessionMiddleware.is_secure(request)
104        same_site = "None" if secure else "Lax"
105        # First check if we need to delete this cookie.
106        # The session should be deleted only if the session is entirely empty.
107        if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
108            response.delete_cookie(
109                settings.SESSION_COOKIE_NAME,
110                path=settings.SESSION_COOKIE_PATH,
111                domain=settings.SESSION_COOKIE_DOMAIN,
112                samesite=same_site,
113            )
114            patch_vary_headers(response, ("Cookie",))
115        else:
116            if accessed:
117                patch_vary_headers(response, ("Cookie",))
118            if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
119                if request.session.get_expire_at_browser_close():
120                    max_age = None
121                    expires = None
122                else:
123                    max_age = request.session.get_expiry_age()
124                    expires_time = time() + max_age
125                    expires = http_date(expires_time)
126                # Save the session data and refresh the client cookie.
127                # Skip session save for 500 responses, refs #3881.
128                if response.status_code != HttpResponseServerError.status_code:
129                    try:
130                        request.session.save()
131                    except UpdateError:
132                        raise SessionInterrupted(
133                            "The request's session was deleted before the "
134                            "request completed. The user may have logged "
135                            "out in a concurrent request, for example."
136                        ) from None
137                    response.set_cookie(
138                        settings.SESSION_COOKIE_NAME,
139                        SessionMiddleware.encode_session(request.session.session_key, request.user),
140                        max_age=max_age,
141                        expires=expires,
142                        domain=settings.SESSION_COOKIE_DOMAIN,
143                        path=settings.SESSION_COOKIE_PATH,
144                        secure=secure,
145                        httponly=settings.SESSION_COOKIE_HTTPONLY or None,
146                        samesite=same_site,
147                    )
148        return response
149
150
151class CsrfViewMiddleware(UpstreamCsrfViewMiddleware):
152    """Dynamically set secure depending if the upstream connection is TLS or not"""
153
154    def _set_csrf_cookie(self, request: HttpRequest, response: HttpResponse):
155        if settings.CSRF_USE_SESSIONS:
156            if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
157                request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
158        else:
159            secure = SessionMiddleware.is_secure(request)
160            response.set_cookie(
161                settings.CSRF_COOKIE_NAME,
162                request.META["CSRF_COOKIE"],
163                max_age=settings.CSRF_COOKIE_AGE,
164                domain=settings.CSRF_COOKIE_DOMAIN,
165                path=settings.CSRF_COOKIE_PATH,
166                secure=secure,
167                httponly=settings.CSRF_COOKIE_HTTPONLY,
168                samesite=settings.CSRF_COOKIE_SAMESITE,
169            )
170            # Set the Vary header since content varies with the CSRF cookie.
171            patch_vary_headers(response, ("Cookie",))
172
173
174class ClientIPMiddleware:
175    """Set a "known-good" client IP on the request, by default based off of x-forwarded-for
176    which is set by the go proxy, but also allowing the remote IP to be overridden by an outpost
177    for protocols like LDAP"""
178
179    get_response: Callable[[HttpRequest], HttpResponse]
180    outpost_remote_ip_header = "HTTP_X_AUTHENTIK_REMOTE_IP"
181    outpost_token_header = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN"  # nosec
182    default_ip = "255.255.255.255"
183
184    request_attr_client_ip = "client_ip"
185    request_attr_outpost_user = "outpost_user"
186
187    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
188        self.get_response = get_response
189        self.logger = get_logger().bind()
190
191    def _get_client_ip_from_meta(self, meta: dict[str, Any]) -> str:
192        """Attempt to get the client's IP by checking common HTTP Headers.
193        Returns none if no IP Could be found
194
195        No additional validation is done here as requests are expected to only arrive here
196        via the go proxy, which deals with validating these headers for us"""
197        headers = (
198            "HTTP_X_FORWARDED_FOR",
199            "REMOTE_ADDR",
200        )
201        try:
202            for _header in headers:
203                if _header in meta:
204                    ips: list[str] = meta.get(_header).split(",")
205                    # Ensure the IP parses as a valid IP
206                    return str(ip_address(ips[0].strip()))
207            return self.default_ip
208        except ValueError as exc:
209            self.logger.debug("Invalid remote IP", exc=exc)
210            return self.default_ip
211
212    # FIXME: this should probably not be in `root` but rather in a middleware in `outposts`
213    # but for now it's fine
214    def _get_outpost_override_ip(self, request: HttpRequest) -> str | None:
215        """Get the actual remote IP when set by an outpost. Only
216        allowed when the request is authenticated, by an outpost internal service account"""
217        if (
218            self.outpost_remote_ip_header not in request.META
219            or self.outpost_token_header not in request.META
220        ):
221            return None
222        delegated_ip = request.META[self.outpost_remote_ip_header]
223        token = (
224            Token.objects.filter(
225                key=request.META.get(self.outpost_token_header), intent=TokenIntents.INTENT_API
226            )
227            .select_related("user")
228            .first()
229        )
230        if not token:
231            LOGGER.warning("Attempted remote-ip override without token", delegated_ip=delegated_ip)
232            return None
233        user: User = token.user
234        if user.type != UserTypes.INTERNAL_SERVICE_ACCOUNT:
235            LOGGER.warning(
236                "Remote-IP override: user doesn't have permission",
237                user=user,
238                delegated_ip=delegated_ip,
239            )
240            return None
241        # Update sentry scope to include correct IP
242        sentry_user = Scope.get_isolation_scope()._user or {}
243        sentry_user["ip_address"] = delegated_ip
244        Scope.get_isolation_scope().set_user(sentry_user)
245        # Set the outpost service account on the request
246        setattr(request, self.request_attr_outpost_user, user)
247        try:
248            return str(ip_address(delegated_ip))
249        except ValueError as exc:
250            self.logger.debug("Invalid remote IP from Outpost", exc=exc)
251            return None
252
253    def _get_client_ip(self, request: HttpRequest | None) -> str:
254        """Attempt to get the client's IP by checking common HTTP Headers.
255        Returns none if no IP Could be found"""
256        if not request:
257            return self.default_ip
258        override = self._get_outpost_override_ip(request)
259        if override:
260            return override
261        return self._get_client_ip_from_meta(request.META)
262
263    @staticmethod
264    def get_outpost_user(request: HttpRequest) -> User | None:
265        """Get outpost user that authenticated this request"""
266        return getattr(request, ClientIPMiddleware.request_attr_outpost_user, None)
267
268    @staticmethod
269    def get_client_ip(request: HttpRequest) -> str:
270        """Get correct client IP, including any overrides from outposts that
271        have the permission to do so"""
272        if request and not hasattr(request, ClientIPMiddleware.request_attr_client_ip):
273            ClientIPMiddleware(lambda request: request).set_ip(request)
274        return getattr(
275            request, ClientIPMiddleware.request_attr_client_ip, ClientIPMiddleware.default_ip
276        )
277
278    def set_ip(self, request: HttpRequest):
279        """Set the IP"""
280        setattr(request, self.request_attr_client_ip, self._get_client_ip(request))
281
282    def __call__(self, request: HttpRequest) -> HttpResponse:
283        self.set_ip(request)
284        return self.get_response(request)
285
286
287class ChannelsLoggingMiddleware:
288    """Logging middleware for channels"""
289
290    def __init__(self, inner):
291        self.inner = inner
292
293    async def __call__(self, scope, receive, send):
294        self.log(scope)
295        try:
296            return await self.inner(scope, receive, send)
297        except DenyConnection:
298            return await send({"type": "websocket.close"})
299        except Exception as exc:
300            if settings.DEBUG or settings.TEST:
301                raise exc
302            LOGGER.warning("Exception in ASGI application", exc=exc)
303            return await send({"type": "websocket.close"})
304
305    def log(self, scope: dict, **kwargs):
306        """Log request"""
307        headers = dict(scope.get("headers", {}))
308        LOGGER.info(
309            scope["path"],
310            scheme="ws",
311            remote=headers.get(b"x-forwarded-for", b"").decode(),
312            user_agent=headers.get(b"user-agent", b"").decode(),
313            **kwargs,
314        )
315
316
317class LoggingMiddleware:
318    """Logger middleware"""
319
320    get_response: Callable[[HttpRequest], HttpResponse]
321
322    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
323        self.get_response = get_response
324        headers = CONFIG.get("log.http_headers", [])
325        if isinstance(headers, str):
326            headers = headers.split(",")
327        self.headers_to_log = headers
328
329    def __call__(self, request: HttpRequest) -> HttpResponse:
330        start = perf_counter()
331        response = self.get_response(request)
332        status_code = response.status_code
333        kwargs = {
334            "request_id": getattr(request, "request_id", None),
335        }
336        kwargs.update(getattr(response, "ak_context", {}))
337        self.log(request, status_code, int((perf_counter() - start) * 1000), **kwargs)
338        return response
339
340    def log(self, request: HttpRequest, status_code: int, runtime: int, **kwargs):
341        """Log request"""
342        for header in self.headers_to_log:
343            header_value = request.headers.get(header)
344            if not header_value:
345                continue
346            kwargs[header.lower().replace("-", "_")] = header_value
347        LOGGER.info(
348            request.get_full_path(),
349            remote=ClientIPMiddleware.get_client_ip(request),
350            method=request.method,
351            scheme=request.scheme,
352            status=status_code,
353            runtime=runtime,
354            **kwargs,
355        )
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=('authentik.asgi',))>
ACR_AUTHENTIK_SESSION = 'goauthentik.io/core/default'
SIGNING_HASH = '3bfedf95cebfd13f2fdb9decf6afd21c4ce0f0749756e659612be2d342684dcbf719df6ad18cffd11ac31d596bef9d12093971c5fe4e5948df26eda502003505'
class SessionMiddleware(django.contrib.sessions.middleware.SessionMiddleware):
 33class SessionMiddleware(UpstreamSessionMiddleware):
 34    """Dynamically set SameSite depending if the upstream connection is TLS or not"""
 35
 36    @staticmethod
 37    def is_secure(request: HttpRequest) -> bool:
 38        """Check if request is TLS'd or localhost"""
 39        if request.is_secure():
 40            return True
 41        host, _, _ = request.get_host().partition(":")
 42        if host == "localhost" and settings.DEBUG:
 43            # Since go does not consider localhost with http a secure origin
 44            # we can't set the secure flag.
 45            user_agent = request.META.get("HTTP_USER_AGENT", "")
 46            if user_agent.startswith("goauthentik.io/outpost/") or (
 47                "safari" in user_agent.lower() and "chrome" not in user_agent.lower()
 48            ):
 49                return False
 50            return True
 51        return False
 52
 53    @staticmethod
 54    def decode_session_key(key: str | None) -> str | None:
 55        """Decode raw session cookie, and parse JWT"""
 56        # We need to support the standard django format of just a session key
 57        # for testing setups, where the session is directly set
 58        session_key = key if settings.TEST else None
 59        try:
 60            session_payload = decode(key, SIGNING_HASH, algorithms=["HS256"])
 61            session_key = session_payload["sid"]
 62        except KeyError, PyJWTError:
 63            pass
 64        return session_key
 65
 66    @staticmethod
 67    def encode_session(session_key: str, user: User):
 68        payload = {
 69            "sid": session_key,
 70            "iss": "authentik",
 71            "sub": "anonymous",
 72            "authenticated": user.is_authenticated,
 73            "acr": ACR_AUTHENTIK_SESSION,
 74        }
 75        if user.is_authenticated:
 76            payload["sub"] = user.uid
 77        value = encode(payload=payload, key=SIGNING_HASH)
 78        if settings.TEST:
 79            value = session_key
 80        return value
 81
 82    def process_request(self, request: HttpRequest):
 83        raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
 84        session_key = SessionMiddleware.decode_session_key(raw_session)
 85        request.session = self.SessionStore(
 86            session_key,
 87            last_ip=ClientIPMiddleware.get_client_ip(request),
 88            last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
 89        )
 90
 91    def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
 92        """
 93        If request.session was modified, or if the configuration is to save the
 94        session every time, save the changes and set a session cookie or delete
 95        the session cookie if the session has been emptied.
 96        """
 97        try:
 98            accessed = request.session.accessed
 99            modified = request.session.modified
100            empty = request.session.is_empty()
101        except AttributeError:
102            return response
103        # Set SameSite based on whether or not the request is secure
104        secure = SessionMiddleware.is_secure(request)
105        same_site = "None" if secure else "Lax"
106        # First check if we need to delete this cookie.
107        # The session should be deleted only if the session is entirely empty.
108        if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
109            response.delete_cookie(
110                settings.SESSION_COOKIE_NAME,
111                path=settings.SESSION_COOKIE_PATH,
112                domain=settings.SESSION_COOKIE_DOMAIN,
113                samesite=same_site,
114            )
115            patch_vary_headers(response, ("Cookie",))
116        else:
117            if accessed:
118                patch_vary_headers(response, ("Cookie",))
119            if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
120                if request.session.get_expire_at_browser_close():
121                    max_age = None
122                    expires = None
123                else:
124                    max_age = request.session.get_expiry_age()
125                    expires_time = time() + max_age
126                    expires = http_date(expires_time)
127                # Save the session data and refresh the client cookie.
128                # Skip session save for 500 responses, refs #3881.
129                if response.status_code != HttpResponseServerError.status_code:
130                    try:
131                        request.session.save()
132                    except UpdateError:
133                        raise SessionInterrupted(
134                            "The request's session was deleted before the "
135                            "request completed. The user may have logged "
136                            "out in a concurrent request, for example."
137                        ) from None
138                    response.set_cookie(
139                        settings.SESSION_COOKIE_NAME,
140                        SessionMiddleware.encode_session(request.session.session_key, request.user),
141                        max_age=max_age,
142                        expires=expires,
143                        domain=settings.SESSION_COOKIE_DOMAIN,
144                        path=settings.SESSION_COOKIE_PATH,
145                        secure=secure,
146                        httponly=settings.SESSION_COOKIE_HTTPONLY or None,
147                        samesite=same_site,
148                    )
149        return response

Dynamically set SameSite depending if the upstream connection is TLS or not

@staticmethod
def is_secure(request: django.http.request.HttpRequest) -> bool:
36    @staticmethod
37    def is_secure(request: HttpRequest) -> bool:
38        """Check if request is TLS'd or localhost"""
39        if request.is_secure():
40            return True
41        host, _, _ = request.get_host().partition(":")
42        if host == "localhost" and settings.DEBUG:
43            # Since go does not consider localhost with http a secure origin
44            # we can't set the secure flag.
45            user_agent = request.META.get("HTTP_USER_AGENT", "")
46            if user_agent.startswith("goauthentik.io/outpost/") or (
47                "safari" in user_agent.lower() and "chrome" not in user_agent.lower()
48            ):
49                return False
50            return True
51        return False

Check if request is TLS'd or localhost

@staticmethod
def decode_session_key(key: str | None) -> str | None:
53    @staticmethod
54    def decode_session_key(key: str | None) -> str | None:
55        """Decode raw session cookie, and parse JWT"""
56        # We need to support the standard django format of just a session key
57        # for testing setups, where the session is directly set
58        session_key = key if settings.TEST else None
59        try:
60            session_payload = decode(key, SIGNING_HASH, algorithms=["HS256"])
61            session_key = session_payload["sid"]
62        except KeyError, PyJWTError:
63            pass
64        return session_key

Decode raw session cookie, and parse JWT

@staticmethod
def encode_session(session_key: str, user: authentik.core.models.User):
66    @staticmethod
67    def encode_session(session_key: str, user: User):
68        payload = {
69            "sid": session_key,
70            "iss": "authentik",
71            "sub": "anonymous",
72            "authenticated": user.is_authenticated,
73            "acr": ACR_AUTHENTIK_SESSION,
74        }
75        if user.is_authenticated:
76            payload["sub"] = user.uid
77        value = encode(payload=payload, key=SIGNING_HASH)
78        if settings.TEST:
79            value = session_key
80        return value
def process_request(self, request: django.http.request.HttpRequest):
82    def process_request(self, request: HttpRequest):
83        raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
84        session_key = SessionMiddleware.decode_session_key(raw_session)
85        request.session = self.SessionStore(
86            session_key,
87            last_ip=ClientIPMiddleware.get_client_ip(request),
88            last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
89        )
def process_response( self, request: django.http.request.HttpRequest, response: django.http.response.HttpResponse) -> django.http.response.HttpResponse:
 91    def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
 92        """
 93        If request.session was modified, or if the configuration is to save the
 94        session every time, save the changes and set a session cookie or delete
 95        the session cookie if the session has been emptied.
 96        """
 97        try:
 98            accessed = request.session.accessed
 99            modified = request.session.modified
100            empty = request.session.is_empty()
101        except AttributeError:
102            return response
103        # Set SameSite based on whether or not the request is secure
104        secure = SessionMiddleware.is_secure(request)
105        same_site = "None" if secure else "Lax"
106        # First check if we need to delete this cookie.
107        # The session should be deleted only if the session is entirely empty.
108        if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
109            response.delete_cookie(
110                settings.SESSION_COOKIE_NAME,
111                path=settings.SESSION_COOKIE_PATH,
112                domain=settings.SESSION_COOKIE_DOMAIN,
113                samesite=same_site,
114            )
115            patch_vary_headers(response, ("Cookie",))
116        else:
117            if accessed:
118                patch_vary_headers(response, ("Cookie",))
119            if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
120                if request.session.get_expire_at_browser_close():
121                    max_age = None
122                    expires = None
123                else:
124                    max_age = request.session.get_expiry_age()
125                    expires_time = time() + max_age
126                    expires = http_date(expires_time)
127                # Save the session data and refresh the client cookie.
128                # Skip session save for 500 responses, refs #3881.
129                if response.status_code != HttpResponseServerError.status_code:
130                    try:
131                        request.session.save()
132                    except UpdateError:
133                        raise SessionInterrupted(
134                            "The request's session was deleted before the "
135                            "request completed. The user may have logged "
136                            "out in a concurrent request, for example."
137                        ) from None
138                    response.set_cookie(
139                        settings.SESSION_COOKIE_NAME,
140                        SessionMiddleware.encode_session(request.session.session_key, request.user),
141                        max_age=max_age,
142                        expires=expires,
143                        domain=settings.SESSION_COOKIE_DOMAIN,
144                        path=settings.SESSION_COOKIE_PATH,
145                        secure=secure,
146                        httponly=settings.SESSION_COOKIE_HTTPONLY or None,
147                        samesite=same_site,
148                    )
149        return response

If request.session was modified, or if the configuration is to save the session every time, save the changes and set a session cookie or delete the session cookie if the session has been emptied.

class CsrfViewMiddleware(django.middleware.csrf.CsrfViewMiddleware):
152class CsrfViewMiddleware(UpstreamCsrfViewMiddleware):
153    """Dynamically set secure depending if the upstream connection is TLS or not"""
154
155    def _set_csrf_cookie(self, request: HttpRequest, response: HttpResponse):
156        if settings.CSRF_USE_SESSIONS:
157            if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
158                request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
159        else:
160            secure = SessionMiddleware.is_secure(request)
161            response.set_cookie(
162                settings.CSRF_COOKIE_NAME,
163                request.META["CSRF_COOKIE"],
164                max_age=settings.CSRF_COOKIE_AGE,
165                domain=settings.CSRF_COOKIE_DOMAIN,
166                path=settings.CSRF_COOKIE_PATH,
167                secure=secure,
168                httponly=settings.CSRF_COOKIE_HTTPONLY,
169                samesite=settings.CSRF_COOKIE_SAMESITE,
170            )
171            # Set the Vary header since content varies with the CSRF cookie.
172            patch_vary_headers(response, ("Cookie",))

Dynamically set secure depending if the upstream connection is TLS or not

class ClientIPMiddleware:
175class ClientIPMiddleware:
176    """Set a "known-good" client IP on the request, by default based off of x-forwarded-for
177    which is set by the go proxy, but also allowing the remote IP to be overridden by an outpost
178    for protocols like LDAP"""
179
180    get_response: Callable[[HttpRequest], HttpResponse]
181    outpost_remote_ip_header = "HTTP_X_AUTHENTIK_REMOTE_IP"
182    outpost_token_header = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN"  # nosec
183    default_ip = "255.255.255.255"
184
185    request_attr_client_ip = "client_ip"
186    request_attr_outpost_user = "outpost_user"
187
188    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
189        self.get_response = get_response
190        self.logger = get_logger().bind()
191
192    def _get_client_ip_from_meta(self, meta: dict[str, Any]) -> str:
193        """Attempt to get the client's IP by checking common HTTP Headers.
194        Returns none if no IP Could be found
195
196        No additional validation is done here as requests are expected to only arrive here
197        via the go proxy, which deals with validating these headers for us"""
198        headers = (
199            "HTTP_X_FORWARDED_FOR",
200            "REMOTE_ADDR",
201        )
202        try:
203            for _header in headers:
204                if _header in meta:
205                    ips: list[str] = meta.get(_header).split(",")
206                    # Ensure the IP parses as a valid IP
207                    return str(ip_address(ips[0].strip()))
208            return self.default_ip
209        except ValueError as exc:
210            self.logger.debug("Invalid remote IP", exc=exc)
211            return self.default_ip
212
213    # FIXME: this should probably not be in `root` but rather in a middleware in `outposts`
214    # but for now it's fine
215    def _get_outpost_override_ip(self, request: HttpRequest) -> str | None:
216        """Get the actual remote IP when set by an outpost. Only
217        allowed when the request is authenticated, by an outpost internal service account"""
218        if (
219            self.outpost_remote_ip_header not in request.META
220            or self.outpost_token_header not in request.META
221        ):
222            return None
223        delegated_ip = request.META[self.outpost_remote_ip_header]
224        token = (
225            Token.objects.filter(
226                key=request.META.get(self.outpost_token_header), intent=TokenIntents.INTENT_API
227            )
228            .select_related("user")
229            .first()
230        )
231        if not token:
232            LOGGER.warning("Attempted remote-ip override without token", delegated_ip=delegated_ip)
233            return None
234        user: User = token.user
235        if user.type != UserTypes.INTERNAL_SERVICE_ACCOUNT:
236            LOGGER.warning(
237                "Remote-IP override: user doesn't have permission",
238                user=user,
239                delegated_ip=delegated_ip,
240            )
241            return None
242        # Update sentry scope to include correct IP
243        sentry_user = Scope.get_isolation_scope()._user or {}
244        sentry_user["ip_address"] = delegated_ip
245        Scope.get_isolation_scope().set_user(sentry_user)
246        # Set the outpost service account on the request
247        setattr(request, self.request_attr_outpost_user, user)
248        try:
249            return str(ip_address(delegated_ip))
250        except ValueError as exc:
251            self.logger.debug("Invalid remote IP from Outpost", exc=exc)
252            return None
253
254    def _get_client_ip(self, request: HttpRequest | None) -> str:
255        """Attempt to get the client's IP by checking common HTTP Headers.
256        Returns none if no IP Could be found"""
257        if not request:
258            return self.default_ip
259        override = self._get_outpost_override_ip(request)
260        if override:
261            return override
262        return self._get_client_ip_from_meta(request.META)
263
264    @staticmethod
265    def get_outpost_user(request: HttpRequest) -> User | None:
266        """Get outpost user that authenticated this request"""
267        return getattr(request, ClientIPMiddleware.request_attr_outpost_user, None)
268
269    @staticmethod
270    def get_client_ip(request: HttpRequest) -> str:
271        """Get correct client IP, including any overrides from outposts that
272        have the permission to do so"""
273        if request and not hasattr(request, ClientIPMiddleware.request_attr_client_ip):
274            ClientIPMiddleware(lambda request: request).set_ip(request)
275        return getattr(
276            request, ClientIPMiddleware.request_attr_client_ip, ClientIPMiddleware.default_ip
277        )
278
279    def set_ip(self, request: HttpRequest):
280        """Set the IP"""
281        setattr(request, self.request_attr_client_ip, self._get_client_ip(request))
282
283    def __call__(self, request: HttpRequest) -> HttpResponse:
284        self.set_ip(request)
285        return self.get_response(request)

Set a "known-good" client IP on the request, by default based off of x-forwarded-for which is set by the go proxy, but also allowing the remote IP to be overridden by an outpost for protocols like LDAP

ClientIPMiddleware( get_response: Callable[[django.http.request.HttpRequest], django.http.response.HttpResponse])
188    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
189        self.get_response = get_response
190        self.logger = get_logger().bind()
get_response: Callable[[django.http.request.HttpRequest], django.http.response.HttpResponse]
outpost_remote_ip_header = 'HTTP_X_AUTHENTIK_REMOTE_IP'
outpost_token_header = 'HTTP_X_AUTHENTIK_OUTPOST_TOKEN'
default_ip = '255.255.255.255'
request_attr_client_ip = 'client_ip'
request_attr_outpost_user = 'outpost_user'
logger
@staticmethod
def get_outpost_user( request: django.http.request.HttpRequest) -> authentik.core.models.User | None:
264    @staticmethod
265    def get_outpost_user(request: HttpRequest) -> User | None:
266        """Get outpost user that authenticated this request"""
267        return getattr(request, ClientIPMiddleware.request_attr_outpost_user, None)

Get outpost user that authenticated this request

@staticmethod
def get_client_ip(request: django.http.request.HttpRequest) -> str:
269    @staticmethod
270    def get_client_ip(request: HttpRequest) -> str:
271        """Get correct client IP, including any overrides from outposts that
272        have the permission to do so"""
273        if request and not hasattr(request, ClientIPMiddleware.request_attr_client_ip):
274            ClientIPMiddleware(lambda request: request).set_ip(request)
275        return getattr(
276            request, ClientIPMiddleware.request_attr_client_ip, ClientIPMiddleware.default_ip
277        )

Get correct client IP, including any overrides from outposts that have the permission to do so

def set_ip(self, request: django.http.request.HttpRequest):
279    def set_ip(self, request: HttpRequest):
280        """Set the IP"""
281        setattr(request, self.request_attr_client_ip, self._get_client_ip(request))

Set the IP

class ChannelsLoggingMiddleware:
288class ChannelsLoggingMiddleware:
289    """Logging middleware for channels"""
290
291    def __init__(self, inner):
292        self.inner = inner
293
294    async def __call__(self, scope, receive, send):
295        self.log(scope)
296        try:
297            return await self.inner(scope, receive, send)
298        except DenyConnection:
299            return await send({"type": "websocket.close"})
300        except Exception as exc:
301            if settings.DEBUG or settings.TEST:
302                raise exc
303            LOGGER.warning("Exception in ASGI application", exc=exc)
304            return await send({"type": "websocket.close"})
305
306    def log(self, scope: dict, **kwargs):
307        """Log request"""
308        headers = dict(scope.get("headers", {}))
309        LOGGER.info(
310            scope["path"],
311            scheme="ws",
312            remote=headers.get(b"x-forwarded-for", b"").decode(),
313            user_agent=headers.get(b"user-agent", b"").decode(),
314            **kwargs,
315        )

Logging middleware for channels

ChannelsLoggingMiddleware(inner)
291    def __init__(self, inner):
292        self.inner = inner
inner
def log(self, scope: dict, **kwargs):
306    def log(self, scope: dict, **kwargs):
307        """Log request"""
308        headers = dict(scope.get("headers", {}))
309        LOGGER.info(
310            scope["path"],
311            scheme="ws",
312            remote=headers.get(b"x-forwarded-for", b"").decode(),
313            user_agent=headers.get(b"user-agent", b"").decode(),
314            **kwargs,
315        )

Log request

class LoggingMiddleware:
318class LoggingMiddleware:
319    """Logger middleware"""
320
321    get_response: Callable[[HttpRequest], HttpResponse]
322
323    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
324        self.get_response = get_response
325        headers = CONFIG.get("log.http_headers", [])
326        if isinstance(headers, str):
327            headers = headers.split(",")
328        self.headers_to_log = headers
329
330    def __call__(self, request: HttpRequest) -> HttpResponse:
331        start = perf_counter()
332        response = self.get_response(request)
333        status_code = response.status_code
334        kwargs = {
335            "request_id": getattr(request, "request_id", None),
336        }
337        kwargs.update(getattr(response, "ak_context", {}))
338        self.log(request, status_code, int((perf_counter() - start) * 1000), **kwargs)
339        return response
340
341    def log(self, request: HttpRequest, status_code: int, runtime: int, **kwargs):
342        """Log request"""
343        for header in self.headers_to_log:
344            header_value = request.headers.get(header)
345            if not header_value:
346                continue
347            kwargs[header.lower().replace("-", "_")] = header_value
348        LOGGER.info(
349            request.get_full_path(),
350            remote=ClientIPMiddleware.get_client_ip(request),
351            method=request.method,
352            scheme=request.scheme,
353            status=status_code,
354            runtime=runtime,
355            **kwargs,
356        )

Logger middleware

LoggingMiddleware( get_response: Callable[[django.http.request.HttpRequest], django.http.response.HttpResponse])
323    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
324        self.get_response = get_response
325        headers = CONFIG.get("log.http_headers", [])
326        if isinstance(headers, str):
327            headers = headers.split(",")
328        self.headers_to_log = headers
get_response: Callable[[django.http.request.HttpRequest], django.http.response.HttpResponse]
headers_to_log
def log( self, request: django.http.request.HttpRequest, status_code: int, runtime: int, **kwargs):
341    def log(self, request: HttpRequest, status_code: int, runtime: int, **kwargs):
342        """Log request"""
343        for header in self.headers_to_log:
344            header_value = request.headers.get(header)
345            if not header_value:
346                continue
347            kwargs[header.lower().replace("-", "_")] = header_value
348        LOGGER.info(
349            request.get_full_path(),
350            remote=ClientIPMiddleware.get_client_ip(request),
351            method=request.method,
352            scheme=request.scheme,
353            status=status_code,
354            runtime=runtime,
355            **kwargs,
356        )

Log request