Compare commits

...

18 Commits

Author SHA1 Message Date
9075270b01 release: 2024.6.1 2024-07-11 21:45:54 +02:00
d17a39a431 website/docs: add 2024.6.1 release notes (cherry-pick #10456) (#10458)
website/docs: add 2024.6.1 release notes (#10456)

* website/docs: add 2024.6.1 release notes



* update



* fix version requirement for sfe



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-11 19:11:28 +02:00
db1d091d2e core: revert backchannel only filtering (cherry-pick #10455) (#10457)
core: revert backchannel only filtering (#10455)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-11 16:58:29 +02:00
f98204e78e core: fix source flow_manager not resuming flow when linking (cherry-pick #10436) (#10438)
core: fix source flow_manager not resuming flow when linking (#10436)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-10 15:20:15 +02:00
3f663cab0f web/admin: fix access token list calling wrong API (cherry-pick #10434) (#10435)
web/admin: fix access token list calling wrong API (#10434)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-10 14:17:47 +02:00
3fe129e107 core: fix migrations missing using db_alias (cherry-pick #10409) (#10410)
core: fix migrations missing using db_alias (#10409)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-07-09 10:48:29 +02:00
f26d41aef9 web: bump API Client version (#10389)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
#	web/package.json
2024-07-05 20:49:31 +02:00
5d8b5998ae web/flows: Simplified flow executor (#10296)
* initial sfe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* build sfe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* downgrade bootstrap

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix path

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make IE compatible

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix query string missing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add autosubmit stage

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add background image

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add code support

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add support for combo ident/password

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix logo rendering

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only use for edge 18 and before

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix lint

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add webauthn support

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate to TS for some creature comforts

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix ci

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* dedupe dependabot

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use API client...kinda

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more polyfills yay

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* turn powered by into span

prevent issues in restricted browsers where users might not be able to return

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* allow non-link footer entries

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tsc errors

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

* auto switch for macos

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* reword

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Update website/docs/flow/executors/if-flow.md

Signed-off-by: Jens L. <jens@beryju.org>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	.github/workflows/ci-web.yml
#	Dockerfile
#	website/developer-docs/api/flow-executor.md
2024-07-05 20:43:14 +02:00
7a5e136346 stages/authenticator_validate: fix friendly_name being required (cherry-pick #10382) (#10385)
stages/authenticator_validate: fix friendly_name being required (#10382)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-05 15:50:14 +02:00
bfbab6357a sources/oauth: fix link not being saved (cherry-pick #10374) (#10376)
sources/oauth: fix link not being saved (#10374)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-04 16:58:38 +02:00
5997b93f15 sources/saml: fix pickle error, add saml auth tests (cherry-pick #10348) (#10352)
sources/saml: fix pickle error, add saml auth tests (#10348)

* test with persistent nameid



* fix pickle



* user_write: dont attempt to write to read only property



* add test for enroll + auth



* unwrap lazy user



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-03 18:34:22 +02:00
6cdae09dc0 providers/saml: fix metadata import error handling (cherry-pick #10349) (#10350)
Co-authored-by: Jens L <jens@goauthentik.io>
fix metadata import error handling (#10349)
2024-07-03 16:01:50 +00:00
ff0ef7a2b3 web: set noopener and noreferrer on all external links (#10304)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-07-02 14:54:03 +02:00
3986104a20 provider/scim: Fix exception handling for missing ServiceProviderConfig (cherry-pick #10322) (#10335)
provider/scim: Fix exception handling for missing ServiceProviderConfig (#10322)

Co-authored-by: Michael Poutre <m1kep.my.mail@gmail.com>
2024-07-02 13:53:27 +02:00
1aa60e7864 core: remove transitionary old JS urls (cherry-pick #10317) (#10321)
core: remove transitionary old JS urls (#10317)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-01 21:00:05 +02:00
045578dd07 web/flows: remove background image link (cherry-pick #10318) (#10320)
web/flows: remove background image link (#10318)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-07-01 20:28:30 +02:00
f23d70dc75 stages/user_login: fix ?next parameter not carried through broken session binding (cherry-pick #10301) (#10302)
stages/user_login: fix ?next parameter not carried through broken session binding (#10301)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-06-29 23:17:13 +02:00
496f3426d9 website/docs: update geoip and asn documentation following field changes (cherry-pick #10265) (#10266)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-06-27 13:26:31 +00:00
78 changed files with 7633 additions and 4509 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.6.0
current_version = 2024.6.1
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -21,7 +21,10 @@ updates:
labels:
- dependencies
- package-ecosystem: npm
directory: "/web"
directories:
- "/web"
- "/tests/wdio"
- "/web/sfe"
schedule:
interval: daily
time: "04:00"
@ -30,7 +33,6 @@ updates:
open-pull-requests-limit: 10
commit-message:
prefix: "web:"
# TODO: deduplicate these groups
groups:
sentry:
patterns:
@ -56,38 +58,6 @@ updates:
patterns:
- "@rollup/*"
- "rollup-*"
- package-ecosystem: npm
directory: "/tests/wdio"
schedule:
interval: daily
time: "04:00"
labels:
- dependencies
open-pull-requests-limit: 10
commit-message:
prefix: "web:"
# TODO: deduplicate these groups
groups:
sentry:
patterns:
- "@sentry/*"
- "@spotlightjs/*"
babel:
patterns:
- "@babel/*"
- "babel-*"
eslint:
patterns:
- "@typescript-eslint/*"
- "eslint"
- "eslint-*"
storybook:
patterns:
- "@storybook/*"
- "*storybook*"
esbuild:
patterns:
- "@esbuild/*"
wdio:
patterns:
- "@wdio/*"

View File

@ -31,7 +31,12 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
- name: Upgrade /web
working-directory: web/
working-directory: web
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION
- name: Upgrade /web/sfe
working-directory: web/sfe
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION

View File

@ -20,6 +20,16 @@ jobs:
project:
- web
- tests/wdio
include:
- command: tsc
project: web
extra_setup: |
cd sfe/ && npm ci
exclude:
- command: lint:lockfile
project: tests/wdio
- command: tsc
project: tests/wdio
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

View File

@ -30,7 +30,12 @@ WORKDIR /work/web
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=bind,target=/work/web/sfe/package.json,src=./web/sfe/package.json \
--mount=type=bind,target=/work/web/sfe/package-lock.json,src=./web/sfe/package-lock.json \
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev && \
cd sfe && \
npm ci --include=dev
COPY ./package.json /work
@ -38,7 +43,9 @@ COPY ./web /work/web/
COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build
RUN npm run build && \
cd sfe && \
npm run build
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-bookworm AS go-builder

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.6.0"
__version__ = "2024.6.1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -24,7 +24,7 @@ from authentik.tenants.utils import get_current_tenant
class FooterLinkSerializer(PassiveSerializer):
"""Links returned in Config API"""
href = CharField(read_only=True)
href = CharField(read_only=True, allow_null=True)
name = CharField(read_only=True)

View File

@ -7,12 +7,13 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.scim.models import SCIMProvider
for model in [LDAPProvider, SCIMProvider]:
try:
for obj in model.objects.only("is_backchannel"):
for obj in model.objects.using(db_alias).only("is_backchannel"):
obj.is_backchannel = True
obj.save()
except (DatabaseError, InternalError, ProgrammingError):

View File

@ -212,7 +212,7 @@ class SourceFlowManager:
def _prepare_flow(
self,
flow: Flow,
flow: Flow | None,
connection: UserSourceConnection,
stages: list[StageView] | None = None,
**kwargs,
@ -309,7 +309,9 @@ class SourceFlowManager:
# When request isn't authenticated we jump straight to auth
if not self.request.user.is_authenticated:
return self.handle_auth(connection)
# Connection has already been saved
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
return self._prepare_flow(None, connection)
connection.save()
Event.new(
EventAction.SOURCE_LINKED,
message="Linked Source",

View File

@ -10,7 +10,7 @@
versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}",
};
window.addEventListener("DOMContentLoaded", () => {
window.addEventListener("DOMContentLoaded", function () {
{% for message in messages %}
window.dispatchEvent(
new CustomEvent("ak-message", {

View File

@ -71,9 +71,9 @@
</li>
{% endfor %}
<li>
<a href="https://goauthentik.io?utm_source=authentik">
<span>
{% trans 'Powered by authentik' %}
</a>
</span>
</li>
</ul>
</footer>

View File

@ -17,11 +17,5 @@ def versioned_script(path: str) -> str:
f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
'" type="module"></script>'
),
# Legacy method of loading scripts used as a fallback, without the version in the filename
# TODO: Remove after 2024.6 or later
(
f'<script src="{static_loader(path.replace("-%v", ""))}?'
f'version={get_full_version()}" type="module"></script>'
),
]
return mark_safe("".join(returned_lines)) # nosec

View File

@ -20,8 +20,9 @@ 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 InterfaceView
from authentik.core.views.session import EndSessionView
from authentik.flows.views.interface import FlowInterfaceView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware
@ -53,6 +54,8 @@ urlpatterns = [
),
path(
"if/flow/<slug:flow_slug>/",
# FIXME: move this url to the flows app...also will cause all
# of the reverse calls to be adjusted
ensure_csrf_cookie(FlowInterfaceView.as_view()),
name="if-flow",
),

View File

@ -3,7 +3,6 @@
from json import dumps
from typing import Any
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView
from rest_framework.request import Request
@ -11,7 +10,6 @@ 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.flows.models import Flow
class InterfaceView(TemplateView):
@ -25,14 +23,3 @@ class InterfaceView(TemplateView):
kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs
return super().get_context_data(**kwargs)
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
template_name = "if/flow.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)

View File

@ -21,7 +21,9 @@ def set_oobe_flow_authentication(apps: Apps, schema_editor: BaseDatabaseSchemaEd
pass
if users.exists():
Flow.objects.filter(slug="initial-setup").update(authentication="require_superuser")
Flow.objects.using(db_alias).filter(slug="initial-setup").update(
authentication="require_superuser"
)
class Migration(migrations.Migration):

View File

@ -0,0 +1,54 @@
{% load static %}
{% load i18n %}
{% load authentik_core %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
<meta name="sentry-trace" content="{{ sentry_trace }}" />
{% include "base/header_js.html" %}
<style>
html,
body {
height: 100%;
}
body {
background-image: url("{{ flow.background_url }}");
background-repeat: no-repeat;
background-size: cover;
}
.card {
padding: 3rem;
}
.form-signin {
max-width: 330px;
padding: 1rem;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.brand-icon {
max-width: 100%;
}
</style>
</head>
<body class="d-flex align-items-center py-4 bg-body-tertiary">
<div class="card m-auto">
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
</main>
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
</div>
<script src="{% static 'dist/sfe/index.js' %}"></script>
</body>
</html>

View File

@ -0,0 +1,41 @@
"""Interface views"""
from typing import Any
from django.shortcuts import get_object_or_404
from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)
def compat_needs_sfe(self) -> bool:
"""Check if we need to use the simplified flow executor for compatibility"""
ua = Parse(self.request.META.get("HTTP_USER_AGENT", ""))
if ua["user_agent"]["family"] == "IE":
return True
# Only use SFE for Edge 18 and older, after Edge 18 MS switched to chromium which supports
# the default flow executor
if (
ua["user_agent"]["family"] == "Edge"
and int(ua["user_agent"]["major"]) <= 18 # noqa: PLR2004
): # noqa: PLR2004
return True
# https://github.com/AzureAD/microsoft-authentication-library-for-objc
# Used by Microsoft Teams/Office on macOS, and also uses a very outdated browser engine
if "PKeyAuth" in ua["string"]:
return True
return False
def get_template_names(self) -> list[str]:
if self.compat_needs_sfe() or "sfe" in self.request.GET:
return ["if/flow-sfe.html"]
return ["if/flow.html"]

View File

@ -2,6 +2,7 @@ from collections.abc import Callable
from django.core.paginator import Paginator
from django.db.models import Model
from django.db.models.query import Q
from django.db.models.signals import m2m_changed, post_save, pre_delete
from authentik.core.models import Group, User
@ -34,7 +35,9 @@ def register_signals(
def model_post_save(sender: type[Model], instance: User | Group, created: bool, **_):
"""Post save handler"""
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
if not provider_type.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
).exists():
return
task_sync_direct.delay(class_to_path(instance.__class__), instance.pk, Direction.add.value)
@ -43,7 +46,9 @@ def register_signals(
def model_pre_delete(sender: type[Model], instance: User | Group, **_):
"""Pre-delete handler"""
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
if not provider_type.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
).exists():
return
task_sync_direct.delay(
class_to_path(instance.__class__), instance.pk, Direction.remove.value
@ -58,7 +63,9 @@ def register_signals(
"""Sync group membership"""
if action not in ["post_add", "post_remove"]:
return
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
if not provider_type.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
).exists():
return
# reverse: instance is a Group, pk_set is a list of user pks
# non-reverse: instance is a User, pk_set is a list of groups

View File

@ -5,6 +5,7 @@ from celery.exceptions import Retry
from celery.result import allow_join_result
from django.core.paginator import Paginator
from django.db.models import Model, QuerySet
from django.db.models.query import Q
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import BoundLogger, get_logger
@ -37,7 +38,9 @@ class SyncTasks:
self._provider_model = provider_model
def sync_all(self, single_sync: Callable[[int], None]):
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
for provider in self._provider_model.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
):
self.trigger_single_task(provider, single_sync)
def trigger_single_task(self, provider: OutgoingSyncProvider, sync_task: Callable[[int], None]):
@ -62,7 +65,8 @@ class SyncTasks:
provider_pk=provider_pk,
)
provider = self._provider_model.objects.filter(
pk=provider_pk, backchannel_application__isnull=False
Q(backchannel_application__isnull=False) | Q(application__isnull=False),
pk=provider_pk,
).first()
if not provider:
return
@ -204,7 +208,9 @@ class SyncTasks:
if not instance:
return
operation = Direction(raw_op)
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
for provider in self._provider_model.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
):
client = provider.client_for_model(instance.__class__)
# Check if the object is allowed within the provider's restrictions
queryset = provider.get_object_qs(instance.__class__)
@ -233,7 +239,9 @@ class SyncTasks:
group = Group.objects.filter(pk=group_pk).first()
if not group:
return
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
for provider in self._provider_model.objects.filter(
Q(backchannel_application__isnull=False) | Q(application__isnull=False)
):
# Check if the object is allowed within the provider's restrictions
queryset: QuerySet = provider.get_object_qs(Group)
# The queryset we get from the provider must include the instance we've got given

View File

@ -13,16 +13,17 @@ import authentik.outposts.models
def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
User = apps.get_model("authentik_core", "User")
Token = apps.get_model("authentik_core", "Token")
from authentik.outposts.models import Outpost
for outpost in Outpost.objects.using(schema_editor.connection.alias).all().only("pk"):
for outpost in Outpost.objects.using(db_alias).all().only("pk"):
user_identifier = outpost.user_identifier
users = User.objects.filter(username=user_identifier)
users = User.objects.using(db_alias).filter(username=user_identifier)
if not users.exists():
continue
tokens = Token.objects.filter(user=users.first())
tokens = Token.objects.using(db_alias).filter(user=users.first())
for token in tokens:
if token.identifier != outpost.token_identifier:
token.identifier = outpost.token_identifier
@ -37,8 +38,8 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
"authentik_outposts", "KubernetesServiceConnection"
)
docker = DockerServiceConnection.objects.filter(local=True).first()
k8s = KubernetesServiceConnection.objects.filter(local=True).first()
docker = DockerServiceConnection.objects.using(db_alias).filter(local=True).first()
k8s = KubernetesServiceConnection.objects.using(db_alias).filter(local=True).first()
try:
for outpost in Outpost.objects.using(db_alias).all().exclude(deployment_type="custom"):
@ -54,21 +55,21 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
def remove_pb_prefix_users(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
alias = schema_editor.connection.alias
db_alias = schema_editor.connection.alias
User = apps.get_model("authentik_core", "User")
Outpost = apps.get_model("authentik_outposts", "Outpost")
for outpost in Outpost.objects.using(alias).all():
matching = User.objects.using(alias).filter(username=f"pb-outpost-{outpost.uuid.hex}")
for outpost in Outpost.objects.using(db_alias).all():
matching = User.objects.using(db_alias).filter(username=f"pb-outpost-{outpost.uuid.hex}")
if matching.exists():
matching.delete()
def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
alias = schema_editor.connection.alias
db_alias = schema_editor.connection.alias
Outpost = apps.get_model("authentik_outposts", "Outpost")
for outpost in Outpost.objects.using(alias).all():
for outpost in Outpost.objects.using(db_alias).all():
config = outpost._config
for key in list(config):
if "passbook" in key:

View File

@ -268,7 +268,7 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
except ValueError as exc: # pragma: no cover
LOGGER.warning(str(exc))
raise ValidationError(
_("Failed to import Metadata: {messages}".format_map({"message": str(exc)})),
_("Failed to import Metadata: {messages}".format_map({"messages": str(exc)})),
) from None
return Response(status=204)

View File

@ -89,6 +89,6 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
return ServiceProviderConfiguration.model_validate(
self._request("GET", "/ServiceProviderConfig")
)
except (ValidationError, SCIMRequestException) as exc:
except (ValidationError, SCIMRequestException, NotFoundSyncException) as exc:
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
return default_config

View File

@ -31,9 +31,9 @@ def set_default_group_mappings(apps: Apps, schema_editor):
db_alias = schema_editor.connection.alias
for source in LDAPSource.objects.using(db_alias).all():
if source.property_mappings_group.exists():
if source.property_mappings_group.using(db_alias).exists():
continue
source.property_mappings_group.set(
source.property_mappings_group.using(db_alias).set(
LDAPPropertyMapping.objects.using(db_alias).filter(
managed="goauthentik.io/sources/ldap/default-name"
)

View File

@ -10,6 +10,8 @@ from authentik.sources.saml.processors import constants
def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
SAMLSource = apps.get_model("authentik_sources_saml", "SAMLSource")
signature_translation_map = {
"rsa-sha1": constants.RSA_SHA1,
@ -22,7 +24,7 @@ def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
"sha256": constants.SHA256,
}
for source in SAMLSource.objects.all():
for source in SAMLSource.objects.using(db_alias).all():
source.signature_algorithm = signature_translation_map.get(
source.signature_algorithm, constants.RSA_SHA256
)

View File

@ -10,6 +10,7 @@ from django.core.cache import cache
from django.core.exceptions import SuspiciousOperation
from django.http import HttpRequest
from django.utils.timezone import now
from lxml import etree # nosec
from structlog.stdlib import get_logger
from authentik.core.models import (
@ -240,7 +241,7 @@ class ResponseProcessor:
name_id.text,
delete_none_values(self.get_attributes()),
)
flow_manager.policy_context["saml_response"] = self._root
flow_manager.policy_context["saml_response"] = etree.tostring(self._root)
return flow_manager

View File

@ -13,7 +13,7 @@ def migrate_configuration_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEdi
for stage in AuthenticatorValidateStage.objects.using(db_alias).all():
if stage.configuration_stage:
stage.configuration_stages.set([stage.configuration_stage])
stage.configuration_stages.using(db_alias).set([stage.configuration_stage])
stage.save()

View File

@ -325,7 +325,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
serializer = SelectableStageSerializer(
data={
"pk": stage.pk,
"name": stage.friendly_name or stage.name,
"name": getattr(stage, "friendly_name", stage.name),
"verbose_name": str(stage._meta.verbose_name)
.replace("Setup Stage", "")
.strip(),

View File

@ -120,7 +120,7 @@
</tr>
<tr>
<td align="center">
Powered by <a href="https://goauthentik.io?utm_source=authentik&utm_medium=email">authentik</a>.
Powered by <a rel="noopener noreferrer" target="_blank" href="https://goauthentik.io?utm_source=authentik&utm_medium=email">authentik</a>.
</td>
</tr>
</table>

View File

@ -13,9 +13,9 @@ def assign_sources(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
IdentificationStage = apps.get_model("authentik_stages_identification", "identificationstage")
Source = apps.get_model("authentik_core", "source")
sources = Source.objects.all()
for stage in IdentificationStage.objects.all().using(db_alias):
stage.sources.set(sources)
sources = Source.objects.using(db_alias).all()
for stage in IdentificationStage.objects.using(db_alias).all():
stage.sources.using(db_alias).set(sources)
stage.save()

View File

@ -12,7 +12,7 @@ def set_generated_name(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
for prompt in Prompt.objects.using(db_alias).all():
name = prompt.field_key
stage = prompt.promptstage_set.order_by("name").first()
stage = prompt.promptstage_set.using(db_alias).order_by("name").first()
if stage:
name += "_" + stage.name
else:

View File

@ -1,10 +1,9 @@
"""Sessions bound to ASN/Network and GeoIP/Continent/etc"""
from django.conf import settings
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.auth.signals import user_logged_out
from django.contrib.auth.views import redirect_to_login
from django.http.request import HttpRequest
from django.shortcuts import redirect
from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession
@ -87,7 +86,7 @@ class BoundSessionMiddleware(SessionMiddleware):
AuthenticationMiddleware(lambda request: request).process_request(request)
logout_extra(request, exc)
request.session.clear()
return redirect(settings.LOGIN_URL)
return redirect_to_login(request.get_full_path())
return None
def recheck_session(self, request: HttpRequest):

View File

@ -6,6 +6,7 @@ from django.contrib.auth import update_session_auth_hash
from django.db import transaction
from django.db.utils import IntegrityError, InternalError
from django.http import HttpRequest, HttpResponse
from django.utils.functional import SimpleLazyObject
from django.utils.translation import gettext as _
from rest_framework.exceptions import ValidationError
@ -118,6 +119,14 @@ class UserWriteStageView(StageView):
UserWriteStageView.write_attribute(user, key, value)
# User has this key already
elif hasattr(user, key):
if isinstance(user, SimpleLazyObject):
user._setup()
user = user._wrapped
attr = getattr(type(user), key)
if isinstance(attr, property):
if not attr.fset:
self.logger.info("discarding key", key=key)
continue
setattr(user, key, value)
# If none of the cases above matched, we have an attribute that the user doesn't have,
# has no setter for, is not a nested attributes value and as such is invalid

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.6.0 Blueprint schema",
"title": "authentik 2024.6.1 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.1}
restart: unless-stopped
command: server
environment:
@ -52,7 +52,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.1}
restart: unless-stopped
command: worker
environment:

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2024.6.0"
const VERSION = "2024.6.1"

View File

@ -48,9 +48,9 @@
<footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline">
<li>
<a href="https://goauthentik.io?utm_source=authentik_outpost&utm_campaign=proxy_error">
<span>
Powered by authentik
</a>
</span>
</li>
</ul>
</footer>

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2024.6.0",
"version": "2024.6.1",
"private": true
}

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.6.0"
version = "2024.6.1"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.6.0
version: 2024.6.1
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -36625,6 +36625,7 @@ components:
href:
type: string
readOnly: true
nullable: true
name:
type: string
readOnly: true

View File

@ -0,0 +1,23 @@
<?php
/**
* SAML 2.0 remote SP metadata for SimpleSAMLphp.
*
* See: https://simplesamlphp.org/docs/stable/simplesamlphp-reference-sp-remote
*/
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array(
'AssertionConsumerService' => getenv('SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE'),
'SingleLogoutService' => getenv('SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE'),
);
if (null != getenv('SIMPLESAMLPHP_SP_NAME_ID_FORMAT')) {
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('NameIDFormat' => getenv('SIMPLESAMLPHP_SP_NAME_ID_FORMAT')));
}
if (null != getenv('SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE')) {
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('simplesaml.nameidattribute' => getenv('SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE')));
}
if (null != getenv('SIMPLESAMLPHP_SP_SIGN_ASSERTION')) {
$metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')] = array_merge($metadata[getenv('SIMPLESAMLPHP_SP_ENTITY_ID')], array('saml20.sign.assertion' => ('true' == getenv('SIMPLESAMLPHP_SP_SIGN_ASSERTION'))));
}

View File

@ -1,5 +1,6 @@
"""test OAuth Source"""
from json import loads
from pathlib import Path
from time import sleep
from typing import Any
@ -194,3 +195,41 @@ class TestSourceOAuth2(SeleniumTestCase):
self.driver.get(self.if_user_url("/settings"))
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-source-authentication.yaml",
"default/flow-default-source-enrollment.yaml",
"default/flow-default-source-pre-authentication.yaml",
)
def test_oauth_link(self):
"""test OAuth Source link OIDC"""
self.create_objects()
self.driver.get(self.live_server_url)
self.login()
self.driver.get(
self.url("authentik_sources_oauth:oauth-client-login", source_slug=self.slug)
)
# Now we should be at the IDP, wait for the login field
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
self.driver.find_element(By.ID, "password").send_keys("password")
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
# Wait until we're logged in
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]")))
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
self.driver.get(self.url("authentik_api:usersourceconnection-list") + "?format=json")
body_json = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
results = body_json["results"]
self.assertEqual(len(results), 1)
connection = results[0]
self.assertEqual(connection["source"]["slug"], self.slug)
self.assertEqual(connection["user"], self.user.pk)

View File

@ -1,5 +1,6 @@
"""test SAML Source"""
from pathlib import Path
from time import sleep
from typing import Any
@ -88,8 +89,20 @@ class TestSourceSAML(SeleniumTestCase):
interval=5 * 1_000 * 1_000_000,
start_period=1 * 1_000 * 1_000_000,
),
"volumes": {
str(
(Path(__file__).parent / Path("test-saml-idp/saml20-sp-remote.php")).absolute()
): {
"bind": "/var/www/simplesamlphp/metadata/saml20-sp-remote.php",
"mode": "ro",
}
},
"environment": {
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
"SIMPLESAMLPHP_SP_NAME_ID_FORMAT": (
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
),
"SIMPLESAMLPHP_SP_NAME_ID_ATTRIBUTE": "email",
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
self.url("authentik_sources_saml:acs", source_slug=self.slug)
),
@ -318,3 +331,109 @@ class TestSourceSAML(SeleniumTestCase):
.exclude(pk=self.user.pk)
.first()
)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-source-authentication.yaml",
"default/flow-default-source-enrollment.yaml",
"default/flow-default-source-pre-authentication.yaml",
)
def test_idp_post_auto_enroll_auth(self):
"""test SAML Source With post binding (auto redirect)"""
# Bootstrap all needed objects
authentication_flow = Flow.objects.get(slug="default-source-authentication")
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
pre_authentication_flow = Flow.objects.get(slug="default-source-pre-authentication")
keypair = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=IDP_CERT,
key_data=IDP_KEY,
)
source = SAMLSource.objects.create(
name=generate_id(),
slug=self.slug,
authentication_flow=authentication_flow,
enrollment_flow=enrollment_flow,
pre_authentication_flow=pre_authentication_flow,
issuer="entity-id",
sso_url=f"http://{self.host}:8080/simplesaml/saml2/idp/SSOService.php",
binding_type=SAMLBindingTypes.POST_AUTO,
signing_kp=keypair,
)
ident_stage = IdentificationStage.objects.first()
ident_stage.sources.set([source])
ident_stage.save()
self.driver.get(self.live_server_url)
flow_executor = self.get_shadow_root("ak-flow-executor")
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(
ec.presence_of_element_located(
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
)
)
identification_stage.find_element(
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click()
# Now we should be at the IDP, wait for the username field
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
self.driver.find_element(By.ID, "username").send_keys("user1")
self.driver.find_element(By.ID, "password").send_keys("user1pass")
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.assert_user(
User.objects.exclude(username="akadmin")
.exclude(username__startswith="ak-outpost")
.exclude_anonymous()
.exclude(pk=self.user.pk)
.first()
)
# Clear all cookies and log in again
self.driver.delete_all_cookies()
self.driver.get(self.live_server_url)
flow_executor = self.get_shadow_root("ak-flow-executor")
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
wait = WebDriverWait(identification_stage, self.wait_timeout)
wait.until(
ec.presence_of_element_located(
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
)
)
identification_stage.find_element(
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
).click()
# Now we should be at the IDP, wait for the username field
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
self.driver.find_element(By.ID, "username").send_keys("user1")
self.driver.find_element(By.ID, "password").send_keys("user1pass")
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"))
# sleep(999999)
self.assert_user(
User.objects.exclude(username="akadmin")
.exclude(username__startswith="ak-outpost")
.exclude_anonymous()
.exclude(pk=self.user.pk)
.first()
)

7817
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.4.2-1718378698",
"@goauthentik/api": "^2024.6.0-1720200294",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.1",
"@lit/reactive-element": "^2.0.4",

529
web/sfe/index.ts Normal file
View File

@ -0,0 +1,529 @@
import { fromByteArray } from "base64-js";
import "formdata-polyfill";
import $ from "jquery";
import "weakmap-polyfill";
import {
type AuthenticatorValidationChallenge,
type AutosubmitChallenge,
type ChallengeTypes,
ChallengeTypesFromJSON,
type ContextualFlowInfo,
type DeviceChallenge,
type ErrorDetail,
type IdentificationChallenge,
type PasswordChallenge,
type RedirectChallenge,
} from "@goauthentik/api";
interface GlobalAuthentik {
brand: {
branding_logo: string;
};
}
function ak(): GlobalAuthentik {
return (
window as unknown as {
authentik: GlobalAuthentik;
}
).authentik;
}
class SimpleFlowExecutor {
challenge?: ChallengeTypes;
flowSlug: string;
container: HTMLDivElement;
constructor(container: HTMLDivElement) {
this.flowSlug = window.location.pathname.split("/")[3];
this.container = container;
}
get apiURL() {
return `/api/v3/flows/executor/${this.flowSlug}/?query=${encodeURIComponent(window.location.search.substring(1))}`;
}
start() {
$.ajax({
type: "GET",
url: this.apiURL,
success: (data) => {
this.challenge = ChallengeTypesFromJSON(data);
this.renderChallenge();
},
});
}
submit(data: { [key: string]: unknown } | FormData) {
$("button[type=submit]").addClass("disabled")
.html(`<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span role="status">Loading...</span>`);
let finalData: { [key: string]: unknown } = {};
if (data instanceof FormData) {
finalData = {};
data.forEach((value, key) => {
finalData[key] = value;
});
} else {
finalData = data;
}
$.ajax({
type: "POST",
url: this.apiURL,
data: JSON.stringify(finalData),
success: (data) => {
this.challenge = ChallengeTypesFromJSON(data);
this.renderChallenge();
},
contentType: "application/json",
dataType: "json",
});
}
renderChallenge() {
switch (this.challenge?.component) {
case "ak-stage-identification":
new IdentificationStage(this, this.challenge).render();
return;
case "ak-stage-password":
new PasswordStage(this, this.challenge).render();
return;
case "xak-flow-redirect":
new RedirectStage(this, this.challenge).render();
return;
case "ak-stage-autosubmit":
new AutosubmitStage(this, this.challenge).render();
return;
case "ak-stage-authenticator-validate":
new AuthenticatorValidateStage(this, this.challenge).render();
return;
default:
this.container.innerText = "Unsupported stage: " + this.challenge?.component;
return;
}
}
}
export interface FlowInfoChallenge {
flowInfo?: ContextualFlowInfo;
responseErrors?: {
[key: string]: Array<ErrorDetail>;
};
}
class Stage<T extends FlowInfoChallenge> {
constructor(
public executor: SimpleFlowExecutor,
public challenge: T,
) {}
error(fieldName: string) {
if (!this.challenge.responseErrors) {
return [];
}
return this.challenge.responseErrors[fieldName] || [];
}
renderInputError(fieldName: string) {
return `${this.error(fieldName)
.map((error) => {
return `<div class="invalid-feedback">
${error.string}
</div>`;
})
.join("")}`;
}
renderNonFieldErrors() {
return `${this.error("non_field_errors")
.map((error) => {
return `<div class="alert alert-danger" role="alert">
${error.string}
</div>`;
})
.join("")}`;
}
html(html: string) {
this.executor.container.innerHTML = html;
}
render() {
throw new Error("Abstract method");
}
}
class IdentificationStage extends Stage<IdentificationChallenge> {
render() {
this.html(`
<form id="ident-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
${
this.challenge.applicationPre
? `<p>
Login to continue to ${this.challenge.applicationPre}.
</p>`
: ""
}
<div class="form-label-group my-3 has-validation">
<input type="text" autofocus class="form-control" name="uid_field" placeholder="Email / Username">
</div>
${
this.challenge.passwordFields
? `<div class="form-label-group my-3 has-validation">
<input type="password" class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
${this.renderInputError("password")}
</div>`
: ""
}
${this.renderNonFieldErrors()}
<button class="btn btn-primary w-100 py-2" type="submit">${this.challenge.primaryAction}</button>
</form>`);
$("#ident-form input[name=uid_field]").trigger("focus");
$("#ident-form").on("submit", (ev) => {
ev.preventDefault();
const data = new FormData(ev.target as HTMLFormElement);
this.executor.submit(data);
});
}
}
class PasswordStage extends Stage<PasswordChallenge> {
render() {
this.html(`
<form id="password-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3 has-validation">
<input type="password" autofocus class="form-control ${this.error("password").length > 0 ? "is-invalid" : ""}" name="password" placeholder="Password">
${this.renderInputError("password")}
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
</form>`);
$("#password-form input").trigger("focus");
$("#password-form").on("submit", (ev) => {
ev.preventDefault();
const data = new FormData(ev.target as HTMLFormElement);
this.executor.submit(data);
});
}
}
class RedirectStage extends Stage<RedirectChallenge> {
render() {
window.location.assign(this.challenge.to);
}
}
class AutosubmitStage extends Stage<AutosubmitChallenge> {
render() {
this.html(`
<form id="autosubmit-form" action="${this.challenge.url}" method="POST">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
${Object.entries(this.challenge.attrs).map(([key, value]) => {
return `<input
type="hidden"
name="${key}"
value="${value}"
/>`;
})}
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</form>`);
$("#autosubmit-form").submit();
}
}
export interface Assertion {
id: string;
rawId: string;
type: string;
registrationClientExtensions: string;
response: {
clientDataJSON: string;
attestationObject: string;
};
}
export interface AuthAssertion {
id: string;
rawId: string;
type: string;
assertionClientExtensions: string;
response: {
clientDataJSON: string;
authenticatorData: string;
signature: string;
userHandle: string | null;
};
}
class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge> {
deviceChallenge?: DeviceChallenge;
b64enc(buf: Uint8Array): string {
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
b64RawEnc(buf: Uint8Array): string {
return fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
}
u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
checkWebAuthnSupport(): boolean {
if ("credentials" in navigator) {
return true;
}
if (window.location.protocol === "http:" && window.location.hostname !== "localhost") {
console.warn("WebAuthn requires this page to be accessed via HTTPS.");
return false;
}
console.warn("WebAuthn not supported by browser.");
return false;
}
/**
* Transforms items in the credentialCreateOptions generated on the server
* into byte arrays expected by the navigator.credentials.create() call
*/
transformCredentialCreateOptions(
credentialCreateOptions: PublicKeyCredentialCreationOptions,
userId: string,
): PublicKeyCredentialCreationOptions {
const user = credentialCreateOptions.user;
// Because json can't contain raw bytes, the server base64-encodes the User ID
// So to get the base64 encoded byte array, we first need to convert it to a regular
// string, then a byte array, re-encode it and wrap that in an array.
const stringId = decodeURIComponent(window.atob(userId));
user.id = this.u8arr(this.b64enc(this.u8arr(stringId)));
const challenge = this.u8arr(credentialCreateOptions.challenge.toString());
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
challenge,
user,
});
return transformedCredentialCreateOptions;
}
/**
* Transforms the binary data in the credential into base64 strings
* for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
const attObj = new Uint8Array(
(newAssertion.response as AuthenticatorAttestationResponse).attestationObject,
);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const registrationClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: this.b64enc(rawId),
type: newAssertion.type,
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
response: {
clientDataJSON: this.b64enc(clientDataJSON),
attestationObject: this.b64enc(attObj),
},
};
}
transformCredentialRequestOptions(
credentialRequestOptions: PublicKeyCredentialRequestOptions,
): PublicKeyCredentialRequestOptions {
const challenge = this.u8arr(credentialRequestOptions.challenge.toString());
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
(credentialDescriptor) => {
const id = this.u8arr(credentialDescriptor.id.toString());
return Object.assign({}, credentialDescriptor, { id });
},
);
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
challenge,
allowCredentials,
});
return transformedCredentialRequestOptions;
}
/**
* Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
const response = newAssertion.response as AuthenticatorAssertionResponse;
const authData = new Uint8Array(response.authenticatorData);
const clientDataJSON = new Uint8Array(response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const sig = new Uint8Array(response.signature);
const assertionClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: this.b64enc(rawId),
type: newAssertion.type,
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
response: {
clientDataJSON: this.b64RawEnc(clientDataJSON),
signature: this.b64RawEnc(sig),
authenticatorData: this.b64RawEnc(authData),
userHandle: null,
},
};
}
render() {
if (!this.deviceChallenge) {
return this.renderChallengePicker();
}
switch (this.deviceChallenge.deviceClass) {
case "static":
case "totp":
this.renderCodeInput();
break;
case "webauthn":
this.renderWebauthn();
break;
default:
break;
}
}
renderChallengePicker() {
const challenges = this.challenge.deviceChallenges.filter((challenge) => {
if (challenge.deviceClass === "webauthn") {
if (!this.checkWebAuthnSupport()) {
return undefined;
}
}
return challenge;
});
this.html(`<form id="picker-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
${
challenges.length > 0
? "<p>Select an authentication method.</p>"
: `
<p>No compatible authentication method available</p>
`
}
${challenges
.map((challenge) => {
let label = undefined;
switch (challenge.deviceClass) {
case "static":
label = "Recovery keys";
break;
case "totp":
label = "Traditional authenticator";
break;
case "webauthn":
label = "Security key";
break;
}
if (!label) {
return "";
}
return `<div class="form-label-group my-3 has-validation">
<button id="${challenge.deviceClass}-${challenge.deviceUid}" class="btn btn-secondary w-100 py-2" type="button">
${label}
</button>
</div>`;
})
.join("")}
</form>`);
this.challenge.deviceChallenges.forEach((challenge) => {
$(`#picker-form button#${challenge.deviceClass}-${challenge.deviceUid}`).on(
"click",
() => {
this.deviceChallenge = challenge;
this.render();
},
);
});
}
renderCodeInput() {
this.html(`
<form id="totp-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="form-label-group my-3 has-validation">
<input type="text" autofocus class="form-control ${this.error("code").length > 0 ? "is-invalid" : ""}" name="code" placeholder="Please enter your code" autocomplete="one-time-code">
${this.renderInputError("code")}
</div>
<button class="btn btn-primary w-100 py-2" type="submit">Continue</button>
</form>`);
$("#totp-form input").trigger("focus");
$("#totp-form").on("submit", (ev) => {
ev.preventDefault();
const data = new FormData(ev.target as HTMLFormElement);
this.executor.submit(data);
});
}
renderWebauthn() {
this.html(`
<form id="totp-form">
<img class="mb-4 brand-icon" src="${ak().brand.branding_logo}" alt="">
<h1 class="h3 mb-3 fw-normal text-center">${this.challenge?.flowInfo?.title}</h1>
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</form>
`);
navigator.credentials
.get({
publicKey: this.transformCredentialRequestOptions(
this.deviceChallenge?.challenge as PublicKeyCredentialRequestOptions,
),
})
.then((assertion) => {
if (!assertion) {
throw new Error("No assertion");
}
try {
// we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server
const transformedAssertionForServer = this.transformAssertionForServer(
assertion as PublicKeyCredential,
);
// post the assertion to the server for verification.
this.executor.submit({
webauthn: transformedAssertionForServer,
});
} catch (err) {
throw new Error(`Error when validating assertion on server: ${err}`);
}
})
.catch((error) => {
console.warn(error);
this.deviceChallenge = undefined;
this.render();
});
}
}
const sfe = new SimpleFlowExecutor($("#flow-sfe-container")[0] as HTMLDivElement);
sfe.start();

3057
web/sfe/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
web/sfe/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "@goauthentik/web-sfe",
"version": "0.0.0",
"private": true,
"license": "MIT",
"dependencies": {
"@goauthentik/api": "^2024.6.0-1719577139",
"base64-js": "^1.5.1",
"bootstrap": "^4.6.1",
"formdata-polyfill": "^4.0.10",
"jquery": "^3.7.1",
"weakmap-polyfill": "^2.0.4"
},
"scripts": {
"build": "rollup -c rollup.config.js --bundleConfigAsCjs",
"watch": "rollup -w -c rollup.config.js --bundleConfigAsCjs"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-swc": "^0.3.1",
"@swc/cli": "^0.3.14",
"@swc/core": "^1.6.6",
"@types/jquery": "^3.5.30",
"rollup": "^4.18.0",
"rollup-plugin-copy": "^3.5.0"
}
}

40
web/sfe/rollup.config.js Normal file
View File

@ -0,0 +1,40 @@
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import swc from "@rollup/plugin-swc";
import copy from "rollup-plugin-copy";
export default {
input: "index.ts",
output: {
dir: "../dist/sfe",
format: "cjs",
},
context: "window",
plugins: [
copy({
targets: [
{ src: "node_modules/bootstrap/dist/css/bootstrap.min.css", dest: "../dist/sfe" },
],
}),
resolve({ browser: true }),
commonjs(),
swc({
swc: {
jsc: {
loose: false,
externalHelpers: false,
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
keepClassNames: false,
},
minify: false,
env: {
targets: {
edge: "17",
ie: "11",
},
mode: "entry",
},
},
}),
],
};

7
web/sfe/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"types": ["jquery"],
"esModuleInterop": true,
"lib": ["DOM", "ES2015", "ES2017"]
},
}

View File

@ -208,7 +208,14 @@ export class AdminOverviewPage extends AdminOverviewBase {
return html`<li>
${ex(
() => html`<a href="${url}" class="pf-u-mb-xl" target="_blank">${content}</a>`,
() =>
html`<a
href="${url}"
class="pf-u-mb-xl"
rel="noopener noreferrer"
target="_blank"
>${content}</a
>`,
() => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`,
)}
</li>`;

View File

@ -56,6 +56,6 @@ export class VersionStatusCard extends AdminStatusCard<Version> {
text = this.value.buildHash?.substring(0, 7);
link = `https://github.com/goauthentik/authentik/commit/${this.value.buildHash}`;
}
return html`<a href=${link} target="_blank">${text}</a>`;
return html`<a rel="noopener noreferrer" href=${link} target="_blank">${text}</a>`;
}
}

View File

@ -15,7 +15,6 @@ const doGroupBy = (items: Provider[]) => groupBy(items, (item) => item.verboseNa
async function fetch(query?: string) {
const args: ProvidersAllListRequest = {
ordering: "name",
backchannel: false,
};
if (query !== undefined) {
args.search = query;

View File

@ -157,6 +157,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
${msg("See more about OCI support here:")}&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink(
"/developer-docs/blueprints/?utm_source=authentik#storage---oci",
)}"

View File

@ -23,6 +23,7 @@ export class OutpostDeploymentModal extends ModalButton {
<a
target="_blank"
href="${docLink("/docs/outposts?utm_source=authentik#deploy")}"
rel="noopener noreferrer"
>${msg("View deployment documentation")}</a
>
</p>

View File

@ -210,9 +210,11 @@ export class OutpostForm extends ModelForm<Outpost, string> {
)}
</p>
<p class="pf-c-form__helper-text">
See
<a target="_blank" href="${docLink("/docs/outposts?utm_source=authentik")}"
>documentation</a
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/outposts?utm_source=authentik")}"
>${msg("See documentation")}</a
>.
</p>
</ak-form-element-horizontal>
@ -245,6 +247,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
${msg("See more here:")}&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink(
"/docs/outposts?utm_source=authentik#configuration",
)}"

View File

@ -85,6 +85,7 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
<p class="pf-c-form__helper-text">
${msg("Expression using Python.")}
<a
rel="noopener noreferrer"
target="_blank"
href="${docLink("/docs/policies/expression?utm_source=authentik")}"
>

View File

@ -62,6 +62,7 @@ export class PropertyMappingGoogleWorkspaceForm extends BasePropertyMappingForm<
${msg("Expression using Python.")}
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
>
${msg("See documentation for a list of all variables.")}

View File

@ -71,6 +71,7 @@ export class PropertyMappingLDAPForm extends BasePropertyMappingForm<LDAPPropert
${msg("Expression using Python.")}
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
>
${msg("See documentation for a list of all variables.")}

View File

@ -62,6 +62,7 @@ export class PropertyMappingMicrosoftEntraForm extends BasePropertyMappingForm<M
${msg("Expression using Python.")}
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
>
${msg("See documentation for a list of all variables.")}

View File

@ -62,6 +62,7 @@ export class PropertyMappingNotification extends ModelForm<NotificationWebhookMa
${msg("Expression using Python.")}
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
>
${msg("See documentation for a list of all variables.")}

View File

@ -160,6 +160,7 @@ export class PropertyMappingLDAPForm extends ModelForm<RACPropertyMapping, strin
${msg("Expression using Python.")}
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink(
"/docs/property-mappings/expression?utm_source=authentik",
)}"

View File

@ -83,6 +83,7 @@ export class PropertyMappingSAMLForm extends BasePropertyMappingForm<SAMLPropert
${msg("Expression using Python.")}
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
>
${msg("See documentation for a list of all variables.")}

View File

@ -56,6 +56,7 @@ export class PropertyMappingSCIMForm extends BasePropertyMappingForm<SCIMMapping
${msg("Expression using Python.")}
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
>
${msg("See documentation for a list of all variables.")}

View File

@ -83,6 +83,7 @@ export class PropertyMappingScopeForm extends BasePropertyMappingForm<ScopeMappi
${msg("Expression using Python.")}
<a
target="_blank"
rel="noopener noreferrer"
href="${docLink("/docs/property-mappings/expression?utm_source=authentik")}"
>
${msg("See documentation for a list of all variables.")}

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.6.0";
export const VERSION = "2024.6.1";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -78,7 +78,7 @@ export class Markdown extends AKElement {
const pathName = path.replace(".md", "");
const link = `docs/${baseName}${pathName}`;
const url = new URL(link, baseUrl).toString();
return `href="${url}" _target="blank"`;
return `href="${url}" _target="blank" rel="noopener noreferrer"`;
});
}

View File

@ -66,15 +66,15 @@ export class UserOAuthAccessTokenList extends Table<TokenModel> {
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Refresh Tokens(s)")}
objectLabel=${msg("Access Tokens(s)")}
.objects=${this.selectedElements}
.usedBy=${(item: ExpiringBaseGrantModel) => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({
return new Oauth2Api(DEFAULT_CONFIG).oauth2AccessTokensUsedByList({
id: item.pk,
});
}}
.delete=${(item: ExpiringBaseGrantModel) => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({
return new Oauth2Api(DEFAULT_CONFIG).oauth2AccessTokensDestroy({
id: item.pk,
});
}}

View File

@ -503,28 +503,18 @@ export class FlowExecutor extends Interface implements StageHost {
<footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline">
${this.brand?.uiFooterLinks?.map((link) => {
if (link.href) {
return html`<li>
<a href="${link.href || ""}"
>${link.name}</a
>
<a href="${link.href}">${link.name}</a>
</li>`;
}
return html`<li>
<span>${link.name}</span>
</li>`;
})}
<li>
<a
href="https://goauthentik.io?utm_source=authentik&amp;utm_medium=flow"
>${msg("Powered by authentik")}</a
>
<span>${msg("Powered by authentik")}</span>
</li>
${this.flowInfo?.background?.startsWith("/static")
? html`
<li>
<a
href="https://unsplash.com/@benjaminpunzalan"
>${msg("Background image")}</a
>
</li>
`
: html``}
</ul>
</footer>
</div>

View File

@ -54,7 +54,7 @@ export class LibraryPageApplicationEmptyList extends AKElement {
>
</div>
<div class="pf-c-empty-state__body">
<a href="${docLink("/docs/applications")}" target="_blank"
<a rel="noopener noreferrer" href="${docLink("/docs/applications")}" target="_blank"
>${msg("Refer to documentation")}</a
>
</div>

View File

@ -51,6 +51,8 @@ The setting can be used as follows:
[{ "name": "Link Name", "href": "https://goauthentik.io" }]
```
Starting with authentik 2024.6.1, the `href` attribute is optional, and this option can be used to add additional text to the flow executor pages.
### GDPR compliance
When enabled, all the events caused by a user will be deleted upon the user's deletion. Defaults to `true`.

View File

@ -6,6 +6,6 @@ The headless flow executor is used by clients which don't have access to the web
The following stages are supported:
- [**identification**](../stages/identification/)
- [**password**](../stages/password/)
- [**authenticator_validate**](../stages/authenticator_validate/)
- [**Identification stage**](../stages/identification/)
- [**Password stage**](../stages/password/)
- [**Authenticator Validation Stage**](../stages/authenticator_validate/)

View File

@ -1,5 +1,9 @@
---
title: Default (Web)
title: Default
---
This is the default, web-based environment flows are executed in. All stages are compatible with this environment and no limitations are imposed.
This is the default, web-based environment that flows are executed in. All stages are compatible with this environment and no limitations are imposed.
:::info
All flow executors use the same [API](../../../developer-docs/api/flow-executor) which allows for the implementation of custom flow executors.
:::

View File

@ -0,0 +1,31 @@
---
title: Simplified flow executor
---
<span class="badge badge--info">authentik 2024.6.1+</span>
A simplified web-based flow executor that authentik automatically uses for older browsers that do not support modern web technologies.
Currently this flow executor is automatically used for the following browsers:
- Internet Explorer
- Microsoft Edge (up to and including version 18)
The following stages are supported:
- [**Identification stage**](../stages/identification/)
:::info
Only user identifier and user identifier + password stage configurations are supported; sources and passwordless configurations are not supported.
:::
- [**Password stage**](../stages/password/)
- [**Authenticator Validation Stage**](../stages/authenticator_validate/)
Compared to the [default flow executor](./if-flow.md), this flow executor does _not_ support the following features:
- Localization
- Theming (Dark / light themes)
- Theming (Custom CSS)
- Stages not listed above
- Flow inspector

View File

@ -1,5 +1,5 @@
---
title: Authenticator Validation Stage
title: Authenticator validation stage
---
This stage validates an already configured Authenticator Device. This device has to be configured using any of the other authenticator stages:

View File

@ -53,16 +53,26 @@ import Objects from "../expressions/_objects.md";
- `request.obj`: A Django Model instance. This is only set if the policy is ran against an object.
- `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
- `geoip`: GeoIP object, see [GeoIP](https://geoip2.readthedocs.io/en/latest/#geoip2.models.City)
- `geoip`: GeoIP dictionary. The following fields are available:
- `continent`: a two character continent code like `NA` (North America) or `OC` (Oceania).
- `country`: the two character [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) alpha code for the country.
- `lat`: the approximate latitude of the location associated with the IP address.
- `long`: the approximate longitude of the location associated with the IP address.
- `city`: the name of the city. May be empty.
```python
return context["geoip"]["country"]["iso_code"] == "US"
return context["geoip"]["country"] == "US"
```
- `asn`: ASN object, see [GeoIP](https://geoip2.readthedocs.io/en/latest/#geoip2.models.ASN)
- `asn`: ASN dictionary. The follow fields are available:
- `asn`: the autonomous system number associated with the IP address.
- `as_org`: the organization associated with the registered autonomous system number for the IP address.
- `network`: the network associated with the record. In particular, this is the largest network where all of the fields except `ip_address` have the same value.
```python
return context["asn"]["autonomous_system_number"] == 64496
return context["asn"]["asn"] == 64496
```
- `ak_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider.

View File

@ -27,8 +27,6 @@ With this release, authentik now enforces unique group names. Existing groups wi
The `context["geoip"]` and `context["asn"]` objects available in expression policies are now dictionaries. Attributes must now be accessed via dictionary accessors. See [our policy examples](../../policies/expression.mdx) for the updated syntax.
For instance, `context["geoip"].country.iso_code` must now become `context["geoip"]["country"]["iso_code"]`.
## New features
- **Google Workspace Provider** <span class="badge badge--primary">Enterprise</span> <span class="badge badge--info">Preview</span>
@ -192,6 +190,23 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.6
- web: fix value handling inside controlled components (#9648)
- web: markdown: display markdown even when frontmatter is missing (#9404)
## Fixed in 2024.6.1
- core: fix migrations missing using db_alias (cherry-pick #10409) (#10410)
- core: fix source flow_manager not resuming flow when linking (cherry-pick #10436) (#10438)
- core: remove transitionary old JS urls (cherry-pick #10317) (#10321)
- core: revert backchannel only filtering (cherry-pick #10455) (#10457)
- providers/saml: fix metadata import error handling (cherry-pick #10349) (#10350)
- providers/scim: Fix exception handling for missing ServiceProviderConfig (cherry-pick #10322) (#10335)
- sources/oauth: fix link not being saved (cherry-pick #10374) (#10376)
- sources/saml: fix pickle error, add saml auth tests (cherry-pick #10348) (#10352)
- stages/authenticator_validate: fix friendly_name being required (cherry-pick #10382) (#10385)
- stages/user_login: fix ?next parameter not carried through broken session binding (cherry-pick #10301) (#10302)
- web: set noopener and noreferrer on all external links (#10304)
- web/admin: fix access token list calling wrong API (cherry-pick #10434) (#10435)
- web/flows: remove background image link (cherry-pick #10318) (#10320)
- web/flows: Simplified flow executor (#10296)
## API Changes
#### What's New

View File

@ -253,6 +253,7 @@ const docsSidebar = {
label: "Executors",
items: [
"flow/executors/if-flow",
"flow/executors/sfe",
"flow/executors/user-settings",
"flow/executors/headless",
],