Compare commits
	
		
			12 Commits
		
	
	
		
			celery-2-d
			...
			core/b2c-i
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 195091ed3b | |||
| 4de3f1f4b8 | |||
| af4f1b3421 | |||
| 77b816ad51 | |||
| b28dd485a0 | |||
| 4701389745 | |||
| 0d0097e956 | |||
| b42eb0706d | |||
| 3afe386e18 | |||
| 34dd9c0b63 | |||
| b2f2fd241d | |||
| 828f477548 | 
| @ -46,6 +46,7 @@ class BrandSerializer(ModelSerializer): | ||||
|         fields = [ | ||||
|             "brand_uuid", | ||||
|             "domain", | ||||
|             "origin", | ||||
|             "default", | ||||
|             "branding_title", | ||||
|             "branding_logo", | ||||
| @ -56,6 +57,7 @@ class BrandSerializer(ModelSerializer): | ||||
|             "flow_unenrollment", | ||||
|             "flow_user_settings", | ||||
|             "flow_device_code", | ||||
|             "default_application", | ||||
|             "web_certificate", | ||||
|             "attributes", | ||||
|         ] | ||||
|  | ||||
| @ -1,12 +1,17 @@ | ||||
| """Inject brand into current request""" | ||||
|  | ||||
| from collections.abc import Callable | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from django.http.request import HttpRequest | ||||
| from django.http.response import HttpResponse | ||||
| from django.utils.translation import activate | ||||
|  | ||||
| from authentik.brands.utils import get_brand_for_request | ||||
| from authentik.lib.config import CONFIG | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from authentik.brands.models import Brand | ||||
|  | ||||
|  | ||||
| class BrandMiddleware: | ||||
| @ -25,3 +30,41 @@ class BrandMiddleware: | ||||
|             if locale != "": | ||||
|                 activate(locale) | ||||
|         return self.get_response(request) | ||||
|  | ||||
|  | ||||
| class BrandHeaderMiddleware: | ||||
|     """Add headers from currently active brand""" | ||||
|  | ||||
|     get_response: Callable[[HttpRequest], HttpResponse] | ||||
|     default_csp_elements: dict[str, list[str]] = {} | ||||
|  | ||||
|     def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): | ||||
|         self.get_response = get_response | ||||
|         self.default_csp_elements = { | ||||
|             "style-src": ["'self'", "'unsafe-inline'"],  # Required due to Lit/ShadowDOM | ||||
|             "script-src": ["'self'", "'unsafe-inline'"],  # Required for generated scripts | ||||
|             "img-src": ["https:", "http:", "data:"], | ||||
|             "default-src": ["'self'"], | ||||
|             "object-src": ["'none'"], | ||||
|             "connect-src": ["'self'"], | ||||
|         } | ||||
|         if CONFIG.get_bool("error_reporting.enabled"): | ||||
|             self.default_csp_elements["connect-src"].append( | ||||
|                 # Required for sentry (TODO: Dynamic) | ||||
|                 "https://authentik.error-reporting.a7k.io" | ||||
|             ) | ||||
|             if CONFIG.get_bool("debug"): | ||||
|                 # Also allow spotlight sidecar connection | ||||
|                 self.default_csp_elements["connect-src"].append("http://localhost:8969") | ||||
|  | ||||
|     def get_csp(self, request: HttpRequest) -> str: | ||||
|         brand: "Brand" = request.brand | ||||
|         elements = self.default_csp_elements.copy() | ||||
|         if brand.origin != "": | ||||
|             elements["frame-ancestors"] = [brand.origin] | ||||
|         return ";".join(f"{attr} {" ".join(value)}" for attr, value in elements.items()) | ||||
|  | ||||
|     def __call__(self, request: HttpRequest) -> HttpResponse: | ||||
|         response = self.get_response(request) | ||||
|         response.headers["Content-Security-Policy"] = self.get_csp(request) | ||||
|         return response | ||||
|  | ||||
| @ -0,0 +1,26 @@ | ||||
| # Generated by Django 5.0.3 on 2024-03-21 15:42 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_brands", "0005_tenantuuid_to_branduuid"), | ||||
|         ("authentik_core", "0033_alter_user_options"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="brand", | ||||
|             name="default_application", | ||||
|             field=models.ForeignKey( | ||||
|                 default=None, | ||||
|                 help_text="When set, external users will be redirected to this application after authenticating.", | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, | ||||
|                 to="authentik_core.application", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										21
									
								
								authentik/brands/migrations/0007_brand_origin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								authentik/brands/migrations/0007_brand_origin.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| # Generated by Django 5.0.3 on 2024-03-26 14:17 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_brands", "0006_brand_default_application"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="brand", | ||||
|             name="origin", | ||||
|             field=models.TextField( | ||||
|                 blank=True, | ||||
|                 help_text="Origin domain that activates this brand. Can be left empty to not allow any origins.", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -23,6 +23,12 @@ class Brand(SerializerModel): | ||||
|             "Domain that activates this brand. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`" | ||||
|         ) | ||||
|     ) | ||||
|     origin = models.TextField( | ||||
|         help_text=_( | ||||
|             "Origin domain that activates this brand. Can be left empty to not allow any origins." | ||||
|         ), | ||||
|         blank=True, | ||||
|     ) | ||||
|     default = models.BooleanField( | ||||
|         default=False, | ||||
|     ) | ||||
| @ -51,6 +57,16 @@ class Brand(SerializerModel): | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code" | ||||
|     ) | ||||
|  | ||||
|     default_application = models.ForeignKey( | ||||
|         "authentik_core.Application", | ||||
|         null=True, | ||||
|         default=None, | ||||
|         on_delete=models.SET_DEFAULT, | ||||
|         help_text=_( | ||||
|             "When set, external users will be redirected to this application after authenticating." | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     web_certificate = models.ForeignKey( | ||||
|         CertificateKeyPair, | ||||
|         null=True, | ||||
|  | ||||
| @ -1,11 +1,15 @@ | ||||
| """Brand utilities""" | ||||
|  | ||||
| from typing import Any | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| from django.db.models import F, Q | ||||
| from django.db.models import Value as V | ||||
| from django.http import HttpResponse | ||||
| from django.http.request import HttpRequest | ||||
| from django.utils.cache import patch_vary_headers | ||||
| from sentry_sdk.hub import Hub | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import get_full_version | ||||
| from authentik.brands.models import Brand | ||||
| @ -13,13 +17,17 @@ from authentik.tenants.models import Tenant | ||||
|  | ||||
| _q_default = Q(default=True) | ||||
| DEFAULT_BRAND = Brand(domain="fallback") | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| def get_brand_for_request(request: HttpRequest) -> Brand: | ||||
|     """Get brand object for current request""" | ||||
|     query = Q(host_domain__iendswith=F("domain")) | ||||
|     if "Origin" in request.headers: | ||||
|         query &= Q(Q(origin=request.headers.get("Origin", "")) | Q(origin="")) | ||||
|     db_brands = ( | ||||
|         Brand.objects.annotate(host_domain=V(request.get_host())) | ||||
|         .filter(Q(host_domain__iendswith=F("domain")) | _q_default) | ||||
|         .filter(Q(query) | _q_default) | ||||
|         .order_by("default") | ||||
|     ) | ||||
|     brands = list(db_brands.all()) | ||||
| @ -42,3 +50,46 @@ def context_processor(request: HttpRequest) -> dict[str, Any]: | ||||
|         "sentry_trace": trace, | ||||
|         "version": get_full_version(), | ||||
|     } | ||||
|  | ||||
|  | ||||
| def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: str): | ||||
|     """Add headers to permit CORS requests from allowed_origins, with or without credentials, | ||||
|     with any headers.""" | ||||
|     origin = request.META.get("HTTP_ORIGIN") | ||||
|     if not origin: | ||||
|         return response | ||||
|  | ||||
|     # OPTIONS requests don't have an authorization header -> hence | ||||
|     # we can't extract the provider this request is for | ||||
|     # so for options requests we allow the calling origin without checking | ||||
|     allowed = request.method == "OPTIONS" | ||||
|     received_origin = urlparse(origin) | ||||
|     for allowed_origin in allowed_origins: | ||||
|         url = urlparse(allowed_origin) | ||||
|         if ( | ||||
|             received_origin.scheme == url.scheme | ||||
|             and received_origin.hostname == url.hostname | ||||
|             and received_origin.port == url.port | ||||
|         ): | ||||
|             allowed = True | ||||
|     if not allowed: | ||||
|         LOGGER.warning( | ||||
|             "CORS: Origin is not an allowed origin", | ||||
|             requested=received_origin, | ||||
|             allowed=allowed_origins, | ||||
|         ) | ||||
|         return response | ||||
|  | ||||
|     # From the CORS spec: The string "*" cannot be used for a resource that supports credentials. | ||||
|     response["Access-Control-Allow-Origin"] = origin | ||||
|     patch_vary_headers(response, ["Origin"]) | ||||
|     response["Access-Control-Allow-Credentials"] = "true" | ||||
|  | ||||
|     if request.method == "OPTIONS": | ||||
|         if "HTTP_ACCESS_CONTROL_REQUEST_HEADERS" in request.META: | ||||
|             response["Access-Control-Allow-Headers"] = request.META[ | ||||
|                 "HTTP_ACCESS_CONTROL_REQUEST_HEADERS" | ||||
|             ] | ||||
|         response["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" | ||||
|  | ||||
|     return response | ||||
|  | ||||
| @ -6,7 +6,6 @@ from django.conf import settings | ||||
| from django.contrib.auth.decorators import login_required | ||||
| from django.urls import path | ||||
| from django.views.decorators.csrf import ensure_csrf_cookie | ||||
| from django.views.generic import RedirectView | ||||
|  | ||||
| from authentik.core.api.applications import ApplicationViewSet | ||||
| from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet | ||||
| @ -20,7 +19,12 @@ from authentik.core.api.transactional_applications import TransactionalApplicati | ||||
| from authentik.core.api.users import UserViewSet | ||||
| from authentik.core.views import apps | ||||
| from authentik.core.views.debug import AccessDeniedView | ||||
| from authentik.core.views.interface import FlowInterfaceView, InterfaceView | ||||
| from authentik.core.views.interface import ( | ||||
|     BrandDefaultRedirectView, | ||||
|     FlowInterfaceView, | ||||
|     InterfaceView, | ||||
|     RootRedirectView, | ||||
| ) | ||||
| from authentik.core.views.session import EndSessionView | ||||
| from authentik.root.asgi_middleware import SessionMiddleware | ||||
| from authentik.root.messages.consumer import MessageConsumer | ||||
| @ -29,13 +33,11 @@ from authentik.root.middleware import ChannelsLoggingMiddleware | ||||
| urlpatterns = [ | ||||
|     path( | ||||
|         "", | ||||
|         login_required( | ||||
|             RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True) | ||||
|         ), | ||||
|         login_required(RootRedirectView.as_view()), | ||||
|         name="root-redirect", | ||||
|     ), | ||||
|     path( | ||||
|         # We have to use this format since everything else uses applications/o or applications/saml | ||||
|         # We have to use this format since everything else uses application/o or application/saml | ||||
|         "application/launch/<slug:application_slug>/", | ||||
|         apps.RedirectToAppLaunch.as_view(), | ||||
|         name="application-launch", | ||||
| @ -43,12 +45,12 @@ urlpatterns = [ | ||||
|     # Interfaces | ||||
|     path( | ||||
|         "if/admin/", | ||||
|         ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")), | ||||
|         ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/admin.html")), | ||||
|         name="if-admin", | ||||
|     ), | ||||
|     path( | ||||
|         "if/user/", | ||||
|         ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")), | ||||
|         ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/user.html")), | ||||
|         name="if-user", | ||||
|     ), | ||||
|     path( | ||||
|  | ||||
| @ -3,15 +3,43 @@ | ||||
| from json import dumps | ||||
| from typing import Any | ||||
|  | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.views.generic.base import TemplateView | ||||
| from django.http import HttpRequest | ||||
| from django.http.response import HttpResponse | ||||
| from django.shortcuts import get_object_or_404, redirect | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic.base import RedirectView, TemplateView | ||||
| from rest_framework.request import Request | ||||
|  | ||||
| from authentik import get_build_hash | ||||
| from authentik.admin.tasks import LOCAL_VERSION | ||||
| from authentik.api.v3.config import ConfigView | ||||
| from authentik.brands.api import CurrentBrandSerializer | ||||
| from authentik.brands.models import Brand | ||||
| from authentik.core.models import UserTypes | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.policies.denied import AccessDeniedResponse | ||||
|  | ||||
|  | ||||
| class RootRedirectView(RedirectView): | ||||
|     """Root redirect view, redirect to brand's default application if set""" | ||||
|  | ||||
|     pattern_name = "authentik_core:if-user" | ||||
|     query_string = True | ||||
|  | ||||
|     def redirect_to_app(self, request: HttpRequest): | ||||
|         if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL: | ||||
|             brand: Brand = request.brand | ||||
|             if brand.default_application: | ||||
|                 return redirect( | ||||
|                     "authentik_core:application-launch", | ||||
|                     application_slug=brand.default_application.slug, | ||||
|                 ) | ||||
|         return None | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||
|         if redirect_response := RootRedirectView().redirect_to_app(request): | ||||
|             return redirect_response | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|  | ||||
| class InterfaceView(TemplateView): | ||||
| @ -27,6 +55,22 @@ class InterfaceView(TemplateView): | ||||
|         return super().get_context_data(**kwargs) | ||||
|  | ||||
|  | ||||
| class BrandDefaultRedirectView(InterfaceView): | ||||
|     """By default redirect to default app""" | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: | ||||
|         if request.user.is_authenticated and request.user.type == UserTypes.EXTERNAL: | ||||
|             brand: Brand = request.brand | ||||
|             if brand.default_application: | ||||
|                 return redirect( | ||||
|                     "authentik_core:application-launch", | ||||
|                     application_slug=brand.default_application.slug, | ||||
|                 ) | ||||
|             response = AccessDeniedResponse(self.request) | ||||
|             response.error_message = _("Interface can only be accessed by internal users.") | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|  | ||||
| class FlowInterfaceView(InterfaceView): | ||||
|     """Flow interface""" | ||||
|  | ||||
|  | ||||
| @ -24,6 +24,7 @@ from sentry_sdk.hub import Hub | ||||
| from structlog.stdlib import BoundLogger, get_logger | ||||
|  | ||||
| from authentik.brands.models import Brand | ||||
| from authentik.brands.utils import cors_allow | ||||
| from authentik.core.models import Application | ||||
| from authentik.events.models import Event, EventAction, cleanse_dict | ||||
| from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME | ||||
| @ -155,6 +156,14 @@ class FlowExecutorView(APIView): | ||||
|         return plan | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: | ||||
|         response = self.dispatch_wrapper(request, flow_slug) | ||||
|         origins = [] | ||||
|         if request.brand.origin != "": | ||||
|             origins.append(request.brand.origin) | ||||
|         cors_allow(request, response, *origins) | ||||
|         return response | ||||
|  | ||||
|     def dispatch_wrapper(self, request: HttpRequest, flow_slug: str) -> HttpResponse: | ||||
|         with Hub.current.start_span( | ||||
|             op="authentik.flow.executor.dispatch", description=self.flow.slug | ||||
|         ) as span: | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| """authentik oauth provider app config""" | ||||
|  | ||||
| from django.apps import AppConfig | ||||
| from authentik.blueprints.apps import ManagedAppConfig | ||||
|  | ||||
|  | ||||
| class AuthentikProviderOAuth2Config(AppConfig): | ||||
| class AuthentikProviderOAuth2Config(ManagedAppConfig): | ||||
|     """authentik oauth provider app config""" | ||||
|  | ||||
|     name = "authentik.providers.oauth2" | ||||
| @ -13,3 +13,4 @@ class AuthentikProviderOAuth2Config(AppConfig): | ||||
|         "authentik.providers.oauth2.urls_root": "", | ||||
|         "authentik.providers.oauth2.urls": "application/o/", | ||||
|     } | ||||
|     default = True | ||||
|  | ||||
							
								
								
									
										15
									
								
								authentik/providers/oauth2/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								authentik/providers/oauth2/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| from hashlib import sha256 | ||||
|  | ||||
| from django.contrib.auth.signals import user_logged_out | ||||
| from django.dispatch import receiver | ||||
| from django.http import HttpRequest | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.providers.oauth2.models import AccessToken | ||||
|  | ||||
|  | ||||
| @receiver(user_logged_out) | ||||
| def user_logged_out_oauth_access_token(sender, request: HttpRequest, user: User, **_): | ||||
|     """Revoke access tokens upon user logout""" | ||||
|     hashed_session_key = sha256(request.session.session_key.encode("ascii")).hexdigest() | ||||
|     AccessToken.objects.filter(user=user, session_id=hashed_session_key).delete() | ||||
| @ -4,11 +4,9 @@ import re | ||||
| from base64 import b64decode | ||||
| from binascii import Error | ||||
| from typing import Any | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| from django.http import HttpRequest, HttpResponse, JsonResponse | ||||
| from django.http.response import HttpResponseRedirect | ||||
| from django.utils.cache import patch_vary_headers | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER | ||||
| @ -30,49 +28,6 @@ class TokenResponse(JsonResponse): | ||||
|         self["Pragma"] = "no-cache" | ||||
|  | ||||
|  | ||||
| def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: str): | ||||
|     """Add headers to permit CORS requests from allowed_origins, with or without credentials, | ||||
|     with any headers.""" | ||||
|     origin = request.META.get("HTTP_ORIGIN") | ||||
|     if not origin: | ||||
|         return response | ||||
|  | ||||
|     # OPTIONS requests don't have an authorization header -> hence | ||||
|     # we can't extract the provider this request is for | ||||
|     # so for options requests we allow the calling origin without checking | ||||
|     allowed = request.method == "OPTIONS" | ||||
|     received_origin = urlparse(origin) | ||||
|     for allowed_origin in allowed_origins: | ||||
|         url = urlparse(allowed_origin) | ||||
|         if ( | ||||
|             received_origin.scheme == url.scheme | ||||
|             and received_origin.hostname == url.hostname | ||||
|             and received_origin.port == url.port | ||||
|         ): | ||||
|             allowed = True | ||||
|     if not allowed: | ||||
|         LOGGER.warning( | ||||
|             "CORS: Origin is not an allowed origin", | ||||
|             requested=received_origin, | ||||
|             allowed=allowed_origins, | ||||
|         ) | ||||
|         return response | ||||
|  | ||||
|     # From the CORS spec: The string "*" cannot be used for a resource that supports credentials. | ||||
|     response["Access-Control-Allow-Origin"] = origin | ||||
|     patch_vary_headers(response, ["Origin"]) | ||||
|     response["Access-Control-Allow-Credentials"] = "true" | ||||
|  | ||||
|     if request.method == "OPTIONS": | ||||
|         if "HTTP_ACCESS_CONTROL_REQUEST_HEADERS" in request.META: | ||||
|             response["Access-Control-Allow-Headers"] = request.META[ | ||||
|                 "HTTP_ACCESS_CONTROL_REQUEST_HEADERS" | ||||
|             ] | ||||
|         response["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" | ||||
|  | ||||
|     return response | ||||
|  | ||||
|  | ||||
| def extract_access_token(request: HttpRequest) -> str | None: | ||||
|     """ | ||||
|     Get the access token using Authorization Request Header Field method. | ||||
|  | ||||
| @ -8,6 +8,7 @@ from django.views import View | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.brands.utils import cors_allow | ||||
| from authentik.core.exceptions import PropertyMappingExpressionException | ||||
| from authentik.core.models import Application | ||||
| from authentik.providers.oauth2.constants import ( | ||||
| @ -28,7 +29,6 @@ from authentik.providers.oauth2.models import ( | ||||
|     ResponseTypes, | ||||
|     ScopeMapping, | ||||
| ) | ||||
| from authentik.providers.oauth2.utils import cors_allow | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @ -20,6 +20,7 @@ from jwt import PyJWK, PyJWT, PyJWTError, decode | ||||
| from sentry_sdk.hub import Hub | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.brands.utils import cors_allow | ||||
| from authentik.core.middleware import CTX_AUTH_VIA | ||||
| from authentik.core.models import ( | ||||
|     USER_ATTRIBUTE_EXPIRES, | ||||
| @ -59,7 +60,7 @@ from authentik.providers.oauth2.models import ( | ||||
|     OAuth2Provider, | ||||
|     RefreshToken, | ||||
| ) | ||||
| from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth | ||||
| from authentik.providers.oauth2.utils import TokenResponse, extract_client_auth | ||||
| from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES | ||||
| from authentik.sources.oauth.models import OAuthSource | ||||
| from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | ||||
|  | ||||
| @ -11,6 +11,7 @@ from django.views import View | ||||
| from django.views.decorators.csrf import csrf_exempt | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.brands.utils import cors_allow | ||||
| from authentik.core.exceptions import PropertyMappingExpressionException | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.challenge import PermissionDict | ||||
| @ -28,7 +29,7 @@ from authentik.providers.oauth2.models import ( | ||||
|     RefreshToken, | ||||
|     ScopeMapping, | ||||
| ) | ||||
| from authentik.providers.oauth2.utils import TokenResponse, cors_allow, protected_resource_view | ||||
| from authentik.providers.oauth2.utils import TokenResponse, protected_resource_view | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @ -241,7 +241,7 @@ MIDDLEWARE = [ | ||||
|     "django.middleware.common.CommonMiddleware", | ||||
|     "authentik.root.middleware.CsrfViewMiddleware", | ||||
|     "django.contrib.messages.middleware.MessageMiddleware", | ||||
|     "django.middleware.clickjacking.XFrameOptionsMiddleware", | ||||
|     "authentik.brands.middleware.BrandHeaderMiddleware", | ||||
|     "authentik.core.middleware.ImpersonateMiddleware", | ||||
|     "django_prometheus.middleware.PrometheusAfterMiddleware", | ||||
| ] | ||||
|  | ||||
| @ -118,6 +118,7 @@ class EmailStageView(ChallengeStageView): | ||||
|                     "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}), | ||||
|                     "user": pending_user, | ||||
|                     "expires": token.expires, | ||||
|                     "token": token.key, | ||||
|                 }, | ||||
|             ) | ||||
|             send_mails(current_stage, message) | ||||
|  | ||||
| @ -7609,6 +7609,11 @@ | ||||
|                     "title": "Domain", | ||||
|                     "description": "Domain that activates this brand. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`" | ||||
|                 }, | ||||
|                 "origin": { | ||||
|                     "type": "string", | ||||
|                     "title": "Origin", | ||||
|                     "description": "Origin domain that activates this brand. Can be left empty to not allow any origins." | ||||
|                 }, | ||||
|                 "default": { | ||||
|                     "type": "boolean", | ||||
|                     "title": "Default" | ||||
| @ -7652,6 +7657,11 @@ | ||||
|                     "type": "integer", | ||||
|                     "title": "Flow device code" | ||||
|                 }, | ||||
|                 "default_application": { | ||||
|                     "type": "integer", | ||||
|                     "title": "Default application", | ||||
|                     "description": "When set, external users will be redirected to this application after authenticating." | ||||
|                 }, | ||||
|                 "web_certificate": { | ||||
|                     "type": "integer", | ||||
|                     "title": "Web certificate", | ||||
|  | ||||
							
								
								
									
										30
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								schema.yml
									
									
									
									
									
								
							| @ -31334,6 +31334,10 @@ components: | ||||
|           type: string | ||||
|           description: Domain that activates this brand. Can be a superset, i.e. `a.b` | ||||
|             for `aa.b` and `ba.b` | ||||
|         origin: | ||||
|           type: string | ||||
|           description: Origin domain that activates this brand. Can be left empty | ||||
|             to not allow any origins. | ||||
|         default: | ||||
|           type: boolean | ||||
|         branding_title: | ||||
| @ -31366,6 +31370,12 @@ components: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         default_application: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: When set, external users will be redirected to this application | ||||
|             after authenticating. | ||||
|         web_certificate: | ||||
|           type: string | ||||
|           format: uuid | ||||
| @ -31384,6 +31394,10 @@ components: | ||||
|           minLength: 1 | ||||
|           description: Domain that activates this brand. Can be a superset, i.e. `a.b` | ||||
|             for `aa.b` and `ba.b` | ||||
|         origin: | ||||
|           type: string | ||||
|           description: Origin domain that activates this brand. Can be left empty | ||||
|             to not allow any origins. | ||||
|         default: | ||||
|           type: boolean | ||||
|         branding_title: | ||||
| @ -31419,6 +31433,12 @@ components: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         default_application: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: When set, external users will be redirected to this application | ||||
|             after authenticating. | ||||
|         web_certificate: | ||||
|           type: string | ||||
|           format: uuid | ||||
| @ -38518,6 +38538,10 @@ components: | ||||
|           minLength: 1 | ||||
|           description: Domain that activates this brand. Can be a superset, i.e. `a.b` | ||||
|             for `aa.b` and `ba.b` | ||||
|         origin: | ||||
|           type: string | ||||
|           description: Origin domain that activates this brand. Can be left empty | ||||
|             to not allow any origins. | ||||
|         default: | ||||
|           type: boolean | ||||
|         branding_title: | ||||
| @ -38553,6 +38577,12 @@ components: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|         default_application: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: When set, external users will be redirected to this application | ||||
|             after authenticating. | ||||
|         web_certificate: | ||||
|           type: string | ||||
|           format: uuid | ||||
|  | ||||
| @ -15,7 +15,13 @@ import { msg } from "@lit/localize"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { Brand, CoreApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api"; | ||||
| import { | ||||
|     Application, | ||||
|     Brand, | ||||
|     CoreApi, | ||||
|     CoreApplicationsListRequest, | ||||
|     FlowsInstancesListDesignationEnum, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-brand-form") | ||||
| export class BrandForm extends ModelForm<Brand, string> { | ||||
| @ -137,6 +143,46 @@ export class BrandForm extends ModelForm<Brand, string> { | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|  | ||||
|             <ak-form-group> | ||||
|                 <span slot="header"> ${msg("External user settings")} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${msg("Default application")} | ||||
|                         name="defaultApplication" | ||||
|                     > | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<Application[]> => { | ||||
|                                 const args: CoreApplicationsListRequest = { | ||||
|                                     ordering: "name", | ||||
|                                     superuserFullList: true, | ||||
|                                 }; | ||||
|                                 if (query !== undefined) { | ||||
|                                     args.search = query; | ||||
|                                 } | ||||
|                                 const users = await new CoreApi( | ||||
|                                     DEFAULT_CONFIG, | ||||
|                                 ).coreApplicationsList(args); | ||||
|                                 return users.results; | ||||
|                             }} | ||||
|                             .renderElement=${(item: Application): string => { | ||||
|                                 return item.name; | ||||
|                             }} | ||||
|                             .renderDescription=${(item: Application): TemplateResult => { | ||||
|                                 return html`${item.slug}`; | ||||
|                             }} | ||||
|                             .value=${(item: Application | undefined): string | undefined => { | ||||
|                                 return item?.pk; | ||||
|                             }} | ||||
|                             .selected=${(item: Application): boolean => { | ||||
|                                 return item.pk === this.instance?.defaultApplication; | ||||
|                             }} | ||||
|                         > | ||||
|                         </ak-search-select> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group> | ||||
|  | ||||
|             <ak-form-group> | ||||
|                 <span slot="header"> ${msg("Default flows")} </span> | ||||
|                 <div slot="body" class="pf-c-form"> | ||||
|  | ||||
| @ -13,13 +13,14 @@ import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { TemplateResult, html, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { Brand, CoreApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api"; | ||||
| import { WithBrandConfig } from "@goauthentik/authentik/elements/Interface/brandProvider"; | ||||
|  | ||||
| @customElement("ak-brand-list") | ||||
| export class BrandListPage extends TablePage<Brand> { | ||||
| export class BrandListPage extends WithBrandConfig(TablePage<Brand>) { | ||||
|     searchEnabled(): boolean { | ||||
|         return true; | ||||
|     } | ||||
| @ -84,7 +85,9 @@ export class BrandListPage extends TablePage<Brand> { | ||||
|  | ||||
|     row(item: Brand): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.domain}`, | ||||
|             html`${item.domain}${this.brand.matchedDomain === item.domain ? html` | ||||
|                  <ak-status-label ?good=${false} type="info" bad-label=${msg("Active")}></ak-status-label> | ||||
|             ` : nothing}`, | ||||
|             html`${item.brandingTitle}`, | ||||
|             html`<ak-status-label ?good=${item._default}></ak-status-label>`, | ||||
|             html`<ak-forms-modal> | ||||
|  | ||||
| @ -29,6 +29,8 @@ class PreviewStageHost implements StageHost { | ||||
|     flowSlug = undefined; | ||||
|     loading = false; | ||||
|     brand = undefined; | ||||
|     frameMode = false; | ||||
|  | ||||
|     async submit(payload: unknown): Promise<boolean> { | ||||
|         this.promptForm.previewResult = payload; | ||||
|         return false; | ||||
|  | ||||
| @ -61,16 +61,18 @@ export function brand(): Promise<CurrentBrand> { | ||||
|     return globalBrandPromise; | ||||
| } | ||||
|  | ||||
| export function getMetaContent(key: string): string { | ||||
| export function getMetaContent(key: string): string | undefined { | ||||
|     const metaEl = document.querySelector<HTMLMetaElement>(`meta[name=${key}]`); | ||||
|     if (!metaEl) return ""; | ||||
|     return metaEl.content; | ||||
|     return metaEl?.content; | ||||
| } | ||||
|  | ||||
| export const DEFAULT_CONFIG = new Configuration({ | ||||
|     basePath: (process.env.AK_API_BASE_PATH || window.location.origin) + "/api/v3", | ||||
|     basePath: | ||||
|         (process.env.AK_API_BASE_PATH || | ||||
|             getMetaContent("authentik-api") || | ||||
|             window.location.origin) + "/api/v3", | ||||
|     headers: { | ||||
|         "sentry-trace": getMetaContent("sentry-trace"), | ||||
|         "sentry-trace": getMetaContent("sentry-trace") || "", | ||||
|     }, | ||||
|     middleware: [ | ||||
|         new CSRFMiddleware(), | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { ensureCSSStyleSheet } from "@goauthentik/authentik/elements/utils/ensureCSSStyleSheet"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { | ||||
|     EVENT_FLOW_ADVANCE, | ||||
| @ -76,7 +77,8 @@ export class FlowExecutor extends Interface implements StageHost { | ||||
|     @state() | ||||
|     flowInfo?: ContextualFlowInfo; | ||||
|  | ||||
|     ws: WebsocketClient; | ||||
|     @state() | ||||
|     frameMode = window !== window.top; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFLogin, PFDrawer, PFButton, PFTitle, PFList, PFBackgroundImage].concat(css` | ||||
| @ -140,6 +142,25 @@ export class FlowExecutor extends Interface implements StageHost { | ||||
|             :host([theme="dark"]) .pf-c-login.sidebar_right .pf-c-list { | ||||
|                 color: var(--ak-dark-foreground); | ||||
|             } | ||||
|             /* frame design */ | ||||
|             .pf-c-login.frame { | ||||
|                 padding: 0; | ||||
|                 min-height: initial !important; | ||||
|             } | ||||
|             .pf-c-login.frame .pf-c-login__main { | ||||
|                 margin-bottom: 0; | ||||
|                 height: 100%; | ||||
|             } | ||||
|             .pf-c-login.frame .ak-login-container { | ||||
|                 height: 100vh; | ||||
|                 width: 100vw; | ||||
|             } | ||||
|             .pf-c-login.frame .pf-c-login__footer { | ||||
|                 display: none; | ||||
|             } | ||||
|             .pf-c-login.frame .pf-c-login__footer .pf-c-list { | ||||
|                 padding: 0; | ||||
|             } | ||||
|             .pf-c-brand { | ||||
|                 padding-top: calc( | ||||
|                     var(--pf-c-login__main-footer-links--PaddingTop) + | ||||
| @ -161,7 +182,21 @@ export class FlowExecutor extends Interface implements StageHost { | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.ws = new WebsocketClient(); | ||||
|         if (this.frameMode) { | ||||
|             // PatternFly sets html and body to 100% height, which we don't | ||||
|             // want in this case since the iframe should only take the space it needs | ||||
|             document.adoptedStyleSheets = [ | ||||
|                 ...document.adoptedStyleSheets, | ||||
|                 ensureCSSStyleSheet(css` | ||||
|                     body, | ||||
|                     html { | ||||
|                         height: unset !important; | ||||
|                     } | ||||
|                 `), | ||||
|             ]; | ||||
|         } else { | ||||
|             new WebsocketClient(); | ||||
|         } | ||||
|         if (window.location.search.includes("inspector")) { | ||||
|             this.inspectorOpen = true; | ||||
|         } | ||||
| @ -437,7 +472,9 @@ export class FlowExecutor extends Interface implements StageHost { | ||||
|     } | ||||
|  | ||||
|     renderChallengeWrapper(): TemplateResult { | ||||
|         const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand"> | ||||
|         const logo = this.frameMode | ||||
|             ? nothing | ||||
|             : html`<div class="pf-c-login__main-header pf-c-brand ak-brand"> | ||||
|                   <img src="${first(this.brand?.brandingLogo, "")}" alt="authentik Logo" /> | ||||
|               </div>`; | ||||
|         if (!this.challenge) { | ||||
| @ -482,6 +519,25 @@ export class FlowExecutor extends Interface implements StageHost { | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         if (this.frameMode) { | ||||
|             return html`<ak-locale-context> | ||||
|                 <div class="pf-c-login frame"> | ||||
|                     <div class="ak-login-container"> | ||||
|                         <div class="pf-c-login__main">${this.renderChallengeWrapper()}</div> | ||||
|                         <footer class="pf-c-login__footer"> | ||||
|                             <ul class="pf-c-list pf-m-inline"> | ||||
|                                 <li> | ||||
|                                     <a | ||||
|                                         href="https://goauthentik.io?utm_source=authentik&utm_medium=flow" | ||||
|                                         >${msg("Powered by authentik")}</a | ||||
|                                     > | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                         </footer> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </ak-locale-context>`; | ||||
|         } | ||||
|         return html`<ak-locale-context> | ||||
|             <div class="pf-c-background-image"></div> | ||||
|             <div class="pf-c-page__drawer"> | ||||
|  | ||||
| @ -49,8 +49,27 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes | ||||
|             "authentik/stages/redirect: redirecting to url from server", | ||||
|             this.challenge.to, | ||||
|         ); | ||||
|         window.location.assign(this.challenge.to); | ||||
|         this.startedRedirect = true; | ||||
|         if (this.host.frameMode && window.top) { | ||||
|             try { | ||||
|                 window.top.location.assign(this.challenge.to); | ||||
|             } catch { | ||||
|                 window.top.postMessage( | ||||
|                     { | ||||
|                         source: "goauthentik.io", | ||||
|                         context: "flow-executor", | ||||
|                         component: this.challenge.component, | ||||
|                         to: this.challenge.to, | ||||
|                     }, | ||||
|                     document.location.ancestorOrigins[0], | ||||
|                 ); | ||||
|             } | ||||
|             if (this.challenge.to.startsWith("http")) { | ||||
|                 return; | ||||
|             } | ||||
|         } else { | ||||
|             window.location.assign(this.challenge.to); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     renderLoading(): TemplateResult { | ||||
|  | ||||
| @ -47,6 +47,10 @@ export class AuthenticatorValidateStage | ||||
|         return this.host.brand; | ||||
|     } | ||||
|  | ||||
|     get frameMode(): boolean { | ||||
|         return this.host.frameMode; | ||||
|     } | ||||
|  | ||||
|     @state() | ||||
|     _selectedDeviceChallenge?: DeviceChallenge; | ||||
|  | ||||
|  | ||||
| @ -16,6 +16,7 @@ export interface StageHost { | ||||
|     challenge?: unknown; | ||||
|     flowSlug?: string; | ||||
|     loading: boolean; | ||||
|     frameMode: boolean; | ||||
|     submit(payload: unknown, options?: SubmitOptions): Promise<boolean>; | ||||
|  | ||||
|     readonly brand?: CurrentBrand; | ||||
|  | ||||
| @ -22,6 +22,7 @@ import "@goauthentik/elements/sidebar/Sidebar"; | ||||
| import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; | ||||
| import "@goauthentik/elements/sidebar/SidebarItem"; | ||||
| import { ROUTES } from "@goauthentik/user/Routes"; | ||||
| import "@goauthentik/user/user-settings/details/UserSettingsFlowExecutor"; | ||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
| import { match } from "ts-pattern"; | ||||
|  | ||||
|  | ||||
| @ -53,6 +53,8 @@ export class UserSettingsFlowExecutor | ||||
|     @property({ type: Boolean }) | ||||
|     loading = false; | ||||
|  | ||||
|     frameMode = false; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [PFBase, PFCard, PFPage, PFButton, PFContent]; | ||||
|     } | ||||
| @ -87,7 +89,7 @@ export class UserSettingsFlowExecutor | ||||
|     } | ||||
|  | ||||
|     firstUpdated(): void { | ||||
|         this.flowSlug = this.brand?.flowUserSettings; | ||||
|         this.flowSlug = this.flowSlug || this.brand?.flowUserSettings; | ||||
|         if (!this.flowSlug) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	