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 = [ |         fields = [ | ||||||
|             "brand_uuid", |             "brand_uuid", | ||||||
|             "domain", |             "domain", | ||||||
|  |             "origin", | ||||||
|             "default", |             "default", | ||||||
|             "branding_title", |             "branding_title", | ||||||
|             "branding_logo", |             "branding_logo", | ||||||
| @ -56,6 +57,7 @@ class BrandSerializer(ModelSerializer): | |||||||
|             "flow_unenrollment", |             "flow_unenrollment", | ||||||
|             "flow_user_settings", |             "flow_user_settings", | ||||||
|             "flow_device_code", |             "flow_device_code", | ||||||
|  |             "default_application", | ||||||
|             "web_certificate", |             "web_certificate", | ||||||
|             "attributes", |             "attributes", | ||||||
|         ] |         ] | ||||||
|  | |||||||
| @ -1,12 +1,17 @@ | |||||||
| """Inject brand into current request""" | """Inject brand into current request""" | ||||||
|  |  | ||||||
| from collections.abc import Callable | from collections.abc import Callable | ||||||
|  | from typing import TYPE_CHECKING | ||||||
|  |  | ||||||
| from django.http.request import HttpRequest | from django.http.request import HttpRequest | ||||||
| from django.http.response import HttpResponse | from django.http.response import HttpResponse | ||||||
| from django.utils.translation import activate | from django.utils.translation import activate | ||||||
|  |  | ||||||
| from authentik.brands.utils import get_brand_for_request | 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: | class BrandMiddleware: | ||||||
| @ -25,3 +30,41 @@ class BrandMiddleware: | |||||||
|             if locale != "": |             if locale != "": | ||||||
|                 activate(locale) |                 activate(locale) | ||||||
|         return self.get_response(request) |         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`" |             "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 = models.BooleanField( | ||||||
|         default=False, |         default=False, | ||||||
|     ) |     ) | ||||||
| @ -51,6 +57,16 @@ class Brand(SerializerModel): | |||||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="brand_device_code" |         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( |     web_certificate = models.ForeignKey( | ||||||
|         CertificateKeyPair, |         CertificateKeyPair, | ||||||
|         null=True, |         null=True, | ||||||
|  | |||||||
| @ -1,11 +1,15 @@ | |||||||
| """Brand utilities""" | """Brand utilities""" | ||||||
|  |  | ||||||
| from typing import Any | from typing import Any | ||||||
|  | from urllib.parse import urlparse | ||||||
|  |  | ||||||
| from django.db.models import F, Q | from django.db.models import F, Q | ||||||
| from django.db.models import Value as V | from django.db.models import Value as V | ||||||
|  | from django.http import HttpResponse | ||||||
| from django.http.request import HttpRequest | from django.http.request import HttpRequest | ||||||
|  | from django.utils.cache import patch_vary_headers | ||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik import get_full_version | from authentik import get_full_version | ||||||
| from authentik.brands.models import Brand | from authentik.brands.models import Brand | ||||||
| @ -13,13 +17,17 @@ from authentik.tenants.models import Tenant | |||||||
|  |  | ||||||
| _q_default = Q(default=True) | _q_default = Q(default=True) | ||||||
| DEFAULT_BRAND = Brand(domain="fallback") | DEFAULT_BRAND = Brand(domain="fallback") | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_brand_for_request(request: HttpRequest) -> Brand: | def get_brand_for_request(request: HttpRequest) -> Brand: | ||||||
|     """Get brand object for current request""" |     """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 = ( |     db_brands = ( | ||||||
|         Brand.objects.annotate(host_domain=V(request.get_host())) |         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") |         .order_by("default") | ||||||
|     ) |     ) | ||||||
|     brands = list(db_brands.all()) |     brands = list(db_brands.all()) | ||||||
| @ -42,3 +50,46 @@ def context_processor(request: HttpRequest) -> dict[str, Any]: | |||||||
|         "sentry_trace": trace, |         "sentry_trace": trace, | ||||||
|         "version": get_full_version(), |         "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.contrib.auth.decorators import login_required | ||||||
| from django.urls import path | from django.urls import path | ||||||
| from django.views.decorators.csrf import ensure_csrf_cookie | 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.applications import ApplicationViewSet | ||||||
| from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet | 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.api.users import UserViewSet | ||||||
| from authentik.core.views import apps | from authentik.core.views import apps | ||||||
| from authentik.core.views.debug import AccessDeniedView | 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.core.views.session import EndSessionView | ||||||
| from authentik.root.asgi_middleware import SessionMiddleware | from authentik.root.asgi_middleware import SessionMiddleware | ||||||
| from authentik.root.messages.consumer import MessageConsumer | from authentik.root.messages.consumer import MessageConsumer | ||||||
| @ -29,13 +33,11 @@ from authentik.root.middleware import ChannelsLoggingMiddleware | |||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path( |     path( | ||||||
|         "", |         "", | ||||||
|         login_required( |         login_required(RootRedirectView.as_view()), | ||||||
|             RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True) |  | ||||||
|         ), |  | ||||||
|         name="root-redirect", |         name="root-redirect", | ||||||
|     ), |     ), | ||||||
|     path( |     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>/", |         "application/launch/<slug:application_slug>/", | ||||||
|         apps.RedirectToAppLaunch.as_view(), |         apps.RedirectToAppLaunch.as_view(), | ||||||
|         name="application-launch", |         name="application-launch", | ||||||
| @ -43,12 +45,12 @@ urlpatterns = [ | |||||||
|     # Interfaces |     # Interfaces | ||||||
|     path( |     path( | ||||||
|         "if/admin/", |         "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", |         name="if-admin", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
|         "if/user/", |         "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", |         name="if-user", | ||||||
|     ), |     ), | ||||||
|     path( |     path( | ||||||
|  | |||||||
| @ -3,15 +3,43 @@ | |||||||
| from json import dumps | from json import dumps | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from django.shortcuts import get_object_or_404 | from django.http import HttpRequest | ||||||
| from django.views.generic.base import TemplateView | 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 rest_framework.request import Request | ||||||
|  |  | ||||||
| from authentik import get_build_hash | from authentik import get_build_hash | ||||||
| from authentik.admin.tasks import LOCAL_VERSION | from authentik.admin.tasks import LOCAL_VERSION | ||||||
| from authentik.api.v3.config import ConfigView | from authentik.api.v3.config import ConfigView | ||||||
| from authentik.brands.api import CurrentBrandSerializer | 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.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): | class InterfaceView(TemplateView): | ||||||
| @ -27,6 +55,22 @@ class InterfaceView(TemplateView): | |||||||
|         return super().get_context_data(**kwargs) |         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): | class FlowInterfaceView(InterfaceView): | ||||||
|     """Flow interface""" |     """Flow interface""" | ||||||
|  |  | ||||||
|  | |||||||
| @ -24,6 +24,7 @@ from sentry_sdk.hub import Hub | |||||||
| from structlog.stdlib import BoundLogger, get_logger | from structlog.stdlib import BoundLogger, get_logger | ||||||
|  |  | ||||||
| from authentik.brands.models import Brand | from authentik.brands.models import Brand | ||||||
|  | from authentik.brands.utils import cors_allow | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.events.models import Event, EventAction, cleanse_dict | from authentik.events.models import Event, EventAction, cleanse_dict | ||||||
| from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME | from authentik.flows.apps import HIST_FLOW_EXECUTION_STAGE_TIME | ||||||
| @ -155,6 +156,14 @@ class FlowExecutorView(APIView): | |||||||
|         return plan |         return plan | ||||||
|  |  | ||||||
|     def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse: |     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( |         with Hub.current.start_span( | ||||||
|             op="authentik.flow.executor.dispatch", description=self.flow.slug |             op="authentik.flow.executor.dispatch", description=self.flow.slug | ||||||
|         ) as span: |         ) as span: | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| """authentik oauth provider app config""" | """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""" |     """authentik oauth provider app config""" | ||||||
|  |  | ||||||
|     name = "authentik.providers.oauth2" |     name = "authentik.providers.oauth2" | ||||||
| @ -13,3 +13,4 @@ class AuthentikProviderOAuth2Config(AppConfig): | |||||||
|         "authentik.providers.oauth2.urls_root": "", |         "authentik.providers.oauth2.urls_root": "", | ||||||
|         "authentik.providers.oauth2.urls": "application/o/", |         "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 base64 import b64decode | ||||||
| from binascii import Error | from binascii import Error | ||||||
| from typing import Any | from typing import Any | ||||||
| from urllib.parse import urlparse |  | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse, JsonResponse | from django.http import HttpRequest, HttpResponse, JsonResponse | ||||||
| from django.http.response import HttpResponseRedirect | from django.http.response import HttpResponseRedirect | ||||||
| from django.utils.cache import patch_vary_headers |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER | from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER | ||||||
| @ -30,49 +28,6 @@ class TokenResponse(JsonResponse): | |||||||
|         self["Pragma"] = "no-cache" |         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: | def extract_access_token(request: HttpRequest) -> str | None: | ||||||
|     """ |     """ | ||||||
|     Get the access token using Authorization Request Header Field method. |     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 guardian.shortcuts import get_anonymous_user | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.brands.utils import cors_allow | ||||||
| from authentik.core.exceptions import PropertyMappingExpressionException | from authentik.core.exceptions import PropertyMappingExpressionException | ||||||
| from authentik.core.models import Application | from authentik.core.models import Application | ||||||
| from authentik.providers.oauth2.constants import ( | from authentik.providers.oauth2.constants import ( | ||||||
| @ -28,7 +29,6 @@ from authentik.providers.oauth2.models import ( | |||||||
|     ResponseTypes, |     ResponseTypes, | ||||||
|     ScopeMapping, |     ScopeMapping, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.utils import cors_allow |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ from jwt import PyJWK, PyJWT, PyJWTError, decode | |||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.brands.utils import cors_allow | ||||||
| from authentik.core.middleware import CTX_AUTH_VIA | from authentik.core.middleware import CTX_AUTH_VIA | ||||||
| from authentik.core.models import ( | from authentik.core.models import ( | ||||||
|     USER_ATTRIBUTE_EXPIRES, |     USER_ATTRIBUTE_EXPIRES, | ||||||
| @ -59,7 +60,7 @@ from authentik.providers.oauth2.models import ( | |||||||
|     OAuth2Provider, |     OAuth2Provider, | ||||||
|     RefreshToken, |     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.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES | ||||||
| from authentik.sources.oauth.models import OAuthSource | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | 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 django.views.decorators.csrf import csrf_exempt | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.brands.utils import cors_allow | ||||||
| from authentik.core.exceptions import PropertyMappingExpressionException | from authentik.core.exceptions import PropertyMappingExpressionException | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.flows.challenge import PermissionDict | from authentik.flows.challenge import PermissionDict | ||||||
| @ -28,7 +29,7 @@ from authentik.providers.oauth2.models import ( | |||||||
|     RefreshToken, |     RefreshToken, | ||||||
|     ScopeMapping, |     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() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  | |||||||
| @ -241,7 +241,7 @@ MIDDLEWARE = [ | |||||||
|     "django.middleware.common.CommonMiddleware", |     "django.middleware.common.CommonMiddleware", | ||||||
|     "authentik.root.middleware.CsrfViewMiddleware", |     "authentik.root.middleware.CsrfViewMiddleware", | ||||||
|     "django.contrib.messages.middleware.MessageMiddleware", |     "django.contrib.messages.middleware.MessageMiddleware", | ||||||
|     "django.middleware.clickjacking.XFrameOptionsMiddleware", |     "authentik.brands.middleware.BrandHeaderMiddleware", | ||||||
|     "authentik.core.middleware.ImpersonateMiddleware", |     "authentik.core.middleware.ImpersonateMiddleware", | ||||||
|     "django_prometheus.middleware.PrometheusAfterMiddleware", |     "django_prometheus.middleware.PrometheusAfterMiddleware", | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -118,6 +118,7 @@ class EmailStageView(ChallengeStageView): | |||||||
|                     "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}), |                     "url": self.get_full_url(**{QS_KEY_TOKEN: token.key}), | ||||||
|                     "user": pending_user, |                     "user": pending_user, | ||||||
|                     "expires": token.expires, |                     "expires": token.expires, | ||||||
|  |                     "token": token.key, | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|             send_mails(current_stage, message) |             send_mails(current_stage, message) | ||||||
|  | |||||||
| @ -7609,6 +7609,11 @@ | |||||||
|                     "title": "Domain", |                     "title": "Domain", | ||||||
|                     "description": "Domain that activates this brand. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`" |                     "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": { |                 "default": { | ||||||
|                     "type": "boolean", |                     "type": "boolean", | ||||||
|                     "title": "Default" |                     "title": "Default" | ||||||
| @ -7652,6 +7657,11 @@ | |||||||
|                     "type": "integer", |                     "type": "integer", | ||||||
|                     "title": "Flow device code" |                     "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": { |                 "web_certificate": { | ||||||
|                     "type": "integer", |                     "type": "integer", | ||||||
|                     "title": "Web certificate", |                     "title": "Web certificate", | ||||||
|  | |||||||
							
								
								
									
										30
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								schema.yml
									
									
									
									
									
								
							| @ -31334,6 +31334,10 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           description: Domain that activates this brand. Can be a superset, i.e. `a.b` |           description: Domain that activates this brand. Can be a superset, i.e. `a.b` | ||||||
|             for `aa.b` and `ba.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: |         default: | ||||||
|           type: boolean |           type: boolean | ||||||
|         branding_title: |         branding_title: | ||||||
| @ -31366,6 +31370,12 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|           nullable: true |           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: |         web_certificate: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
| @ -31384,6 +31394,10 @@ components: | |||||||
|           minLength: 1 |           minLength: 1 | ||||||
|           description: Domain that activates this brand. Can be a superset, i.e. `a.b` |           description: Domain that activates this brand. Can be a superset, i.e. `a.b` | ||||||
|             for `aa.b` and `ba.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: |         default: | ||||||
|           type: boolean |           type: boolean | ||||||
|         branding_title: |         branding_title: | ||||||
| @ -31419,6 +31433,12 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|           nullable: true |           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: |         web_certificate: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
| @ -38518,6 +38538,10 @@ components: | |||||||
|           minLength: 1 |           minLength: 1 | ||||||
|           description: Domain that activates this brand. Can be a superset, i.e. `a.b` |           description: Domain that activates this brand. Can be a superset, i.e. `a.b` | ||||||
|             for `aa.b` and `ba.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: |         default: | ||||||
|           type: boolean |           type: boolean | ||||||
|         branding_title: |         branding_title: | ||||||
| @ -38553,6 +38577,12 @@ components: | |||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|           nullable: true |           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: |         web_certificate: | ||||||
|           type: string |           type: string | ||||||
|           format: uuid |           format: uuid | ||||||
|  | |||||||
| @ -15,7 +15,13 @@ import { msg } from "@lit/localize"; | |||||||
| import { TemplateResult, html } from "lit"; | import { TemplateResult, html } from "lit"; | ||||||
| import { customElement } from "lit/decorators.js"; | 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") | @customElement("ak-brand-form") | ||||||
| export class BrandForm extends ModelForm<Brand, string> { | export class BrandForm extends ModelForm<Brand, string> { | ||||||
| @ -137,6 +143,46 @@ export class BrandForm extends ModelForm<Brand, string> { | |||||||
|                     </ak-form-element-horizontal> |                     </ak-form-element-horizontal> | ||||||
|                 </div> |                 </div> | ||||||
|             </ak-form-group> |             </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> |             <ak-form-group> | ||||||
|                 <span slot="header"> ${msg("Default flows")} </span> |                 <span slot="header"> ${msg("Default flows")} </span> | ||||||
|                 <div slot="body" class="pf-c-form"> |                 <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 "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { TemplateResult, html } from "lit"; | import { TemplateResult, html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import { Brand, CoreApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api"; | import { Brand, CoreApi, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api"; | ||||||
|  | import { WithBrandConfig } from "@goauthentik/authentik/elements/Interface/brandProvider"; | ||||||
|  |  | ||||||
| @customElement("ak-brand-list") | @customElement("ak-brand-list") | ||||||
| export class BrandListPage extends TablePage<Brand> { | export class BrandListPage extends WithBrandConfig(TablePage<Brand>) { | ||||||
|     searchEnabled(): boolean { |     searchEnabled(): boolean { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| @ -84,7 +85,9 @@ export class BrandListPage extends TablePage<Brand> { | |||||||
|  |  | ||||||
|     row(item: Brand): TemplateResult[] { |     row(item: Brand): TemplateResult[] { | ||||||
|         return [ |         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`${item.brandingTitle}`, | ||||||
|             html`<ak-status-label ?good=${item._default}></ak-status-label>`, |             html`<ak-status-label ?good=${item._default}></ak-status-label>`, | ||||||
|             html`<ak-forms-modal> |             html`<ak-forms-modal> | ||||||
|  | |||||||
| @ -29,6 +29,8 @@ class PreviewStageHost implements StageHost { | |||||||
|     flowSlug = undefined; |     flowSlug = undefined; | ||||||
|     loading = false; |     loading = false; | ||||||
|     brand = undefined; |     brand = undefined; | ||||||
|  |     frameMode = false; | ||||||
|  |  | ||||||
|     async submit(payload: unknown): Promise<boolean> { |     async submit(payload: unknown): Promise<boolean> { | ||||||
|         this.promptForm.previewResult = payload; |         this.promptForm.previewResult = payload; | ||||||
|         return false; |         return false; | ||||||
|  | |||||||
| @ -61,16 +61,18 @@ export function brand(): Promise<CurrentBrand> { | |||||||
|     return globalBrandPromise; |     return globalBrandPromise; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getMetaContent(key: string): string { | export function getMetaContent(key: string): string | undefined { | ||||||
|     const metaEl = document.querySelector<HTMLMetaElement>(`meta[name=${key}]`); |     const metaEl = document.querySelector<HTMLMetaElement>(`meta[name=${key}]`); | ||||||
|     if (!metaEl) return ""; |     return metaEl?.content; | ||||||
|     return metaEl.content; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export const DEFAULT_CONFIG = new Configuration({ | 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: { |     headers: { | ||||||
|         "sentry-trace": getMetaContent("sentry-trace"), |         "sentry-trace": getMetaContent("sentry-trace") || "", | ||||||
|     }, |     }, | ||||||
|     middleware: [ |     middleware: [ | ||||||
|         new CSRFMiddleware(), |         new CSRFMiddleware(), | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import { ensureCSSStyleSheet } from "@goauthentik/authentik/elements/utils/ensureCSSStyleSheet"; | ||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { | import { | ||||||
|     EVENT_FLOW_ADVANCE, |     EVENT_FLOW_ADVANCE, | ||||||
| @ -76,7 +77,8 @@ export class FlowExecutor extends Interface implements StageHost { | |||||||
|     @state() |     @state() | ||||||
|     flowInfo?: ContextualFlowInfo; |     flowInfo?: ContextualFlowInfo; | ||||||
|  |  | ||||||
|     ws: WebsocketClient; |     @state() | ||||||
|  |     frameMode = window !== window.top; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [PFBase, PFLogin, PFDrawer, PFButton, PFTitle, PFList, PFBackgroundImage].concat(css` |         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 { |             :host([theme="dark"]) .pf-c-login.sidebar_right .pf-c-list { | ||||||
|                 color: var(--ak-dark-foreground); |                 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 { |             .pf-c-brand { | ||||||
|                 padding-top: calc( |                 padding-top: calc( | ||||||
|                     var(--pf-c-login__main-footer-links--PaddingTop) + |                     var(--pf-c-login__main-footer-links--PaddingTop) + | ||||||
| @ -161,7 +182,21 @@ export class FlowExecutor extends Interface implements StageHost { | |||||||
|  |  | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         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")) { |         if (window.location.search.includes("inspector")) { | ||||||
|             this.inspectorOpen = true; |             this.inspectorOpen = true; | ||||||
|         } |         } | ||||||
| @ -437,9 +472,11 @@ export class FlowExecutor extends Interface implements StageHost { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderChallengeWrapper(): TemplateResult { |     renderChallengeWrapper(): TemplateResult { | ||||||
|         const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand"> |         const logo = this.frameMode | ||||||
|             <img src="${first(this.brand?.brandingLogo, "")}" alt="authentik Logo" /> |             ? nothing | ||||||
|         </div>`; |             : 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) { |         if (!this.challenge) { | ||||||
|             return html`${logo}<ak-empty-state ?loading=${true} header=${msg("Loading")}> |             return html`${logo}<ak-empty-state ?loading=${true} header=${msg("Loading")}> | ||||||
|                 </ak-empty-state>`; |                 </ak-empty-state>`; | ||||||
| @ -482,7 +519,26 @@ export class FlowExecutor extends Interface implements StageHost { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         return html` <ak-locale-context> |         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-background-image"></div> | ||||||
|             <div class="pf-c-page__drawer"> |             <div class="pf-c-page__drawer"> | ||||||
|                 <div class="pf-c-drawer ${this.inspectorOpen ? "pf-m-expanded" : "pf-m-collapsed"}"> |                 <div class="pf-c-drawer ${this.inspectorOpen ? "pf-m-expanded" : "pf-m-collapsed"}"> | ||||||
|  | |||||||
| @ -49,8 +49,27 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes | |||||||
|             "authentik/stages/redirect: redirecting to url from server", |             "authentik/stages/redirect: redirecting to url from server", | ||||||
|             this.challenge.to, |             this.challenge.to, | ||||||
|         ); |         ); | ||||||
|         window.location.assign(this.challenge.to); |  | ||||||
|         this.startedRedirect = true; |         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 { |     renderLoading(): TemplateResult { | ||||||
|  | |||||||
| @ -47,6 +47,10 @@ export class AuthenticatorValidateStage | |||||||
|         return this.host.brand; |         return this.host.brand; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     get frameMode(): boolean { | ||||||
|  |         return this.host.frameMode; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @state() |     @state() | ||||||
|     _selectedDeviceChallenge?: DeviceChallenge; |     _selectedDeviceChallenge?: DeviceChallenge; | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ export interface StageHost { | |||||||
|     challenge?: unknown; |     challenge?: unknown; | ||||||
|     flowSlug?: string; |     flowSlug?: string; | ||||||
|     loading: boolean; |     loading: boolean; | ||||||
|  |     frameMode: boolean; | ||||||
|     submit(payload: unknown, options?: SubmitOptions): Promise<boolean>; |     submit(payload: unknown, options?: SubmitOptions): Promise<boolean>; | ||||||
|  |  | ||||||
|     readonly brand?: CurrentBrand; |     readonly brand?: CurrentBrand; | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ import "@goauthentik/elements/sidebar/Sidebar"; | |||||||
| import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; | import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; | ||||||
| import "@goauthentik/elements/sidebar/SidebarItem"; | import "@goauthentik/elements/sidebar/SidebarItem"; | ||||||
| import { ROUTES } from "@goauthentik/user/Routes"; | import { ROUTES } from "@goauthentik/user/Routes"; | ||||||
|  | import "@goauthentik/user/user-settings/details/UserSettingsFlowExecutor"; | ||||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||||
| import { match } from "ts-pattern"; | import { match } from "ts-pattern"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -53,6 +53,8 @@ export class UserSettingsFlowExecutor | |||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean }) | ||||||
|     loading = false; |     loading = false; | ||||||
|  |  | ||||||
|  |     frameMode = false; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [PFBase, PFCard, PFPage, PFButton, PFContent]; |         return [PFBase, PFCard, PFPage, PFButton, PFContent]; | ||||||
|     } |     } | ||||||
| @ -87,7 +89,7 @@ export class UserSettingsFlowExecutor | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     firstUpdated(): void { |     firstUpdated(): void { | ||||||
|         this.flowSlug = this.brand?.flowUserSettings; |         this.flowSlug = this.flowSlug || this.brand?.flowUserSettings; | ||||||
|         if (!this.flowSlug) { |         if (!this.flowSlug) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	