Compare commits

..

138 Commits

Author SHA1 Message Date
authentik-automation[bot]
319008dec8 release: 2026.2.2-rc1 2026-04-01 09:15:29 +00:00
authentik-automation[bot]
8beb2fac18 core: fix provider not nullable (cherry-pick #21275 to version-2026.2) (#21282)
Cherry-pick #21275 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #21275
Original commit: 06408cba59

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-31 19:01:57 +02:00
authentik-automation[bot]
ac7b28d0b0 website/docs: ad source: add note about ldap signing (cherry-pick #21274 to version-2026.2) (#21279)
website/docs: ad source: add note about ldap signing (#21274)

Add note

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-03-31 15:42:26 +00:00
authentik-automation[bot]
073acf92c2 website/docs: document group_uuid as a property for group object (cherry-pick #20865 to version-2026.2) (#21271)
website/docs: document group_uuid as a property for group object (#20865)

The application might need a unique id for a group to uniquely identify it. It can help in various cases like detecting group renames and more.
We should document `group_uuid` field of the group object to make users aware that it can be used in custom property mappings.

Signed-off-by: Shiv Tyagi <67995771+shiv-tyagi@users.noreply.github.com>
Co-authored-by: Shiv Tyagi <67995771+shiv-tyagi@users.noreply.github.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-03-31 11:19:14 +02:00
authentik-automation[bot]
ad107c19af proviers/ldap: avoid concurrent header writes in API Client (cherry-pick #21223 to version-2026.2) (#21228)
proviers/ldap: avoid concurrent header writes in API Client (#21223)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-29 20:12:22 +01:00
authentik-automation[bot]
d285fcd8a7 sources/ldap: fix exception in ldap debug endpoint (cherry-pick #21219 to version-2026.2) (#21222)
sources/ldap: fix exception in ldap debug endpoint (#21219)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-29 17:06:02 +02:00
authentik-automation[bot]
84066cab48 sources/oauth: Allow patching without provider type (cherry-pick #21211 to version-2026.2) (#21213)
sources/oauth: Allow patching without provider type (#21211)

* sources/oauth: Allow patching without provider type

* fix, add test



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marcus Yanello <94466282+MYanello@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-03-28 15:16:58 +01:00
authentik-automation[bot]
e623d93ff5 web/applications: add wsfed to app wizard (cherry-pick #20880 to version-2026.2) (#21184)
* Cherry-pick #20880 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #20880
Original commit: 0a73322b0d

* fix conflicts

---------

Co-authored-by: Connor Peshek <connor@connorpeshek.me>
2026-03-27 15:48:19 +01:00
authentik-automation[bot]
1d0628dfbe web/flow: reset stale authenticator selection between consecutive validate stages (cherry-pick #20802 to version-2026.2) (#21014)
Cherry-pick #20802 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #20802
Original commit: a10ec34aec

Co-authored-by: Oluwatobi Mustapha <oluwatobimustapha539@gmail.com>
2026-03-27 14:24:48 +01:00
authentik-automation[bot]
996645105c endpoints/connectors: fix enabled flag not respected (cherry-pick #21144 to version-2026.2) (#21145)
endpoints/connectors: fix enabled flag not respected (#21144)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-25 19:27:54 +01:00
authentik-automation[bot]
63d7ca6ef0 providers/proxy: Add a default maxResponseBodySize to Traefik Middleware (cherry-pick #21111 to version-2026.2) (#21140)
providers/proxy: Add a default maxResponseBodySize to Traefik Middleware (#21111)

* Add default maxResponseBodySize to traefik middleware component

* Fix AttributeError when patching custom kubernetes objects

* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: William Howell <wiiam24@gmail.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-03-25 15:19:33 +01:00
authentik-automation[bot]
5b24f4ad80 core: bump cbor2 from 5.8.0 to 5.9.0 (cherry-pick #21094 to version-2026.2) (#21096)
core: bump cbor2 from 5.8.0 to 5.9.0 (#21094)

Bumps [cbor2](https://github.com/agronholm/cbor2) from 5.8.0 to 5.9.0.
- [Release notes](https://github.com/agronholm/cbor2/releases)
- [Commits](https://github.com/agronholm/cbor2/compare/5.8.0...5.9.0)

---
updated-dependencies:
- dependency-name: cbor2
  dependency-version: 5.9.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-24 01:14:33 +01:00
authentik-automation[bot]
ed2e6cfb9c website/docs: add missing dependencies for linux dev environment (cherry-pick #21020 to version-2026.2) (#21093)
website/docs: add missing dependencies for linux dev environment (#21020)

Add missing dependencies for linux dev environment

Signed-off-by: chrisjsimpson <chris15leicester@gmail.com>
Co-authored-by: chrisjsimpson <chris15leicester@gmail.com>
2026-03-23 19:47:04 +01:00
authentik-automation[bot]
a1431ea48e flows: continous login debug (cherry-pick #21044 to version-2026.2) (#21090)
flows: continous login debug 2025.12 (#21044)

* flows: continous login debug 2025.12



* no hardcoded prefix



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-23 19:28:01 +01:00
Jens L.
b30e77b363 ci: fix cherry-pick action generating empty title (#21091) (#21092)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-03-23 19:21:53 +01:00
Jens L.
2f50cdd9fe ci: rotate GH App private key (version-2026.2) (#21087) 2026-03-23 15:17:45 +01:00
Jens L.
494bdcaa09 ci: fix escaping in cherry-pick action (#21082) (#21083)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-03-23 14:51:38 +01:00
authentik-automation[bot]
e36ce1789e events: prevent exception when events contains incompatible unicode (cherry-pick #21048 to version-2026.2) (#21053)
events: prevent exception when events contains incompatible unicode (#21048)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-20 23:56:07 +01:00
authentik-automation[bot]
5a72ed83e0 events: avoid implicitly setting context from login_failed event (cherry-pick #21045 to version-2026.2) (#21050)
events: avoid implicitly setting context from login_failed event (#21045)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-20 23:41:00 +01:00
authentik-automation[bot]
f72d257e43 web/admin: handle non-string values in formatUUID to prevent Event Log crash (cherry-pick #20804 to version-2026.2) (#21052)
web/admin: handle non-string values in formatUUID to prevent Event Log crash (#20804)

fix(web): handle non-string values in formatUUID to prevent Event Log crash

When event context contains a device with a non-string pk value,
formatUUID crashes with TypeError: s.substring is not a function,
preventing the entire Event Log page from loading.

Add a type guard to coerce non-string values to their string
representation instead of crashing.

Fixes #20803

Co-authored-by: Tyson Cung <45380903+tysoncung@users.noreply.github.com>
2026-03-20 23:40:55 +01:00
authentik-automation[bot]
cbedb16cc4 enterprise/endpoints/connectors/agent: add login_hint support for interactive auth (cherry-pick #20647 to version-2026.2) (#21047)
enterprise/endpoints/connectors/agent: add login_hint support for interactive auth (#20647)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-20 18:48:20 +01:00
authentik-automation[bot]
6fc1b5ce90 sources/ldap: fix incorrect error response for invalid sync_users_password (cherry-pick #21016 to version-2026.2) (#21039)
sources/ldap: fix incorrect error response for invalid sync_users_password (#21016)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-20 15:03:57 +01:00
authentik-automation[bot]
57b0fa48c1 website: switch docs analytics to gtag (cherry-pick #20993 to version-2026.2) (#21015)
website: switch docs analytics to gtag (#20993)

Co-authored-by: Dominic R <dominic@sdko.org>
2026-03-19 16:03:22 +00:00
authentik-automation[bot]
84a344ed87 website/docs: fix swapped sidebar label (cherry-pick #21011 to version-2026.2) (#21012)
website/docs: fix swapped sidebar label (#21011)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-19 13:19:16 +01:00
authentik-automation[bot]
f864cb56ab website/docs: update kubernetes install guide for Gateway API (cherry-pick #20961 to version-2026.2) (#20997)
Co-authored-by: Dominic R <dominic@sdko.org>
2026-03-19 02:04:15 +00:00
authentik-automation[bot]
692735f9e1 website/docs: fix notification transport etc (cherry-pick #20982 to version-2026.2) (#20992)
website/docs: fix notification transport etc (#20982)

* fix mismatched caps



* transport rules??



* structure



* less redundant title



* fix label



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-19 00:20:58 +01:00
authentik-automation[bot]
e24fb300b1 web/admin: fix missing OSM referrerPolicy header (cherry-pick #20984 to version-2026.2) (#20990)
web/admin: fix missing OSM referrerPolicy header (#20984)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-19 00:05:28 +01:00
authentik-automation[bot]
f0e90d6873 core: bump pyasn1 from 0.6.2 to 0.6.3 (cherry-pick #20956 to version-2026.2) (#20957)
core: bump pyasn1 from 0.6.2 to 0.6.3 (#20956)

Bumps [pyasn1](https://github.com/pyasn1/pyasn1) from 0.6.2 to 0.6.3.
- [Release notes](https://github.com/pyasn1/pyasn1/releases)
- [Changelog](https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst)
- [Commits](https://github.com/pyasn1/pyasn1/compare/v0.6.2...v0.6.3)

---
updated-dependencies:
- dependency-name: pyasn1
  dependency-version: 0.6.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:56:03 +01:00
authentik-automation[bot]
0cf45835a0 website/docs: use full path for sysd on windows (cherry-pick #20951 to version-2026.2) (#20952)
website/docs: use full path for sysd on windows (#20951)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-17 15:48:53 +01:00
Marc 'risson' Schmitt
69d35c1d26 packages/django-dramatiq-postgres: scheduler: only dispatch tasks if they're not running yet (cherry-pick #20921 to version-2026.2) (#20950)
packages/django-dramatiq-postgres: scheduler: only dispatch tasks if they're not running yet (#20921)

* packages/django-dramatiq-postgres: scheduler: only dispatch tasks if they're not running yet



* lint



---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-03-17 14:00:03 +01:00
authentik-automation[bot]
ac803b210d outposts: only dispatch logout task if any outpost exists (cherry-pick #20920 to version-2026.2) (#20949)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-03-17 13:54:01 +01:00
authentik-automation[bot]
c9728b4607 lifecycle/migrate: add flag to skip migrations (cherry-pick #20863 to version-2026.2) (#20932)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-03-17 13:53:40 +01:00
authentik-automation[bot]
6e45584563 docs: Add note on skipping object syncing (cherry-pick #20882 to version-2026.2) (#20894)
docs: Add note on skipping object syncing (#20882)

Co-authored-by: Connor Peshek <connor@connorpeshek.me>
2026-03-17 12:17:11 +01:00
authentik-automation[bot]
59a2e84b35 web/admin: Fix SCIM page_size UI issue (cherry-pick #20890 to version-2026.2) (#20929)
web/admin: Fix SCIM 'page_size' UI issue (#20890)

Fix SCIM page size UI issue

Co-authored-by: Pavel Pavel <53437649+bitpavel-l25@users.noreply.github.com>
Co-authored-by: Pavel Sinkevych <pavelsinkevych@gmail.com>
2026-03-17 12:14:54 +01:00
authentik-automation[bot]
6025dbb9c9 core: expiring model: ignore DoesNotExist error (cherry-pick #20922 to version-2026.2) (#20925)
core: expiring model: ignore DoesNotExist error (#20922)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-03-16 16:49:52 +00:00
authentik-automation[bot]
d07bcd5025 core: bump orjson from 3.11.5 to 3.11.6 (cherry-pick #20870 to version-2026.2) (#20889)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 15:08:00 +00:00
authentik-automation[bot]
e80655d285 providers/proxy: remove redundant logout event (cherry-pick #20860 to version-2026.2) (#20866)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-03-12 17:48:01 +01:00
authentik-automation[bot]
e0d3d4d38c website/docs: update agent docs (cherry-pick #20782 to version-2026.2) (#20826)
website/docs: update agent docs (#20782)

* remove serial number requirement



* add notes for 0.40



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-10 13:31:22 +00:00
authentik-automation[bot]
62112404ee endpoints: prevent selection of incompatible connector (cherry-pick #20806 to version-2026.2) (#20807)
* Cherry-pick #20806 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #20806
Original commit: 36e1987817

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

* fix typo

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-10 01:28:40 +01:00
authentik-automation[bot]
1c9e12fcd9 web/rbac: disambiguate duplicate permission names in initial permissions (cherry-pick #20786 to version-2026.2) (#20805)
web/rbac: disambiguate duplicate permission names in initial permissions (#20786)

Co-authored-by: Oluwatobi Mustapha <oluwatobimustapha539@gmail.com>
2026-03-09 20:57:51 +01:00
authentik-automation[bot]
42c6c257ec providers/oauth2: decode percent-encoded basic auth (cherry-pick #20779 to version-2026.2) (#20781)
providers/oauth2: decode percent-encoded basic auth (#20779)

Fixes #20739

Decode percent-encoded client credentials from HTTP Basic authentication before provider lookup while preserving existing behavior for raw plus characters. Add unit and endpoint coverage for encoded client IDs and client secrets.

Co-authored-by: Oluwatobi Mustapha <oluwatobimustapha539@gmail.com>
2026-03-07 20:08:47 +01:00
authentik-automation[bot]
41bd9d7913 providers/scim: fix out-of-scope users and groups not being deleted from destination application (cherry-pick #20742 to version-2026.2) (#20780)
providers/scim: fix out-of-scope users and groups not being deleted from destination application (#20742)

* providers/scim: fix out-of-scope users and groups not being deleted from destination application

* provider/scim: add retry mechanism for transient exceptions during cleanup

* fix: fixed google provider http requests following addition of sync_cleanup method

* test: updated unit tests to validate sync behaviour for deletion of out-of-scope users and groups

Co-authored-by: Ollie Beenham <73618201+ElBeenMachine@users.noreply.github.com>
2026-03-07 15:59:56 +01:00
authentik-automation[bot]
2c84935732 website: override DocSearch button colors in light mode (cherry-pick #20770 to version-2026.2) (#20773)
Co-authored-by: Dominic R <dominic@sdko.org>
2026-03-06 21:47:33 -05:00
authentik-automation[bot]
819c13a9bc website/docs: remove potatoes card sigh (cherry-pick #20767 to version-2026.2) (#20768)
website/docs: remove potatoes card sigh (#20767)

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2026-03-06 14:33:00 -06:00
authentik-automation[bot]
0d8f366af8 packages/django-channels-postgres: provide sync API for group_send (cherry-pick #20740 to version-2026.2) (#20741)
packages/django-channels-postgres: provide sync API for group_send (#20740)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-03-05 19:02:49 +01:00
authentik-automation[bot]
093e60c753 lifecycle: make gunicorn --max-requests configurable (cherry-pick #20736 to version-2026.2) (#20744)
Co-authored-by: Severin Schoepke <severin@users.noreply.github.com>
2026-03-05 16:49:01 +01:00
authentik-automation[bot]
af646f32d2 core: bump django from 5.2.11 to 5.2.12 (cherry-pick #20719 to version-2026.2) (#20738)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 12:59:36 +00:00
authentik-automation[bot]
de4afc7322 web/flows: continuous login (cherry-pick #19862 to version-2026.2) (#20712)
* Cherry-pick #19862 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #19862
Original commit: 6245809eae

* fix conflict

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-03-04 11:58:18 +00:00
authentik-automation[bot]
bc1983106f web/admin: bad width on policy test results (cherry-pick #20668 to version-2026.2) (#20697)
web/admin: bad width on policy test results (#20668)

web/admin/bugfix: bad width on policy test results

## What

1.  Set a 100% width on the container for polcy test log messages.

## Why

A classic bug, made more complex by modern sensibilities. The group to be rendered is in a slot, but its parent doesn’t have a set width by default, and so it’s “projected” into a zero-width container. As a result, the `1fr` (“100/100 width”) doesn’t matter here; we need to go old-skool and force its parent to take up the full width of *its* container with a hard `width` setting, which the gives us some room to be 100/100 in.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2026-03-04 10:37:51 +00:00
Simonyi Gergő
8c2c1474f1 ci: fix reason change in versions repo bump (cherry-pick #20696 to version-2026.2) (#20709)
ci: fix `reason` change in versions repo bump (#20696)

fix `reason` change in versions repo bump
2026-03-04 10:32:54 +00:00
authentik-automation[bot]
0dccbd4193 release: 2026.2.1 2026-03-03 19:49:59 +00:00
authentik-automation[bot]
6a70894e01 website/docs: add release notes for 2026.2.1 (cherry-pick #20659 to version-2026.2) (#20695)
website/docs: add release notes for `2026.2.1` (#20659)

* add release notes for `2026.2.1`

* Update release notes for version 2026.2



---------

Signed-off-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Connor Peshek <connor@connorpeshek.me>
2026-03-03 20:10:10 +01:00
authentik-automation[bot]
2f5eb9b2e4 providers/proxy: move search path to query instead of runtime parameter (cherry-pick #20662 to version-2026.2) (#20693)
providers/proxy: move search path to query instead of runtime parameter (#20662)

Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-03-03 19:08:49 +01:00
authentik-automation[bot]
12aedb3a9e web: fix identification stage styling in compatibility mode (cherry-pick #20684 to version-2026.2) (#20694)
web: fix identification stage styling in compatibility mode (#20684)

fix identification stage styling in compatibility mode

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-03-03 19:00:54 +01:00
authentik-automation[bot]
303dc93514 website/docs: add 2025 pentest (cherry-pick #20626 to version-2026.2) (#20691)
website/docs: add 2025 pentest (#20626)

* Start

* Add links

* Links

* sidebar

* Update website/docs/security/audits-and-certs/2025-09-includesec.md




* Update website/docs/security/audits-and-certs/2025-09-includesec.md




* Update website/docs/security/audits-and-certs/2025-09-includesec.md




* Update 2025-09-includesec.md



* Apply suggestions from code review





* Update website/docs/security/audits-and-certs/2025-09-includesec.md




* Add link

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2026-03-03 18:48:13 +01:00
authentik-automation[bot]
fbb217db57 outpost/proxyv2: prevent panic in handleSignOut (cherry-pick #20097 to version-2026.2) (#20689)
outpost/proxyv2: prevent panic in handleSignOut (#20097)

outpost/proxyv2: use safe claims extraction in handleSignOut to prevent panic

Signed-off-by: Xabier Napal <xabier.napal@dvzr.io>
Co-authored-by: Xabier Napal <xabier.napal@dvzr.io>
2026-03-03 18:23:17 +01:00
authentik-automation[bot]
4de253653f packages/django-channels-postgres: eagerly delete messages (cherry-pick #20687 to version-2026.2) (#20688)
packages/django-channels-postgres: eagerly delete messages (#20687)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-03-03 16:50:37 +01:00
authentik-automation[bot]
4154c06831 core: fix get_provider returning base Provider instead of subclass (cherry-pick #19064 to version-2026.2) (#20670)
core: fix get_provider returning base Provider instead of subclass (#19064)

Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-03-03 09:00:58 +01:00
authentik-automation[bot]
4750ed5e2a website/docs: kerberos: add note about caching (cherry-pick #20663 to version-2026.2) (#20664)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-03-02 18:32:18 +01:00
authentik-automation[bot]
361017127d website/docs: entra id provider: add custom email domain info (cherry-pick #20444 to version-2026.2) (#20660)
website/docs: entra id provider: add custom email domain info (#20444)

* WIP

* WIP

* Apply suggestions from code review




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-03-02 13:46:12 +00:00
authentik-automation[bot]
0ca5a54307 release: 2026.2.1-rc1 2026-03-02 13:12:40 +00:00
authentik-automation[bot]
ef1aad5dbb enterprise/wsfed: Fix metadata export and signing logic (cherry-pick #20643 to version-2026.2) (#20649)
enterprise/wsfed: Fix metadata export and signing logic (#20643)

Co-authored-by: Connor Peshek <connor@connorpeshek.me>
2026-03-02 08:13:45 +01:00
authentik-automation[bot]
29d880920e packages/django-dramatiq-postgres: fix worker startup on macos (cherry-pick #20637 to version-2026.2) (#20641)
packages/django-dramatiq-postgres: fix worker startup on macos (#20637)

fix worker startup on macos

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-03-01 01:31:21 +00:00
authentik-automation[bot]
fc6f8374e6 sources/ldap: add connection logging & downgrade message (cherry-pick #20519 to version-2026.2) (#20636)
sources/ldap: add connection logging & downgrade message (#20519)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-28 13:44:33 +00:00
authentik-automation[bot]
a8668bbac4 crypto: fix kid legacy signal (cherry-pick #20627 to version-2026.2) (#20628)
crypto: fix kid legacy signal (#20627)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-27 16:21:12 +01:00
authentik-automation[bot]
d686932166 web/flows: fix source icons being always inverted (cherry-pick #20419 to version-2026.2) (#20607)
web/flows: fix source icons being always inverted (#20419)

* web/flows: fix inverted source icons



* fix actually



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-26 21:14:29 +01:00
authentik-automation[bot]
feceb220b1 packages/django-dramatiq-postgres: use fork (cherry-pick #20606 to version-2026.2) (#20608)
packages/django-dramatiq-postgres: use fork (#20606)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-26 21:07:00 +01:00
authentik-automation[bot]
937df6e07f internal: make http timeouts configurable (cherry-pick #20472 to version-2026.2) (#20567)
internal: make http timeouts configurable (#20472)

* internal: make http timeouts configurable



* Changed formatting to match the rest of the doc

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-25 15:57:03 +00:00
Simonyi Gergő
48e6b968a6 ci: add reason change to versions repo bump (cherry-pick #20562 to version-2026.2) (#20569)
ci: add `reason` change to versions repo bump (#20562)

add `reason` change to versions repo bump
2026-02-25 15:06:39 +01:00
authentik-automation[bot]
cd89c45e75 docs: fix typos and wording in docs and integrations (cherry-pick #20550 to version-2026.2) (#20563)
* Cherry-pick #20550 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #20550
Original commit: 4c8916adde

* Veeam conflict fix

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-25 10:38:44 +00:00
authentik-automation[bot]
e53995e2c1 website/docs: revamp enterprise section (cherry-pick #20379 to version-2026.2) (#20546)
website/docs: revamp enterprise section (#20379)

* Begin

* WIP

* WIP

* WIP

* Fix link

* Fix spellig and links

* Enterprise vs enterprise plus

* Changes based on Tana's comment

* Update website/docs/enterprise/enterprise-features.md




* Update website/docs/enterprise/enterprise-features.md




* Update website/docs/enterprise/enterprise-features.md




* Update website/docs/enterprise/enterprise-features.md




* Apply suggestions

* Apply suggestion from Eric

* Update doc title after discussion with Tana

* Fix links

* Update website/docs/enterprise/manage-enterprise.mdx




* Update website/docs/enterprise/manage-enterprise.mdx




* Apply suggestions

* US dollars

* Apply Fletcher's suggestions

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-25 09:48:21 +00:00
authentik-automation[bot]
33d5f11f0e website/docs: remove bad logs redirect (cherry-pick #20522 to version-2026.2) (#20548)
website/docs: remove bad logs redirect (#20522)

* Remove bad redirect

* Remove space

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-25 01:32:45 +00:00
authentik-automation[bot]
565e16eca7 website/docs: fix upgrade link in 2026.2 release notes (cherry-pick #20539 to version-2026.2) (#20542)
website/docs: fix upgrade link in `2026.2` release notes (#20539)

fix upgrade link in `2026.2` release notes

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-25 01:05:26 +01:00
authentik-automation[bot]
9a0164b722 website/docs: update supported versions (cherry-pick #20534 to version-2026.2) (#20535)
website/docs: update supported versions (#20534)

update supported versions

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-24 23:25:39 +01:00
authentik-automation[bot]
8af491630b release: 2026.2.0 2026-02-24 20:12:56 +00:00
authentik-automation[bot]
8e25e7a213 website/docs: autogenerate release notes (cherry-pick #20527 to version-2026.2) (#20531)
* Cherry-pick #20527 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #20527
Original commit: 884e662277

* fix conflicts

---------

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <gergo@goauthentik.io>
2026-02-24 20:28:58 +01:00
authentik-automation[bot]
4d183657da providers/oauth2: add jti claim (cherry-pick #20484 to version-2026.2) (#20528)
providers/oauth2: add jti claim (#20484)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-24 19:08:59 +01:00
authentik-automation[bot]
be89b6052d providers/oauth2: deactivate locale after testing (cherry-pick #20518 to version-2026.2) (#20526)
providers/oauth2: deactivate locale after testing (#20518)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-24 16:48:34 +01:00
authentik-automation[bot]
ad5d2bb611 policies: fix PolicyEngineMode ALL with static binding optimization (cherry-pick #20430 to version-2026.2) (#20524)
policies: fix PolicyEngineMode ALL with static binding optimization (#20430)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-24 16:48:19 +01:00
authentik-automation[bot]
8d30fb3d25 website/docs: fix GitHub social-login wording and capitalization (cherry-pick #20489 to version-2026.2) (#20505)
* Cherry-pick #20489 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #20489
Original commit: 9da1014271

* Update index.mdx

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-24 14:11:23 +01:00
authentik-automation[bot]
cea3fbfa9b website/docs: fix linux setup docs (cherry-pick #20508 to version-2026.2) (#20517)
website/docs: fix linux setup docs (#20508)

* docs: add auth config steps

* tweak



* Changed wording

* Fix broken link

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-24 14:11:13 +01:00
authentik-automation[bot]
151d889ff4 endpoints: fix infinite recursion in stage with unsupported connector (cherry-pick #20485 to version-2026.2) (#20514)
endpoints: fix infinite recursion in stage with unsupported connector (#20485)

* stages: fix infinite recursion

* respect mode



* add tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-02-24 13:47:04 +01:00
authentik-automation[bot]
58ca3ecbd5 web: fix Edit Policy button on Flow view page (cherry-pick #20511 to version-2026.2) (#20515)
web: fix Edit Policy button on Flow view page (#20511)

fix Edit Policy button on Flow view page

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-24 13:24:50 +01:00
authentik-automation[bot]
1a6c7082a3 web/admin/bugfix: Edit Stage not working. Invoking IdentificationStageForm not working (cherry-pick #20429 to version-2026.2) (#20512)
* Cherry-pick #20429 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #20429
Original commit: ab981dec86

* revert miscellaneous changes

These don't need to be in 2026.2

---------

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <gergo@goauthentik.io>
2026-02-24 12:51:31 +01:00
authentik-automation[bot]
1dc60276f9 enterprise: add ES384 to enterprise license algorithms (cherry-pick #20507 to version-2026.2) (#20510)
enterprise: add `ES384` to enterprise license algorithms (#20507)

add `ES384` to enterprise license algorithms

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-24 11:59:27 +01:00
authentik-automation[bot]
de045c6d7b release: 2026.2.0-rc5 2026-02-24 09:44:14 +00:00
authentik-automation[bot]
850728e9bb providers/oauth2: device code flow client id via auth header (cherry-pick #20457 to version-2026.2) (#20503)
providers/oauth2: device code flow client id via auth header (#20457)

* Use `extract_client_auth` which can get client id from either HTTP
Authorization header or POST body

* Update documentation to reflect allow sending client id via header

* Add tests for using HTTP Basic Auth to pass in client id

Co-authored-by: Michael Beigelmacher <brooklynbagel@gmail.com>
2026-02-24 09:53:06 +01:00
authentik-automation[bot]
84a605a4ba website/docs: add info about make install and recovery key (cherry-pick #20447 to version-2026.2) (#20486)
website/docs: add info about make install and recovery key (#20447)

* add info about make install and recovery key

* fix formatting on troubleshooting tip

* Apply suggestion from @dominic-r



* tweak to bump

* tweak

* tweaked words abouot make install per jens

* build

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2026-02-24 09:15:44 +01:00
authentik-automation[bot]
1780bb0cf0 web: Center footer links. (cherry-pick #20345 to version-2026.2) (#20425)
web: Center footer links. (#20345)

* web: Center footer links.

* Refine track resizing behavior.

* Fix odd scenario.

* Tidy padding.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-24 03:10:22 +01:00
authentik-automation[bot]
cd75fe235d providers/proxy: preserve URL-encoded path characters in redirect (cherry-pick #20476 to version-2026.2) (#20482)
providers/proxy: preserve URL-encoded path characters in redirect (#20476)

Use r.URL.EscapedPath() instead of r.URL.Path when building the
redirect URL in redirectToStart(). The decoded Path field converts
%2F to /, which url.JoinPath then collapses via path.Clean, stripping
encoded slashes from the URL. EscapedPath() preserves the original
encoding, fixing 301 redirects that break apps like RabbitMQ which
use %2F in their API paths.

Co-authored-by: Brolywood <44068132+Brolywood@users.noreply.github.com>
2026-02-23 18:10:04 +01:00
authentik-automation[bot]
e6e62e9de1 policies: measure policy process from manager (cherry-pick #20477 to version-2026.2) (#20481)
policies: measure policy process from manager (#20477)

* policies: measure policy process from manager



* fix constructor



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-23 18:09:10 +01:00
authentik-automation[bot]
ac7a4f8a22 enterprise/lifecycle: use datetime instead of date to track review cycles (cherry-pick #20283 to version-2026.2) (#20473)
enterprise/lifecycle: use datetime instead of date to track review cycles (#20283)

* enterprise/lifecycle: use datetime instead of date to track review cycles (fix for #20265)

* Update authentik/enterprise/lifecycle/api/iterations.py




* enterprise/lifecycle: replace extend_schema_field with type annotations

---------

Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>
Co-authored-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens L. <jens@beryju.org>
2026-02-23 17:04:30 +01:00
authentik-automation[bot]
0290ed3342 enterprise: monkey patch pyjwt to accept mismatching key (cherry-pick #20402 to version-2026.2) (#20474)
enterprise: monkey patch pyjwt to accept mismatching key (#20402)

* monkey patch pyjwt to accept mismatching key

* restore `_validate_curve` after monkeypatch

* add explanatory comment

* next year is 2027, dummy

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-23 16:06:09 +01:00
authentik-automation[bot]
e367525794 stages/user_login: log correct user when session binding is broken (cherry-pick #20094 to version-2026.2) (#20453) 2026-02-21 18:48:42 +00:00
authentik-automation[bot]
93c319baee enterprise/providers/microsoft_entra: only check upn when set (cherry-pick #20441 to version-2026.2) (#20442)
enterprise/providers/microsoft_entra: only check upn when set (#20441)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-21 18:36:44 +01:00
Marc 'risson' Schmitt
1d02ee7d74 ci: pull latest changes before tagging new version (cherry-pick #20413 to version-2026.2) (#20414) 2026-02-19 14:32:15 +01:00
authentik-automation[bot]
93439b5742 enterprise/providers/microsoft_entra: fix dangling comma (cherry-pick #20391 to version-2026.2) (#20395)
enterprise/providers/microsoft_entra: fix dangling comma (#20391)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-19 13:35:14 +01:00
authentik-automation[bot]
6682a6664e web/admin: bug: stage update forms not rendering, several modal form buttons missing (cherry-pick #20373 to version-2026.2) (#20394)
* web/admin: bug: stage update forms not rendering, several modal form buttons missing (#20373)

## What

Names being passed to the browser were being incorrectly rendered. This commit updates the code in `StrictUnsafe` so that after the correct-use assertion is passed, the elementProperties are checked to see if the attribute has been named differently from the typed attribute field, and if so, retrieves the attribute name and passes it, rather than the field name, to the browser.

## Why

Since we have a lot of components with similar interfaces, it makes sense to try and check that they’re being used correctly and that the types associated with them are correct. Plus Lit, unlike React, doesn’t have a self-erasing syntax: every Lit element *is* an element, whereas JSX is an esoteric function call syntax that happens to look like XML. JavaScript templates aren’t as pretty as JSX, but they get the job done just as readily.

But in this case, cleverness bit us: we want to use the component’s JavaScript field names and types to validate that we’re using it correctly and passing the right types, but in the end we’re constructing a tag that will trigger the browser to construct the component and use it– and the field names don’t always correspond to the attribute name. Lit has a syntax for mapping the one to the other and stores it in the `elementProperties` field.

This code checks that, after we’ve determined the correct prefix for an property field that has been passed into the component, that we’ve also checked and extracted the correct *attribute name* for that property field. Most of the time it will be the same as the property field, but it muts always be checked.

* web: Fix element property names with custom attributes.

---------

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-19 02:38:15 +01:00
authentik-automation[bot]
0b5bac74e9 website/docs: correct reference to overriden S3 variable (cherry-pick #20156 to version-2026.2) (#20378)
website/docs: correct reference to overriden S3 variable (#20156)

docs: correct reference to overriden S3 variable

Fixes: c30d1a478d ("files: rework (#17535)")

Signed-off-by: Georg Pfuetzenreuter <georg.pfuetzenreuter@suse.com>
Co-authored-by: Georg <georg@lysergic.dev>
2026-02-18 11:47:13 +00:00
authentik-automation[bot]
062823f1b2 core: add cause to ak_groups deprecation event and logs (cherry-pick #20361 to version-2026.2) (#20368)
core: add cause to `ak_groups` deprecation event and logs (#20361)

add cause to `ak_groups` deprecation event and logs

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-17 22:32:50 +01:00
authentik-automation[bot]
a17fe58971 website/docs: Fix broken link to flow executor (cherry-pick #20364 to version-2026.2) (#20370)
website/docs: Fix broken link to flow executor (#20364)

Fix broken link

I obviously can't test this, but it looks like the redirects should work.

Signed-off-by: nsw42 <nsw42@users.noreply.github.com>
Co-authored-by: nsw42 <nsw42@users.noreply.github.com>
2026-02-17 19:48:15 +00:00
authentik-automation[bot]
422ea893b1 enterprise/providers/ws_federation: fix incorrect metadata download URL (cherry-pick #20173 to version-2026.2) (#20365)
enterprise/providers/ws_federation: fix incorrect metadata download URL (#20173)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-17 19:07:48 +01:00
authentik-automation[bot]
15c9f93851 web: Flow Executor layout fixes (cherry-pick #20134 to version-2026.2) (#20331)
web: Flow Executor layout fixes (#20134)

* Fix footer alignment.

* Fix loading position in compatibility mode.

* Apply min height only when placeholder content is present.

* Fix alignment in compatibility mode.

* Add compatibility mode host selectors.

* Fix nullish challenge height. Clarify selector behavior.

* Add type defintion

* Fix padding.

* Fix misapplication of pf-* class to container.

* Fix huge base64 encoded attribute.

* Clean up layering issues, order of styles.

* Disable dev override.

* Document parts.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-17 18:03:07 +00:00
authentik-automation[bot]
e2202d498b rbac: fix object permission request (cherry-pick #20304 to version-2026.2) (#20366)
rbac: fix object permission request (#20304)

fix object permission request

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-17 18:34:07 +01:00
authentik-automation[bot]
9ea9a86ad3 release: 2026.2.0-rc4 2026-02-17 13:14:27 +00:00
Simonyi Gergő
4bac1edd61 web: revert package-lock.json by tag workflow (#20349)
revert changes to `package-lock.json` by tag workflow

Specifically by a01c0575db
2026-02-17 13:31:06 +01:00
Marc 'risson' Schmitt
24726be3c9 ci: fix setup altering package-lock (cherry-pick #20348 to version-2026.2) (#20356)
ci: fix setup altering package-lock (#20348)

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-17 13:14:14 +01:00
authentik-automation[bot]
411f06756f website/docs, integrations: fix language (cherry-pick #20338 to version-2026.2) (#20347)
* Cherry-pick #20338 to version-2026.2 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #20338
Original commit: e056dbdadd

* Fix conflict

* Fix conflicts

---------

Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: dewi-tik <dewi@goauthentik.io>
2026-02-17 12:11:46 +00:00
authentik-automation[bot]
4bdcab48c3 website/docs: rac: update rac provider docs (cherry-pick #20225 to version-2026.2) (#20337)
website/docs: rac: update rac provider docs (#20225)

* WIP

* Sentence

* Delete image

* WIP

* adjust wording

---------

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-16 21:49:07 -05:00
authentik-automation[bot]
00dbd377a7 website/docs: add okta source doc (cherry-pick #20296 to version-2026.2) (#20335)
website/docs: add okta source doc (#20296)

* Begin

* Add steps

* Apply suggestions

* Update website/docs/users-sources/sources/social-logins/okta/index.md




* Apply suggestion from @dominic-r



---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-17 01:07:43 +00:00
authentik-automation[bot]
a01c0575db release: 2026.2.0-rc3 2026-02-16 11:22:42 +00:00
authentik-automation[bot]
6e51d044bb root: do not rely on npm cli for version bump (cherry-pick #20276 to version-2026.2) (#20321)
root: do not rely on npm cli for version bump (#20276)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-16 11:41:36 +01:00
authentik-automation[bot]
6d1b168dc4 website/docs: add affine to release notes (cherry-pick #20299 to version-2026.2) (#20308)
website/docs: add affine to release notes (#20299)

* add affine to release notes

* use built-in github linking

* add missing credits to Arcane integration

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-15 18:00:41 +00:00
authentik-automation[bot]
43675c2b22 web: fix italic formatting in lifecycle rule help text (cherry-pick #20263 to version-2026.2) (#20267)
web: fix italic formatting in lifecycle rule help text (#20263)

* web: fix italic formatting in lifecycle rule help text

* r

Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-14 21:22:43 +00:00
authentik-automation[bot]
8645273eaf stage/identification: recovery: make wording more generic (cherry-pick #20209 to version-2026.2) (#20293)
stage/identification: recovery: make wording more generic (#20209)

Make wording more generic

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-14 05:47:47 +00:00
authentik-automation[bot]
eb6f4712fe website/docs: Custom CSS (cherry-pick #19991 to version-2026.2) (#20287)
website/docs: Custom CSS (#19991)

* website/docs: Custom CSS

* Revise.

* Fix paths.

* Update links.

* Update header capitalization



---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-13 21:56:29 +00:00
authentik-automation[bot]
7b9505242e web: add pretty names for lifecycle review events in event logs (cherry-pick #20264 to version-2026.2) (#20268)
web: add pretty names for lifecycle review events in event logs (#20264)

Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-13 18:30:37 +01:00
authentik-automation[bot]
3dda20ebc7 enterprise/lifecycle: fix multiple reviews showing up in "Reviews" when the user is a member of multiple reviewer groups (cherry-pick #20266 to version-2026.2) (#20278)
Co-authored-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>
fix multiple reviews showing up in "Reviews" when the user is a member of multiple reviewer groups (#20266)
2026-02-13 13:43:19 +01:00
Marc 'risson' Schmitt
dfd2bc5c3c ci: fix binary outpost build on release (cherry-pick #20248 to version-2026.2) (#20279)
fix binary outpost build on release (#20248)
2026-02-13 13:38:31 +01:00
authentik-automation[bot]
06a270913c website/docs: draft of new WS-Fed provider docs (cherry-pick #20091 to version-2026.2) (#20262)
website/docs: draft of new WS-Fed provider docs  (#20091)

* first draft

* add table of parms

* tweak

* add section about certs

* a little more content

* more info on wa

* new procedurla file and edit sidebar

* tweaks

* dewi and jens edits

* tweak to remove bullet

* add docs link to the Rel Notes

* dewi edits thx

* ooops missed that last edit

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2026-02-13 09:51:42 +00:00
Marc 'risson' Schmitt
430507fc72 web: re-update package-lock.json to include missing tree-sitter references
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 17:45:50 +01:00
authentik-automation[bot]
847af7f9ea website/docs: 2025.8.6 release notes (cherry-pick #20243 to version-2026.2) (#20257)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 16:57:14 +01:00
authentik-automation[bot]
8f1cb636e8 website/docs: 2025.12.4 release notes (cherry-pick #20226 to version-2026.2) (#20253)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 16:56:31 +01:00
authentik-automation[bot]
e61c876002 website/docs: 2025.10.4 release notes (cherry-pick #20242 to version-2026.2) (#20251)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 16:55:02 +01:00
authentik-automation[bot]
33c0d3df0a release: 2026.2.0-rc2 2026-02-12 15:48:24 +00:00
Marc 'risson' Schmitt
3a03e1ebfd web: updated package-lock.json to include missing tree-sitter references (cherry-pick #20244 to version-2026.2) (#20246)
Co-authored-by: Ken Sternberg <ken@goauthentik.io>
2026-02-12 16:00:39 +01:00
Marc 'risson' Schmitt
1e41b77761 website/docs: fix lint
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 15:37:57 +01:00
authentik-automation[bot]
6c1662f99f security: CVE-2026-25227 (#20236)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 15:27:42 +01:00
authentik-automation[bot]
bb5bc5c8da security: CVE-2026-25748 (#20237)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 15:27:30 +01:00
authentik-automation[bot]
30670c9070 security: CVE-2026-25922 (#20238)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 15:27:04 +01:00
Marc 'risson' Schmitt
fdbf9ffedc ci: fix release testing (cherry-pick #20207 to version-2026.2) (#20224)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
fix release testing (#20207)
2026-02-12 13:44:55 +01:00
authentik-automation[bot]
2ec433d724 website/docs: ssf: update SSF documentation (cherry-pick #20195 to version-2026.2) (#20211)
website/docs: ssf: update SSF documentation (#20195)

* Update SSF documentation

* Fix tags

* Update website/docs/add-secure-apps/providers/ssf/create-ssf-provider.md




* Update website/docs/add-secure-apps/providers/ssf/index.md




* Apply suggestions from code review




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2026-02-11 20:14:02 +00:00
authentik-automation[bot]
55297b9e6a website/docs: add email verification scope doc (cherry-pick #20141 to version-2026.2) (#20206)
website/docs: add email verification scope doc (#20141)

* WIP

* Add link to 2025.10 release notes

* Apply suggestions from code review




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-11 16:49:00 +00:00
authentik-automation[bot]
f9dda6582c website/docs: rac: fixes the property mapping formatting (cherry-pick #20200 to version-2026.2) (#20203)
website/docs: rac: fixes the property mapping formatting (#20200)

Fixes the property mapping formatting

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-11 15:44:50 +00:00
authentik-automation[bot]
3394c17bfd release: 2026.2.0-rc1 2026-02-11 14:37:37 +00:00
authentik-automation[bot]
a37d101b10 api: fix test_build_schema (cherry-pick #20196 to version-2026.2) (#20199)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix `test_build_schema` (#20196)
2026-02-11 15:00:00 +01:00
authentik-automation[bot]
4774b4db87 core: bump cryptography from 46.0.4 to 46.0.5 (cherry-pick #20171 to version-2026.2) (#20193) 2026-02-11 11:45:35 +01:00
authentik-automation[bot]
fdb52c9394 core: fix test_docker.sh (cherry-pick #20179 to version-2026.2) (#20192)
core: fix `test_docker.sh` (#20179)

Broken by 646a0d3692

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-11 10:46:47 +01:00
587 changed files with 12780 additions and 20785 deletions

View File

@@ -115,20 +115,13 @@ runs:
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
PR_NUMBER: ${{ steps.should_run.outputs.pr_number }}
REASON: ${{ steps.should_run.outputs.reason }}
run: |
set -e -o pipefail
PR_NUMBER="${{ steps.should_run.outputs.pr_number }}"
# Get PR details
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.user.login')
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT
# Determine which labels to process
if [ "${{ steps.should_run.outputs.reason }}" = "label_added_to_merged_pr" ]; then
if [ "${REASON}" = "label_added_to_merged_pr" ]; then
# Only process the specific label that was just added
if [ "${{ github.event_name }}" = "issues" ]; then
LABEL_NAME="${{ github.event.label.name }}"
@@ -152,13 +145,13 @@ runs:
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
PR_NUMBER: '${{ steps.should_run.outputs.pr_number }}'
COMMIT_SHA: '${{ steps.should_run.outputs.merge_commit_sha }}'
PR_TITLE: ${{ github.event.pull_request.title }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
LABELS: '${{ steps.pr_details.outputs.labels }}'
run: |
set -e -o pipefail
PR_NUMBER='${{ steps.should_run.outputs.pr_number }}'
COMMIT_SHA='${{ steps.should_run.outputs.merge_commit_sha }}'
PR_TITLE='${{ steps.pr_details.outputs.pr_title }}'
PR_AUTHOR='${{ steps.pr_details.outputs.pr_author }}'
LABELS='${{ steps.pr_details.outputs.labels }}'
echo "Processing PR #$PR_NUMBER (reason: ${{ steps.should_run.outputs.reason }})"
echo "Found backport labels: $LABELS"

View File

@@ -22,7 +22,7 @@ runs:
sudo rm -rf /usr/local/lib/android
- name: Install uv
if: ${{ contains(inputs.dependencies, 'python') }}
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v5
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v5
with:
enable-cache: true
- name: Setup python
@@ -34,29 +34,17 @@ runs:
if: ${{ contains(inputs.dependencies, 'python') }}
shell: bash
run: uv sync --all-extras --dev --frozen
- name: Setup node (web)
- name: Setup node
if: ${{ contains(inputs.dependencies, 'node') }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
registry-url: "https://registry.npmjs.org"
- name: Setup node (root)
if: ${{ contains(inputs.dependencies, 'node') }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
with:
node-version-file: package.json
cache: "npm"
cache-dependency-path: package-lock.json
registry-url: "https://registry.npmjs.org"
- name: Install Node deps
if: ${{ contains(inputs.dependencies, 'node') }}
shell: bash
run: npm ci
registry-url: 'https://registry.npmjs.org'
- name: Setup go
if: ${{ contains(inputs.dependencies, 'go') }}
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v5
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v5
with:
go-version-file: "go.mod"
- name: Setup docker cache

View File

@@ -11,13 +11,6 @@ services:
ports:
- 5432:5432
restart: always
pgdog:
image: ghcr.io/pgdogdev/pgdog:latest
volumes:
- ./pgdog.toml:/pgdog/pgdog.toml:ro
ports:
- 127.0.0.1:6432:6432
restart: always
s3:
container_name: s3
image: docker.io/zenko/cloudserver

View File

@@ -1,18 +0,0 @@
[general]
host = "[::]"
port = 6432
passthrough_auth = "enabled_plain"
prepared_statements = "extended_anonymous"
pub_sub_channel_size = 8192
[admin]
password = "admin"
[[databases]]
host = "postgresql"
name = "postgres"
[[databases]]
host = "postgresql"
name = "authentik"
[[databases]]
host = "postgresql"
name = "test_authentik"

1
.github/codespell-dictionary.txt vendored Normal file
View File

@@ -0,0 +1 @@
authentic->authentik

32
.github/codespell-words.txt vendored Normal file
View File

@@ -0,0 +1,32 @@
akadmin
asgi
assertIn
authentik
authn
crate
docstrings
entra
goauthentik
gunicorn
hass
jwe
jwks
keypair
keypairs
kubernetes
oidc
ontext
openid
passwordless
plex
saml
scim
singed
slo
sso
totp
traefik
# https://github.com/codespell-project/codespell/issues/1224
upToDate
warmup
webauthn

View File

@@ -38,21 +38,6 @@ updates:
#endregion
#region Rust
- package-ecosystem: rust-toolchain
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "core:"
labels:
- dependencies
#endregion
#region Web
- package-ecosystem: npm
@@ -249,7 +234,7 @@ updates:
- package-ecosystem: docker
directories:
- /lifecycle/container
- /
- /website
schedule:
interval: daily

View File

@@ -43,8 +43,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -56,23 +56,23 @@ jobs:
release: ${{ inputs.release }}
- name: Login to Docker Hub
if: ${{ inputs.registry_dockerhub }}
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ inputs.registry_ghcr }}
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version-file: "go.mod"
- name: Generate API Clients
@@ -80,7 +80,7 @@ jobs:
make gen-client-ts
make gen-client-go
- name: Build Docker Image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
id: push
with:
context: .
@@ -95,7 +95,7 @@ jobs:
platforms: linux/${{ inputs.image_arch }}
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames }}:buildcache-${{ inputs.image_arch }}
cache-to: ${{ steps.ev.outputs.cacheTo }}
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@@ -79,25 +79,25 @@ jobs:
image-name: ${{ inputs.image_name }}
- name: Login to Docker Hub
if: ${{ inputs.registry_dockerhub }}
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ inputs.registry_ghcr }}
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: int128/docker-manifest-create-action@8aac06098a12365ccdf99372dcfb453ccce8a0b0 # v2
- uses: int128/docker-manifest-create-action@1a059c021f1d5e9f2bd39de745d5dd3a0ef6df90 # v2
id: build
with:
tags: ${{ matrix.tag }}
sources: |
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-amd64.outputs.image-digest }}
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-arm64.outputs.image-digest }}
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}

View File

@@ -21,11 +21,11 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: web/package.json
registry-url: "https://registry.npmjs.org"

View File

@@ -33,7 +33,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: website/package.json
cache: "npm"
@@ -55,7 +55,7 @@ jobs:
env:
NODE_ENV: production
run: npm run build -w api
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v4
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v4
with:
name: api-docs
path: website/api/build
@@ -67,11 +67,11 @@ jobs:
- build
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v5
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v5
with:
name: api-docs
path: website/api/build
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: website/package.json
cache: "npm"

View File

@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: lifecycle/aws/package.json
cache: "npm"

View File

@@ -36,7 +36,7 @@ jobs:
NODE_ENV: production
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: website/package.json
cache: "npm"
@@ -53,7 +53,7 @@ jobs:
NODE_ENV: production
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: website/package.json
cache: "npm"
@@ -77,9 +77,9 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -89,14 +89,14 @@ jobs:
image-name: ghcr.io/goauthentik/dev-docs
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
@@ -105,7 +105,7 @@ jobs:
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache,mode=max' || '' }}
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@@ -6,10 +6,6 @@ on:
schedule:
# Every night at 3am
- cron: "0 3 * * *"
pull_request:
paths:
# Needs to refer to itself
- .github/workflows/ci-main-daily.yml
jobs:
test-container:
@@ -19,14 +15,14 @@ jobs:
matrix:
version:
- docs
- version-2025-12
- version-2025-10
- version-2025-4
- version-2025-2
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- run: |
current="$(pwd)"
dir="/tmp/authentik/${{ matrix.version }}"
mkdir -p "${dir}/lifecycle/container"
cd "${dir}"
wget "https://${{ matrix.version }}.goauthentik.io/docker-compose.yml" -O "${dir}/lifecycle/container/compose.yml"
"${current}/scripts/test_docker.sh"
mkdir -p $dir
cd $dir
wget https://${{ matrix.version }}.goauthentik.io/compose.yml
${current}/scripts/test_docker.sh

View File

@@ -31,7 +31,7 @@ jobs:
job:
- bandit
- black
- spellcheck
- codespell
- pending-migrations
- ruff
- mypy
@@ -42,16 +42,6 @@ jobs:
uses: ./.github/actions/setup
- name: run job
run: uv run make ci-${{ matrix.job }}
test-gen-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: generate schema
run: make migrate gen-build
- name: ensure schema is up-to-date
run: git diff --exit-code -- schema.yml blueprints/schema.json
test-migrations:
runs-on: ubuntu-latest
steps:
@@ -128,7 +118,7 @@ jobs:
CI_RUN_ID: ${{ matrix.run_id }}
CI_TOTAL_RUNS: "5"
run: |
make ci-test glob="authentik"
uv run make ci-test
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
@@ -157,7 +147,7 @@ jobs:
CI_RUN_ID: ${{ matrix.run_id }}
CI_TOTAL_RUNS: "5"
run: |
make ci-test glob="authentik"
uv run make ci-test
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
@@ -170,10 +160,11 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0
- name: run integration
run: |
make ci-test glob="tests/integration"
uv run coverage run manage.py test tests/integration
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
@@ -228,7 +219,8 @@ jobs:
npm run build:sfe
- name: run e2e
run: |
make ci-test glob="${{ matrix.job.glob }}"
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
@@ -270,13 +262,14 @@ jobs:
npm run build:sfe
- name: run conformance
run: |
make ci-test glob="${{ matrix.job.glob }}"
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
flags: conformance
- if: ${{ !cancelled() }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: conformance-certification-${{ matrix.job.name }}
path: tests/openid_conformance/exports/
@@ -284,7 +277,6 @@ jobs:
if: always()
needs:
- lint
- test-gen-build
- test-migrations
- test-migrations-from-stable
- test-unittest

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version-file: "go.mod"
- name: Prepare and generate API
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version-file: "go.mod"
- name: Setup authentik env
@@ -90,9 +90,9 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -102,7 +102,7 @@ jobs:
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -111,7 +111,7 @@ jobs:
run: make gen-client-go
- name: Build Docker Image
id: push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: lifecycle/container/${{ matrix.type }}.Dockerfile
@@ -122,7 +122,7 @@ jobs:
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:
@@ -148,10 +148,10 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version-file: "go.mod"
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: web/package.json
cache: "npm"

View File

@@ -32,7 +32,7 @@ jobs:
project: web
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: web/package.json
cache: "npm"
@@ -77,7 +77,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: web/package.json
cache: "npm"

View File

@@ -32,7 +32,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -14,7 +14,7 @@ jobs:
if: ${{ env.GH_APP_ID != '' }}
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
env:
GH_APP_ID: ${{ secrets.GH_APP_ID }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- name: Delete 'dev' containers older than a week
uses: snok/container-retention-policy@3b0972b2276b171b212f8c4efbca59ebba26eceb # v3.0.1
with:

View File

@@ -29,19 +29,18 @@ jobs:
- packages/eslint-config
- packages/prettier-config
- packages/docusaurus-config
- packages/logger-js
- packages/esbuild-plugin-live-reload
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
fetch-depth: 2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: ${{ matrix.package }}/package.json
registry-url: "https://registry.npmjs.org"
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
uses: tj-actions/changed-files@8cba46e29c11878d930bca7870bb54394d3e8b21 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
with:
files: |
${{ matrix.package }}/package.json

View File

@@ -32,7 +32,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
@@ -60,7 +60,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:

View File

@@ -33,9 +33,9 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -44,21 +44,21 @@ jobs:
with:
image-name: ghcr.io/goauthentik/docs
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
id: attest
if: true
with:
@@ -84,18 +84,18 @@ jobs:
- rac
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version-file: "go.mod"
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -108,18 +108,18 @@ jobs:
make gen-client-ts
make gen-client-go
- name: Docker Login Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
id: push
with:
push: true
@@ -129,7 +129,7 @@ jobs:
file: lifecycle/container/${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v3
- uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
@@ -152,10 +152,10 @@ jobs:
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version-file: "go.mod"
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v5
with:
node-version-file: web/package.json
cache: "npm"
@@ -180,7 +180,7 @@ jobs:
export CGO_ENABLED=0
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
- name: Upload binaries to release
uses: svenstaro/upload-release-action@b98a3b12e86552593f3e4e577ca8a62aa2f3f22b # v2
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}

View File

@@ -70,7 +70,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- id: get-user-id
name: Get GitHub app user ID
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
@@ -118,7 +118,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
repositories: helm
- id: get-user-id
name: Get GitHub app user ID
@@ -160,7 +160,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
repositories: version
- id: get-user-id
name: Get GitHub app user ID

View File

@@ -18,8 +18,8 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
with:
repo-token: ${{ steps.generate_token.outputs.token }}
days-before-stale: 60

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
if: ${{ github.event_name != 'pull_request' }}
with:

3
.gitignore vendored
View File

@@ -15,9 +15,6 @@ media
node_modules
.cspellcache
cspell-report.*
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/

12
.vscode/settings.json vendored
View File

@@ -14,10 +14,6 @@
"[xml]": {
"editor.minimap.markSectionHeaderRegex": "<!--\\s*#\\bregion\\s*(?<separator>-?)\\s*(?<label>.*)\\s*-->"
},
"files.associations": {
// The built-in "ignore" language gives us enough syntax highlighting to make these files readable.
"**/dictionaries/*.txt": "ignore"
},
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"yaml.customTags": [
@@ -53,9 +49,13 @@
"ignoreCase": false
}
],
"go.testFlags": ["-count=1"],
"go.testFlags": [
"-count=1"
],
"go.testEnvVars": {
"WORKSPACE_DIR": "${workspaceFolder}"
},
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"]
"github-actions.workflows.pinned.workflows": [
".github/workflows/ci-main.yml"
]
}

View File

@@ -34,7 +34,6 @@ packages/docusaurus-config @goauthentik/frontend
packages/esbuild-plugin-live-reload @goauthentik/frontend
packages/eslint-config @goauthentik/frontend
packages/prettier-config @goauthentik/frontend
packages/logger-js @goauthentik/frontend
packages/tsconfig @goauthentik/frontend
# Web
web/ @goauthentik/frontend

View File

@@ -77,12 +77,12 @@ test: ## Run the server tests and produce a coverage report (locally)
$(UV) run coverage html
$(UV) run coverage report
lint-fix: lint-spellcheck ## Lint and automatically fix errors in the python source code. Reports spelling errors.
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
$(UV) run black $(PY_SOURCES)
$(UV) run ruff check --fix $(PY_SOURCES)
lint-spellcheck: ## Reports spelling errors.
npm run lint:spellcheck
lint-codespell: ## Reports spelling errors.
$(UV) run codespell -w
lint: ci-bandit ci-mypy ## Lint the python and golang sources
golangci-lint run -v
@@ -126,22 +126,17 @@ install: node-install docs-install core-install ## Install all requires depende
dev-drop-db:
$(eval pg_user := $(shell $(UV) run python -m authentik.lib.config postgresql.user 2>/dev/null))
$(eval pg_pass := $(shell $(UV) run python -m authentik.lib.config postgresql.password 2>/dev/null))
$(eval pg_host := $(shell $(UV) run python -m authentik.lib.config postgresql.host 2>/dev/null))
$(eval pg_name := $(shell $(UV) run python -m authentik.lib.config postgresql.name 2>/dev/null))
PGPASSWORD="${pg_pass}" psql -U ${pg_user} -h ${pg_host} postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${pg_name}'"
PGPASSWORD="${pg_pass}" dropdb -U ${pg_user} -h ${pg_host} ${pg_name} || true
dropdb -U ${pg_user} -h ${pg_host} ${pg_name} || true
# Also remove the test-db if it exists
PGPASSWORD="${pg_pass}" psql -U ${pg_user} -h ${pg_host} postgres -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'test_${pg_name}'"
PGPASSWORD="${pg_pass}" dropdb -U ${pg_user} -h ${pg_host} test_${pg_name} || true
dropdb -U ${pg_user} -h ${pg_host} test_${pg_name} || true
dev-create-db:
$(eval pg_user := $(shell $(UV) run python -m authentik.lib.config postgresql.user 2>/dev/null))
$(eval pg_pass := $(shell $(UV) run python -m authentik.lib.config postgresql.password 2>/dev/null))
$(eval pg_host := $(shell $(UV) run python -m authentik.lib.config postgresql.host 2>/dev/null))
$(eval pg_name := $(shell $(UV) run python -m authentik.lib.config postgresql.name 2>/dev/null))
PGPASSWORD="${pg_pass}" createdb -U ${pg_user} -h ${pg_host} ${pg_name} || true
PGPASSWORD="${pg_pass}" createdb -U ${pg_user} -h ${pg_host} test_${pg_name} || true
createdb -U ${pg_user} -h ${pg_host} ${pg_name}
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
@@ -173,22 +168,12 @@ gen-build: ## Extract the schema from the database
gen-compose:
$(UV) run scripts/generate_compose.py
gen-changelog: ## (Release) generate the changelog based from the commits since the last version
# These are best-effort guesses based on commit messages
$(eval last_version := $(shell git tag --list 'version/*' --sort 'version:refname' | grep -vE 'rc\d+$$' | tail -1))
$(eval current_commit := $(shell git rev-parse HEAD))
git log --pretty=format:"- %s" $(shell git merge-base ${last_version} ${current_commit})...${current_commit} > merged_to_current
git log --pretty=format:"- %s" $(shell git merge-base ${last_version} ${current_commit})...${last_version} > merged_to_last
grep -Eo 'cherry-pick (#\d+)' merged_to_last | cut -d ' ' -f 2 | sed 's/.*/(&)$$/' > cherry_picked_to_last
grep -vf cherry_picked_to_last merged_to_current | sort > changelog.md
rm merged_to_current
rm merged_to_last
rm cherry_picked_to_last
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
npx prettier --write changelog.md
gen-diff: ## (Release) generate the changelog diff between the current schema and the last version
$(eval last_version := $(shell git tag --list 'version/*' --sort 'version:refname' | grep -vE 'rc\d+$$' | tail -1))
git show ${last_version}:schema.yml > schema-old.yml
gen-diff: ## (Release) generate the changelog diff between the current schema and the last tag
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > schema-old.yml
docker compose -f scripts/api/compose.yml run --rm --user "${UID}:${GID}" diff \
--markdown \
/local/diff.md \
@@ -291,7 +276,7 @@ docs: docs-lint-fix docs-build ## Automatically fix formatting issues in the Au
docs-install:
npm ci --prefix website
docs-lint-fix: lint-spellcheck
docs-lint-fix: lint-codespell
npm run --prefix website prettier
docs-build:
@@ -348,8 +333,8 @@ ci-black: ci--meta-debug
ci-ruff: ci--meta-debug
$(UV) run ruff check $(PY_SOURCES)
ci-spellcheck: ci--meta-debug
npm run lint:spellcheck
ci-codespell: ci--meta-debug
$(UV) run codespell -s
ci-bandit: ci--meta-debug
$(UV) run bandit -c pyproject.toml -r $(PY_SOURCES) -iii
@@ -357,7 +342,7 @@ ci-bandit: ci--meta-debug
ci-pending-migrations: ci--meta-debug
$(UV) run ak makemigrations --check
ci-test: ci--meta-debug dev-create-db
$(UV) run coverage run manage.py test --keepdb $(glob)
ci-test: ci--meta-debug
$(UV) run coverage run manage.py test --keepdb authentik
$(UV) run coverage report
$(UV) run coverage xml

View File

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2026.5.0-rc1"
VERSION = "2026.2.2-rc1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -94,7 +94,7 @@ class Backend:
Args:
file_path: Relative file path
request: Optional Django HttpRequest for fully qualified URL building
request: Optional Django HttpRequest for fully qualifed URL building
use_cache: whether to retrieve the URL from cache
Returns:

View File

@@ -100,25 +100,13 @@ class S3Backend(ManageableBackend):
f"storage.{self.usage.value}.{self.name}.addressing_style",
CONFIG.get(f"storage.{self.name}.addressing_style", "auto"),
)
signature_version = CONFIG.get(
f"storage.{self.usage.value}.{self.name}.signature_version",
CONFIG.get(f"storage.{self.name}.signature_version", "s3v4"),
)
# Keep signature_version pass-through and let boto3/botocore handle it.
# In boto3's S3 configuration docs, `s3v4` (default) and deprecated `s3`
# are the documented values:
# https://github.com/boto/boto3/blob/791a3e8f36d83664a47b4281a0586b3546cef3ec/docs/source/guide/configuration.rst?plain=1#L398-L407
# Botocore also supports additional signer names, so we intentionally do
# not enforce a restricted allowlist here.
return self.session.client(
"s3",
endpoint_url=endpoint_url,
use_ssl=use_ssl,
region_name=region_name,
config=Config(
signature_version=signature_version, s3={"addressing_style": addressing_style}
),
config=Config(signature_version="s3v4", s3={"addressing_style": addressing_style}),
)
@property

View File

@@ -1,6 +1,5 @@
from unittest import skipUnless
from botocore.exceptions import UnsupportedSignatureVersionError
from django.test import TestCase
from authentik.admin.files.tests.utils import FileTestS3BackendMixin, s3_test_server_available
@@ -82,27 +81,6 @@ class TestS3Backend(FileTestS3BackendMixin, TestCase):
self.assertIn("X-Amz-Signature=", url)
self.assertIn("test.png", url)
def test_client_signature_version_default_v4(self):
"""Test S3 client defaults to v4 signature when not configured."""
self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3v4")
@CONFIG.patch("storage.s3.signature_version", "s3")
def test_client_signature_version_global_override(self):
"""Test S3 client respects globally configured signature version."""
self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3")
@CONFIG.patch("storage.s3.signature_version", "s3v4")
@CONFIG.patch("storage.media.s3.signature_version", "s3")
def test_client_signature_version_media_override(self):
"""Test usage-specific signature version takes precedence over global."""
self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3")
@CONFIG.patch("storage.media.s3.signature_version", "not-a-real-signature")
def test_client_signature_version_unsupported(self):
"""Test unsupported signature version raises botocore error."""
with self.assertRaises(UnsupportedSignatureVersionError):
self.media_s3_backend.file_url("test.png", use_cache=False)
@CONFIG.patch("storage.s3.bucket_name", "test-bucket")
def test_file_exists_true(self):
"""Test file_exists returns True for existing file"""

View File

@@ -71,7 +71,7 @@ def postprocess_schema_responses(
def postprocess_schema_query_params(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Optimize pagination parameters, instead of redeclaring parameters for each endpoint
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
declare them globally and refer to them"""
LOGGER.debug("Deduplicating query parameters")
for path in result["paths"].values():

View File

@@ -272,7 +272,7 @@ class Importer:
and entry.state != BlueprintEntryDesiredState.MUST_CREATED
):
self.logger.debug(
"Initialize serializer with instance",
"Initialise serializer with instance",
model=model,
instance=model_instance,
pk=model_instance.pk,
@@ -290,7 +290,7 @@ class Importer:
)
else:
self.logger.debug(
"Initialized new serializer instance",
"Initialised new serializer instance",
model=model,
**cleanse_dict(updated_identifiers),
)

View File

@@ -47,7 +47,12 @@ class ApplicationSerializer(ModelSerializer):
"""Application Serializer"""
launch_url = SerializerMethodField()
provider_obj = ProviderSerializer(source="get_provider", required=False, read_only=True)
provider_obj = ProviderSerializer(
source="get_provider",
required=False,
read_only=True,
allow_null=True,
)
backchannel_providers_obj = ProviderSerializer(
source="backchannel_providers", required=False, read_only=True, many=True
)
@@ -154,14 +159,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
return queryset
def _get_allowed_applications(
self, paginated_apps: Iterator[Application], user: User | None = None
self, pagined_apps: Iterator[Application], user: User | None = None
) -> list[Application]:
applications = []
request = self.request._request
if user:
request = copy(request)
request.user = user
for application in paginated_apps:
for application in pagined_apps:
engine = PolicyEngine(application, request.user, request)
engine.build()
if engine.passing:

View File

@@ -1115,7 +1115,11 @@ class ExpiringModel(models.Model):
default the object is deleted. This is less efficient compared
to bulk deleting objects, but classes like Token() need to change
values instead of being deleted."""
return self.delete(*args, **kwargs)
try:
return self.delete(*args, **kwargs)
except self.DoesNotExist:
# Object has already been deleted, so this should be fine
return None
@classmethod
def filter_not_expired(cls, **kwargs) -> QuerySet[Self]:

View File

@@ -24,7 +24,8 @@ from authentik.root.ws.consumer import build_device_group
# Arguments: user: User, password: str
password_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
# Arguments: credentials: dict[str, any], request: HttpRequest,
# stage: Stage, context: dict[str, any]
login_failed = Signal()
LOGGER = get_logger()

View File

@@ -21,10 +21,6 @@
{% block head_before %}
{% endblock %}
{% block interface_stylesheet %}
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/interface-%v.css' %}" />
{% endblock %}
{% include "base/theme.html" %}
<style data-id="brand-css">{{ brand_css }}</style>

View File

@@ -1,6 +1,8 @@
{% load static %}
{% load authentik_core %}
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/interface-%v.css' %}" />
{% if ui_theme == "dark" %}
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#18191a">

View File

@@ -63,7 +63,7 @@ class TestPropertyMappingAPI(APITestCase):
PropertyMappingSerializer().validate_expression("/")
def test_types(self):
"""Test PropertyMapping's types endpoint"""
"""Test PropertyMappigns's types endpoint"""
response = self.client.get(
reverse("authentik_api:propertymapping-types"),
)

View File

@@ -18,7 +18,10 @@ from authentik.rbac.decorators import permission_required
class EnrollmentTokenSerializer(ModelSerializer):
device_group_obj = DeviceAccessGroupSerializer(
source="device_group", read_only=True, required=False
source="device_group",
read_only=True,
required=False,
allow_null=True,
)
def __init__(self, *args, **kwargs) -> None:

View File

@@ -37,6 +37,8 @@ class AgentEnrollmentAuth(BaseAuthentication):
token = EnrollmentToken.filter_not_expired(key=key).first()
if not token:
raise PermissionDenied()
if not token.connector.enabled:
raise PermissionDenied()
CTX_AUTH_VIA.set("endpoint_token_enrollment")
return (DeviceUser(), token)
@@ -51,6 +53,8 @@ class AgentAuth(BaseAuthentication):
device_token = DeviceToken.filter_not_expired(key=key).first()
if not device_token:
raise PermissionDenied()
if not device_token.device.connector.enabled:
raise PermissionDenied()
if device_token.device.device.is_expired:
raise PermissionDenied()
CTX_AUTH_VIA.set("endpoint_token")

View File

@@ -58,6 +58,16 @@ class TestAgentAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
def test_enroll_disabled(self):
self.connector.enabled = False
self.connector.save()
response = self.client.post(
reverse("authentik_api:agentconnector-enroll"),
data={"device_serial": generate_id(), "device_name": "bar"},
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 403)
def test_enroll_token_delete(self):
response = self.client.post(
reverse("authentik_api:agentconnector-enroll"),
@@ -104,6 +114,16 @@ class TestAgentAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
@reconcile_app("authentik_crypto")
def test_config_disabled(self):
self.connector.enabled = False
self.connector.save()
response = self.client.get(
reverse("authentik_api:agentconnector-agent-config"),
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
)
self.assertEqual(response.status_code, 403)
def test_check_in(self):
response = self.client.post(
reverse("authentik_api:agentconnector-check-in"),
@@ -112,6 +132,16 @@ class TestAgentAPI(APITestCase):
)
self.assertEqual(response.status_code, 204)
def test_check_in_disabled(self):
self.connector.enabled = False
self.connector.save()
response = self.client.post(
reverse("authentik_api:agentconnector-check-in"),
data=CHECK_IN_DATA_VALID,
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
)
self.assertEqual(response.status_code, 403)
def test_check_in_token_expired(self):
self.device_token.expiring = True
self.device_token.expires = now() - timedelta(hours=1)

View File

@@ -63,7 +63,7 @@ class OperatingSystemSerializer(Serializer):
"Operating System version, must always be the version number but may contain build name"
),
)
arch = CharField(required=False)
arch = CharField(required=True)
class NetworkInterfaceSerializer(Serializer):

View File

@@ -1,4 +1,4 @@
from authentik.endpoints.models import EndpointStage, StageMode
from authentik.endpoints.models import Connector, EndpointStage, StageMode
from authentik.flows.stage import StageView
PLAN_CONTEXT_ENDPOINT_CONNECTOR = "endpoint_connector"
@@ -8,7 +8,10 @@ class EndpointStageView(StageView):
def _get_inner(self) -> StageView | None:
stage: EndpointStage = self.executor.current_stage
inner_stage: type[StageView] | None = stage.connector.stage
connector: Connector = stage.connector
if not connector.enabled:
return None
inner_stage: type[StageView] | None = connector.stage
if not inner_stage:
return None
return inner_stage(self.executor, request=self.request)

View File

@@ -17,7 +17,7 @@ def endpoints_sync(connector_pk: Any):
connector: Connector | None = (
Connector.objects.filter(pk=connector_pk).select_subclasses().first()
)
if not connector:
if not connector or not connector.enabled:
return
controller = connector.controller
ctrl = controller(connector)

View File

@@ -3,6 +3,7 @@ from hmac import compare_digest
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseBadRequest, QueryDict
from authentik.common.oauth.constants import QS_LOGIN_HINT
from authentik.endpoints.connectors.agent.auth import (
agent_auth_issue_token,
check_device_policies,
@@ -14,7 +15,7 @@ from authentik.enterprise.policy import EnterprisePolicyAccessView
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_DEVICE, FlowPlanner
from authentik.flows.stage import StageView
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
from authentik.providers.oauth2.utils import HttpResponseRedirectScheme
QS_AGENT_IA_TOKEN = "ak-auth-ia-token" # nosec
@@ -64,14 +65,14 @@ class AgentInteractiveAuth(EnterprisePolicyAccessView):
planner = FlowPlanner(self.connector.authorization_flow)
planner.allow_empty_flows = True
context = {
PLAN_CONTEXT_DEVICE: self.device,
PLAN_CONTEXT_DEVICE_AUTH_TOKEN: self.auth_token,
}
if QS_LOGIN_HINT in request.GET:
context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = request.GET[QS_LOGIN_HINT]
try:
plan = planner.plan(
self.request,
{
PLAN_CONTEXT_DEVICE: self.device,
PLAN_CONTEXT_DEVICE_AUTH_TOKEN: self.auth_token,
},
)
plan = planner.plan(self.request, context)
except FlowNonApplicableException:
return self.handle_no_permission_authenticated()
plan.append_stage(in_memory_stage(AgentAuthFulfillmentStage))
@@ -84,7 +85,6 @@ class AgentInteractiveAuth(EnterprisePolicyAccessView):
class AgentAuthFulfillmentStage(StageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
device: Device = self.executor.plan.context.pop(PLAN_CONTEXT_DEVICE)
auth_token: DeviceAuthenticationToken = self.executor.plan.context.pop(

View File

@@ -1,42 +0,0 @@
"""GoogleChromeConnector API Views"""
from django.urls import reverse
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.endpoints.api.connectors import ConnectorSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
class GoogleChromeConnectorSerializer(EnterpriseRequiredMixin, ConnectorSerializer):
"""GoogleChromeConnector Serializer"""
chrome_url = SerializerMethodField()
def get_chrome_url(self, _: GoogleChromeConnector) -> str | None:
"""Full URL to be used in Google Workspace configuration"""
request: Request = self.context.get("request", None)
if not request:
return True
return request.build_absolute_uri(
reverse("authentik_endpoints_connectors_google_chrome:chrome")
)
class Meta:
model = GoogleChromeConnector
fields = ConnectorSerializer.Meta.fields + ["credentials", "chrome_url"]
class GoogleChromeConnectorViewSet(UsedByMixin, ModelViewSet):
"""GoogleChromeConnector Viewset"""
queryset = GoogleChromeConnector.objects.all()
serializer_class = GoogleChromeConnectorSerializer
filterset_fields = [
"name",
]
search_fields = ["name"]
ordering = ["name"]

View File

@@ -1,13 +0,0 @@
"""authentik Endpoint app config"""
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEndpointsConnectorGoogleChromeAppConfig(EnterpriseConfig):
"""authentik endpoint config"""
name = "authentik.enterprise.endpoints.connectors.google_chrome"
label = "authentik_endpoints_connectors_google_chrome"
verbose_name = "authentik Enterprise.Endpoints.Connectors.Google Chrome"
default = True
mountpoint = "endpoints/google/"

View File

@@ -1,116 +0,0 @@
from json import dumps, loads
from django.http import HttpRequest, HttpResponseRedirect
from django.urls import reverse
from googleapiclient.discovery import build
from authentik.endpoints.controller import BaseController, Capabilities
from authentik.endpoints.facts import DeviceFacts, OSFamily
from authentik.endpoints.models import Device, DeviceConnection
from authentik.enterprise.endpoints.connectors.google_chrome.google_schema import (
DeviceSignals,
VerifyChallengeResponseResult,
)
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
from authentik.policies.utils import delete_none_values
# Header we get from chrome that initiates verified access
HEADER_DEVICE_TRUST = "X-Device-Trust"
# Header we send to the client with the challenge
HEADER_ACCESS_CHALLENGE = "X-Verified-Access-Challenge"
# Header we get back from the client that we verify with google
HEADER_ACCESS_CHALLENGE_RESPONSE = "X-Verified-Access-Challenge-Response"
# Header value for x-device-trust that initiates the flow
DEVICE_TRUST_VERIFIED_ACCESS = "VerifiedAccess"
class GoogleChromeController(BaseController[GoogleChromeConnector]):
def __init__(self, connector):
super().__init__(connector)
self.google_client = build(
"verifiedaccess",
"v2",
cache_discovery=False,
**connector.google_credentials(),
)
@staticmethod
def vendor_identifier() -> str:
return "chrome.google.com"
def capabilities(self) -> list[Capabilities]:
return [Capabilities.STAGE_ENDPOINTS, Capabilities.ENROLL_AUTOMATIC_USER]
def generate_challenge(self, request: HttpRequest) -> HttpResponseRedirect:
challenge = self.google_client.challenge().generate().execute()
res = HttpResponseRedirect(
request.build_absolute_uri(
reverse("authentik_endpoints_connectors_google_chrome:chrome")
)
)
res[HEADER_ACCESS_CHALLENGE] = dumps(challenge)
return res
def validate_challenge(self, response: str) -> Device:
response = VerifyChallengeResponseResult(
self.google_client.challenge().verify(body=loads(response)).execute()
)
# Remove deprecated string representation of deviceSignals
response.pop("deviceSignal", None)
signals = DeviceSignals(response["deviceSignals"])
device, _ = Device.objects.update_or_create(
identifier=signals["serialNumber"],
defaults={
"name": signals["hostname"],
},
)
conn, _ = DeviceConnection.objects.update_or_create(
device=device,
connector=self.connector,
)
conn.create_snapshot(self.convert_data(signals))
return device
def convert_os_family(self, family) -> OSFamily:
return {
"CHROME_OS": OSFamily.linux,
"CHROMIUM_OS": OSFamily.linux,
"WINDOWS": OSFamily.windows,
"MAC_OS_X": OSFamily.macOS,
"LINUX": OSFamily.linux,
}.get(family, OSFamily.other)
def convert_data(self, raw_signals: DeviceSignals):
data = {
"os": delete_none_values(
{
"family": self.convert_os_family(raw_signals["operatingSystem"]),
"version": raw_signals["osVersion"],
}
),
"disks": [],
"network": delete_none_values(
{
"hostname": raw_signals["hostname"],
"interfaces": [],
"firewall_enabled": raw_signals["osFirewall"] == "OS_FIREWALL_ENABLED",
},
),
"hardware": delete_none_values(
{
"model": raw_signals["deviceModel"],
"manufacturer": raw_signals["deviceManufacturer"],
"serial": raw_signals["serialNumber"],
}
),
"vendor": {
self.vendor_identifier(): {
"agent_version": raw_signals["browserVersion"],
"raw": raw_signals,
},
},
}
facts = DeviceFacts(data=data)
facts.is_valid(raise_exception=True)
return facts.validated_data

View File

@@ -1,129 +0,0 @@
from typing import Literal, TypedDict
# Based on https://github.com/henribru/google-api-python-client-stubs/blob/master/googleapiclient-stubs/_apis/verifiedaccess/v2/schemas.pyi
class Antivirus(TypedDict, total=False):
state: Literal["STATE_UNSPECIFIED", "MISSING", "DISABLED", "ENABLED"]
class Challenge(TypedDict, total=False):
challenge: str
class CrowdStrikeAgent(TypedDict, total=False):
agentId: str
customerId: str
class DeviceSignals(TypedDict, total=False):
allowScreenLock: bool
antivirus: Antivirus
browserVersion: str
builtInDnsClientEnabled: bool
chromeRemoteDesktopAppBlocked: bool
crowdStrikeAgent: CrowdStrikeAgent
deviceAffiliationIds: list[str]
deviceEnrollmentDomain: str
deviceManufacturer: str
deviceModel: str
diskEncryption: Literal[
"DISK_ENCRYPTION_UNSPECIFIED",
"DISK_ENCRYPTION_UNKNOWN",
"DISK_ENCRYPTION_DISABLED",
"DISK_ENCRYPTION_ENCRYPTED",
]
displayName: str
hostname: str
imei: list[str]
macAddresses: list[str]
meid: list[str]
operatingSystem: Literal[
"OPERATING_SYSTEM_UNSPECIFIED",
"CHROME_OS",
"CHROMIUM_OS",
"WINDOWS",
"MAC_OS_X",
"LINUX",
]
osFirewall: Literal[
"OS_FIREWALL_UNSPECIFIED",
"OS_FIREWALL_UNKNOWN",
"OS_FIREWALL_DISABLED",
"OS_FIREWALL_ENABLED",
]
osVersion: str
passwordProtectionWarningTrigger: Literal[
"PASSWORD_PROTECTION_WARNING_TRIGGER_UNSPECIFIED",
"POLICY_UNSET",
"PASSWORD_PROTECTION_OFF",
"PASSWORD_REUSE",
"PHISHING_REUSE",
]
profileAffiliationIds: list[str]
profileEnrollmentDomain: str
realtimeUrlCheckMode: Literal[
"REALTIME_URL_CHECK_MODE_UNSPECIFIED",
"REALTIME_URL_CHECK_MODE_DISABLED",
"REALTIME_URL_CHECK_MODE_ENABLED_MAIN_FRAME",
]
safeBrowsingProtectionLevel: Literal[
"SAFE_BROWSING_PROTECTION_LEVEL_UNSPECIFIED", "INACTIVE", "STANDARD", "ENHANCED"
]
screenLockSecured: Literal[
"SCREEN_LOCK_SECURED_UNSPECIFIED",
"SCREEN_LOCK_SECURED_UNKNOWN",
"SCREEN_LOCK_SECURED_DISABLED",
"SCREEN_LOCK_SECURED_ENABLED",
]
secureBootMode: Literal[
"SECURE_BOOT_MODE_UNSPECIFIED",
"SECURE_BOOT_MODE_UNKNOWN",
"SECURE_BOOT_MODE_DISABLED",
"SECURE_BOOT_MODE_ENABLED",
]
serialNumber: str
siteIsolationEnabled: bool
systemDnsServers: list[str]
thirdPartyBlockingEnabled: bool
trigger: Literal["TRIGGER_UNSPECIFIED", "TRIGGER_BROWSER_NAVIGATION", "TRIGGER_LOGIN_SCREEN"]
windowsMachineDomain: str
windowsUserDomain: str
class Empty(TypedDict, total=False): ...
class VerifyChallengeResponseRequest(TypedDict, total=False):
challengeResponse: str
expectedIdentity: str
class VerifyChallengeResponseResult(TypedDict, total=False):
attestedDeviceId: str
customerId: str
deviceEnrollmentId: str
devicePermanentId: str
deviceSignal: str
deviceSignals: DeviceSignals
keyTrustLevel: Literal[
"KEY_TRUST_LEVEL_UNSPECIFIED",
"CHROME_OS_VERIFIED_MODE",
"CHROME_OS_DEVELOPER_MODE",
"CHROME_BROWSER_HW_KEY",
"CHROME_BROWSER_OS_KEY",
"CHROME_BROWSER_NO_KEY",
]
profileCustomerId: str
profileKeyTrustLevel: Literal[
"KEY_TRUST_LEVEL_UNSPECIFIED",
"CHROME_OS_VERIFIED_MODE",
"CHROME_OS_DEVELOPER_MODE",
"CHROME_BROWSER_HW_KEY",
"CHROME_BROWSER_OS_KEY",
"CHROME_BROWSER_NO_KEY",
]
profilePermanentId: str
signedPublicKeyAndChallenge: str
virtualDeviceId: str
virtualProfileId: str

View File

@@ -1,38 +0,0 @@
# Generated by Django 5.2.11 on 2026-03-01 18:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_endpoints", "0004_deviceaccessgroup_attributes"),
]
operations = [
migrations.CreateModel(
name="GoogleChromeConnector",
fields=[
(
"connector_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_endpoints.connector",
),
),
("credentials", models.JSONField()),
],
options={
"verbose_name": "Google Device Trust Connector",
"verbose_name_plural": "Google Device Trust Connectors",
},
bases=("authentik_endpoints.connector",),
),
]

View File

@@ -1,69 +0,0 @@
"""Endpoint stage"""
from typing import TYPE_CHECKING
from django.db import models
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from google.oauth2.service_account import Credentials
from rest_framework.serializers import BaseSerializer
from authentik.endpoints.models import Connector
from authentik.flows.stage import StageView
if TYPE_CHECKING:
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
GoogleChromeController,
)
class GoogleChromeConnector(Connector):
"""Verify Google Chrome Device Trust connection for the user's browser."""
credentials = models.JSONField()
def google_credentials(self):
return {
"credentials": Credentials.from_service_account_info(
self.credentials, scopes=["https://www.googleapis.com/auth/verifiedaccess"]
),
}
@property
def icon_url(self):
return static("authentik/sources/google.svg")
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.endpoints.connectors.google_chrome.api import (
GoogleChromeConnectorSerializer,
)
return GoogleChromeConnectorSerializer
@property
def stage(self) -> type[StageView] | None:
from authentik.enterprise.endpoints.connectors.google_chrome.stage import (
GoogleChromeStageView,
)
return GoogleChromeStageView
@property
def controller(self) -> type[GoogleChromeController]:
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
GoogleChromeController,
)
return GoogleChromeController
@property
def component(self) -> str:
return "ak-endpoints-connector-gdtc-form"
def __str__(self) -> str:
return f"Google Device Trust Connector {self.name}"
class Meta:
verbose_name = _("Google Device Trust Connector")
verbose_name_plural = _("Google Device Trust Connectors")

View File

@@ -1,32 +0,0 @@
from django.http import HttpResponse
from django.urls import reverse
from django.utils.translation import gettext as _
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
FrameChallenge,
FrameChallengeResponse,
)
from authentik.flows.stage import ChallengeStageView
class GoogleChromeStageView(ChallengeStageView):
"""Endpoint stage"""
response_class = FrameChallengeResponse
def get_challenge(self, *args, **kwargs) -> Challenge:
return FrameChallenge(
data={
"component": "xak-flow-frame",
"url": self.request.build_absolute_uri(
reverse("authentik_endpoints_connectors_google_chrome:chrome")
),
"loading_overlay": True,
"loading_text": _("Verifying your browser..."),
}
)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return self.executor.stage_ok()

View File

@@ -1,36 +0,0 @@
{
"devicePermanentId": "6f30327d-e436-4f7a-9f89-c37a7b6bf408",
"keyTrustLevel": "CHROME_BROWSER_HW_KEY",
"virtualDeviceId": "Z5DDF07GK6",
"customerId": "qewrqer",
"deviceSignals": {
"deviceManufacturer": "Apple Inc.",
"deviceModel": "MacBookPro18,1",
"operatingSystem": "MAC_OS_X",
"osVersion": "26.2.0",
"displayName": "jens-mac-vm",
"diskEncryption": "DISK_ENCRYPTION_ENCRYPTED",
"serialNumber": "Z5DDF07GK6",
"osFirewall": "OS_FIREWALL_DISABLED",
"systemDnsServers": [
"10.120.20.250:53"
],
"hostname": "jens-mac-vm.lab.beryju.org",
"macAddresses": [
"f4:d4:88:79:07:0e"
],
"screenLockSecured": "SCREEN_LOCK_SECURED_ENABLED",
"deviceEnrollmentDomain": "beryju.org",
"browserVersion": "145.0.7632.76",
"deviceAffiliationIds": [
"qewrqer"
],
"builtInDnsClientEnabled": true,
"chromeRemoteDesktopAppBlocked": false,
"safeBrowsingProtectionLevel": "STANDARD",
"siteIsolationEnabled": true,
"passwordProtectionWarningTrigger": "POLICY_UNSET",
"realtimeUrlCheckMode": "REALTIME_URL_CHECK_MODE_DISABLED",
"trigger": "TRIGGER_BROWSER_NAVIGATION"
}
}

View File

@@ -1,67 +0,0 @@
from json import dumps
from unittest.mock import MagicMock, patch
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import RequestFactory
from authentik.endpoints.facts import OSFamily
from authentik.endpoints.models import Device
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
HEADER_ACCESS_CHALLENGE,
GoogleChromeController,
)
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
class TestGoogleChromeConnector(APITestCase):
def setUp(self):
self.connector = GoogleChromeConnector.objects.create(
name=generate_id(),
credentials={},
)
self.factory = RequestFactory()
self.api_key = generate_id()
def test_generate_challenge(self):
req = self.factory.get("/")
challenge = generate_id()
http = MockHTTP()
http.add_response(
f"https://verifiedaccess.googleapis.com/v2/challenge:generate?key={self.api_key}&alt=json",
{"challenge": challenge},
method="POST",
)
with patch(
"authentik.enterprise.endpoints.connectors.google_chrome.models.GoogleChromeConnector.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
controller = GoogleChromeController(self.connector)
res = controller.generate_challenge(req)
self.assertEqual(
res["Location"],
req.build_absolute_uri(
reverse("authentik_endpoints_connectors_google_chrome:chrome")
),
)
self.assertEqual(res.headers[HEADER_ACCESS_CHALLENGE], dumps({"challenge": challenge}))
def test_validate_challenge(self):
http = MockHTTP()
http.add_response(
f"https://verifiedaccess.googleapis.com/v2/challenge:verify?key={self.api_key}&alt=json",
load_fixture("fixtures/host_macos.json"),
method="POST",
)
with patch(
"authentik.enterprise.endpoints.connectors.google_chrome.models.GoogleChromeConnector.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
controller = GoogleChromeController(self.connector)
controller.validate_challenge(dumps("{}"))
device = Device.objects.get(identifier="Z5DDF07GK6")
self.assertIsNotNone(device)
self.assertEqual(device.cached_facts.data["os"]["family"], OSFamily.macOS)

View File

@@ -1,91 +0,0 @@
from json import dumps
from unittest.mock import MagicMock, patch
from django.urls import reverse
from authentik.core.tests.utils import RequestFactory, create_test_flow
from authentik.endpoints.models import Device, EndpointStage
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP
from authentik.flows.models import FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_DEVICE
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
class TestChromeDTCView(FlowTestCase):
def setUp(self):
self.flow = create_test_flow()
self.connector = GoogleChromeConnector.objects.create(
name=generate_id(),
credentials={},
)
self.factory = RequestFactory()
self.api_key = generate_id()
self.stage = EndpointStage.objects.create(
name=generate_id(),
connector=self.connector,
)
FlowStageBinding.objects.create(
target=self.flow,
stage=self.stage,
order=0,
)
def test_dtc_generate_verify(self):
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
self.assertStageResponse(
res,
self.flow,
component="xak-flow-frame",
url="http://testserver/endpoints/google/chrome/",
)
challenge = generate_id()
http = MockHTTP()
http.add_response(
f"https://verifiedaccess.googleapis.com/v2/challenge:generate?key={self.api_key}&alt=json",
{"challenge": challenge},
method="POST",
)
http.add_response(
f"https://verifiedaccess.googleapis.com/v2/challenge:verify?key={self.api_key}&alt=json",
load_fixture("fixtures/host_macos.json"),
method="POST",
)
with patch(
"authentik.enterprise.endpoints.connectors.google_chrome.models.GoogleChromeConnector.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
# Generate challenge
res = self.client.get(
reverse("authentik_endpoints_connectors_google_chrome:chrome"),
HTTP_X_DEVICE_TRUST="VerifiedAccess",
)
self.assertEqual(res.status_code, 302)
self.assertEqual(
res.headers["X-Verified-Access-Challenge"],
dumps({"challenge": challenge}),
)
# Validate challenge
res = self.client.get(
reverse("authentik_endpoints_connectors_google_chrome:chrome"),
HTTP_X_VERIFIED_ACCESS_CHALLENGE_RESPONSE=dumps({}),
)
self.assertEqual(res.status_code, 200)
device = Device.objects.get(identifier="Z5DDF07GK6")
self.assertIsNotNone(device)
# Continue flow
with self.assertFlowFinishes() as plan:
res = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
self.assertStageRedirects(res, "/")
plan = plan()
plan_device = plan.context[PLAN_CONTEXT_DEVICE]
self.assertEqual(device.pk, plan_device.pk)

View File

@@ -1,16 +0,0 @@
"""API URLs"""
from django.urls import path
from authentik.enterprise.endpoints.connectors.google_chrome.api import GoogleChromeConnectorViewSet
from authentik.enterprise.endpoints.connectors.google_chrome.views.dtc import (
GoogleChromeDeviceTrustConnector,
)
urlpatterns = [
path("chrome/", GoogleChromeDeviceTrustConnector.as_view(), name="chrome"),
]
api_urlpatterns = [
("endpoints/google_chrome/connectors", GoogleChromeConnectorViewSet),
]

View File

@@ -1,46 +0,0 @@
from typing import Any
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.clickjacking import xframe_options_sameorigin
from authentik.endpoints.models import EndpointStage
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
HEADER_ACCESS_CHALLENGE_RESPONSE,
HEADER_DEVICE_TRUST,
GoogleChromeController,
)
from authentik.enterprise.endpoints.connectors.google_chrome.models import GoogleChromeConnector
from authentik.flows.planner import PLAN_CONTEXT_DEVICE, FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
@method_decorator(xframe_options_sameorigin, name="dispatch")
class GoogleChromeDeviceTrustConnector(View):
"""Google Chrome Device-trust connector based endpoint authenticator"""
def get_flow_plan(self) -> FlowPlan:
flow_plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
return flow_plan
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
super().setup(request, *args, **kwargs)
stage: EndpointStage = self.get_flow_plan().bindings[0].stage
connector = GoogleChromeConnector.objects.filter(pk=stage.connector_id).first()
if not connector:
return HttpResponseBadRequest()
self.controller: GoogleChromeController = connector.controller(connector)
def get(self, request: HttpRequest) -> HttpResponse:
x_device_trust = request.headers.get(HEADER_DEVICE_TRUST)
x_access_challenge_response = request.headers.get(HEADER_ACCESS_CHALLENGE_RESPONSE)
if x_device_trust == "VerifiedAccess" and x_access_challenge_response is None:
return self.controller.generate_challenge(request)
if x_access_challenge_response:
device = self.controller.validate_challenge(x_access_challenge_response)
flow_plan = self.get_flow_plan()
flow_plan.context[PLAN_CONTEXT_DEVICE] = device
self.request.session[SESSION_KEY_PLAN] = flow_plan
return TemplateResponse(request, "flows/frame-submit.html")

View File

@@ -4,7 +4,6 @@ TENANT_APPS = [
"authentik.enterprise.audit",
"authentik.enterprise.endpoints.connectors.agent",
"authentik.enterprise.endpoints.connectors.fleet",
"authentik.enterprise.endpoints.connectors.google_chrome",
"authentik.enterprise.lifecycle",
"authentik.enterprise.policies.unique_password",
"authentik.enterprise.providers.google_workspace",

View File

@@ -9,11 +9,6 @@ from django.views import View
from django.views.decorators.clickjacking import xframe_options_sameorigin
from googleapiclient.discovery import build
from authentik.enterprise.endpoints.connectors.google_chrome.controller import (
HEADER_ACCESS_CHALLENGE,
HEADER_ACCESS_CHALLENGE_RESPONSE,
HEADER_DEVICE_TRUST,
)
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
AuthenticatorEndpointGDTCStage,
EndpointDevice,
@@ -24,6 +19,15 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
from authentik.stages.user_login.stage import PLAN_CONTEXT_METHOD_ARGS_KNOWN_DEVICE
# Header we get from chrome that initiates verified access
HEADER_DEVICE_TRUST = "X-Device-Trust"
# Header we send to the client with the challenge
HEADER_ACCESS_CHALLENGE = "X-Verified-Access-Challenge"
# Header we get back from the client that we verify with google
HEADER_ACCESS_CHALLENGE_RESPONSE = "X-Verified-Access-Challenge-Response"
# Header value for x-device-trust that initiates the flow
DEVICE_TRUST_VERIFIED_ACCESS = "VerifiedAccess"
PLAN_CONTEXT_METHOD_ARGS_ENDPOINTS = "endpoints"
@@ -90,4 +94,4 @@ class GoogleChromeDeviceTrustConnector(View):
PLAN_CONTEXT_METHOD_ARGS_KNOWN_DEVICE, True
)
request.session[SESSION_KEY_PLAN] = flow_plan
return TemplateResponse(request, "flows/frame-submit.html")
return TemplateResponse(request, "stages/authenticator_endpoint/google_chrome_dtc.html")

View File

@@ -93,11 +93,13 @@ def on_login_failed(
credentials: dict[str, str],
request: HttpRequest,
stage: Stage | None = None,
context: dict[str, Any] | None = None,
**kwargs,
):
"""Failed Login, authentik custom event"""
user = User.objects.filter(username=credentials.get("username")).first()
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(
context = context or {}
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **context).from_http(
request, user
)

View File

@@ -207,3 +207,9 @@ class TestEvents(TestCase):
"username": user.username,
},
)
def test_invalid_string(self):
"""Test creating an event with invalid unicode string data"""
event = Event.new("unittest", foo="foo bar \u0000 baz")
event.save()
self.assertEqual(event.context["foo"], "foo bar baz")

View File

@@ -36,6 +36,10 @@ ALLOWED_SPECIAL_KEYS = re.compile(
)
def cleanse_str(raw: Any) -> str:
return str(raw).replace("\u0000", "")
def cleanse_item(key: str, value: Any) -> Any:
"""Cleanse a single item"""
if isinstance(value, dict):
@@ -66,7 +70,7 @@ def cleanse_dict(source: dict[Any, Any]) -> dict[Any, Any]:
def model_to_dict(model: Model) -> dict[str, Any]:
"""Convert model to dict"""
name = str(model)
name = cleanse_str(model)
if hasattr(model, "name"):
name = model.name
return {
@@ -133,11 +137,11 @@ def sanitize_item(value: Any) -> Any: # noqa: PLR0911, PLR0912
if isinstance(value, ASN):
return ASN_CONTEXT_PROCESSOR.asn_to_dict(value)
if isinstance(value, Path):
return str(value)
return cleanse_str(value)
if isinstance(value, Exception):
return str(value)
return cleanse_str(value)
if isinstance(value, YAMLTag):
return str(value)
return cleanse_str(value)
if isinstance(value, Enum):
return value.value
if isinstance(value, type):
@@ -161,7 +165,7 @@ def sanitize_item(value: Any) -> Any: # noqa: PLR0911, PLR0912
raise ValueError("JSON can't represent timezone-aware times.")
return value.isoformat()
if isinstance(value, timedelta):
return str(value.total_seconds())
return cleanse_str(value.total_seconds())
if callable(value):
return {
"type": "callable",
@@ -174,8 +178,8 @@ def sanitize_item(value: Any) -> Any: # noqa: PLR0911, PLR0912
try:
return DjangoJSONEncoder().default(value)
except TypeError:
return str(value)
return str(value)
return cleanse_str(value)
return cleanse_str(value)
def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:

View File

@@ -27,10 +27,8 @@
"layout": "{{ flow.layout }}",
};
</script>
{% endblock %}
{% block interface_stylesheet %}
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/flow-%v.css' %}" />
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/static-%v.css' %}" />
{% endblock %}
{% block head %}

View File

@@ -166,7 +166,6 @@ storage:
# region: "us-east-1"
# use_ssl: True
# endpoint: "https://s3.us-east-1.amazonaws.com"
# signature_version: "s3v4"
# access_key: ""
# secret_key: ""
# bucket_name: "authentik-data"
@@ -183,3 +182,5 @@ storage:
# backend: file # or s3
# file: {}
# s3: {}
skip_migrations: false

View File

@@ -185,8 +185,10 @@ class KubernetesObjectReconciler[T]:
patch = self.get_patch()
if patch is not None:
current_json = ApiClient().sanitize_for_serialization(current)
try:
current_json = ApiClient().sanitize_for_serialization(current)
except AttributeError:
current_json = asdict(current)
try:
if apply_patch(current_json, patch) != current_json:
raise NeedsUpdate()

View File

@@ -163,4 +163,5 @@ def outpost_pre_delete_cleanup(sender, instance: Outpost, **_):
@receiver(pre_delete, sender=AuthenticatedSession)
def outpost_logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
"""Catch logout by expiring sessions being deleted"""
outpost_session_end.send(instance.session.session_key)
if Outpost.objects.exists():
outpost_session_end.send(instance.session.session_key)

View File

@@ -1,31 +1,19 @@
"""Shared logout stages for SAML and OIDC providers"""
from django.http import HttpResponse
from rest_framework.fields import CharField, ListField
from rest_framework.fields import CharField, DictField, ListField
from authentik.common.oauth.constants import PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.stage import ChallengeStageView
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS
class LogoutURL(PassiveSerializer):
"""Data for a single logout URL"""
url = CharField()
provider_name = CharField(required=False, allow_null=True)
binding = CharField(required=False, allow_null=True)
saml_request = CharField(required=False, allow_null=True)
saml_response = CharField(required=False, allow_null=True)
saml_relay_state = CharField(required=False, allow_null=True)
class IframeLogoutChallenge(Challenge):
"""Challenge for iframe logout"""
component = CharField(default="ak-provider-iframe-logout")
logout_urls = ListField(child=LogoutURL(), default=list)
logout_urls = ListField(child=DictField(), default=list)
class IframeLogoutChallengeResponse(ChallengeResponse):

View File

@@ -432,7 +432,7 @@ class AuthorizationFlowInitView(BufferedPolicyAccessView):
return response
def dispatch(self, request: HttpRequest, *args, **kwargs):
# Activate language before parsing params (error messages should be localized)
# Activate language before parsing params (error messages should be localised)
return self.dispatch_with_language(request, *args, **kwargs)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@@ -368,7 +368,7 @@ class TokenParams:
) -> tuple[dict, OAuthSource] | tuple[None, None]:
# Fully decode the JWT without verifying the signature, so we can get access to
# the header.
# Get the Key ID from the header, and use that to optimize our source query to only find
# Get the Key ID from the header, and use that to optimise our source query to only find
# sources that have a JWK for that Key ID
# The Key ID doesn't have a fixed format, but must match between an issued JWT
# and whatever is returned by the JWKS endpoint

View File

@@ -27,6 +27,8 @@ class TraefikMiddlewareSpecForwardAuth:
trustForwardHeader: bool = field(default=True)
maxResponseBodySize: int = field(default=1024 * 1024 * 4)
@dataclass(slots=True)
class TraefikMiddlewareSpec:
@@ -140,6 +142,7 @@ class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
],
authResponseHeadersRegex="",
trustForwardHeader=True,
maxResponseBodySize=1024 * 1024 * 4,
)
),
)

View File

@@ -1,13 +0,0 @@
"""Proxy provider signals"""
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from authentik.core.models import AuthenticatedSession
from authentik.providers.proxy.tasks import proxy_on_logout
@receiver(pre_delete, sender=AuthenticatedSession)
def logout_proxy_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
"""Catch logout by expiring sessions being deleted"""
proxy_on_logout.send(instance.session.session_key)

View File

@@ -1,25 +0,0 @@
"""proxy provider tasks"""
from channels.layers import get_channel_layer
from django.utils.translation import gettext_lazy as _
from dramatiq.actor import actor
from authentik.outposts.consumer import build_outpost_group
from authentik.outposts.models import Outpost, OutpostType
from authentik.providers.oauth2.id_token import hash_session_key
@actor(description=_("Terminate session on Proxy outpost."))
def proxy_on_logout(session_id: str):
layer = get_channel_layer()
hashed_session_id = hash_session_key(session_id)
for outpost in Outpost.objects.filter(type=OutpostType.PROXY):
group = build_outpost_group(outpost.pk)
layer.group_send_blocking(
group,
{
"type": "event.provider.specific",
"sub_type": "logout",
"session_id": hashed_session_id,
},
)

View File

@@ -213,7 +213,6 @@ class SAMLProviderSerializer(ProviderSerializer):
"sign_assertion",
"sign_response",
"sign_logout_request",
"sign_logout_response",
"sp_binding",
"sls_binding",
"logout_method",
@@ -233,7 +232,7 @@ class SAMLMetadataSerializer(PassiveSerializer):
"""SAML Provider Metadata serializer"""
metadata = CharField(read_only=True)
download_url = CharField(read_only=True, required=False)
download_url = CharField(read_only=True, required=False, allow_null=True)
class SAMLProviderImportSerializer(PassiveSerializer):

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-24 18:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0020_samlprovider_logout_method_and_more"),
]
operations = [
migrations.AddField(
model_name="samlprovider",
name="sign_logout_response",
field=models.BooleanField(default=False),
),
]

View File

@@ -227,7 +227,6 @@ class SAMLProvider(Provider):
sign_assertion = models.BooleanField(default=True)
sign_response = models.BooleanField(default=False)
sign_logout_request = models.BooleanField(default=False)
sign_logout_response = models.BooleanField(default=False)
@property
def launch_url(self) -> str | None:

View File

@@ -1,12 +1,11 @@
"""SAML Logout stages for automatic injection"""
from django.http import HttpResponse
from rest_framework.fields import BooleanField, CharField, ChoiceField
from rest_framework.fields import BooleanField, CharField
from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeResponse, HttpChallengeResponse
from authentik.flows.stage import ChallengeStageView
from authentik.providers.saml.models import SAMLBindings
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS
LOGGER = get_logger()
@@ -20,16 +19,13 @@ class NativeLogoutChallenge(Challenge):
"""Challenge for native browser logout"""
component = CharField(default="ak-provider-saml-native-logout")
provider_name = CharField(required=False)
is_complete = BooleanField(required=False, default=False)
post_url = CharField(required=False)
redirect_url = CharField(required=False)
saml_binding = ChoiceField(choices=SAMLBindings.choices, required=False)
saml_request = CharField(required=False)
saml_response = CharField(required=False)
saml_relay_state = CharField(required=False)
relay_state = CharField(required=False)
provider_name = CharField(required=False)
binding = CharField(required=False)
redirect_url = CharField(required=False)
is_complete = BooleanField(required=False, default=False)
class NativeLogoutChallengeResponse(ChallengeResponse):

View File

@@ -1,196 +0,0 @@
"""LogoutResponse processor"""
import base64
from urllib.parse import quote, urlencode
import xmlsec
from lxml import etree
from lxml.etree import Element, SubElement
from authentik.common.saml.constants import (
DIGEST_ALGORITHM_TRANSLATION_MAP,
NS_MAP,
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
SIGN_ALGORITHM_TRANSFORM_MAP,
)
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.utils import get_random_id
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
from authentik.providers.saml.utils.time import get_time_string
class LogoutResponseProcessor:
"""Generate a SAML LogoutResponse"""
provider: SAMLProvider
logout_request: LogoutRequest
destination: str | None
relay_state: str | None
_issue_instant: str
_response_id: str
def __init__(
self,
provider: SAMLProvider,
logout_request: LogoutRequest,
destination: str | None = None,
relay_state: str | None = None,
):
self.provider = provider
self.logout_request = logout_request
self.destination = destination
self.relay_state = relay_state or (logout_request.relay_state if logout_request else None)
self._issue_instant = get_time_string()
self._response_id = get_random_id()
def get_issuer(self) -> Element:
"""Get Issuer element"""
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
issuer.text = self.provider.issuer
return issuer
def build(self, status: str = "Success") -> Element:
"""Build a SAML LogoutResponse as etree Element"""
response = Element(f"{{{NS_SAML_PROTOCOL}}}LogoutResponse", nsmap=NS_MAP)
response.attrib["Version"] = "2.0"
response.attrib["IssueInstant"] = self._issue_instant
response.attrib["ID"] = self._response_id
if self.destination:
response.attrib["Destination"] = self.destination
if self.logout_request and self.logout_request.id:
response.attrib["InResponseTo"] = self.logout_request.id
response.append(self.get_issuer())
# Add Status element
status_element = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status")
status_code = SubElement(status_element, f"{{{NS_SAML_PROTOCOL}}}StatusCode")
status_code.attrib["Value"] = f"urn:oasis:names:tc:SAML:2.0:status:{status}"
return response
def build_response(self, status: str = "Success") -> str:
"""Build and sign LogoutResponse, return as XML string (not encoded)"""
response = self.build(status)
if self.provider.signing_kp and self.provider.sign_logout_response:
self._add_signature(response)
self._sign_response(response)
return etree.tostring(response).decode()
def encode_post(self, status: str = "Success") -> str:
"""Encode LogoutResponse for POST binding"""
response = self.build(status)
if self.provider.signing_kp and self.provider.sign_logout_response:
self._add_signature(response)
self._sign_response(response)
return base64.b64encode(etree.tostring(response)).decode()
def encode_redirect(self, status: str = "Success") -> str:
"""Encode LogoutResponse for Redirect binding"""
response = self.build(status)
# Note: For redirect binding, signatures are added as query parameters, not in XML
xml_str = etree.tostring(response, encoding="UTF-8", xml_declaration=True)
return deflate_and_base64_encode(xml_str.decode("UTF-8"))
def get_redirect_url(self, status: str = "Success") -> str:
"""Build complete logout response URL for redirect binding with signature if needed"""
encoded_response = self.encode_redirect(status)
params = {
"SAMLResponse": encoded_response,
}
if self.relay_state:
params["RelayState"] = self.relay_state
if self.provider.signing_kp and self.provider.sign_logout_response:
sig_alg = self.provider.signature_algorithm
params["SigAlg"] = sig_alg
# Build the string to sign
query_string = self._build_signable_query_string(params)
signature = self._sign_query_string(query_string)
params["Signature"] = base64.b64encode(signature).decode()
# Some SP's use query params on their sls endpoint
if not self.destination:
raise ValueError("destination is required for redirect URL")
separator = "&" if "?" in self.destination else "?"
return f"{self.destination}{separator}{urlencode(params)}"
def _add_signature(self, element: Element):
"""Add signature placeholder to element"""
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
signature = xmlsec.template.create(
element,
xmlsec.constants.TransformExclC14N,
sign_algorithm_transform,
ns=xmlsec.constants.DSigNs,
)
element.insert(1, signature) # Insert after Issuer
def _sign_response(self, response: Element):
"""Sign the response element"""
digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get(
self.provider.digest_algorithm, xmlsec.constants.TransformSha1
)
xmlsec.tree.add_ids(response, ["ID"])
signature_node = xmlsec.tree.find_node(response, xmlsec.constants.NodeSignature)
ref = xmlsec.template.add_reference(
signature_node,
digest_algorithm_transform,
uri="#" + response.attrib["ID"],
)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N)
key_info = xmlsec.template.ensure_key_info(signature_node)
xmlsec.template.add_x509_data(key_info)
ctx = xmlsec.SignatureContext()
ctx.key = xmlsec.Key.from_memory(
self.provider.signing_kp.key_data, # Use key_data for the private key
xmlsec.constants.KeyDataFormatPem,
)
ctx.key.load_cert_from_memory(
self.provider.signing_kp.certificate_data, xmlsec.constants.KeyDataFormatPem
)
ctx.sign(signature_node)
def _build_signable_query_string(self, params: dict) -> str:
"""Build query string for signing (order matters per SAML spec)"""
# SAML spec requires specific order: SAMLResponse, RelayState, SigAlg
# Values must be URL-encoded individually before concatenation
ordered = []
if "SAMLResponse" in params:
ordered.append(f"SAMLResponse={quote(params['SAMLResponse'], safe='')}")
if "RelayState" in params:
ordered.append(f"RelayState={quote(params['RelayState'], safe='')}")
if "SigAlg" in params:
ordered.append(f"SigAlg={quote(params['SigAlg'], safe='')}")
return "&".join(ordered)
def _sign_query_string(self, query_string: str) -> bytes:
"""Sign the query string for redirect binding"""
signature_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha256
)
key = xmlsec.Key.from_memory(
self.provider.signing_kp.key_data,
xmlsec.constants.KeyDataFormatPem,
None,
)
ctx = xmlsec.SignatureContext()
ctx.key = key
return ctx.sign_binary(query_string.encode("utf-8"), signature_algorithm_transform)

View File

@@ -175,16 +175,16 @@ def handle_flow_pre_user_logout(
logout_data = {
"post_url": session.provider.sls_url,
"saml_request": form_data["SAMLRequest"],
"saml_relay_state": form_data["RelayState"],
"relay_state": form_data["RelayState"],
"provider_name": session.provider.name,
"saml_binding": SAMLBindings.POST,
"binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"redirect_url": logout_url,
"provider_name": session.provider.name,
"saml_binding": SAMLBindings.REDIRECT,
"binding": SAMLBindings.REDIRECT,
}
native_sessions.append(logout_data)

View File

@@ -5,11 +5,8 @@ from django.contrib.auth import get_user_model
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
LOGGER = get_logger()
User = get_user_model()
@@ -81,86 +78,3 @@ def send_post_logout_request(provider: SAMLProvider, processor: LogoutRequestPro
)
return True
@actor(description="Send SAML LogoutResponse to a Service Provider (backchannel)")
def send_saml_logout_response(
provider_pk: int,
sls_url: str,
logout_request_id: str | None = None,
relay_state: str | None = None,
):
"""Send SAML LogoutResponse to a Service Provider using backchannel (server-to-server)"""
provider = SAMLProvider.objects.filter(pk=provider_pk).first()
if not provider:
LOGGER.error(
"Provider not found for SAML logout response",
provider_pk=provider_pk,
)
return False
LOGGER.debug(
"Sending backchannel SAML logout response",
provider=provider.name,
sls_url=sls_url,
)
# Create a minimal LogoutRequest object for the response processor
# We only need the ID and relay_state for building the response
logout_request = None
if logout_request_id:
logout_request = LogoutRequest()
logout_request.id = logout_request_id
logout_request.relay_state = relay_state
# Build the logout response
processor = LogoutResponseProcessor(
provider=provider,
logout_request=logout_request,
destination=sls_url,
relay_state=relay_state,
)
encoded_response = processor.encode_post()
form_data = {
"SAMLResponse": encoded_response,
}
if relay_state:
form_data["RelayState"] = relay_state
# Send the logout response to the SP
try:
response = requests.post(
sls_url,
data=form_data,
timeout=10,
headers={
"Content-Type": "application/x-www-form-urlencoded",
},
allow_redirects=True,
)
response.raise_for_status()
LOGGER.info(
"Successfully sent backchannel logout response to SP",
provider=provider.name,
sls_url=sls_url,
status_code=response.status_code,
)
return True
except requests.exceptions.RequestException as exc:
LOGGER.warning(
"Failed to send backchannel logout response to SP",
provider=provider.name,
sls_url=sls_url,
error=str(exc),
)
Event.new(
EventAction.CONFIGURATION_ERROR,
provider=provider,
message=f"Backchannel logout response failed: {str(exc)}",
).save()
return False

View File

@@ -69,7 +69,7 @@ class TestNativeLogoutStageView(TestCase):
{
"redirect_url": "https://sp1.example.com/sls?SAMLRequest=encoded",
"provider_name": "test-provider-1",
"saml_binding": "redirect",
"binding": "redirect",
}
]
stage_view = NativeLogoutStageView(
@@ -85,7 +85,7 @@ class TestNativeLogoutStageView(TestCase):
# Should return a NativeLogoutChallenge
self.assertIsInstance(challenge, NativeLogoutChallenge)
self.assertEqual(challenge.initial_data["saml_binding"], "redirect")
self.assertEqual(challenge.initial_data["binding"], "redirect")
self.assertEqual(challenge.initial_data["provider_name"], "test-provider-1")
self.assertIn("redirect_url", challenge.initial_data)
@@ -102,9 +102,9 @@ class TestNativeLogoutStageView(TestCase):
{
"post_url": "https://sp2.example.com/sls",
"saml_request": "encoded_saml_request",
"saml_relay_state": "https://idp.example.com/flow/test-flow",
"relay_state": "https://idp.example.com/flow/test-flow",
"provider_name": "test-provider-2",
"saml_binding": "post",
"binding": "post",
}
]
stage_view = NativeLogoutStageView(
@@ -120,11 +120,11 @@ class TestNativeLogoutStageView(TestCase):
# Should return a NativeLogoutChallenge
self.assertIsInstance(challenge, NativeLogoutChallenge)
self.assertEqual(challenge.initial_data["saml_binding"], "post")
self.assertEqual(challenge.initial_data["binding"], "post")
self.assertEqual(challenge.initial_data["provider_name"], "test-provider-2")
self.assertEqual(challenge.initial_data["post_url"], "https://sp2.example.com/sls")
self.assertIn("saml_request", challenge.initial_data)
self.assertIn("saml_relay_state", challenge.initial_data)
self.assertIn("relay_state", challenge.initial_data)
def test_get_challenge_all_complete(self):
"""Test get_challenge when all providers are done"""

View File

@@ -1,139 +0,0 @@
"""logout response tests"""
from defusedxml import ElementTree
from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.common.saml.constants import (
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
NS_SIGNATURE,
)
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
class TestLogoutResponse(TestCase):
"""Test LogoutResponse processor"""
@apply_blueprint("system/providers-saml.yaml")
def setUp(self):
cert = create_test_cert()
self.provider: SAMLProvider = SAMLProvider.objects.create(
authorization_flow=create_test_flow(),
acs_url="http://testserver/source/saml/provider/acs/",
sls_url="http://testserver/source/saml/provider/sls/",
signing_kp=cert,
verification_kp=cert,
)
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
self.provider.save()
def test_build_response(self):
"""Test building a LogoutResponse"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="test-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
response_xml = processor.build_response(status="Success")
# Parse and verify
root = ElementTree.fromstring(response_xml)
self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
self.assertEqual(root.attrib["Version"], "2.0")
self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
# Check Issuer
issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
self.assertEqual(issuer.text, self.provider.issuer)
# Check Status
status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")
self.assertEqual(status.attrib["Value"], "urn:oasis:names:tc:SAML:2.0:status:Success")
def test_build_response_signed(self):
"""Test building a signed LogoutResponse"""
self.provider.sign_logout_response = True
self.provider.save()
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="test-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
response_xml = processor.build_response(status="Success")
# Parse and verify signature is present
root = ElementTree.fromstring(response_xml)
signature = root.find(f".//{{{NS_SIGNATURE}}}Signature")
self.assertIsNotNone(signature)
# Verify signature structure
signed_info = signature.find(f"{{{NS_SIGNATURE}}}SignedInfo")
self.assertIsNotNone(signed_info)
signature_value = signature.find(f"{{{NS_SIGNATURE}}}SignatureValue")
self.assertIsNotNone(signature_value)
self.assertIsNotNone(signature_value.text)
def test_no_inresponseto(self):
"""Test building response without a logout request omits InResponseTo attribute"""
processor = LogoutResponseProcessor(self.provider, None, destination=self.provider.sls_url)
response_xml = processor.build_response(status="Success")
root = ElementTree.fromstring(response_xml)
self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
self.assertNotIn("InResponseTo", root.attrib)
def test_no_destination(self):
"""Test building response without destination"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
)
processor = LogoutResponseProcessor(self.provider, logout_request, destination=None)
response_xml = processor.build_response(status="Success")
root = ElementTree.fromstring(response_xml)
self.assertNotIn("Destination", root.attrib)
def test_relay_state_from_logout_request(self):
"""Test that relay_state is taken from logout_request if not provided"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="request-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
self.assertEqual(processor.relay_state, "request-relay-state")
def test_relay_state_override(self):
"""Test that explicit relay_state overrides logout_request relay_state"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="request-relay-state",
)
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
relay_state="explicit-relay-state",
)
self.assertEqual(processor.relay_state, "explicit-relay-state")

View File

@@ -1,291 +0,0 @@
"""Tests for SAML provider tasks"""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from requests.exceptions import ConnectionError, HTTPError
from authentik.common.saml.constants import SAML_NAME_ID_FORMAT_EMAIL
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.tasks import (
send_post_logout_request,
send_saml_logout_request,
send_saml_logout_response,
)
class TestSendSamlLogoutResponse(TestCase):
"""Tests for send_saml_logout_response task"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_response(self, mock_post):
"""Test successful POST to SP returns True"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state="https://sp.example.com/return",
)
self.assertTrue(result)
mock_post.assert_called_once()
# Verify the POST was made with correct data
call_kwargs = mock_post.call_args[1]
self.assertEqual(call_kwargs["timeout"], 10)
self.assertEqual(
call_kwargs["headers"]["Content-Type"], "application/x-www-form-urlencoded"
)
# Verify form data contains SAMLResponse and RelayState
form_data = call_kwargs["data"]
self.assertIn("SAMLResponse", form_data)
self.assertIn("RelayState", form_data)
self.assertEqual(form_data["RelayState"], "https://sp.example.com/return")
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_response_no_relay_state(self, mock_post):
"""Test successful POST without relay_state"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state=None,
)
self.assertTrue(result)
# Verify form data does not contain RelayState
form_data = mock_post.call_args[1]["data"]
self.assertIn("SAMLResponse", form_data)
self.assertNotIn("RelayState", form_data)
def test_provider_not_found(self):
"""Test returns False when provider doesn't exist"""
result = send_saml_logout_response(
provider_pk=99999, # Non-existent provider
sls_url="https://sp.example.com/sls",
logout_request_id="test-request-id",
relay_state=None,
)
self.assertFalse(result)
@patch("authentik.providers.saml.tasks.Event")
@patch("authentik.providers.saml.tasks.requests.post")
def test_http_error_creates_event(self, mock_post, mock_event_class):
"""Test HTTP error creates an error event"""
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = HTTPError("500 Server Error")
mock_post.return_value = mock_response
mock_event = MagicMock()
mock_event_class.new.return_value = mock_event
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state=None,
)
self.assertFalse(result)
# Verify error event was created
mock_event_class.new.assert_called_once()
call_kwargs = mock_event_class.new.call_args[1]
self.assertIn("Backchannel logout response failed", call_kwargs["message"])
mock_event.save.assert_called_once()
class TestSendSamlLogoutRequest(TestCase):
"""Tests for send_saml_logout_request task"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_request(self, mock_post):
"""Test successful POST logout request returns True"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_request(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
self.assertTrue(result)
mock_post.assert_called_once()
# Verify the POST was made with correct data
call_kwargs = mock_post.call_args[1]
self.assertEqual(call_kwargs["timeout"], 10)
self.assertEqual(
call_kwargs["headers"]["Content-Type"], "application/x-www-form-urlencoded"
)
# Verify form data contains SAMLRequest
form_data = call_kwargs["data"]
self.assertIn("SAMLRequest", form_data)
def test_provider_not_found(self):
"""Test returns False when provider doesn't exist"""
result = send_saml_logout_request(
provider_pk=99999, # Non-existent provider
sls_url="https://sp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
self.assertFalse(result)
@patch("authentik.providers.saml.tasks.requests.post")
def test_http_error_raises(self, mock_post):
"""Test HTTP error raises exception (no try/catch in send_post_logout_request)"""
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = HTTPError("500 Server Error")
mock_post.return_value = mock_response
with self.assertRaises(HTTPError):
send_saml_logout_request(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
class TestSendPostLogoutRequest(TestCase):
"""Tests for send_post_logout_request function"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_post(self, mock_post):
"""Test successful POST returns True"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
result = send_post_logout_request(self.provider, processor)
self.assertTrue(result)
mock_post.assert_called_once()
@patch("authentik.providers.saml.tasks.requests.post")
def test_with_relay_state(self, mock_post):
"""Test POST includes RelayState when present"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
relay_state="https://sp.example.com/return",
)
result = send_post_logout_request(self.provider, processor)
self.assertTrue(result)
# Verify RelayState is included
form_data = mock_post.call_args[1]["data"]
self.assertIn("RelayState", form_data)
self.assertEqual(form_data["RelayState"], "https://sp.example.com/return")
@patch("authentik.providers.saml.tasks.requests.post")
def test_connection_error_raises(self, mock_post):
"""Test connection error raises exception"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_post.side_effect = ConnectionError("Connection refused")
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
with self.assertRaises(ConnectionError):
send_post_logout_request(self.provider, processor)

View File

@@ -8,15 +8,13 @@ from django.urls import reverse
from authentik.common.saml.constants import SAML_NAME_ID_FORMAT_EMAIL
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_brand, create_test_cert, create_test_flow
from authentik.core.tests.utils import create_test_brand, create_test_flow
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLBindings, SAMLLogoutMethods, SAMLProvider
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
from authentik.providers.saml.views.flows import (
PLAN_CONTEXT_SAML_RELAY_STATE,
)
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_RELAY_STATE
from authentik.providers.saml.views.sp_slo import (
SPInitiatedSLOBindingPOSTView,
SPInitiatedSLOBindingRedirectView,
@@ -438,290 +436,3 @@ class TestSPInitiatedSLOViews(TestCase):
# Should treat it as plain URL and redirect to it
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/some/invalid/path")
class TestSPInitiatedSLOLogoutMethods(TestCase):
"""Test SP-initiated SAML SLO logout method branching"""
def setUp(self):
"""Set up test fixtures"""
self.factory = RequestFactory()
self.brand = create_test_brand()
self.flow = create_test_flow()
self.invalidation_flow = create_test_flow()
self.cert = create_test_cert()
# Create provider with sls_url
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
invalidation_flow=self.invalidation_flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
sp_binding="redirect",
sls_binding="redirect",
signing_kp=self.cert,
)
# Create application
self.application = Application.objects.create(
name="test-app",
slug="test-app-logout-methods",
provider=self.provider,
)
# Create logout request processor for generating test requests
self.processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination="https://idp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
relay_state="https://sp.example.com/return",
)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_frontchannel_native_post_binding(self, mock_auth_session):
"""Test FRONTCHANNEL_NATIVE with POST binding parses request correctly"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_NATIVE
self.provider.sls_binding = SAMLBindings.POST
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed and provider is configured correctly
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
self.assertEqual(view.provider.logout_method, SAMLLogoutMethods.FRONTCHANNEL_NATIVE)
self.assertEqual(view.provider.sls_binding, SAMLBindings.POST)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_frontchannel_native_redirect_binding(self, mock_auth_session):
"""Test FRONTCHANNEL_NATIVE with REDIRECT binding creates redirect URL"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_NATIVE
self.provider.sls_binding = SAMLBindings.REDIRECT
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_frontchannel_iframe_post_binding(self, mock_auth_session):
"""Test FRONTCHANNEL_IFRAME with POST binding creates IframeLogoutStageView"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_IFRAME
self.provider.sls_binding = SAMLBindings.POST
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_frontchannel_iframe_redirect_binding(self, mock_auth_session):
"""Test FRONTCHANNEL_IFRAME with REDIRECT binding"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_IFRAME
self.provider.sls_binding = SAMLBindings.REDIRECT
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_backchannel_parses_request(self, mock_auth_session):
"""Test BACKCHANNEL mode parses request correctly"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.BACKCHANNEL
self.provider.sls_binding = SAMLBindings.POST
self.provider.save()
encoded_request = self.processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": "https://sp.example.com/return",
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the logout request was parsed and provider is configured correctly
self.assertIn("authentik/providers/saml/logout_request", view.plan_context)
self.assertEqual(view.provider.logout_method, SAMLLogoutMethods.BACKCHANNEL)
self.assertEqual(view.provider.sls_binding, SAMLBindings.POST)
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_no_sls_url_only_session_end(self, mock_auth_session):
"""Test that only SessionEndStage is appended when sls_url is empty"""
mock_auth_session.from_request.return_value = None
# Create provider without sls_url
provider_no_sls = SAMLProvider.objects.create(
name="no-sls-provider",
authorization_flow=self.flow,
invalidation_flow=self.invalidation_flow,
acs_url="https://sp.example.com/acs",
sls_url="", # No SLS URL
issuer="https://idp.example.com",
)
app_no_sls = Application.objects.create(
name="no-sls-app",
slug="no-sls-app",
provider=provider_no_sls,
)
processor = LogoutRequestProcessor(
provider=provider_no_sls,
user=None,
destination="https://idp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
encoded_request = processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{app_no_sls.slug}/",
{
"SAMLRequest": encoded_request,
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=app_no_sls.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify the provider has no sls_url
self.assertEqual(view.provider.sls_url, "")
@patch("authentik.providers.saml.views.sp_slo.AuthenticatedSession")
def test_relay_state_propagation(self, mock_auth_session):
"""Test that relay state from logout request is passed through to response"""
mock_auth_session.from_request.return_value = None
self.provider.logout_method = SAMLLogoutMethods.FRONTCHANNEL_IFRAME
self.provider.save()
expected_relay_state = "https://sp.example.com/custom-return"
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination="https://idp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
relay_state=expected_relay_state,
)
encoded_request = processor.encode_redirect()
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLRequest": encoded_request,
"RelayState": expected_relay_state,
},
)
request.session = {}
request.brand = self.brand
request.user = MagicMock()
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
view.resolve_provider_application()
view.check_saml_request()
# Verify relay state was captured
logout_request = view.plan_context.get("authentik/providers/saml/logout_request")
self.assertEqual(logout_request.relay_state, expected_relay_state)

View File

@@ -15,22 +15,10 @@ from authentik.flows.stage import SessionEndStage
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.iframe_logout import IframeLogoutStageView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import (
SAMLBindings,
SAMLLogoutMethods,
SAMLProvider,
SAMLSession,
)
from authentik.providers.saml.native_logout import NativeLogoutStageView
from authentik.providers.saml.models import SAMLProvider, SAMLSession
from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
from authentik.providers.saml.tasks import send_saml_logout_response
from authentik.providers.saml.utils.encoding import nice64
from authentik.providers.saml.views.flows import (
PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS,
PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS,
PLAN_CONTEXT_SAML_LOGOUT_REQUEST,
PLAN_CONTEXT_SAML_RELAY_STATE,
REQUEST_KEY_RELAY_STATE,
@@ -80,102 +68,7 @@ class SPInitiatedSLOView(PolicyAccessView):
**self.plan_context,
},
)
if self.provider.sls_url:
# Get logout request and extract relay state
logout_request = self.plan_context.get(PLAN_CONTEXT_SAML_LOGOUT_REQUEST)
relay_state = logout_request.relay_state if logout_request else None
# Store relay state for the logout response
plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
# Native mode - user will be redirected/posted away from authentik
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
)
if self.provider.sls_binding == SAMLBindings.POST:
logout_response = processor.encode_post()
logout_data = {
"post_url": self.provider.sls_url,
"saml_response": logout_response,
"saml_relay_state": relay_state,
"provider_name": self.provider.name,
"saml_binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"redirect_url": logout_url,
"provider_name": self.provider.name,
"saml_binding": SAMLBindings.REDIRECT,
}
plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
plan.append_stage(in_memory_stage(NativeLogoutStageView))
elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
# Backchannel mode - server sends logout response directly to SP in background
# No user interaction needed
if self.provider.sls_binding != SAMLBindings.POST:
LOGGER.warning(
"Backchannel logout requires POST binding, but provider is configured "
"with %s binding",
self.provider.sls_binding,
provider=self.provider,
)
# Queue the logout response to be sent in the background
# This doesn't block the user's logout from completing
send_saml_logout_response.send(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id=logout_request.id if logout_request else None,
relay_state=relay_state,
)
LOGGER.debug(
"Queued backchannel logout response",
provider=self.provider,
sls_url=self.provider.sls_url,
)
# Just end the session - no user interaction needed
plan.append_stage(in_memory_stage(SessionEndStage))
else:
# Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
)
logout_response = processor.build_response()
if self.provider.sls_binding == SAMLBindings.POST:
logout_data = {
"url": self.provider.sls_url,
"saml_response": nice64(logout_response),
"saml_relay_state": relay_state,
"provider_name": self.provider.name,
"binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"url": logout_url,
"provider_name": self.provider.name,
"binding": SAMLBindings.REDIRECT,
}
plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
plan.append_stage(in_memory_stage(IframeLogoutStageView))
plan.append_stage(in_memory_stage(SessionEndStage))
else:
# No SLS URL configured, just end session
plan.append_stage(in_memory_stage(SessionEndStage))
plan.append_stage(in_memory_stage(SessionEndStage))
# Remove samlsession from database
auth_session = AuthenticatedSession.from_request(self.request, self.request.user)

View File

@@ -380,29 +380,3 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
for x in user_ids
],
)
def discover(self):
res = self._request("GET", "/Groups")
seen_items = 0
expected_items = int(res["totalResults"])
while True:
for group in res["Resources"]:
self._discover_group_single(group)
seen_items += 1
if seen_items >= expected_items:
break
res = self._request("GET", f"/Groups?startIndex={seen_items + 1}")
def _discover_group_single(self, group: dict):
scim_group = SCIMGroupSchema.model_validate(group)
if SCIMProviderGroup.objects.filter(scim_id=scim_group.id, provider=self.provider).exists():
return
ak_group = Group.objects.filter(name=scim_group.displayName).first()
if not ak_group:
return
SCIMProviderGroup.objects.create(
provider=self.provider,
group=ak_group,
scim_id=scim_group.id,
attributes=group,
)

View File

@@ -65,7 +65,7 @@ class EnterpriseUser(BaseModel):
employeeNumber: str | None = Field(
None,
description="Numeric or alphanumeric identifier assigned to a person, "
"typically based on order of hire or association with an organization.",
"typically based on order of hire or association with anorganization.",
)
costCenter: str | None = Field(None, description="Identifies the name of a cost center.")
organization: str | None = Field(None, description="Identifies the name of an organization.")
@@ -73,7 +73,7 @@ class EnterpriseUser(BaseModel):
department: str | None = Field(
None,
description="Numeric or alphanumeric identifier assigned to a person,"
" typically based on order of hire or association with an organization.",
" typically based on order of hire or association with anorganization.",
)
manager: Manager | None = Field(
None,

View File

@@ -3,7 +3,6 @@
from typing import Any
from django.db import transaction
from django.db.models import Q
from django.utils.http import urlencode
from orjson import dumps
from pydantic import ValidationError
@@ -119,32 +118,3 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
)
connection.attributes = response
connection.save()
def discover(self):
res = self._request("GET", "/Users")
seen_items = 0
expected_items = int(res["totalResults"])
while True:
for user in res["Resources"]:
self._discover_user_single(user)
seen_items += 1
if seen_items >= expected_items:
break
res = self._request("GET", f"/Users?startIndex={seen_items+1}")
def _discover_user_single(self, user: dict):
scim_user = SCIMUserSchema.model_validate(user)
if SCIMProviderUser.objects.filter(scim_id=scim_user.id, provider=self.provider).exists():
return
user_query = Q(username=scim_user.userName)
for email in scim_user.emails:
user_query |= Q(username=email.value) | Q(email=email.value)
ak_user = User.objects.filter(user_query).first()
if not ak_user:
return
SCIMProviderUser.objects.create(
provider=self.provider,
user=ak_user,
scim_id=scim_user.id,
attributes=user,
)

View File

@@ -52,10 +52,10 @@ class SCIMApplicationPoliciesTests(TestCase):
email=f"{uid}@goauthentik.io",
)
self.users[1].groups.add(self.group1)
self.users[2].groups.add(self.group2)
self.users[4].groups.add(self.group1)
self.users[4].groups.add(self.group2)
self.users[1].ak_groups.add(self.group1)
self.users[2].ak_groups.add(self.group2)
self.users[4].ak_groups.add(self.group1)
self.users[4].ak_groups.add(self.group2)
def test_no_group_policy(self):
"""Test with no group policy set"""

View File

@@ -207,54 +207,6 @@ class SCIMGroupTests(TestCase):
self.assertEqual(mock.request_history[2].method, "GET")
self.assertNotIn("PUT", [req.method for req in mock.request_history])
@Mocker()
def test_discover(self, mock: Mocker):
group = Group.objects.create(name="acl_admins")
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
)
mock.get(
"https://localhost/Groups",
json={
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 2,
"startIndex": 1,
"itemsPerPage": 1,
"Resources": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "3",
"displayName": "acl_admins",
"meta": {"resourceType": "Group"},
"members": [],
},
],
},
)
mock.get(
"https://localhost/Groups?startIndex=2",
json={
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 2,
"startIndex": 2,
"itemsPerPage": 1,
"Resources": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "10",
"displayName": "test",
"meta": {"resourceType": "Group"},
"members": [],
},
],
},
)
self.provider.client_for_model(Group).discover()
connection = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first()
self.assertIsNotNone(connection)
self.assertEqual(connection.scim_id, "3")
def _create_stale_provider_group(self, scim_id: str) -> Group:
"""Create a group that is outside the provider's scope (via group_filters) with an
existing SCIMProviderGroup, simulating a previously synced group now out of scope."""

View File

@@ -464,6 +464,7 @@ class SCIMUserTests(TestCase):
def test_user_create_update_noop(self, mock: Mocker):
"""Test user creation and update"""
scim_id = generate_id()
mock: Mocker
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
@@ -539,64 +540,6 @@ class SCIMUserTests(TestCase):
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[1].method, "POST")
@Mocker()
def test_discover(self, mock: Mocker):
user = User.objects.create(username="admin@goauthentik.io")
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
)
mock.get(
"https://localhost/Users",
json={
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 2,
"startIndex": 1,
"itemsPerPage": 1,
"Resources": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "1",
"userName": "admin@goauthentik.io",
"name": {"givenName": "N/A", "familyName": "N/A"},
"emails": [
{"primary": True, "value": "admin@goauthentik.io", "type": "work"}
],
"meta": {"resourceType": "User"},
"sentryOrgRole": "owner",
"active": True,
},
],
},
)
mock.get(
"https://localhost/Users?startIndex=2",
json={
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 2,
"startIndex": 2,
"itemsPerPage": 1,
"Resources": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "2",
"userName": "jens@goauthentik.io",
"name": {"givenName": "N/A", "familyName": "N/A"},
"emails": [
{"primary": True, "value": "jens@goauthentik.io", "type": "work"}
],
"meta": {"resourceType": "User"},
"sentryOrgRole": "member",
"active": True,
},
],
},
)
self.provider.client_for_model(User).discover()
connection = SCIMProviderUser.objects.filter(provider=self.provider, user=user).first()
self.assertIsNotNone(connection)
self.assertEqual(connection.scim_id, "1")
def _create_stale_provider_user(self, scim_id: str, uid: str) -> User:
"""Create a service-account user (excluded from provider scope) with an existing
SCIMProviderUser, simulating a previously synced user that is now out of scope."""

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