core: b2c improvements p1 (#9257)
* add default app and restrict Signed-off-by: Jens Langhammer <jens@goauthentik.io> * also pass raw email token for custom email templates Signed-off-by: Jens Langhammer <jens@goauthentik.io> * revoke access token when user logs out Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remigrate Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add command to change user types Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add some docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * blankable Signed-off-by: Jens Langhammer <jens@goauthentik.io> * actually fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -55,6 +55,7 @@ class BrandSerializer(ModelSerializer):
|
||||
"flow_unenrollment",
|
||||
"flow_user_settings",
|
||||
"flow_device_code",
|
||||
"default_application",
|
||||
"web_certificate",
|
||||
"attributes",
|
||||
]
|
||||
|
@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.0.6 on 2024-07-04 20:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_brands", "0006_brand_authentik_b_domain_b9b24a_idx_and_more"),
|
||||
("authentik_core", "0035_alter_group_options_and_more"),
|
||||
]
|
||||
|
||||
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",
|
||||
),
|
||||
),
|
||||
]
|
@ -51,6 +51,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,
|
||||
|
28
authentik/core/management/commands/change_user_type.py
Normal file
28
authentik/core/management/commands/change_user_type.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Change user type"""
|
||||
|
||||
from authentik.core.models import User, UserTypes
|
||||
from authentik.tenants.management import TenantCommand
|
||||
|
||||
|
||||
class Command(TenantCommand):
|
||||
"""Change user type"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--type", type=str, required=True)
|
||||
parser.add_argument("--all", action="store_true")
|
||||
parser.add_argument("usernames", nargs="+", type=str)
|
||||
|
||||
def handle_per_tenant(self, **options):
|
||||
new_type = UserTypes(options["type"])
|
||||
qs = (
|
||||
User.objects.exclude_anonymous()
|
||||
.exclude(type=UserTypes.SERVICE_ACCOUNT)
|
||||
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
)
|
||||
if options["usernames"] and options["all"]:
|
||||
self.stderr.write("--all and usernames specified, only one can be specified")
|
||||
return
|
||||
if options["usernames"] and not options["all"]:
|
||||
qs = qs.filter(username__in=options["usernames"])
|
||||
updated = qs.update(type=new_type)
|
||||
self.stdout.write(f"Updated {updated} users.")
|
@ -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
|
||||
@ -18,9 +17,13 @@ from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSe
|
||||
from authentik.core.api.tokens import TokenViewSet
|
||||
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||
from authentik.core.api.users import UserViewSet
|
||||
from authentik.core.views import apps
|
||||
from authentik.core.views.apps import RedirectToAppLaunch
|
||||
from authentik.core.views.debug import AccessDeniedView
|
||||
from authentik.core.views.interface import InterfaceView
|
||||
from authentik.core.views.interface import (
|
||||
BrandDefaultRedirectView,
|
||||
InterfaceView,
|
||||
RootRedirectView,
|
||||
)
|
||||
from authentik.core.views.session import EndSessionView
|
||||
from authentik.flows.views.interface import FlowInterfaceView
|
||||
from authentik.root.asgi_middleware import SessionMiddleware
|
||||
@ -30,26 +33,24 @@ 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(),
|
||||
RedirectToAppLaunch.as_view(),
|
||||
name="application-launch",
|
||||
),
|
||||
# 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,13 +3,42 @@
|
||||
from json import dumps
|
||||
from typing import Any
|
||||
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.http import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from django.shortcuts import 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.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):
|
||||
@ -23,3 +52,20 @@ class InterfaceView(TemplateView):
|
||||
kwargs["build"] = get_build_hash()
|
||||
kwargs["url_kwargs"] = self.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 response
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
@ -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()
|
@ -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)
|
||||
|
@ -8046,6 +8046,11 @@
|
||||
"format": "uuid",
|
||||
"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": "string",
|
||||
"format": "uuid",
|
||||
|
18
schema.yml
18
schema.yml
@ -34285,6 +34285,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
|
||||
@ -34338,6 +34344,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
|
||||
@ -41601,6 +41613,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
|
||||
|
@ -37,14 +37,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.initial_stages()
|
||||
|
||||
interface_user = self.get_shadow_root("ak-interface-user")
|
||||
wait = WebDriverWait(interface_user, self.wait_timeout)
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "ak-interface-user-presentation"))
|
||||
)
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
sleep(2)
|
||||
|
||||
user = User.objects.get(username="foo")
|
||||
self.assertEqual(user.username, "foo")
|
||||
@ -93,13 +86,6 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
self.driver.switch_to.window(self.driver.window_handles[0])
|
||||
|
||||
sleep(2)
|
||||
# We're now logged in
|
||||
wait = WebDriverWait(self.get_shadow_root("ak-interface-user"), self.wait_timeout)
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "ak-interface-user-presentation"))
|
||||
)
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
|
||||
self.assert_user(User.objects.get(username="foo"))
|
||||
|
||||
|
@ -97,15 +97,9 @@ class TestFlowsRecovery(SeleniumTestCase):
|
||||
new_password
|
||||
)
|
||||
prompt_stage.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
sleep(2)
|
||||
|
||||
# We're now logged in
|
||||
wait = WebDriverWait(self.get_shadow_root("ak-interface-user"), self.wait_timeout)
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "ak-interface-user-presentation"))
|
||||
)
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
|
||||
self.assert_user(user)
|
||||
user.refresh_from_db()
|
||||
self.assertTrue(user.check_password(new_password))
|
||||
|
@ -136,7 +136,6 @@ class TestSourceOAuth1(SeleniumTestCase):
|
||||
# Wait until we've loaded the user info page
|
||||
sleep(2)
|
||||
# Wait until we've logged in
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
self.assert_user(User(username="example-user", name="test name", email="foo@example.com"))
|
||||
|
@ -155,8 +155,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
prompt_stage.find_element(By.CSS_SELECTOR, "input[name=username]").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we've logged in
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
||||
|
||||
@ -191,8 +190,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
# Wait until we've logged in
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
||||
|
||||
|
@ -168,8 +168,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
@ -251,8 +250,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
@ -321,8 +319,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
@ -391,8 +388,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
@ -426,8 +422,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url("/library"))
|
||||
self.driver.get(self.if_user_url("/settings"))
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
# sleep(999999)
|
||||
self.assert_user(
|
||||
|
@ -176,9 +176,12 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
|
||||
return self.live_server_url + reverse(view, kwargs=kwargs)
|
||||
|
||||
def if_user_url(self, view) -> str:
|
||||
def if_user_url(self, path: str | None = None) -> str:
|
||||
"""same as self.url() but show URL in shell"""
|
||||
return f"{self.live_server_url}/if/user/#{view}"
|
||||
url = self.url("authentik_core:if-user")
|
||||
if path:
|
||||
return f"{url}#{path}"
|
||||
return url
|
||||
|
||||
def get_shadow_root(
|
||||
self, selector: str, container: WebElement | WebDriver | None = None
|
||||
|
@ -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,52 @@ 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
|
||||
blankable
|
||||
.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>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When configured, external users will automatically be redirected to this application when not attempting to access a different application",
|
||||
)}
|
||||
</p>
|
||||
</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">
|
||||
@ -174,11 +226,6 @@ export class BrandForm extends ModelForm<Brand, string> {
|
||||
flowType=${FlowsInstancesListDesignationEnum.Recovery}
|
||||
.currentFlow=${this.instance?.flowRecovery}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Recovery flow. If left empty, the first applicable flow sorted by the slug is used.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Unenrollment flow")}
|
||||
|
@ -20,3 +20,7 @@ This means that if you want to select a default flow based on policy, you can le
|
||||
## Branding
|
||||
|
||||
The brand configuration controls the branding title (shown in website document title and several other places), the sidebar/header logo that appears in the upper left of the product interface, and the favicon on a browser tab.
|
||||
|
||||
## External user settings
|
||||
|
||||
The **Default application** configuration can be used to redirect external users to an application when they successfully authenticate without being sent from a specific application.
|
||||
|
@ -11,7 +11,19 @@ To try out the release candidate, replace your Docker image tag with the latest
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- **Changed HTTP Healthcheck endpoints status code**
|
||||
### Manual action may be required
|
||||
|
||||
- **Changes to the external user type**
|
||||
|
||||
Since the introduction of user types with [2023.8](../2023/v2023.8.md), the main difference between internal and external users has mostly been relevant when using the [Enterprise](../../enterprise/index.md) version of authentik.
|
||||
|
||||
With this release, authentik improves support for B2C use-cases, which external users are intended for. It is now possible to configure a default application. External users _not_ attempting to access a specific application will always be redirected to this default application.
|
||||
|
||||
As part of this, external users will no longer have access to the User and Admin interfaces. If you're using the open-source version and you require this workflow, you can change users to be Internal, which will have no side-effects. For enterprise customers, please reach out to us with any questions.
|
||||
|
||||
<details><summary>Bulk changing the user type</summary>In the container, run the command `ak change_user_type --all --type internal` to change all users to Internal. Instead of using `--all` you can also pass usernames to the command to only change individual users to internal.</details>
|
||||
|
||||
- **Changed HTTP healthcheck endpoints status code**
|
||||
|
||||
For increased compatibility, the `/-/health/live/` and `/-/health/ready/` endpoints return 200 HTTP Status codes for successful checks. Previously these endpoints returned 204, which means in most cases no changes are required.
|
||||
|
||||
|
Reference in New Issue
Block a user