Compare commits

..

1 Commits

Author SHA1 Message Date
5797a51993 initial steps for concurrent execution
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-03-19 23:03:59 +00:00
340 changed files with 6374 additions and 8723 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2025.2.3 current_version = 2025.2.2
tag = True tag = True
commit = 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*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@ -17,8 +17,6 @@ optional_value = final
[bumpversion:file:pyproject.toml] [bumpversion:file:pyproject.toml]
[bumpversion:file:uv.lock]
[bumpversion:file:package.json] [bumpversion:file:package.json]
[bumpversion:file:docker-compose.yml] [bumpversion:file:docker-compose.yml]

View File

@ -1,22 +0,0 @@
---
name: Documentation issue
about: Suggest an improvement or report a problem
title: ""
labels: documentation
assignees: ""
---
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
**Provide the URL or link to the exact page in the documentation to which you are referring.**
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the documentation issue here.
**Consider opening a PR!**
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).

View File

@ -44,6 +44,7 @@ if is_release:
] ]
if not prerelease: if not prerelease:
image_tags += [ image_tags += [
f"{name}:latest",
f"{name}:{version_family}", f"{name}:{version_family}",
] ]
else: else:

View File

@ -29,7 +29,7 @@ jobs:
- name: Generate API - name: Generate API
run: make gen-client-go run: make gen-client-go
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v7 uses: golangci/golangci-lint-action@v6
with: with:
version: latest version: latest
args: --timeout 5000s --verbose args: --timeout 5000s --verbose

View File

@ -1,27 +0,0 @@
name: authentik-semgrep
on:
workflow_dispatch: {}
pull_request: {}
push:
branches:
- main
- master
paths:
- .github/workflows/semgrep.yml
schedule:
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
- cron: '12 15 * * *'
jobs:
semgrep:
name: semgrep/ci
runs-on: ubuntu-latest
permissions:
contents: read
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
container:
image: semgrep/semgrep
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@v4
- run: semgrep ci

View File

@ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build RUN npm run build
# Stage 3: Build go proxy # Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@ -76,7 +76,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/authentik ./cmd/server go build -o /go/authentik ./cmd/server
# Stage 4: MaxMind GeoIP # Stage 4: MaxMind GeoIP
@ -94,9 +94,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv # Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.6.11 AS uv FROM ghcr.io/astral-sh/uv:0.6.8 AS uv
# Stage 6: Base python image # Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \ ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \ PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2025.2.3" __version__ = "2025.2.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer):
if not isinstance(value, str): if not isinstance(value, str):
continue continue
actual_value = value actual_value = value
if raw_session is not None and raw_session in actual_value: if raw_session in actual_value:
actual_value = actual_value.replace( actual_value = actual_value.replace(
raw_session, SafeExceptionReporterFilter.cleansed_substitute raw_session, SafeExceptionReporterFilter.cleansed_substitute
) )

View File

@ -49,8 +49,6 @@ class BrandSerializer(ModelSerializer):
"branding_title", "branding_title",
"branding_logo", "branding_logo",
"branding_favicon", "branding_favicon",
"branding_custom_css",
"branding_default_flow_background",
"flow_authentication", "flow_authentication",
"flow_invalidation", "flow_invalidation",
"flow_recovery", "flow_recovery",
@ -88,7 +86,6 @@ class CurrentBrandSerializer(PassiveSerializer):
branding_title = CharField() branding_title = CharField()
branding_logo = CharField(source="branding_logo_url") branding_logo = CharField(source="branding_logo_url")
branding_favicon = CharField(source="branding_favicon_url") branding_favicon = CharField(source="branding_favicon_url")
branding_custom_css = CharField()
ui_footer_links = ListField( ui_footer_links = ListField(
child=FooterLinkSerializer(), child=FooterLinkSerializer(),
read_only=True, read_only=True,
@ -128,7 +125,6 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
"branding_title", "branding_title",
"branding_logo", "branding_logo",
"branding_favicon", "branding_favicon",
"branding_default_flow_background",
"flow_authentication", "flow_authentication",
"flow_invalidation", "flow_invalidation",
"flow_recovery", "flow_recovery",

View File

@ -1,35 +0,0 @@
# Generated by Django 5.0.12 on 2025-02-22 01:51
from pathlib import Path
from django.db import migrations, models
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Brand = apps.get_model("authentik_brands", "brand")
db_alias = schema_editor.connection.alias
path = Path("/web/dist/custom.css")
if not path.exists():
return
css = path.read_text()
Brand.objects.using(db_alias).update(branding_custom_css=css)
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0007_brand_default_application"),
]
operations = [
migrations.AddField(
model_name="brand",
name="branding_custom_css",
field=models.TextField(blank=True, default=""),
),
migrations.RunPython(migrate_custom_css),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-19 22:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0008_brand_branding_custom_css"),
]
operations = [
migrations.AddField(
model_name="brand",
name="branding_default_flow_background",
field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"),
),
]

View File

@ -33,10 +33,6 @@ class Brand(SerializerModel):
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg") branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png") branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
branding_custom_css = models.TextField(default="", blank=True)
branding_default_flow_background = models.TextField(
default="/static/dist/assets/images/flow_background.jpg"
)
flow_authentication = models.ForeignKey( flow_authentication = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication" Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
@ -88,12 +84,6 @@ class Brand(SerializerModel):
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
return self.branding_favicon return self.branding_favicon
def branding_default_flow_background_url(self) -> str:
"""Get branding_default_flow_background with the correct prefix"""
if self.branding_default_flow_background.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background
return self.branding_default_flow_background
@property @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.brands.api import BrandSerializer from authentik.brands.api import BrandSerializer

View File

@ -24,7 +24,6 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png", "branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik", "branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": brand.domain, "matched_domain": brand.domain,
"ui_footer_links": [], "ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC, "ui_theme": Themes.AUTOMATIC,
@ -44,7 +43,6 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png", "branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "custom", "branding_title": "custom",
"branding_custom_css": "",
"matched_domain": "bar.baz", "matched_domain": "bar.baz",
"ui_footer_links": [], "ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC, "ui_theme": Themes.AUTOMATIC,
@ -61,7 +59,6 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png", "branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik", "branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": "fallback", "matched_domain": "fallback",
"ui_footer_links": [], "ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC, "ui_theme": Themes.AUTOMATIC,
@ -124,27 +121,3 @@ class TestBrands(APITestCase):
"subject": None, "subject": None,
}, },
) )
def test_branding_url(self):
"""Test branding attributes return correct values"""
brand = create_test_brand()
brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png"
brand.branding_favicon = "https://goauthentik.io/img/icon.png"
brand.branding_logo = "https://goauthentik.io/img/icon.png"
brand.save()
self.assertEqual(
brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png"
)
self.assertJSONEqual(
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
{
"branding_logo": "https://goauthentik.io/img/icon.png",
"branding_favicon": "https://goauthentik.io/img/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": brand.domain,
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",
},
)

View File

@ -46,7 +46,7 @@ LOGGER = get_logger()
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str: def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
"""Cache key where application list for user is saved""" """Cache key where application list for user is saved"""
key = f"{CACHE_PREFIX}app_access/{user_pk}" key = f"{CACHE_PREFIX}/app_access/{user_pk}"
if page_number: if page_number:
key += f"/{page_number}" key += f"/{page_number}"
return key return key

View File

@ -1,14 +1,13 @@
"""User API Views""" """User API Views"""
from datetime import timedelta from datetime import timedelta
from importlib import import_module
from json import loads from json import loads
from typing import Any from typing import Any
from django.conf import settings
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.sessions.backends.base import SessionBase from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.db.models.functions import ExtractHour from django.db.models.functions import ExtractHour
from django.db.transaction import atomic from django.db.transaction import atomic
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
@ -92,7 +91,6 @@ from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger() LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
class UserGroupSerializer(ModelSerializer): class UserGroupSerializer(ModelSerializer):
@ -375,7 +373,7 @@ class UsersFilter(FilterSet):
method="filter_attributes", method="filter_attributes",
) )
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser") is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
uuid = UUIDFilter(field_name="uuid") uuid = UUIDFilter(field_name="uuid")
path = CharFilter(field_name="path") path = CharFilter(field_name="path")
@ -393,11 +391,6 @@ class UsersFilter(FilterSet):
queryset=Group.objects.all().order_by("name"), queryset=Group.objects.all().order_by("name"),
) )
def filter_is_superuser(self, queryset, name, value):
if value:
return queryset.filter(ak_groups__is_superuser=True).distinct()
return queryset.exclude(ak_groups__is_superuser=True).distinct()
def filter_attributes(self, queryset, name, value): def filter_attributes(self, queryset, name, value):
"""Filter attributes by query args""" """Filter attributes by query args"""
try: try:
@ -776,8 +769,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not instance.is_active: if not instance.is_active:
sessions = AuthenticatedSession.objects.filter(user=instance) sessions = AuthenticatedSession.objects.filter(user=instance)
session_ids = sessions.values_list("session_key", flat=True) session_ids = sessions.values_list("session_key", flat=True)
for session in session_ids: cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
SessionStore(session).delete()
sessions.delete() sessions.delete()
LOGGER.debug("Deleted user's sessions", user=instance.username) LOGGER.debug("Deleted user's sessions", user=instance.username)
return response return response

View File

@ -761,17 +761,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
@property @property
def component(self) -> str: def component(self) -> str:
"""Return component used to edit this object""" """Return component used to edit this object"""
if self.managed == self.MANAGED_INBUILT:
return ""
raise NotImplementedError raise NotImplementedError
@property @property
def property_mapping_type(self) -> "type[PropertyMapping]": def property_mapping_type(self) -> "type[PropertyMapping]":
"""Return property mapping type used by this object""" """Return property mapping type used by this object"""
if self.managed == self.MANAGED_INBUILT:
from authentik.core.models import PropertyMapping
return PropertyMapping
raise NotImplementedError raise NotImplementedError
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None: def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
@ -786,14 +780,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]: def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a user to build final properties upon.""" """Get base properties for a user to build final properties upon."""
if self.managed == self.MANAGED_INBUILT:
return {}
raise NotImplementedError raise NotImplementedError
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]: def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a group to build final properties upon.""" """Get base properties for a group to build final properties upon."""
if self.managed == self.MANAGED_INBUILT:
return {}
raise NotImplementedError raise NotImplementedError
def __str__(self): def __str__(self):

View File

@ -1,10 +1,7 @@
"""authentik core signals""" """authentik core signals"""
from importlib import import_module
from django.conf import settings
from django.contrib.auth.signals import user_logged_in, user_logged_out from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.base import SessionBase from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache from django.core.cache import cache
from django.core.signals import Signal from django.core.signals import Signal
from django.db.models import Model from django.db.models import Model
@ -28,7 +25,6 @@ password_changed = Signal()
login_failed = Signal() login_failed = Signal()
LOGGER = get_logger() LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
@receiver(post_save, sender=Application) @receiver(post_save, sender=Application)
@ -64,7 +60,8 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
@receiver(pre_delete, sender=AuthenticatedSession) @receiver(pre_delete, sender=AuthenticatedSession)
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_): def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted""" """Delete session when authenticated session is deleted"""
SessionStore(instance.session_key).delete() cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)
@receiver(pre_save) @receiver(pre_save)

View File

@ -36,7 +36,6 @@ from authentik.flows.planner import (
) )
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_values from authentik.policies.utils import delete_none_values
@ -49,7 +48,6 @@ LOGGER = get_logger()
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups" PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages" SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
SESSION_KEY_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context"
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
@ -210,8 +208,6 @@ class SourceFlowManager:
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user" NEXT_ARG_NAME, "authentik_core:if-user"
) )
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
flow_context.update( flow_context.update(
{ {
# Since we authenticate the user by their token, they have no backend set # Since we authenticate the user by their token, they have no backend set
@ -265,7 +261,6 @@ class SourceFlowManager:
plan.append_stage(stage) plan.append_stage(stage)
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []): for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
plan.append_stage(stage) plan.append_stage(stage)
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
return plan.to_redirect(self.request, flow) return plan.to_redirect(self.request, flow)
def handle_auth( def handle_auth(

View File

@ -16,7 +16,7 @@
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<style>{{ brand.branding_custom_css }}</style> <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %} {% block head %}

View File

@ -4,7 +4,7 @@
{% load i18n %} {% load i18n %}
{% block head_before %} {% block head_before %}
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" /> <link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)"> <link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
@ -13,7 +13,7 @@
{% block head %} {% block head %}
<style> <style>
:root { :root {
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}"); --ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background); --pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background); --pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);

View File

@ -1,19 +0,0 @@
from django.apps import apps
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
class TestSourceAPI(APITestCase):
def setUp(self) -> None:
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_builtin_source_used_by(self):
"""Test Providers's types endpoint"""
apps.get_app_config("authentik_core").source_inbuilt()
response = self.client.get(
reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}),
)
self.assertEqual(response.status_code, 200)

View File

@ -1,7 +1,6 @@
"""Test Users API""" """Test Users API"""
from datetime import datetime from datetime import datetime
from json import loads
from django.contrib.sessions.backends.cache import KEY_PREFIX from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache from django.core.cache import cache
@ -16,12 +15,7 @@ from authentik.core.models import (
User, User,
UserTypes, UserTypes,
) )
from authentik.core.tests.utils import ( from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
create_test_admin_user,
create_test_brand,
create_test_flow,
create_test_user,
)
from authentik.flows.models import FlowDesignation from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id, generate_key from authentik.lib.generators import generate_id, generate_key
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
@ -32,7 +26,7 @@ class TestUsersAPI(APITestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.admin = create_test_admin_user() self.admin = create_test_admin_user()
self.user = create_test_user() self.user = User.objects.create(username="test-user")
def test_filter_type(self): def test_filter_type(self):
"""Test API filtering by type""" """Test API filtering by type"""
@ -47,35 +41,6 @@ class TestUsersAPI(APITestCase):
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_filter_is_superuser(self):
"""Test API filtering by superuser status"""
User.objects.all().delete()
admin = create_test_admin_user()
self.client.force_login(admin)
# Test superuser
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": True,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1)
self.assertEqual(body["results"][0]["username"], admin.username)
# Test non-superuser
user = create_test_user()
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": False,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1, body)
self.assertEqual(body["results"][0]["username"], user.username)
def test_list_with_groups(self): def test_list_with_groups(self):
"""Test listing with groups""" """Test listing with groups"""
self.client.force_login(self.admin) self.client.force_login(self.admin)
@ -134,8 +99,6 @@ class TestUsersAPI(APITestCase):
def test_recovery_email_no_flow(self): def test_recovery_email_no_flow(self):
"""Test user recovery link (no recovery flow set)""" """Test user recovery link (no recovery flow set)"""
self.client.force_login(self.admin) self.client.force_login(self.admin)
self.user.email = ""
self.user.save()
response = self.client.post( response = self.client.post(
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
) )

View File

@ -11,14 +11,13 @@ from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Source, User from authentik.core.models import Source, User
from authentik.core.sources.flow_manager import ( from authentik.core.sources.flow_manager import (
SESSION_KEY_OVERRIDE_FLOW_TOKEN, SESSION_KEY_OVERRIDE_FLOW_TOKEN,
SESSION_KEY_SOURCE_FLOW_CONTEXT,
SESSION_KEY_SOURCE_FLOW_STAGES, SESSION_KEY_SOURCE_FLOW_STAGES,
) )
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton
from authentik.enterprise.stages.source.models import SourceStage from authentik.enterprise.stages.source.models import SourceStage
from authentik.flows.challenge import Challenge, ChallengeResponse from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.models import FlowToken, in_memory_stage from authentik.flows.models import FlowToken, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_IS_REDIRECTED, PLAN_CONTEXT_IS_RESTORED from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
from authentik.flows.stage import ChallengeStageView, StageView from authentik.flows.stage import ChallengeStageView, StageView
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
@ -54,9 +53,6 @@ class SourceStageView(ChallengeStageView):
resume_token = self.create_flow_token() resume_token = self.create_flow_token()
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)] self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
self.request.session[SESSION_KEY_SOURCE_FLOW_CONTEXT] = {
PLAN_CONTEXT_IS_REDIRECTED: self.executor.flow,
}
return self.login_button.challenge return self.login_button.challenge
def create_flow_token(self) -> FlowToken: def create_flow_token(self) -> FlowToken:

View File

@ -50,8 +50,7 @@ class NotificationTransportSerializer(ModelSerializer):
"mode", "mode",
"mode_verbose", "mode_verbose",
"webhook_url", "webhook_url",
"webhook_mapping_body", "webhook_mapping",
"webhook_mapping_headers",
"send_once", "send_once",
] ]

View File

@ -1,43 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-20 19:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0008_event_authentik_e_expires_8c73a8_idx_and_more"),
]
operations = [
migrations.RenameField(
model_name="notificationtransport",
old_name="webhook_mapping",
new_name="webhook_mapping_body",
),
migrations.AlterField(
model_name="notificationtransport",
name="webhook_mapping_body",
field=models.ForeignKey(
default=None,
help_text="Customize the body of the request. Mapping should return data that is JSON-serializable.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="authentik_events.notificationwebhookmapping",
),
),
migrations.AddField(
model_name="notificationtransport",
name="webhook_mapping_headers",
field=models.ForeignKey(
default=None,
help_text="Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="authentik_events.notificationwebhookmapping",
),
),
]

View File

@ -336,27 +336,8 @@ class NotificationTransport(SerializerModel):
mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL) mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL)
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()]) webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
webhook_mapping_body = models.ForeignKey( webhook_mapping = models.ForeignKey(
"NotificationWebhookMapping", "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
on_delete=models.SET_DEFAULT,
null=True,
default=None,
related_name="+",
help_text=_(
"Customize the body of the request. "
"Mapping should return data that is JSON-serializable."
),
)
webhook_mapping_headers = models.ForeignKey(
"NotificationWebhookMapping",
on_delete=models.SET_DEFAULT,
null=True,
default=None,
related_name="+",
help_text=_(
"Configure additional headers to be sent. "
"Mapping should return a dictionary of key-value pairs"
),
) )
send_once = models.BooleanField( send_once = models.BooleanField(
default=False, default=False,
@ -379,8 +360,8 @@ class NotificationTransport(SerializerModel):
def send_local(self, notification: "Notification") -> list[str]: def send_local(self, notification: "Notification") -> list[str]:
"""Local notification delivery""" """Local notification delivery"""
if self.webhook_mapping_body: if self.webhook_mapping:
self.webhook_mapping_body.evaluate( self.webhook_mapping.evaluate(
user=notification.user, user=notification.user,
request=None, request=None,
notification=notification, notification=notification,
@ -399,18 +380,9 @@ class NotificationTransport(SerializerModel):
if notification.event and notification.event.user: if notification.event and notification.event.user:
default_body["event_user_email"] = notification.event.user.get("email", None) default_body["event_user_email"] = notification.event.user.get("email", None)
default_body["event_user_username"] = notification.event.user.get("username", None) default_body["event_user_username"] = notification.event.user.get("username", None)
headers = {} if self.webhook_mapping:
if self.webhook_mapping_body:
default_body = sanitize_item( default_body = sanitize_item(
self.webhook_mapping_body.evaluate( self.webhook_mapping.evaluate(
user=notification.user,
request=None,
notification=notification,
)
)
if self.webhook_mapping_headers:
headers = sanitize_item(
self.webhook_mapping_headers.evaluate(
user=notification.user, user=notification.user,
request=None, request=None,
notification=notification, notification=notification,
@ -420,7 +392,6 @@ class NotificationTransport(SerializerModel):
response = get_http_session().post( response = get_http_session().post(
self.webhook_url, self.webhook_url,
json=default_body, json=default_body,
headers=headers,
) )
response.raise_for_status() response.raise_for_status()
except RequestException as exc: except RequestException as exc:

View File

@ -120,7 +120,7 @@ class TestEventsNotifications(APITestCase):
) )
transport = NotificationTransport.objects.create( transport = NotificationTransport.objects.create(
name=generate_id(), webhook_mapping_body=mapping, mode=TransportMode.LOCAL name=generate_id(), webhook_mapping=mapping, mode=TransportMode.LOCAL
) )
NotificationRule.objects.filter(name__startswith="default").delete() NotificationRule.objects.filter(name__startswith="default").delete()
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group) trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)

View File

@ -60,25 +60,20 @@ class TestEventTransports(TestCase):
def test_transport_webhook_mapping(self): def test_transport_webhook_mapping(self):
"""Test webhook transport with custom mapping""" """Test webhook transport with custom mapping"""
mapping_body = NotificationWebhookMapping.objects.create( mapping = NotificationWebhookMapping.objects.create(
name=generate_id(), expression="return request.user" name=generate_id(), expression="return request.user"
) )
mapping_headers = NotificationWebhookMapping.objects.create(
name=generate_id(), expression="""return {"foo": "bar"}"""
)
transport: NotificationTransport = NotificationTransport.objects.create( transport: NotificationTransport = NotificationTransport.objects.create(
name=generate_id(), name=generate_id(),
mode=TransportMode.WEBHOOK, mode=TransportMode.WEBHOOK,
webhook_url="http://localhost:1234/test", webhook_url="http://localhost:1234/test",
webhook_mapping_body=mapping_body, webhook_mapping=mapping,
webhook_mapping_headers=mapping_headers,
) )
with Mocker() as mocker: with Mocker() as mocker:
mocker.post("http://localhost:1234/test") mocker.post("http://localhost:1234/test")
transport.send(self.notification) transport.send(self.notification)
self.assertEqual(mocker.call_count, 1) self.assertEqual(mocker.call_count, 1)
self.assertEqual(mocker.request_history[0].method, "POST") self.assertEqual(mocker.request_history[0].method, "POST")
self.assertEqual(mocker.request_history[0].headers["foo"], "bar")
self.assertJSONEqual( self.assertJSONEqual(
mocker.request_history[0].body.decode(), mocker.request_history[0].body.decode(),
{"email": self.user.email, "pk": self.user.pk, "username": self.user.username}, {"email": self.user.email, "pk": self.user.pk, "username": self.user.username},

View File

@ -54,6 +54,7 @@ class Challenge(PassiveSerializer):
flow_info = ContextualFlowInfo(required=False) flow_info = ContextualFlowInfo(required=False)
component = CharField(default="") component = CharField(default="")
xid = CharField(required=False)
response_errors = DictField( response_errors = DictField(
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False child=ErrorDetailSerializer(many=True), allow_empty=True, required=False

View File

@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer from rest_framework.serializers import BaseSerializer
@ -179,12 +178,11 @@ class Flow(SerializerModel, PolicyBindingModel):
help_text=_("Required level of authentication and authorization to access a flow."), help_text=_("Required level of authentication and authorization to access a flow."),
) )
def background_url(self, request: HttpRequest | None = None) -> str: @property
def background_url(self) -> str:
"""Get the URL to the background image. If the name is /static or starts with http """Get the URL to the background image. If the name is /static or starts with http
it is returned as-is""" it is returned as-is"""
if not self.background: if not self.background:
if request:
return request.brand.branding_default_flow_background_url()
return ( return (
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg" CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
) )

View File

@ -143,10 +143,12 @@ class FlowPlan:
request: HttpRequest, request: HttpRequest,
flow: Flow, flow: Flow,
allowed_silent_types: list["StageView"] | None = None, allowed_silent_types: list["StageView"] | None = None,
**get_params,
) -> HttpResponse: ) -> HttpResponse:
"""Redirect to the flow executor for this flow plan""" """Redirect to the flow executor for this flow plan"""
from authentik.flows.views.executor import ( from authentik.flows.views.executor import (
SESSION_KEY_PLAN, SESSION_KEY_PLAN,
FlowContainer,
FlowExecutorView, FlowExecutorView,
) )
@ -157,6 +159,7 @@ class FlowPlan:
# No unskippable stages found, so we can directly return the response of the last stage # No unskippable stages found, so we can directly return the response of the last stage
final_stage: type[StageView] = self.bindings[-1].stage.view final_stage: type[StageView] = self.bindings[-1].stage.view
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self) temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
temp_exec.container = FlowContainer(request)
temp_exec.current_stage = self.bindings[-1].stage temp_exec.current_stage = self.bindings[-1].stage
temp_exec.current_stage_view = final_stage temp_exec.current_stage_view = final_stage
temp_exec.setup(request, flow.slug) temp_exec.setup(request, flow.slug)
@ -174,6 +177,9 @@ class FlowPlan:
): ):
get_qs["inspector"] = "available" get_qs["inspector"] = "available"
for key, value in get_params:
get_qs[key] = value
return redirect_with_qs( return redirect_with_qs(
"authentik_core:if-flow", "authentik_core:if-flow",
get_qs, get_qs,

View File

@ -184,13 +184,14 @@ class ChallengeStageView(StageView):
flow_info = ContextualFlowInfo( flow_info = ContextualFlowInfo(
data={ data={
"title": self.format_title(), "title": self.format_title(),
"background": self.executor.flow.background_url(self.request), "background": self.executor.flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"), "cancel_url": reverse("authentik_flows:cancel"),
"layout": self.executor.flow.layout, "layout": self.executor.flow.layout,
} }
) )
flow_info.is_valid() flow_info.is_valid()
challenge.initial_data["flow_info"] = flow_info.data challenge.initial_data["flow_info"] = flow_info.data
challenge.initial_data["xid"] = self.executor.container.exec_id
if isinstance(challenge, WithUserInfoChallenge): if isinstance(challenge, WithUserInfoChallenge):
# If there's a pending user, update the `username` field # If there's a pending user, update the `username` field
# this field is only used by password managers. # this field is only used by password managers.

View File

@ -28,7 +28,7 @@ window.authentik.flow = {
{% block body %} {% block body %}
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
<ak-flow-executor flowSlug="{{ flow.slug }}"> <ak-flow-executor flowSlug="{{ flow.slug }}" xid="{{ xid }}">
<ak-loading></ak-loading> <ak-loading></ak-loading>
</ak-flow-executor> </ak-flow-executor>
{% endblock %} {% endblock %}

View File

@ -27,6 +27,7 @@ class FlowTestCase(APITestCase):
self.assertIsNotNone(raw_response["component"]) self.assertIsNotNone(raw_response["component"])
if flow: if flow:
self.assertIn("flow_info", raw_response) self.assertIn("flow_info", raw_response)
self.assertEqual(raw_response["flow_info"]["background"], flow.background_url)
self.assertEqual( self.assertEqual(
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel") raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
) )

View File

@ -1,11 +1,9 @@
"""API flow tests""" """API flow tests"""
from json import loads
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.api.stages import StageSerializer, StageViewSet from authentik.flows.api.stages import StageSerializer, StageViewSet
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
@ -79,22 +77,6 @@ class TestFlowsAPI(APITestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertJSONEqual(response.content, {"diagram": DIAGRAM_EXPECTED}) self.assertJSONEqual(response.content, {"diagram": DIAGRAM_EXPECTED})
def test_api_background(self):
"""Test custom background"""
user = create_test_admin_user()
self.client.force_login(user)
flow = create_test_flow()
response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug}))
body = loads(response.content.decode())
self.assertEqual(body["background"], "/static/dist/assets/images/flow_background.jpg")
flow.background = "https://goauthentik.io/img/icon.png"
flow.save()
response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug}))
body = loads(response.content.decode())
self.assertEqual(body["background"], "https://goauthentik.io/img/icon.png")
def test_api_diagram_no_stages(self): def test_api_diagram_no_stages(self):
"""Test flow diagram with no stages.""" """Test flow diagram with no stages."""
user = create_test_admin_user() user = create_test_admin_user()

View File

@ -49,7 +49,7 @@ class TestFlowInspector(APITestCase):
"captcha_stage": None, "captcha_stage": None,
"component": "ak-stage-identification", "component": "ak-stage-identification",
"flow_info": { "flow_info": {
"background": "/static/dist/assets/images/flow_background.jpg", "background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"), "cancel_url": reverse("authentik_flows:cancel"),
"title": flow.title, "title": flow.title,
"layout": "stacked", "layout": "stacked",

View File

@ -1,6 +1,7 @@
"""authentik multi-stage authentication engine""" """authentik multi-stage authentication engine"""
from copy import deepcopy from copy import deepcopy
from uuid import uuid4
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
@ -64,14 +65,15 @@ from authentik.policies.engine import PolicyEngine
LOGGER = get_logger() LOGGER = get_logger()
# Argument used to redirect user after login # Argument used to redirect user after login
NEXT_ARG_NAME = "next" NEXT_ARG_NAME = "next"
SESSION_KEY_PLAN_CONTAINER = "authentik/flows/plan_container/%s"
SESSION_KEY_PLAN = "authentik/flows/plan" SESSION_KEY_PLAN = "authentik/flows/plan"
SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre" SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
SESSION_KEY_GET = "authentik/flows/get" SESSION_KEY_GET = "authentik/flows/get"
SESSION_KEY_POST = "authentik/flows/post" SESSION_KEY_POST = "authentik/flows/post"
SESSION_KEY_HISTORY = "authentik/flows/history" SESSION_KEY_HISTORY = "authentik/flows/history"
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
QS_KEY_TOKEN = "flow_token" # nosec QS_KEY_TOKEN = "flow_token" # nosec
QS_QUERY = "query" QS_QUERY = "query"
QS_EXEC_ID = "xid"
def challenge_types(): def challenge_types():
@ -98,6 +100,88 @@ class InvalidStageError(SentryIgnoredException):
"""Error raised when a challenge from a stage is not valid""" """Error raised when a challenge from a stage is not valid"""
class FlowContainer:
"""Allow for multiple concurrent flow executions in the same session"""
def __init__(self, request: HttpRequest, exec_id: str | None = None) -> None:
self.request = request
self.exec_id = exec_id
@staticmethod
def new(request: HttpRequest):
exec_id = str(uuid4())
request.session[SESSION_KEY_PLAN_CONTAINER % exec_id] = {}
return FlowContainer(request, exec_id)
def exists(self) -> bool:
"""Check if flow exists in container/session"""
return SESSION_KEY_PLAN in self.session
def save(self):
self.request.session.modified = True
@property
def session(self):
# Backwards compatibility: store session plan/etc directly in session
if not self.exec_id:
return self.request.session
self.request.session.setdefault(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {})
return self.request.session.get(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {})
@property
def plan(self) -> FlowPlan:
return self.session.get(SESSION_KEY_PLAN)
def to_redirect(
self,
request: HttpRequest,
flow: Flow,
allowed_silent_types: list[StageView] | None = None,
**get_params,
) -> HttpResponse:
get_params[QS_EXEC_ID] = self.exec_id
return self.plan.to_redirect(
request, flow, allowed_silent_types=allowed_silent_types, **get_params
)
@plan.setter
def plan(self, value: FlowPlan):
self.session[SESSION_KEY_PLAN] = value
self.request.session.modified = True
self.save()
@property
def application_pre(self):
return self.session.get(SESSION_KEY_APPLICATION_PRE)
@property
def get(self) -> QueryDict:
return self.session.get(SESSION_KEY_GET)
@get.setter
def get(self, value: QueryDict):
self.session[SESSION_KEY_GET] = value
self.save()
@property
def post(self) -> QueryDict:
return self.session.get(SESSION_KEY_POST)
@post.setter
def post(self, value: QueryDict):
self.session[SESSION_KEY_POST] = value
self.save()
@property
def history(self) -> list[FlowPlan]:
return self.session.get(SESSION_KEY_HISTORY)
@history.setter
def history(self, value: list[FlowPlan]):
self.session[SESSION_KEY_HISTORY] = value
self.save()
@method_decorator(xframe_options_sameorigin, name="dispatch") @method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(APIView): class FlowExecutorView(APIView):
"""Flow executor, passing requests to Stage Views""" """Flow executor, passing requests to Stage Views"""
@ -105,8 +189,9 @@ class FlowExecutorView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
flow: Flow = None flow: Flow = None
plan: FlowPlan | None = None plan: FlowPlan | None = None
container: FlowContainer
current_binding: FlowStageBinding | None = None current_binding: FlowStageBinding | None = None
current_stage: Stage current_stage: Stage
current_stage_view: View current_stage_view: View
@ -161,10 +246,12 @@ class FlowExecutorView(APIView):
if QS_KEY_TOKEN in get_params: if QS_KEY_TOKEN in get_params:
plan = self._check_flow_token(get_params[QS_KEY_TOKEN]) plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
if plan: if plan:
self.request.session[SESSION_KEY_PLAN] = plan container = FlowContainer.new(request)
container.plan = plan
# Early check if there's an active Plan for the current session # Early check if there's an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session: self.container = FlowContainer(request, request.GET.get(QS_EXEC_ID))
self.plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] if self.container.exists():
self.plan: FlowPlan = self.container.plan
if self.plan.flow_pk != self.flow.pk.hex: if self.plan.flow_pk != self.flow.pk.hex:
self._logger.warning( self._logger.warning(
"f(exec): Found existing plan for other flow, deleting plan", "f(exec): Found existing plan for other flow, deleting plan",
@ -177,13 +264,14 @@ class FlowExecutorView(APIView):
self._logger.debug("f(exec): Continuing existing plan") self._logger.debug("f(exec): Continuing existing plan")
# Initial flow request, check if we have an upstream query string passed in # Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params self.container.get = get_params
# Don't check session again as we've either already loaded the plan or we need to plan # Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan: if not self.plan:
request.session[SESSION_KEY_HISTORY] = [] self.container.history = []
self._logger.debug("f(exec): No active Plan found, initiating planner") self._logger.debug("f(exec): No active Plan found, initiating planner")
try: try:
self.plan = self._initiate_plan() self.plan = self._initiate_plan()
self.container.plan = self.plan
except FlowNonApplicableException as exc: except FlowNonApplicableException as exc:
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc) self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
return self.handle_invalid_flow(exc) return self.handle_invalid_flow(exc)
@ -255,12 +343,19 @@ class FlowExecutorView(APIView):
request=OpenApiTypes.NONE, request=OpenApiTypes.NONE,
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="query", name=QS_QUERY,
location=OpenApiParameter.QUERY, location=OpenApiParameter.QUERY,
required=True, required=True,
description="Querystring as received", description="Querystring as received",
type=OpenApiTypes.STR, type=OpenApiTypes.STR,
) ),
OpenApiParameter(
name=QS_EXEC_ID,
location=OpenApiParameter.QUERY,
required=False,
description="Flow execution ID",
type=OpenApiTypes.STR,
),
], ],
operation_id="flows_executor_get", operation_id="flows_executor_get",
) )
@ -287,7 +382,7 @@ class FlowExecutorView(APIView):
span.set_data("authentik Stage", self.current_stage_view) span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug) span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.dispatch(request) stage_response = self.current_stage_view.dispatch(request)
return to_stage_response(request, stage_response) return to_stage_response(request, stage_response, self.container.exec_id)
except Exception as exc: except Exception as exc:
return self.handle_exception(exc) return self.handle_exception(exc)
@ -306,12 +401,19 @@ class FlowExecutorView(APIView):
), ),
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="query", name=QS_QUERY,
location=OpenApiParameter.QUERY, location=OpenApiParameter.QUERY,
required=True, required=True,
description="Querystring as received", description="Querystring as received",
type=OpenApiTypes.STR, type=OpenApiTypes.STR,
) ),
OpenApiParameter(
name=QS_EXEC_ID,
location=OpenApiParameter.QUERY,
required=True,
description="Flow execution ID",
type=OpenApiTypes.STR,
),
], ],
operation_id="flows_executor_solve", operation_id="flows_executor_solve",
) )
@ -338,14 +440,15 @@ class FlowExecutorView(APIView):
span.set_data("authentik Stage", self.current_stage_view) span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug) span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.dispatch(request) stage_response = self.current_stage_view.dispatch(request)
return to_stage_response(request, stage_response) return to_stage_response(request, stage_response, self.container.exec_id)
except Exception as exc: except Exception as exc:
return self.handle_exception(exc) return self.handle_exception(exc)
def _initiate_plan(self) -> FlowPlan: def _initiate_plan(self) -> FlowPlan:
planner = FlowPlanner(self.flow) planner = FlowPlanner(self.flow)
plan = planner.plan(self.request) plan = planner.plan(self.request)
self.request.session[SESSION_KEY_PLAN] = plan container = FlowContainer.new(self.request)
container.plan = plan
try: try:
# Call the has_stages getter to check that # Call the has_stages getter to check that
# there are no issues with the class we might've gotten # there are no issues with the class we might've gotten
@ -369,7 +472,7 @@ class FlowExecutorView(APIView):
except FlowNonApplicableException as exc: except FlowNonApplicableException as exc:
self._logger.warning("f(exec): Flow restart not applicable to current user", exc=exc) self._logger.warning("f(exec): Flow restart not applicable to current user", exc=exc)
return self.handle_invalid_flow(exc) return self.handle_invalid_flow(exc)
self.request.session[SESSION_KEY_PLAN] = plan self.container.plan = plan
kwargs = self.kwargs kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug}) kwargs.update({"flow_slug": self.flow.slug})
return redirect_with_qs("authentik_api:flow-executor", self.request.GET, **kwargs) return redirect_with_qs("authentik_api:flow-executor", self.request.GET, **kwargs)
@ -391,9 +494,13 @@ class FlowExecutorView(APIView):
) )
self.cancel() self.cancel()
if next_param and not is_url_absolute(next_param): if next_param and not is_url_absolute(next_param):
return to_stage_response(self.request, redirect_with_qs(next_param)) return to_stage_response(
self.request, redirect_with_qs(next_param), self.container.exec_id
)
return to_stage_response( return to_stage_response(
self.request, self.stage_invalid(error_message=_("Invalid next URL")) self.request,
self.stage_invalid(error_message=_("Invalid next URL")),
self.container.exec_id,
) )
def stage_ok(self) -> HttpResponse: def stage_ok(self) -> HttpResponse:
@ -407,7 +514,7 @@ class FlowExecutorView(APIView):
self.current_stage_view.cleanup() self.current_stage_view.cleanup()
self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan)) self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan))
self.plan.pop() self.plan.pop()
self.request.session[SESSION_KEY_PLAN] = self.plan self.container.plan = self.plan
if self.plan.bindings: if self.plan.bindings:
self._logger.debug( self._logger.debug(
"f(exec): Continuing with next stage", "f(exec): Continuing with next stage",
@ -450,11 +557,11 @@ class FlowExecutorView(APIView):
def cancel(self): def cancel(self):
"""Cancel current flow execution""" """Cancel current flow execution"""
# TODO: Clean up container
keys_to_delete = [ keys_to_delete = [
SESSION_KEY_APPLICATION_PRE, SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN, SESSION_KEY_PLAN,
SESSION_KEY_GET, SESSION_KEY_GET,
SESSION_KEY_AUTH_STARTED,
# We might need the initial POST payloads for later requests # We might need the initial POST payloads for later requests
# SESSION_KEY_POST, # SESSION_KEY_POST,
# We don't delete the history on purpose, as a user might # We don't delete the history on purpose, as a user might
@ -473,8 +580,8 @@ class CancelView(View):
def get(self, request: HttpRequest) -> HttpResponse: def get(self, request: HttpRequest) -> HttpResponse:
"""View which canels the currently active plan""" """View which canels the currently active plan"""
if SESSION_KEY_PLAN in request.session: if FlowContainer(request, request.GET.get(QS_EXEC_ID)).exists():
del request.session[SESSION_KEY_PLAN] del request.session[SESSION_KEY_PLAN_CONTAINER % request.GET.get(QS_EXEC_ID)]
LOGGER.debug("Canceled current plan") LOGGER.debug("Canceled current plan")
return redirect("authentik_flows:default-invalidation") return redirect("authentik_flows:default-invalidation")
@ -522,19 +629,12 @@ class ToDefaultFlow(View):
def dispatch(self, request: HttpRequest) -> HttpResponse: def dispatch(self, request: HttpRequest) -> HttpResponse:
flow = self.get_flow() flow = self.get_flow()
# If user already has a pending plan, clear it so we don't have to later. get_qs = request.GET.copy()
if SESSION_KEY_PLAN in self.request.session: get_qs[QS_EXEC_ID] = str(uuid4())
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] return redirect_with_qs("authentik_core:if-flow", get_qs, flow_slug=flow.slug)
if plan.flow_pk != flow.pk.hex:
LOGGER.warning(
"f(def): Found existing plan for other flow, deleting plan",
flow_slug=flow.slug,
)
del self.request.session[SESSION_KEY_PLAN]
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse: def to_stage_response(request: HttpRequest, source: HttpResponse, xid: str) -> HttpResponse:
"""Convert normal HttpResponse into JSON Response""" """Convert normal HttpResponse into JSON Response"""
if ( if (
isinstance(source, HttpResponseRedirect) isinstance(source, HttpResponseRedirect)
@ -553,6 +653,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
RedirectChallenge( RedirectChallenge(
{ {
"to": str(redirect_url), "to": str(redirect_url),
"xid": xid,
} }
) )
) )
@ -561,6 +662,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
ShellChallenge( ShellChallenge(
{ {
"body": source.render().content.decode("utf-8"), "body": source.render().content.decode("utf-8"),
"xid": xid,
} }
) )
) )
@ -570,6 +672,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
ShellChallenge( ShellChallenge(
{ {
"body": source.content.decode("utf-8"), "body": source.content.decode("utf-8"),
"xid": xid,
} }
) )
) )
@ -601,4 +704,6 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
except FlowNonApplicableException: except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user") LOGGER.warning("Flow not applicable to user")
raise Http404 from None raise Http404 from None
return plan.to_redirect(request, stage.configure_flow) container = FlowContainer.new(request)
container.plan = plan
return container.to_redirect(request, stage.configure_flow)

View File

@ -6,23 +6,17 @@ from django.shortcuts import get_object_or_404
from ua_parser.user_agent_parser import Parse from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.models import Flow
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED from authentik.flows.views.executor import QS_EXEC_ID
class FlowInterfaceView(InterfaceView): class FlowInterfaceView(InterfaceView):
"""Flow interface""" """Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["flow"] = flow
if (
not self.request.user.is_authenticated
and flow.designation == FlowDesignation.AUTHENTICATION
):
self.request.session[SESSION_KEY_AUTH_STARTED] = True
self.request.session.save()
kwargs["inspector"] = "inspector" in self.request.GET kwargs["inspector"] = "inspector" in self.request.GET
kwargs["xid"] = self.request.GET.get(QS_EXEC_ID)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def compat_needs_sfe(self) -> bool: def compat_needs_sfe(self) -> bool:

View File

@ -1,20 +1,5 @@
# authentik configuration # update website/docs/install-config/configuration/configuration.mdx
# # This is the default configuration file
# https://docs.goauthentik.io/docs/install-config/configuration/
#
# To override the settings in this file, run the following command from the repository root:
#
# ```shell
# make gen-dev-config
# ```
#
# You may edit the generated file to override the configuration below.
#
# When making modifying the default configuration file,
# ensure that the corresponding documentation is updated to match.
#
# @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information.
postgresql: postgresql:
host: localhost host: localhost
name: authentik name: authentik
@ -60,8 +45,6 @@ redis:
# url: "" # url: ""
# transport_options: "" # transport_options: ""
http_timeout: 30
cache: cache:
# url: "" # url: ""
timeout: 300 timeout: 300

View File

@ -18,15 +18,6 @@ class SerializerModel(models.Model):
@property @property
def serializer(self) -> type[BaseSerializer]: def serializer(self) -> type[BaseSerializer]:
"""Get serializer for this model""" """Get serializer for this model"""
# Special handling for built-in source
if (
hasattr(self, "managed")
and hasattr(self, "MANAGED_INBUILT")
and self.managed == self.MANAGED_INBUILT
):
from authentik.core.api.sources import SourceSerializer
return SourceSerializer
raise NotImplementedError raise NotImplementedError

View File

@ -16,40 +16,7 @@ def authentik_user_agent() -> str:
return f"authentik@{get_full_version()}" return f"authentik@{get_full_version()}"
class TimeoutSession(Session): class DebugSession(Session):
"""Always set a default HTTP request timeout"""
def __init__(self, default_timeout=None):
super().__init__()
self.timeout = default_timeout
def send(
self,
request,
*,
stream=...,
verify=...,
proxies=...,
cert=...,
timeout=...,
allow_redirects=...,
**kwargs,
):
if not timeout and self.timeout:
timeout = self.timeout
return super().send(
request,
stream=stream,
verify=verify,
proxies=proxies,
cert=cert,
timeout=timeout,
allow_redirects=allow_redirects,
**kwargs,
)
class DebugSession(TimeoutSession):
"""requests session which logs http requests and responses""" """requests session which logs http requests and responses"""
def send(self, req: PreparedRequest, *args, **kwargs): def send(self, req: PreparedRequest, *args, **kwargs):
@ -75,9 +42,8 @@ class DebugSession(TimeoutSession):
def get_http_session() -> Session: def get_http_session() -> Session:
"""Get a requests session with common headers""" """Get a requests session with common headers"""
session = TimeoutSession() session = Session()
if CONFIG.get_bool("debug") or CONFIG.get("log_level") == "trace": if CONFIG.get_bool("debug") or CONFIG.get("log_level") == "trace":
session = DebugSession() session = DebugSession()
session.headers["User-Agent"] = authentik_user_agent() session.headers["User-Agent"] = authentik_user_agent()
session.timeout = CONFIG.get_optional_int("http_timeout")
return session return session

View File

@ -13,7 +13,6 @@ from paramiko.ssh_exception import SSHException
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from yaml import safe_dump from yaml import safe_dump
from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
@ -185,7 +184,7 @@ class DockerController(BaseController):
try: try:
self.client.images.pull(image) self.client.images.pull(image)
except DockerException: # pragma: no cover except DockerException: # pragma: no cover
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}" image = f"ghcr.io/goauthentik/{self.outpost.type}:latest"
self.client.images.pull(image) self.client.images.pull(image)
return image return image

View File

@ -35,4 +35,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
label = "authentik_policies" label = "authentik_policies"
verbose_name = "authentik Policies" verbose_name = "authentik Policies"
default = True default = True
mountpoint = "policy/"

View File

@ -1,89 +0,0 @@
{% extends 'login/base_full.html' %}
{% load static %}
{% load i18n %}
{% block head %}
{{ block.super }}
<script>
let redirecting = false;
const checkAuth = async () => {
if (redirecting) return true;
const url = "{{ check_auth_url }}";
console.debug("authentik/policies/buffer: Checking authentication...");
try {
const result = await fetch(url, {
method: "HEAD",
});
if (result.status >= 400) {
return false
}
console.debug("authentik/policies/buffer: Continuing");
redirecting = true;
if ("{{ auth_req_method }}" === "post") {
document.querySelector("form").submit();
} else {
window.location.assign("{{ continue_url|escapejs }}");
}
} catch {
return false;
}
};
let timeout = 100;
let offset = 20;
let attempt = 0;
const main = async () => {
attempt += 1;
await checkAuth();
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
setTimeout(main, timeout);
timeout += (offset * attempt);
if (timeout >= 2000) {
timeout = 2000;
}
}
document.addEventListener("visibilitychange", async () => {
if (document.hidden) return;
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
await checkAuth();
});
main();
</script>
{% endblock %}
{% block title %}
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
{% endblock %}
{% block card_title %}
{% trans 'Waiting for authentication...' %}
{% endblock %}
{% block card %}
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
{% if auth_req_method == "post" %}
{% for key, value in auth_req_body.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
{% endif %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<div class="pf-c-empty-state__icon">
<span class="pf-c-spinner pf-m-xl" role="progressbar">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
<h1 class="pf-c-title pf-m-lg">
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
</h1>
</div>
</div>
<div class="pf-c-form__group pf-m-action">
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
{% trans "Authenticate in this tab" %}
</a>
</div>
</form>
{% endblock %}

View File

@ -1,121 +0,0 @@
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpResponse
from django.test import RequestFactory, TestCase
from django.urls import reverse
from authentik.core.models import Application, Provider
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.flows.models import FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response
from authentik.policies.views import (
QS_BUFFER_ID,
SESSION_KEY_BUFFER,
BufferedPolicyAccessView,
BufferView,
PolicyAccessView,
)
class TestPolicyViews(TestCase):
"""Test PolicyAccessView"""
def setUp(self):
super().setUp()
self.factory = RequestFactory()
self.user = create_test_user()
def test_pav(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
class TestView(PolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = self.user
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.content, b"foo")
def test_pav_buffer(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
def test_pav_buffer_skip(self):
"""Test simple policy access view (skip buffer)"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/?skip_buffer=true")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
def test_buffer(self):
"""Test buffer view"""
uid = generate_id()
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
ts = generate_id()
req.session[SESSION_KEY_BUFFER % uid] = {
"method": "get",
"body": {},
"url": f"/{ts}",
}
req.session.save()
res = BufferView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertIn(ts, res.render().content.decode())

View File

@ -1,14 +1,7 @@
"""API URLs""" """API URLs"""
from django.urls import path
from authentik.policies.api.bindings import PolicyBindingViewSet from authentik.policies.api.bindings import PolicyBindingViewSet
from authentik.policies.api.policies import PolicyViewSet from authentik.policies.api.policies import PolicyViewSet
from authentik.policies.views import BufferView
urlpatterns = [
path("buffer", BufferView.as_view(), name="buffer"),
]
api_urlpatterns = [ api_urlpatterns = [
("policies/all", PolicyViewSet), ("policies/all", PolicyViewSet),

View File

@ -1,37 +1,23 @@
"""authentik access helper classes""" """authentik access helper classes"""
from typing import Any from typing import Any
from uuid import uuid4
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import AccessMixin from django.contrib.auth.mixins import AccessMixin
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.http import HttpRequest, HttpResponse, QueryDict from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic.base import TemplateView, View from django.views.generic.base import View
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider, User from authentik.core.models import Application, Provider, User
from authentik.flows.models import Flow, FlowDesignation from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import (
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_AUTH_STARTED,
SESSION_KEY_PLAN,
SESSION_KEY_POST,
)
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
QS_BUFFER_ID = "af_bf_id"
QS_SKIP_BUFFER = "skip_buffer"
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
class RequestValidationError(SentryIgnoredException): class RequestValidationError(SentryIgnoredException):
@ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View):
for message in result.messages: for message in result.messages:
messages.error(self.request, _(message)) messages.error(self.request, _(message))
return result return result
def url_with_qs(url: str, **kwargs):
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
parameters are retained"""
if "?" not in url:
return url + f"?{urlencode(kwargs)}"
url, _, qs = url.partition("?")
qs = QueryDict(qs, mutable=True)
qs.update(kwargs)
return url + f"?{urlencode(qs.items())}"
class BufferView(TemplateView):
"""Buffer view"""
template_name = "policies/buffer.html"
def get_context_data(self, **kwargs):
buf_id = self.request.GET.get(QS_BUFFER_ID)
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
kwargs["auth_req_method"] = buffer["method"]
kwargs["auth_req_body"] = buffer["body"]
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
return super().get_context_data(**kwargs)
class BufferedPolicyAccessView(PolicyAccessView):
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
def handle_no_permission(self):
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED)
if plan:
flow = Flow.objects.filter(pk=plan.flow_pk).first()
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
return super().handle_no_permission()
if not plan and authenticating is None:
LOGGER.debug("Not buffering request, no flow plan active")
return super().handle_no_permission()
if self.request.GET.get(QS_SKIP_BUFFER):
LOGGER.debug("Not buffering request, explicit skip")
return super().handle_no_permission()
buffer_id = str(uuid4())
LOGGER.debug("Buffering access request", bf_id=buffer_id)
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
"body": self.request.POST,
"url": self.request.build_absolute_uri(self.request.get_full_path()),
"method": self.request.method.lower(),
}
return redirect(
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
)
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if QS_BUFFER_ID in self.request.GET:
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
return response

View File

@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest from authentik.policies.types import PolicyRequest
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError from authentik.policies.views import PolicyAccessView, RequestValidationError
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
PKCE_METHOD_PLAIN, PKCE_METHOD_PLAIN,
PKCE_METHOD_S256, PKCE_METHOD_S256,
@ -328,7 +328,7 @@ class OAuthAuthorizationParams:
return code return code
class AuthorizationFlowInitView(BufferedPolicyAccessView): class AuthorizationFlowInitView(PolicyAccessView):
"""OAuth2 Flow initializer, checks access to application and starts flow""" """OAuth2 Flow initializer, checks access to application and starts flow"""
params: OAuthAuthorizationParams params: OAuthAuthorizationParams

View File

@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage from authentik.flows.stage import RedirectStage
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.views import BufferedPolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
class RACStartView(BufferedPolicyAccessView): class RACStartView(PolicyAccessView):
"""Start a RAC connection by checking access and creating a connection token""" """Start a RAC connection by checking access and creating a connection token"""
endpoint: Endpoint endpoint: Endpoint

View File

@ -1,22 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-31 13:50
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0017_samlprovider_authn_context_class_ref_mapping"),
]
operations = [
migrations.AlterField(
model_name="samlprovider",
name="acs_url",
field=models.TextField(
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="ACS URL",
),
),
]

View File

@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
from authentik.core.api.object_types import CreatableType from authentik.core.api.object_types import CreatableType
from authentik.core.models import PropertyMapping, Provider from authentik.core.models import PropertyMapping, Provider
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator
from authentik.lib.utils.time import timedelta_string_validator from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.constants import (
DSA_SHA1, DSA_SHA1,
@ -41,9 +40,7 @@ class SAMLBindings(models.TextChoices):
class SAMLProvider(Provider): class SAMLProvider(Provider):
"""SAML 2.0 Endpoint for applications which support SAML.""" """SAML 2.0 Endpoint for applications which support SAML."""
acs_url = models.TextField( acs_url = models.URLField(verbose_name=_("ACS URL"))
validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL")
)
audience = models.TextField( audience = models.TextField(
default="", default="",
blank=True, blank=True,

View File

@ -15,7 +15,7 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_POST from authentik.flows.views.executor import SESSION_KEY_POST
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.views import BufferedPolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLBindings, SAMLProvider from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
@ -35,7 +35,7 @@ from authentik.stages.consent.stage import (
LOGGER = get_logger() LOGGER = get_logger()
class SAMLSSOView(BufferedPolicyAccessView): class SAMLSSOView(PolicyAccessView):
"""SAML SSO Base View, which plans a flow and injects our final stage. """SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler.""" Calls get/post handler."""
@ -83,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""GET and POST use the same handler, but we can't """GET and POST use the same handler, but we can't
override .dispatch easily because BufferedPolicyAccessView's dispatch""" override .dispatch easily because PolicyAccessView's dispatch"""
return self.get(request, application_slug) return self.get(request, application_slug)

View File

@ -243,10 +243,9 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
if user.value not in users_should: if user.value not in users_should:
users_to_remove.append(user.value) users_to_remove.append(user.value)
# Check users that should be in the group and add them # Check users that should be in the group and add them
if current_group.members is not None: for user in users_should:
for user in users_should: if len([x for x in current_group.members if x.value == user]) < 1:
if len([x for x in current_group.members if x.value == user]) < 1: users_to_add.append(user)
users_to_add.append(user)
# Only send request if we need to make changes # Only send request if we need to make changes
if len(users_to_add) < 1 and len(users_to_remove) < 1: if len(users_to_add) < 1 and len(users_to_remove) < 1:
return return

View File

@ -1,35 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-31 13:53
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_saml", "0017_fix_x509subjectname"),
]
operations = [
migrations.AlterField(
model_name="samlsource",
name="slo_url",
field=models.TextField(
blank=True,
default=None,
help_text="Optional URL if your IDP supports Single-Logout.",
null=True,
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="SLO URL",
),
),
migrations.AlterField(
model_name="samlsource",
name="sso_url",
field=models.TextField(
help_text="URL that the initial Login request is sent to.",
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="SSO URL",
),
),
]

View File

@ -20,7 +20,6 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.flows.challenge import RedirectChallenge from authentik.flows.challenge import RedirectChallenge
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.expression.evaluator import BaseEvaluator from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.models import DomainlessURLValidator
from authentik.lib.utils.time import timedelta_string_validator from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.constants import (
DSA_SHA1, DSA_SHA1,
@ -92,13 +91,11 @@ class SAMLSource(Source):
help_text=_("Also known as Entity ID. Defaults the Metadata URL."), help_text=_("Also known as Entity ID. Defaults the Metadata URL."),
) )
sso_url = models.TextField( sso_url = models.URLField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
verbose_name=_("SSO URL"), verbose_name=_("SSO URL"),
help_text=_("URL that the initial Login request is sent to."), help_text=_("URL that the initial Login request is sent to."),
) )
slo_url = models.TextField( slo_url = models.URLField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
default=None, default=None,
blank=True, blank=True,
null=True, null=True,

View File

@ -33,7 +33,6 @@ from authentik.flows.planner import (
) )
from authentik.flows.stage import ChallengeStageView from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64 from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
@ -74,8 +73,6 @@ class InitiateView(View):
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user" NEXT_ARG_NAME, "authentik_core:if-user"
) )
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
kwargs.update( kwargs.update(
{ {
PLAN_CONTEXT_SSO: True, PLAN_CONTEXT_SSO: True,

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse from django.urls import reverse
from authentik.core.models import User from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.models import FlowDesignation, FlowStageBinding
@ -67,36 +67,6 @@ class TestEmailStageSending(FlowTestCase):
self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"]) self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"])
self.assertEqual(event.context["from_email"], "system@authentik.local") self.assertEqual(event.context["from_email"], "system@authentik.local")
def test_newlines_long_name(self):
"""Test with pending user"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
long_user = create_test_user()
long_user.name = "Test User\r\n Many Words\r\n"
long_user.save()
plan.context[PLAN_CONTEXT_PENDING_USER] = long_user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
):
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
response_errors={
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
},
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik")
self.assertEqual(mail.outbox[0].to, [f"Test User Many Words <{long_user.email}>"])
def test_pending_fake_user(self): def test_pending_fake_user(self):
"""Test with pending (fake) user""" """Test with pending (fake) user"""
self.flow.designation = FlowDesignation.RECOVERY self.flow.designation = FlowDesignation.RECOVERY

View File

@ -32,14 +32,7 @@ class TemplateEmailMessage(EmailMultiAlternatives):
sanitized_to = [] sanitized_to = []
# Ensure that all recipients are valid # Ensure that all recipients are valid
for recipient_name, recipient_email in to: for recipient_name, recipient_email in to:
# Remove any newline characters from name and email before sanitizing sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8"))
clean_name = (
recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else ""
)
clean_email = (
recipient_email.replace("\n", "").replace("\r", "") if recipient_email else ""
)
sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8"))
super().__init__(to=sanitized_to, **kwargs) super().__init__(to=sanitized_to, **kwargs)
if not template_name: if not template_name:
return return

View File

@ -142,38 +142,35 @@ class IdentificationChallengeResponse(ChallengeResponse):
raise ValidationError("Failed to authenticate.") raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user self.pre_user = pre_user
# Password check
if current_stage.password_stage:
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
# Captcha check # Captcha check
if captcha_stage := current_stage.captcha_stage: if captcha_stage := current_stage.captcha_stage:
captcha_token = attrs.get("captcha_token", None) captcha_token = attrs.get("captcha_token", None)
if not captcha_token: if not captcha_token:
self.stage.logger.warning("Token not set for captcha attempt") self.stage.logger.warning("Token not set for captcha attempt")
verify_captcha_token(captcha_stage, captcha_token, client_ip) verify_captcha_token(captcha_stage, captcha_token, client_ip)
# Password check
if not current_stage.password_stage:
# No password stage select, don't validate the password
return attrs
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
return attrs return attrs

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2025.2.3 Blueprint schema", "title": "authentik 2025.2.2 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"
@ -6423,6 +6423,8 @@
}, },
"acs_url": { "acs_url": {
"type": "string", "type": "string",
"format": "uri",
"maxLength": 200,
"minLength": 1, "minLength": 1,
"title": "ACS URL" "title": "ACS URL"
}, },
@ -8731,6 +8733,8 @@
}, },
"sso_url": { "sso_url": {
"type": "string", "type": "string",
"format": "uri",
"maxLength": 200,
"minLength": 1, "minLength": 1,
"title": "SSO URL", "title": "SSO URL",
"description": "URL that the initial Login request is sent to." "description": "URL that the initial Login request is sent to."
@ -8740,6 +8744,8 @@
"string", "string",
"null" "null"
], ],
"format": "uri",
"maxLength": 200,
"title": "SLO URL", "title": "SLO URL",
"description": "Optional URL if your IDP supports Single-Logout." "description": "Optional URL if your IDP supports Single-Logout."
}, },
@ -13010,15 +13016,6 @@
"minLength": 1, "minLength": 1,
"title": "Branding favicon" "title": "Branding favicon"
}, },
"branding_custom_css": {
"type": "string",
"title": "Branding custom css"
},
"branding_default_flow_background": {
"type": "string",
"minLength": 1,
"title": "Branding default flow background"
},
"flow_authentication": { "flow_authentication": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid",
@ -14900,15 +14897,9 @@
"type": "string", "type": "string",
"title": "Webhook url" "title": "Webhook url"
}, },
"webhook_mapping_body": { "webhook_mapping": {
"type": "integer", "type": "integer",
"title": "Webhook mapping body", "title": "Webhook mapping"
"description": "Customize the body of the request. Mapping should return data that is JSON-serializable."
},
"webhook_mapping_headers": {
"type": "integer",
"title": "Webhook mapping headers",
"description": "Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs"
}, },
"send_once": { "send_once": {
"type": "boolean", "type": "boolean",

View File

@ -31,7 +31,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -54,7 +54,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

13
go.mod
View File

@ -1,6 +1,9 @@
module goauthentik.io module goauthentik.io
go 1.24.0 go 1.23.0
toolchain go1.24.0
require ( require (
beryju.io/ldap v0.1.0 beryju.io/ldap v0.1.0
github.com/coreos/go-oidc/v3 v3.13.0 github.com/coreos/go-oidc/v3 v3.13.0
@ -8,7 +11,7 @@ require (
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.10 github.com/go-ldap/ldap/v3 v3.4.10
github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/runtime v0.28.0
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2 github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
@ -20,13 +23,13 @@ require (
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.8.0 github.com/pires/go-proxyproto v0.8.0
github.com/prometheus/client_golang v1.21.1 github.com/prometheus/client_golang v1.21.1
github.com/redis/go-redis/v9 v9.7.3 github.com/redis/go-redis/v9 v9.7.1
github.com/sethvargo/go-envconfig v1.1.1 github.com/sethvargo/go-envconfig v1.1.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025023.2 goauthentik.io/api/v3 v3.2025022.3
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.28.0 golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.12.0 golang.org/x/sync v0.12.0
@ -79,3 +82,5 @@ require (
google.golang.org/protobuf v1.36.1 // indirect google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
replace goauthentik.io/api/v3 => ./gen-go-api

12
go.sum
View File

@ -113,8 +113,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -248,8 +248,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025023.2 h1:4XHlnykN5jQH78liQ4cp2Jf8eigvQImIJp+A+bsq1nA= goauthentik.io/api/v3 v3.2025022.3 h1:cipaxl0il4/s1fU2f6+CD7nzgAktbV0XD7r5qHh0fUc=
goauthentik.io/api/v3 v3.2025023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2025022.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -162,14 +162,13 @@ func (c *Config) parseScheme(rawVal string) string {
if err != nil { if err != nil {
return rawVal return rawVal
} }
switch u.Scheme { if u.Scheme == "env" {
case "env":
e, ok := os.LookupEnv(u.Host) e, ok := os.LookupEnv(u.Host)
if ok { if ok {
return e return e
} }
return u.RawQuery return u.RawQuery
case "file": } else if u.Scheme == "file" {
d, err := os.ReadFile(u.Path) d, err := os.ReadFile(u.Path)
if err != nil { if err != nil {
return u.RawQuery return u.RawQuery

View File

@ -10,7 +10,7 @@ import (
) )
func TestConfigEnv(t *testing.T) { func TestConfigEnv(t *testing.T) {
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "bar")) os.Setenv("AUTHENTIK_SECRET_KEY", "bar")
cfg = nil cfg = nil
if err := Get().fromEnv(); err != nil { if err := Get().fromEnv(); err != nil {
panic(err) panic(err)
@ -19,8 +19,8 @@ func TestConfigEnv(t *testing.T) {
} }
func TestConfigEnv_Scheme(t *testing.T) { func TestConfigEnv_Scheme(t *testing.T) {
assert.NoError(t, os.Setenv("foo", "bar")) os.Setenv("foo", "bar")
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")) os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")
cfg = nil cfg = nil
if err := Get().fromEnv(); err != nil { if err := Get().fromEnv(); err != nil {
panic(err) panic(err)
@ -33,15 +33,13 @@ func TestConfigEnv_File(t *testing.T) {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer func() { defer os.Remove(file.Name())
assert.NoError(t, os.Remove(file.Name()))
}()
_, err = file.Write([]byte("bar")) _, err = file.Write([]byte("bar"))
if err != nil { if err != nil {
panic(err) panic(err)
} }
assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))) os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))
cfg = nil cfg = nil
if err := Get().fromEnv(); err != nil { if err := Get().fromEnv(); err != nil {
panic(err) panic(err)

View File

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

View File

@ -0,0 +1,5 @@
//go:build requirefips
package backend
var FipsEnabled = true

View File

@ -0,0 +1,5 @@
//go:build !requirefips
package backend
var FipsEnabled = false

View File

@ -35,7 +35,7 @@ func EnableDebugServer() {
if err != nil { if err != nil {
return nil return nil
} }
_, err = fmt.Fprintf(w, "<a href='%[1]s'>%[1]s</a><br>", tpl) _, err = w.Write([]byte(fmt.Sprintf("<a href='%[1]s'>%[1]s</a><br>", tpl)))
if err != nil { if err != nil {
l.WithError(err).Warning("failed to write index") l.WithError(err).Warning("failed to write index")
return nil return nil

View File

@ -44,11 +44,10 @@ func New(healthcheck func() bool) *GoUnicorn {
signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2) signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2)
go func() { go func() {
for sig := range c { for sig := range c {
switch sig { if sig == syscall.SIGHUP {
case syscall.SIGHUP:
g.log.Info("SIGHUP received, forwarding to gunicorn") g.log.Info("SIGHUP received, forwarding to gunicorn")
g.Reload() g.Reload()
case syscall.SIGUSR2: } else if sig == syscall.SIGUSR2 {
g.log.Info("SIGUSR2 received, restarting gunicorn") g.log.Info("SIGUSR2 received, restarting gunicorn")
g.Restart() g.Restart()
} }

View File

@ -2,7 +2,6 @@ package ak
import ( import (
"context" "context"
"crypto/fips140"
"fmt" "fmt"
"math/rand" "math/rand"
"net/http" "net/http"
@ -204,7 +203,7 @@ func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
"golangVersion": runtime.Version(), "golangVersion": runtime.Version(),
"opensslEnabled": cryptobackend.OpensslEnabled, "opensslEnabled": cryptobackend.OpensslEnabled,
"opensslVersion": cryptobackend.OpensslVersion(), "opensslVersion": cryptobackend.OpensslVersion(),
"fipsEnabled": fips140.Enabled(), "fipsEnabled": cryptobackend.FipsEnabled,
} }
hostname, err := os.Hostname() hostname, err := os.Hostname()
if err == nil { if err == nil {

View File

@ -35,19 +35,13 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
req PaginatorRequest[Treq, Tres], req PaginatorRequest[Treq, Tres],
opts PaginatorOptions, opts PaginatorOptions,
) ([]Tobj, error) { ) ([]Tobj, error) {
if opts.Logger == nil {
opts.Logger = log.NewEntry(log.StandardLogger())
}
var bfreq, cfreq interface{} var bfreq, cfreq interface{}
fetchOffset := func(page int32) (Tres, error) { fetchOffset := func(page int32) (Tres, error) {
bfreq = req.Page(page) bfreq = req.Page(page)
cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize)) cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize))
res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute() res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute()
if err != nil { if err != nil {
opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page") opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page")
if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 {
return res, err
}
} }
return res, err return res, err
} }
@ -57,9 +51,6 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]](
for { for {
apiObjects, err := fetchOffset(page) apiObjects, err := fetchOffset(page)
if err != nil { if err != nil {
if page == 1 {
return objects, err
}
errs = append(errs, err) errs = append(errs, err)
continue continue
} }

View File

@ -1,64 +1,5 @@
package ak package ak
import (
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"goauthentik.io/api/v3"
)
type fakeAPIType struct{}
type fakeAPIResponse struct {
results []fakeAPIType
pagination api.Pagination
}
func (fapi *fakeAPIResponse) GetResults() []fakeAPIType { return fapi.results }
func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination }
type fakeAPIRequest struct {
res *fakeAPIResponse
http *http.Response
err error
}
func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest { return fapi }
func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi }
func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) {
return fapi.res, fapi.http, fapi.err
}
func Test_Simple(t *testing.T) {
req := &fakeAPIRequest{
res: &fakeAPIResponse{
results: []fakeAPIType{
{},
},
pagination: api.Pagination{
TotalPages: 1,
},
},
}
res, err := Paginator(req, PaginatorOptions{})
assert.NoError(t, err)
assert.Len(t, res, 1)
}
func Test_BadRequest(t *testing.T) {
req := &fakeAPIRequest{
http: &http.Response{
StatusCode: 400,
},
err: errors.New("foo"),
}
res, err := Paginator(req, PaginatorOptions{})
assert.Error(t, err)
assert.Equal(t, []fakeAPIType{}, res)
}
// func Test_PaginatorCompile(t *testing.T) { // func Test_PaginatorCompile(t *testing.T) {
// req := api.ApiCoreUsersListRequest{} // req := api.ApiCoreUsersListRequest{}
// Paginator(req, PaginatorOptions{ // Paginator(req, PaginatorOptions{

View File

@ -148,8 +148,7 @@ func (ac *APIController) startWSHandler() {
"outpost_type": ac.Server.Type(), "outpost_type": ac.Server.Type(),
"uuid": ac.instanceUUID.String(), "uuid": ac.instanceUUID.String(),
}).Set(1) }).Set(1)
switch wsMsg.Instruction { if wsMsg.Instruction == WebsocketInstructionTriggerUpdate {
case WebsocketInstructionTriggerUpdate:
time.Sleep(ac.reloadOffset) time.Sleep(ac.reloadOffset)
logger.Debug("Got update trigger...") logger.Debug("Got update trigger...")
err := ac.OnRefresh() err := ac.OnRefresh()
@ -164,7 +163,7 @@ func (ac *APIController) startWSHandler() {
"build": constants.BUILD(""), "build": constants.BUILD(""),
}).SetToCurrentTime() }).SetToCurrentTime()
} }
case WebsocketInstructionProviderSpecific: } else if wsMsg.Instruction == WebsocketInstructionProviderSpecific {
for _, h := range ac.wsHandlers { for _, h := range ac.wsHandlers {
h(context.Background(), wsMsg.Args) h(context.Background(), wsMsg.Args)
} }

View File

@ -66,12 +66,7 @@ func (ls *LDAPServer) StartLDAPServer() error {
return err return err
} }
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() { defer proxyListener.Close()
err := proxyListener.Close()
if err != nil {
ls.log.WithError(err).Warning("failed to close proxy listener")
}
}()
ls.log.WithField("listen", listen).Info("Starting LDAP server") ls.log.WithField("listen", listen).Info("Starting LDAP server")
err = ls.s.Serve(proxyListener) err = ls.s.Serve(proxyListener)

View File

@ -49,12 +49,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
} }
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() { defer proxyListener.Close()
err := proxyListener.Close()
if err != nil {
ls.log.WithError(err).Warning("failed to close proxy listener")
}
}()
tln := tls.NewListener(proxyListener, tlsConfig) tln := tls.NewListener(proxyListener, tlsConfig)

View File

@ -98,7 +98,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
entries := make([]*ldap.Entry, 0) entries := make([]*ldap.Entry, 0)
scope := req.Scope scope := req.SearchRequest.Scope
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass) needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) { if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {

View File

@ -56,7 +56,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo
if !embedded && hostBrowser == "" { if !embedded && hostBrowser == "" {
return ep return ep
} }
var newHost = aku var newHost *url.URL = aku
var newBrowserHost *url.URL var newBrowserHost *url.URL
if embedded { if embedded {
if authentikHost == "" { if authentikHost == "" {

View File

@ -130,12 +130,7 @@ func (ps *ProxyServer) ServeHTTP() {
return return
} }
proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()} proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() { defer proxyListener.Close()
err := proxyListener.Close()
if err != nil {
ps.log.WithError(err).Warning("failed to close proxy listener")
}
}()
ps.log.WithField("listen", listenAddress).Info("Starting HTTP server") ps.log.WithField("listen", listenAddress).Info("Starting HTTP server")
ps.serve(proxyListener) ps.serve(proxyListener)
@ -154,12 +149,7 @@ func (ps *ProxyServer) ServeHTTPS() {
return return
} }
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()} proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() { defer proxyListener.Close()
err := proxyListener.Close()
if err != nil {
ps.log.WithError(err).Warning("failed to close proxy listener")
}
}()
tlsListener := tls.NewListener(proxyListener, tlsConfig) tlsListener := tls.NewListener(proxyListener, tlsConfig)
ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server") ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server")

View File

@ -72,13 +72,11 @@ func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error
session.ID = c.Value session.ID = c.Value
err = s.load(r.Context(), session) err = s.load(r.Context(), session)
if err != nil { if err == nil {
if errors.Is(err, redis.Nil) { session.IsNew = false
return session, nil } else if err == redis.Nil {
} err = nil // no data stored
return session, err
} }
session.IsNew = false
return session, err return session, err
} }

View File

@ -8,6 +8,7 @@
<link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png"> <link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css"> <link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css"> <link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css">
<link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/custom.css">
<link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" /> <link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" />
<style> <style>
.pf-c-background-image::before { .pf-c-background-image::before {

View File

@ -156,12 +156,7 @@ func (ws *WebServer) listenPlain() {
return return
} }
proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() { defer proxyListener.Close()
err := proxyListener.Close()
if err != nil {
ws.log.WithError(err).Warning("failed to close proxy listener")
}
}()
ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server") ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server")
ws.serve(proxyListener) ws.serve(proxyListener)

View File

@ -46,12 +46,7 @@ func (ws *WebServer) listenTLS() {
return return
} }
proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()} proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()}
defer func() { defer proxyListener.Close()
err := proxyListener.Close()
if err != nil {
ws.log.WithError(err).Warning("failed to close proxy listener")
}
}()
tlsListener := tls.NewListener(proxyListener, tlsConfig) tlsListener := tls.NewListener(proxyListener, tlsConfig)
ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server") ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server")

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Stage 1: Build # Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@ -27,7 +27,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/ldap ./cmd/ldap go build -o /go/ldap ./cmd/ldap
# Stage 2: Run # Stage 2: Run

View File

@ -9,7 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1006.0", "aws-cdk": "^2.1005.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
}, },
"engines": { "engines": {
@ -17,9 +17,9 @@
} }
}, },
"node_modules/aws-cdk": { "node_modules/aws-cdk": {
"version": "2.1006.0", "version": "2.1005.0",
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz", "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz",
"integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==", "integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

@ -10,7 +10,7 @@
"node": ">=20" "node": ">=20"
}, },
"devDependencies": { "devDependencies": {
"aws-cdk": "^2.1006.0", "aws-cdk": "^2.1005.0",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
} }
} }

View File

@ -26,7 +26,7 @@ Parameters:
Description: authentik Docker image Description: authentik Docker image
AuthentikVersion: AuthentikVersion:
Type: String Type: String
Default: 2025.2.3 Default: 2025.2.2
Description: authentik Docker image tag Description: authentik Docker image tag
AuthentikServerCPU: AuthentikServerCPU:
Type: Number Type: Number

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n" "POT-Creation-Date: 2025-03-13 00:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -616,18 +616,6 @@ msgstr ""
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: authentik/events/models.py
msgid ""
"Customize the body of the request. Mapping should return data that is JSON-"
"serializable."
msgstr ""
#: authentik/events/models.py
msgid ""
"Configure additional headers to be sent. Mapping should return a dictionary "
"of key-value pairs"
msgstr ""
#: authentik/events/models.py #: authentik/events/models.py
msgid "" msgid ""
"Only send notification once, for example when sending a webhook into a chat " "Only send notification once, for example when sending a webhook into a chat "
@ -1220,20 +1208,6 @@ msgstr ""
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "" msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "" msgstr ""
@ -1782,17 +1756,6 @@ msgid ""
"NameIDPolicy of the incoming request will be considered" "NameIDPolicy of the incoming request will be considered"
msgstr "" msgstr ""
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr ""
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
#: authentik/providers/saml/models.py #: authentik/providers/saml/models.py
msgid "" msgid ""
"Assertion valid not before current time + this value (Format: hours=-1;" "Assertion valid not before current time + this value (Format: hours=-1;"

View File

@ -10,8 +10,8 @@
# Manuel Viens, 2023 # Manuel Viens, 2023
# Mordecai, 2023 # Mordecai, 2023
# nerdinator <florian.dupret@gmail.com>, 2024 # nerdinator <florian.dupret@gmail.com>, 2024
# Tina, 2024
# Charles Leclerc, 2025 # Charles Leclerc, 2025
# Tina, 2025
# Marc Schmitt, 2025 # Marc Schmitt, 2025
# #
#, fuzzy #, fuzzy
@ -19,7 +19,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n" "POT-Creation-Date: 2025-03-13 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n" "Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -676,22 +676,6 @@ msgstr "Webhook Slack (ou Discord)"
msgid "Email" msgid "Email"
msgstr "Courriel" msgstr "Courriel"
#: authentik/events/models.py
msgid ""
"Customize the body of the request. Mapping should return data that is JSON-"
"serializable."
msgstr ""
"Personnalise le corps de la requête. Le mappage doit renvoyer des données "
"sérialisables en JSON."
#: authentik/events/models.py
msgid ""
"Configure additional headers to be sent. Mapping should return a dictionary "
"of key-value pairs"
msgstr ""
"Configure les en-têtes supplémentaires à envoyer. Le mappage doit renvoyer "
"un dictionnaire de paires clé-valeur."
#: authentik/events/models.py #: authentik/events/models.py
msgid "" msgid ""
"Only send notification once, for example when sending a webhook into a chat " "Only send notification once, for example when sending a webhook into a chat "
@ -1347,22 +1331,6 @@ msgstr "Score de Réputation"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "Scores de Réputation" msgstr "Scores de Réputation"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "En attente de l'authentification..."
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
"Vous êtes déjà en cours d'authentification dans un autre onglet. Cette page "
"se rafraîchira lorsque l'authentification sera terminée."
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr "S'authentifier dans cet onglet"
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "Permission refusée" msgstr "Permission refusée"
@ -1988,20 +1956,6 @@ msgstr ""
"Configure la manière dont la valeur NameID sera créée. Si laissé vide, la " "Configure la manière dont la valeur NameID sera créée. Si laissé vide, la "
"NameIDPolicy de la requête entrante sera prise en compte" "NameIDPolicy de la requête entrante sera prise en compte"
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr "Mappage de propriété AuthnContextClassRef"
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
"Configure comment la valeur AuthnContextClassRef sera créée. Lorsque non "
"sélectionné, AuthnContextClassRef sera défini en fonction de quelle méthode "
"d'authentification l'utilisateur a utilisé pour s'authentifier."
#: authentik/providers/saml/models.py #: authentik/providers/saml/models.py
msgid "" msgid ""
"Assertion valid not before current time + this value (Format: " "Assertion valid not before current time + this value (Format: "

Binary file not shown.

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-22 00:10+0000\n" "POT-Creation-Date: 2025-03-13 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -627,18 +627,6 @@ msgstr "Slack WebhookSlack/Discord"
msgid "Email" msgid "Email"
msgstr "电子邮箱" msgstr "电子邮箱"
#: authentik/events/models.py
msgid ""
"Customize the body of the request. Mapping should return data that is JSON-"
"serializable."
msgstr "自定义请求体。映射应该返回 JSON 序列化的数据。"
#: authentik/events/models.py
msgid ""
"Configure additional headers to be sent. Mapping should return a dictionary "
"of key-value pairs"
msgstr "配置要发送的额外标头。映射应该返回键值对字典。"
#: authentik/events/models.py #: authentik/events/models.py
msgid "" msgid ""
"Only send notification once, for example when sending a webhook into a chat " "Only send notification once, for example when sending a webhook into a chat "
@ -1794,18 +1782,6 @@ msgid ""
"NameIDPolicy of the incoming request will be considered" "NameIDPolicy of the incoming request will be considered"
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy" msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr "AuthnContextClassRef 属性映射"
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
"配置如何创建 AuthnContextClassRef 值。留空时AuthnContextClassRef 会基于用户使用的身份验证方式设置。"
#: authentik/providers/saml/models.py #: authentik/providers/saml/models.py
msgid "" msgid ""
"Assertion valid not before current time + this value (Format: " "Assertion valid not before current time + this value (Format: "

Binary file not shown.

View File

@ -14,7 +14,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-31 00:10+0000\n" "POT-Creation-Date: 2025-03-13 00:10+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -626,18 +626,6 @@ msgstr "Slack WebhookSlack/Discord"
msgid "Email" msgid "Email"
msgstr "电子邮箱" msgstr "电子邮箱"
#: authentik/events/models.py
msgid ""
"Customize the body of the request. Mapping should return data that is JSON-"
"serializable."
msgstr "自定义请求体。映射应该返回 JSON 序列化的数据。"
#: authentik/events/models.py
msgid ""
"Configure additional headers to be sent. Mapping should return a dictionary "
"of key-value pairs"
msgstr "配置要发送的额外标头。映射应该返回键值对字典。"
#: authentik/events/models.py #: authentik/events/models.py
msgid "" msgid ""
"Only send notification once, for example when sending a webhook into a chat " "Only send notification once, for example when sending a webhook into a chat "
@ -1234,20 +1222,6 @@ msgstr "信誉分数"
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "信誉分数" msgstr "信誉分数"
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr "正在等待身份验证…"
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr "您正在另一个标签页中验证身份。身份验证完成后,此页面会刷新。"
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr "在此标签页中验证身份"
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "权限被拒绝" msgstr "权限被拒绝"
@ -1807,18 +1781,6 @@ msgid ""
"NameIDPolicy of the incoming request will be considered" "NameIDPolicy of the incoming request will be considered"
msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy" msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy"
#: authentik/providers/saml/models.py
msgid "AuthnContextClassRef Property Mapping"
msgstr "AuthnContextClassRef 属性映射"
#: authentik/providers/saml/models.py
msgid ""
"Configure how the AuthnContextClassRef value will be created. When left "
"empty, the AuthnContextClassRef will be set based on which authentication "
"methods the user used to authenticate."
msgstr ""
"配置如何创建 AuthnContextClassRef 值。留空时AuthnContextClassRef 会基于用户使用的身份验证方式设置。"
#: authentik/providers/saml/models.py #: authentik/providers/saml/models.py
msgid "" msgid ""
"Assertion valid not before current time + this value (Format: " "Assertion valid not before current time + this value (Format: "

View File

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

View File

@ -17,7 +17,7 @@ COPY web .
RUN npm run build-proxy RUN npm run build-proxy
# Stage 2: Build # Stage 2: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@ -43,7 +43,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/proxy ./cmd/proxy go build -o /go/proxy ./cmd/proxy
# Stage 3: Run # Stage 3: Run

View File

@ -1,6 +1,6 @@
[project] [project]
name = "authentik" name = "authentik"
version = "2025.2.3" version = "2025.2.2"
description = "" description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.12.*" requires-python = "==3.12.*"
@ -103,7 +103,7 @@ dev = [
[tool.uv.sources] [tool.uv.sources]
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" } django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" }
opencontainers = { git = "https://github.com/BeryJu/oci-python", rev = "c791b19056769cd67957322806809ab70f5bead8" } opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" }
[project.scripts] [project.scripts]
ak = "lifecycle.ak:main" ak = "lifecycle.ak:main"

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Stage 1: Build # Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@ -27,7 +27,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/rac ./cmd/rac go build -o /go/rac ./cmd/rac
# Stage 2: Run # Stage 2: Run

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Stage 1: Build # Stage 1: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@ -27,7 +27,7 @@ COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/radius ./cmd/radius go build -o /go/radius ./cmd/radius
# Stage 2: Run # Stage 2: Run

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2025.2.3 version: 2025.2.2
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io
@ -4447,10 +4447,6 @@ paths:
schema: schema:
type: string type: string
format: uuid format: uuid
- in: query
name: branding_default_flow_background
schema:
type: string
- in: query - in: query
name: branding_favicon name: branding_favicon
schema: schema:
@ -8921,6 +8917,11 @@ paths:
type: string type: string
description: Querystring as received description: Querystring as received
required: true required: true
- in: query
name: xid
schema:
type: string
description: Flow execution ID
tags: tags:
- flows - flows
security: security:
@ -8961,6 +8962,12 @@ paths:
type: string type: string
description: Querystring as received description: Querystring as received
required: true required: true
- in: query
name: xid
schema:
type: string
description: Flow execution ID
required: true
tags: tags:
- flows - flows
requestBody: requestBody:
@ -39441,6 +39448,8 @@ components:
component: component:
type: string type: string
default: ak-stage-access-denied default: ak-stage-access-denied
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -39456,6 +39465,7 @@ components:
required: required:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
AlgEnum: AlgEnum:
enum: enum:
- rsa - rsa
@ -39555,6 +39565,8 @@ components:
component: component:
type: string type: string
default: ak-source-oauth-apple default: ak-source-oauth-apple
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -39574,6 +39586,7 @@ components:
- redirect_uri - redirect_uri
- scope - scope
- state - state
- xid
Application: Application:
type: object type: object
description: Application Serializer description: Application Serializer
@ -39882,6 +39895,8 @@ components:
component: component:
type: string type: string
default: ak-stage-authenticator-duo default: ak-stage-authenticator-duo
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -39904,6 +39919,7 @@ components:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- stage_uuid - stage_uuid
- xid
AuthenticatorDuoChallengeResponseRequest: AuthenticatorDuoChallengeResponseRequest:
type: object type: object
description: Pseudo class for duo response description: Pseudo class for duo response
@ -40041,6 +40057,8 @@ components:
component: component:
type: string type: string
default: ak-stage-authenticator-email default: ak-stage-authenticator-email
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -40060,6 +40078,7 @@ components:
required: required:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
AuthenticatorEmailChallengeResponseRequest: AuthenticatorEmailChallengeResponseRequest:
type: object type: object
description: Authenticator Email Challenge response, device is set by get_response_instance description: Authenticator Email Challenge response, device is set by get_response_instance
@ -40297,6 +40316,8 @@ components:
component: component:
type: string type: string
default: ak-stage-authenticator-sms default: ak-stage-authenticator-sms
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -40313,6 +40334,7 @@ components:
required: required:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
AuthenticatorSMSChallengeResponseRequest: AuthenticatorSMSChallengeResponseRequest:
type: object type: object
description: SMS Challenge response, device is set by get_response_instance description: SMS Challenge response, device is set by get_response_instance
@ -40460,6 +40482,8 @@ components:
component: component:
type: string type: string
default: ak-stage-authenticator-static default: ak-stage-authenticator-static
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -40478,6 +40502,7 @@ components:
- codes - codes
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
AuthenticatorStaticChallengeResponseRequest: AuthenticatorStaticChallengeResponseRequest:
type: object type: object
description: Pseudo class for static response description: Pseudo class for static response
@ -40581,6 +40606,8 @@ components:
component: component:
type: string type: string
default: ak-stage-authenticator-totp default: ak-stage-authenticator-totp
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -40597,6 +40624,7 @@ components:
- config_url - config_url
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
AuthenticatorTOTPChallengeResponseRequest: AuthenticatorTOTPChallengeResponseRequest:
type: object type: object
description: TOTP Challenge response, device is set by get_response_instance description: TOTP Challenge response, device is set by get_response_instance
@ -40808,6 +40836,8 @@ components:
component: component:
type: string type: string
default: ak-stage-authenticator-validate default: ak-stage-authenticator-validate
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -40831,6 +40861,7 @@ components:
- device_challenges - device_challenges
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
AuthenticatorValidationChallengeResponseRequest: AuthenticatorValidationChallengeResponseRequest:
type: object type: object
description: Challenge used for Code-based and WebAuthn authenticators description: Challenge used for Code-based and WebAuthn authenticators
@ -40861,6 +40892,8 @@ components:
component: component:
type: string type: string
default: ak-stage-authenticator-webauthn default: ak-stage-authenticator-webauthn
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -40878,6 +40911,7 @@ components:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- registration - registration
- xid
AuthenticatorWebAuthnChallengeResponseRequest: AuthenticatorWebAuthnChallengeResponseRequest:
type: object type: object
description: WebAuthn Challenge response description: WebAuthn Challenge response
@ -41010,6 +41044,8 @@ components:
component: component:
type: string type: string
default: ak-stage-autosubmit default: ak-stage-autosubmit
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -41027,6 +41063,7 @@ components:
required: required:
- attrs - attrs
- url - url
- xid
BackendsEnum: BackendsEnum:
enum: enum:
- authentik.core.auth.InbuiltBackend - authentik.core.auth.InbuiltBackend
@ -41149,10 +41186,6 @@ components:
type: string type: string
branding_favicon: branding_favicon:
type: string type: string
branding_custom_css:
type: string
branding_default_flow_background:
type: string
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid
@ -41212,11 +41245,6 @@ components:
branding_favicon: branding_favicon:
type: string type: string
minLength: 1 minLength: 1
branding_custom_css:
type: string
branding_default_flow_background:
type: string
minLength: 1
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid
@ -41282,6 +41310,8 @@ components:
component: component:
type: string type: string
default: ak-stage-captcha default: ak-stage-captcha
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -41304,6 +41334,7 @@ components:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- site_key - site_key
- xid
CaptchaChallengeResponseRequest: CaptchaChallengeResponseRequest:
type: object type: object
description: Validate captcha token description: Validate captcha token
@ -41687,6 +41718,8 @@ components:
component: component:
type: string type: string
default: ak-stage-consent default: ak-stage-consent
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -41715,6 +41748,7 @@ components:
- pending_user_avatar - pending_user_avatar
- permissions - permissions
- token - token
- xid
ConsentChallengeResponseRequest: ConsentChallengeResponseRequest:
type: object type: object
description: Consent challenge response, any valid response request is valid description: Consent challenge response, any valid response request is valid
@ -42109,8 +42143,6 @@ components:
type: string type: string
branding_favicon: branding_favicon:
type: string type: string
branding_custom_css:
type: string
ui_footer_links: ui_footer_links:
type: array type: array
items: items:
@ -42137,7 +42169,6 @@ components:
type: string type: string
readOnly: true readOnly: true
required: required:
- branding_custom_css
- branding_favicon - branding_favicon
- branding_logo - branding_logo
- branding_title - branding_title
@ -42491,6 +42522,8 @@ components:
component: component:
type: string type: string
default: ak-stage-dummy default: ak-stage-dummy
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -42501,6 +42534,7 @@ components:
type: string type: string
required: required:
- name - name
- xid
DummyChallengeResponseRequest: DummyChallengeResponseRequest:
type: object type: object
description: Dummy challenge response description: Dummy challenge response
@ -42693,12 +42727,16 @@ components:
component: component:
type: string type: string
default: ak-stage-email default: ak-stage-email
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
type: array type: array
items: items:
$ref: '#/components/schemas/ErrorDetail' $ref: '#/components/schemas/ErrorDetail'
required:
- xid
EmailChallengeResponseRequest: EmailChallengeResponseRequest:
type: object type: object
description: |- description: |-
@ -43617,6 +43655,8 @@ components:
component: component:
type: string type: string
default: ak-stage-flow-error default: ak-stage-flow-error
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -43631,6 +43671,7 @@ components:
type: string type: string
required: required:
- request_id - request_id
- xid
FlowImportResult: FlowImportResult:
type: object type: object
description: Logs of an attempted flow import description: Logs of an attempted flow import
@ -43945,6 +43986,8 @@ components:
component: component:
type: string type: string
default: xak-flow-frame default: xak-flow-frame
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -43961,6 +44004,7 @@ components:
required: required:
- loading_text - loading_text
- url - url
- xid
FrameChallengeResponseRequest: FrameChallengeResponseRequest:
type: object type: object
description: Base class for all challenge responses description: Base class for all challenge responses
@ -44763,6 +44807,8 @@ components:
component: component:
type: string type: string
default: ak-stage-identification default: ak-stage-identification
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -44807,6 +44853,7 @@ components:
- primary_action - primary_action
- show_source_labels - show_source_labels
- user_fields - user_fields
- xid
IdentificationChallengeResponseRequest: IdentificationChallengeResponseRequest:
type: object type: object
description: Identification challenge description: Identification challenge
@ -46890,18 +46937,10 @@ components:
webhook_url: webhook_url:
type: string type: string
format: uri format: uri
webhook_mapping_body: webhook_mapping:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
description: Customize the body of the request. Mapping should return data
that is JSON-serializable.
webhook_mapping_headers:
type: string
format: uuid
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
send_once: send_once:
type: boolean type: boolean
description: Only send notification once, for example when sending a webhook description: Only send notification once, for example when sending a webhook
@ -46929,18 +46968,10 @@ components:
webhook_url: webhook_url:
type: string type: string
format: uri format: uri
webhook_mapping_body: webhook_mapping:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
description: Customize the body of the request. Mapping should return data
that is JSON-serializable.
webhook_mapping_headers:
type: string
format: uuid
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
send_once: send_once:
type: boolean type: boolean
description: Only send notification once, for example when sending a webhook description: Only send notification once, for example when sending a webhook
@ -47265,12 +47296,16 @@ components:
component: component:
type: string type: string
default: ak-provider-oauth2-device-code default: ak-provider-oauth2-device-code
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
type: array type: array
items: items:
$ref: '#/components/schemas/ErrorDetail' $ref: '#/components/schemas/ErrorDetail'
required:
- xid
OAuthDeviceCodeChallengeResponseRequest: OAuthDeviceCodeChallengeResponseRequest:
type: object type: object
description: Response that includes the user-entered device code description: Response that includes the user-entered device code
@ -47293,12 +47328,16 @@ components:
component: component:
type: string type: string
default: ak-provider-oauth2-device-code-finish default: ak-provider-oauth2-device-code-finish
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
type: array type: array
items: items:
$ref: '#/components/schemas/ErrorDetail' $ref: '#/components/schemas/ErrorDetail'
required:
- xid
OAuthDeviceCodeFinishChallengeResponseRequest: OAuthDeviceCodeFinishChallengeResponseRequest:
type: object type: object
description: Response that device has been authenticated and tab can be closed description: Response that device has been authenticated and tab can be closed
@ -49443,6 +49482,8 @@ components:
component: component:
type: string type: string
default: ak-stage-password default: ak-stage-password
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -49461,6 +49502,7 @@ components:
required: required:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
PasswordChallengeResponseRequest: PasswordChallengeResponseRequest:
type: object type: object
description: Password challenge response description: Password challenge response
@ -50157,11 +50199,6 @@ components:
branding_favicon: branding_favicon:
type: string type: string
minLength: 1 minLength: 1
branding_custom_css:
type: string
branding_default_flow_background:
type: string
minLength: 1
flow_authentication: flow_authentication:
type: string type: string
format: uuid format: uuid
@ -51374,18 +51411,10 @@ components:
webhook_url: webhook_url:
type: string type: string
format: uri format: uri
webhook_mapping_body: webhook_mapping:
type: string type: string
format: uuid format: uuid
nullable: true nullable: true
description: Customize the body of the request. Mapping should return data
that is JSON-serializable.
webhook_mapping_headers:
type: string
format: uuid
nullable: true
description: Configure additional headers to be sent. Mapping should return
a dictionary of key-value pairs
send_once: send_once:
type: boolean type: boolean
description: Only send notification once, for example when sending a webhook description: Only send notification once, for example when sending a webhook
@ -52245,8 +52274,9 @@ components:
format: uuid format: uuid
acs_url: acs_url:
type: string type: string
minLength: 1
format: uri format: uri
minLength: 1
maxLength: 200
audience: audience:
type: string type: string
description: Value of the audience restriction field of the assertion. When description: Value of the audience restriction field of the assertion. When
@ -52403,14 +52433,16 @@ components:
description: Also known as Entity ID. Defaults the Metadata URL. description: Also known as Entity ID. Defaults the Metadata URL.
sso_url: sso_url:
type: string type: string
format: uri
minLength: 1 minLength: 1
description: URL that the initial Login request is sent to. description: URL that the initial Login request is sent to.
format: uri maxLength: 200
slo_url: slo_url:
type: string type: string
format: uri
nullable: true nullable: true
description: Optional URL if your IDP supports Single-Logout. description: Optional URL if your IDP supports Single-Logout.
format: uri maxLength: 200
allow_idp_initiated: allow_idp_initiated:
type: boolean type: boolean
description: Allows authentication flows initiated by the IdP. This can description: Allows authentication flows initiated by the IdP. This can
@ -53032,6 +53064,8 @@ components:
component: component:
type: string type: string
default: ak-source-plex default: ak-source-plex
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -53045,6 +53079,7 @@ components:
required: required:
- client_id - client_id
- slug - slug
- xid
PlexAuthenticationChallengeResponseRequest: PlexAuthenticationChallengeResponseRequest:
type: object type: object
description: Pseudo class for plex response description: Pseudo class for plex response
@ -53557,6 +53592,8 @@ components:
component: component:
type: string type: string
default: ak-stage-prompt default: ak-stage-prompt
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -53569,6 +53606,7 @@ components:
$ref: '#/components/schemas/StagePrompt' $ref: '#/components/schemas/StagePrompt'
required: required:
- fields - fields
- xid
PromptChallengeResponseRequest: PromptChallengeResponseRequest:
type: object type: object
description: |- description: |-
@ -54753,6 +54791,8 @@ components:
component: component:
type: string type: string
default: xak-flow-redirect default: xak-flow-redirect
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -54763,6 +54803,7 @@ components:
type: string type: string
required: required:
- to - to
- xid
RedirectChallengeResponseRequest: RedirectChallengeResponseRequest:
type: object type: object
description: Redirect challenge response description: Redirect challenge response
@ -55211,6 +55252,7 @@ components:
acs_url: acs_url:
type: string type: string
format: uri format: uri
maxLength: 200
audience: audience:
type: string type: string
description: Value of the audience restriction field of the assertion. When description: Value of the audience restriction field of the assertion. When
@ -55377,8 +55419,9 @@ components:
format: uuid format: uuid
acs_url: acs_url:
type: string type: string
minLength: 1
format: uri format: uri
minLength: 1
maxLength: 200
audience: audience:
type: string type: string
description: Value of the audience restriction field of the assertion. When description: Value of the audience restriction field of the assertion. When
@ -55551,13 +55594,15 @@ components:
description: Also known as Entity ID. Defaults the Metadata URL. description: Also known as Entity ID. Defaults the Metadata URL.
sso_url: sso_url:
type: string type: string
description: URL that the initial Login request is sent to.
format: uri format: uri
description: URL that the initial Login request is sent to.
maxLength: 200
slo_url: slo_url:
type: string type: string
format: uri
nullable: true nullable: true
description: Optional URL if your IDP supports Single-Logout. description: Optional URL if your IDP supports Single-Logout.
format: uri maxLength: 200
allow_idp_initiated: allow_idp_initiated:
type: boolean type: boolean
description: Allows authentication flows initiated by the IdP. This can description: Allows authentication flows initiated by the IdP. This can
@ -55740,14 +55785,16 @@ components:
description: Also known as Entity ID. Defaults the Metadata URL. description: Also known as Entity ID. Defaults the Metadata URL.
sso_url: sso_url:
type: string type: string
format: uri
minLength: 1 minLength: 1
description: URL that the initial Login request is sent to. description: URL that the initial Login request is sent to.
format: uri maxLength: 200
slo_url: slo_url:
type: string type: string
format: uri
nullable: true nullable: true
description: Optional URL if your IDP supports Single-Logout. description: Optional URL if your IDP supports Single-Logout.
format: uri maxLength: 200
allow_idp_initiated: allow_idp_initiated:
type: boolean type: boolean
description: Allows authentication flows initiated by the IdP. This can description: Allows authentication flows initiated by the IdP. This can
@ -56652,6 +56699,8 @@ components:
component: component:
type: string type: string
default: ak-stage-session-end default: ak-stage-session-end
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -56674,6 +56723,7 @@ components:
- brand_name - brand_name
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
SessionUser: SessionUser:
type: object type: object
description: |- description: |-
@ -56786,6 +56836,8 @@ components:
component: component:
type: string type: string
default: xak-flow-shell default: xak-flow-shell
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -56796,6 +56848,7 @@ components:
type: string type: string
required: required:
- body - body
- xid
SignatureAlgorithmEnum: SignatureAlgorithmEnum:
enum: enum:
- http://www.w3.org/2000/09/xmldsig#rsa-sha1 - http://www.w3.org/2000/09/xmldsig#rsa-sha1
@ -58070,6 +58123,8 @@ components:
component: component:
type: string type: string
default: ak-stage-user-login default: ak-stage-user-login
xid:
type: string
response_errors: response_errors:
type: object type: object
additionalProperties: additionalProperties:
@ -58083,6 +58138,7 @@ components:
required: required:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- xid
UserLoginChallengeResponseRequest: UserLoginChallengeResponseRequest:
type: object type: object
description: User login challenge description: User login challenge

Some files were not shown because too many files have changed in this diff Show More