Compare commits

..

258 Commits

Author SHA1 Message Date
Marcelo Elizeche Landó
119c9d018a lifecycle: use subprocess.run for ak script wrapper 2026-05-04 19:29:13 -03:00
Teffen Ellis
d6c0ae21de web: Clear remember me before navigation. (#21647)
* web: Clear remember me before navigation.

* web: fix stray > in "Not you?" link and add Playwright regression for #21571

Move the closing > of the opening <a> tag so the rendered link text no longer
carries a leading > glyph. Add a browser test that seeds the identification
stage with enable_remember_me, walks the identify -> password -> "Not you?"
path, and asserts the link text, the cleared username field, and the cleared
remember-me localStorage key.
Co-Authored-By: Agent (authentik-i21647-current-instant-chili) <279763771+playpen-agent@users.noreply.github.com>

* Flesh out remember me lifecycle. Fix edgecases where it doesn't keep up with the e2e suite.

* Fix for submit events, labels.

---------

Co-authored-by: Agent (authentik-i21647-current-instant-chili) <279763771+playpen-agent@users.noreply.github.com>
2026-04-29 23:54:42 +02:00
dependabot[bot]
2c35df35b6 web: bump knip from 6.4.1 to 6.6.0 in /web (#21957)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 6.4.1 to 6.6.0.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.6.0/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 6.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-29 12:37:12 +02:00
dependabot[bot]
90d4f4296b core: bump github.com/getsentry/sentry-go from 0.45.1 to 0.46.0 (#21955)
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.45.1 to 0.46.0.
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.45.1...v0.46.0)

---
updated-dependencies:
- dependency-name: github.com/getsentry/sentry-go
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-29 12:36:43 +02:00
dependabot[bot]
bf7747268b core: bump uvicorn[standard] from 0.44.0 to 0.45.0 (#21956)
Bumps [uvicorn[standard]](https://github.com/Kludex/uvicorn) from 0.44.0 to 0.45.0.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.44.0...0.45.0)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-29 12:35:09 +02:00
dependabot[bot]
552cb78458 core: bump rustls from 0.23.39 to 0.23.40 (#21958)
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.39 to 0.23.40.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.39...v/0.23.40)

---
updated-dependencies:
- dependency-name: rustls
  dependency-version: 0.23.40
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-29 12:34:27 +02:00
Dominic R
899994027d core: support hashed password in users API + automated install (#18686)
* core: add hash_password command and password_hash bootstrap support

* core: prevent hash format exposure in validation error

* core: remove redundant password length check

* core: remove extra blank lines from hash_password command

* core: add password_hash serializer tests, refine validation and imports

* core: add null password fields test, add hash warning to docs

* core: move hash validation to User.set_password_from_hash method

* core: emit password_changed signal in set_password_from_hash

* website: remove redundant hash security warning

* core: wrap conflict error message for translation

* core: wrap invalid hash error message for translation

* web, core: add set_password_hash API endpoint and admin UI

* core: simplify password_hash check to None comparison

* core: use None check for password conflict validation

* website: clarify Docker Compose $ escaping for .env vs compose.yml

* website: lint

* web: lint

* core: add nosec comment for empty password string in signal

* core: lint

* web: Fix Password Hash help text

* sources/kerberos,ldap: Gergo's review

* add testing for ^^ and type fix

* more general signal tests; not provider specific

* only used in tests

* add warning

* we can do this

* signals fix????

* core, web, website: review fixes

* style(docs): format automated install guide

* web: restore modal invoker import after rebase

Co-authored-by: Codex <codex@openai.com>

* fix generated clients

* core: trim hash password command tests

* core: add password hash permission

* core: cover service account password hashes

* web: remove password hash form

* core: regenerate password hash migration

* core: reuse password serializer for hashes

* docs: clarify hashed password imports

* Regenerate

* core: deduplicate user serializer writes

* core: deduplicate password update actions

* core: deduplicate password change signaling

* tests: reuse password hash API helper

* tests: reuse SSF credential assertions

* docs: centralize hashed password caveat

* core: name password hash signal source

* core: centralize password hash validation

* core: deduplicate serializer password saves

* docs: link source writeback caveats

* api: clarify password hash request field

* tests: deduplicate password hash API assertions

* web: reuse user display-name helper

* web: use existing user display formatter

* core: reuse reset password permission for hash endpoint

* core: keep separate password hash serializer

* tests: remove redundant password hash permission test

* 21745

Co-authored-by: Gergo <gergo@goauthentik.io>

* core: preserve empty password handling in user serializer

* core: inline blueprint user serializer fields

* Use password hash constant

* Simplify user serializer flow

* Inline password update handling

* Apply serializer cleanup

* Clean blueprint password handling

* Drop extra returns

* Split password hash signal

* Align hash signal receivers

* Remove stale password guards

* Inline password signal

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Gergo <gergo@goauthentik.io>
2026-04-29 06:27:59 +02:00
authentik-automation[bot]
99250b0498 core, web: update translations (#21952)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-04-29 04:08:12 +02:00
Connor Peshek
a2ca19d718 providers/saml: generate issuer url when provider is set on app (#18022)
* providers/saml: generate issuer url in saml processors unless overridded

* remove issuer

* remove duplicate

* Generate url when assertion is created and save to session

* cleanup

* Fix front-end rendering of issuer

* Update web/src/admin/providers/saml/SAMLProviderViewPage.ts

Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* Update authentik/providers/saml/models.py

Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* Update authentik/providers/saml/models.py

Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Connor Peshek <connor@connorpeshek.me>

* use reverse for urls and update tests

* update issuer description

* Don't absorb sp entity id

* rename issuer_url to issuer_override

* fix migration file to rename to override

* fix migration file order

* lint, fix tests

* fix tests

* fix once again not importing the sp issuer

* build

* use const for default issuer

---------

Signed-off-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: connor peshek <connorpeshek@connors-MacBook-Pro.local>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-04-28 17:31:12 -05:00
Marc 'risson' Schmitt
aed634734b root: fix rust build with uv-installed Python (#21858) 2026-04-28 18:11:22 +02:00
Marcelo Elizeche Landó
05005f4eb9 core: add support for hiding applications from the user dashboard (#21530)
* Add meta_hide field to hide apps

* exclude hidden applications from user dashboard

* Add the hide option to the UI

* Add schema

* Add hide setting to application wizard

* Add typescript client changes

* fix linting

* Convert blank://blank to meta_hide=True in the migration

* fix tests

* update docs

* fix continuous login

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

* Apply suggestions from code review

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* fix linting

* fix migrations

* Apply suggestions from code review

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* rename all mentions of dashboard to My applications

* generate schema

* generate TS client

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-04-28 13:05:56 -03:00
dependabot[bot]
baf61056c7 core: bump ruff from 0.15.11 to 0.15.12 (#21871)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.11 to 0.15.12.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.11...0.15.12)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 16:36:23 +02:00
Marc 'risson' Schmitt
e4b0ea7d15 packages/ak-axum/router: add X-Powered-By to all responses (#21940) 2026-04-28 15:35:17 +02:00
Marcelo Elizeche Landó
740a5b85e3 core: bump microsoft-kiota-serialization-form from 1.9.8 to v1.10.1 (#21909) 2026-04-28 13:12:37 +00:00
dependabot[bot]
8fd17966ab core: bump pytest-randomly from 4.0.1 to 4.1.0 (#21873)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 13:09:34 +00:00
Ryan Pesek
e63ff698da core: users/groups reduce number of database queries (#20431)
* reduce number of db queries

* optimize group membership updates too

* further optimize include_user=false and also members_by_pk

* lint

---------

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-04-28 13:00:38 +00:00
dependabot[bot]
7d0ec4de23 core: bump types-channels from 4.3.0.20260408 to 4.3.0.20260421 (#21872)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 14:09:08 +02:00
dependabot[bot]
b2b5f6400d ci: bump taiki-e/install-action from 2.75.21 to 2.75.22 in /.github/actions/setup (#21877)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 14:08:44 +02:00
authentik-automation[bot]
94ce30adb5 core, web: update translations (#21870)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-04-28 14:08:06 +02:00
slavb18
52c573bfe2 sources/oauth: ensure user ID is returned as str (#21880) 2026-04-28 14:07:37 +02:00
transifex-integration[bot]
74e2c63888 translate: Updates for project authentik and language no_NO (#21862)
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2026-04-28 14:06:17 +02:00
Marcelo Elizeche Landó
b390b679b7 core: bump maxminddb from 3.0.0 to v3.1.1 (#21907) 2026-04-28 13:57:40 +02:00
Marcelo Elizeche Landó
501b851f3f core: bump prometheus-client from 0.24.0 to v0.25.0 (#21919) 2026-04-28 13:57:03 +02:00
Marcelo Elizeche Landó
f75a03e4ba core: bump azure-identity from 1.25.1 to v1.25.3 (#21886) 2026-04-28 13:56:37 +02:00
Marcelo Elizeche Landó
95a7d8c92d core: bump aiohttp from 3.13.4 to v3.13.5 (#21882) 2026-04-28 13:56:24 +02:00
Marcelo Elizeche Landó
27d1be0b85 core: bump anyio from 4.12.1 to v4.13.0 (#21883) 2026-04-28 13:56:21 +02:00
Marcelo Elizeche Landó
7ee653cf0a core: bump asgiref from 3.11.0 to v3.11.1 (#21884) 2026-04-28 13:56:18 +02:00
Marcelo Elizeche Landó
079017c799 core: bump azure-core from 1.38.0 to v1.39.0 (#21885) 2026-04-28 13:56:16 +02:00
Marcelo Elizeche Landó
d647976b98 core: bump blessed from 1.25.0 to v1.38.0 (#21887) 2026-04-28 13:56:09 +02:00
Marcelo Elizeche Landó
46862cca22 core: bump boto3 from 1.42.26 to v1.42.97 (#21888) 2026-04-28 13:56:07 +02:00
Marcelo Elizeche Landó
edce545f0d core: bump certifi from 2026.1.4 to v2026.4.22 (#21889) 2026-04-28 13:56:03 +02:00
Marcelo Elizeche Landó
028b711746 core: bump charset-normalizer from 3.4.4 to v3.4.7 (#21890) 2026-04-28 13:56:00 +02:00
Marcelo Elizeche Landó
fa18e71ca4 core: bump microsoft-kiota-serialization-text from 1.9.8 to v1.9.10 (#21912) 2026-04-28 13:54:05 +02:00
Marcelo Elizeche Landó
1fd472c0d9 core: bump librt from 0.8.1 to v0.9.0 (#21905) 2026-04-28 13:54:02 +02:00
Marcelo Elizeche Landó
b4adb7ee70 core: bump jsii from 1.127.0 to v1.128.0 (#21903) 2026-04-28 13:53:59 +02:00
Marcelo Elizeche Landó
98f6726d95 core: bump click from 8.3.1 to v8.3.3 (#21891) 2026-04-28 13:53:46 +02:00
Marcelo Elizeche Landó
ced8db0b65 core: bump django-stubs-ext from 6.0.2 to v6.0.3 (#21892) 2026-04-28 13:53:43 +02:00
Marcelo Elizeche Landó
d8e575c631 core: bump google-api-core from 2.29.0 to v2.30.3 (#21893) 2026-04-28 13:53:40 +02:00
Marcelo Elizeche Landó
b7c03362a3 core: bump google-auth from 2.47.0 to v2.49.2 (#21894) 2026-04-28 13:53:37 +02:00
Marcelo Elizeche Landó
1ca4a34da7 core: bump google-auth-httplib2 from 0.3.0 to v0.3.1 (#21895) 2026-04-28 13:53:34 +02:00
Marcelo Elizeche Landó
94c3686065 core: bump googleapis-common-protos from 1.72.0 to v1.74.0 (#21896) 2026-04-28 13:53:31 +02:00
Marcelo Elizeche Landó
3b61bf04d2 core: bump greenlet from 3.3.0 to v3.5.0 (#21897) 2026-04-28 13:53:28 +02:00
Marcelo Elizeche Landó
e310468a6e core: bump httplib2 from 0.31.1 to v0.31.2 (#21898) 2026-04-28 13:53:25 +02:00
Marcelo Elizeche Landó
0947d38f0b core: bump idna from 3.11 to v3.13 (#21899) 2026-04-28 13:53:22 +02:00
Marcelo Elizeche Landó
f207491cf6 core: bump importlib-resources from 6.5.2 to v7.1.0 (#21900) 2026-04-28 13:53:18 +02:00
Marcelo Elizeche Landó
83294f4866 core: bump invoke from 2.2.1 to v3.0.3 (#21901) 2026-04-28 13:53:15 +02:00
Marcelo Elizeche Landó
4af2d51f50 core: bump jmespath from 1.0.1 to v1.1.0 (#21902) 2026-04-28 13:53:12 +02:00
Marcelo Elizeche Landó
87bd0d7436 core: bump jsonpointer from 3.0.0 to v3.1.1 (#21904) 2026-04-28 13:53:05 +02:00
Marcelo Elizeche Landó
cf0c2881b1 core: bump markdown2 from 2.5.4 to v2.5.5 (#21906) 2026-04-28 13:52:58 +02:00
Marcelo Elizeche Landó
9fe96b6e82 core: bump microsoft-kiota-abstractions from 1.9.8 to v1.9.10 (#21908) 2026-04-28 13:52:55 +02:00
Marcelo Elizeche Landó
48084c0051 core: bump microsoft-kiota-serialization-json from 1.9.8 to v1.9.10 (#21910) 2026-04-28 13:52:48 +02:00
Marcelo Elizeche Landó
1c057517c2 core: bump microsoft-kiota-serialization-multipart from 1.9.8 to v1.9.10 (#21911) 2026-04-28 13:52:45 +02:00
Marcelo Elizeche Landó
a529f2be86 core: bump msal from 1.34.0 to v1.36.0 (#21913) 2026-04-28 13:52:38 +02:00
Marcelo Elizeche Landó
9fdad4d686 core: bump platformdirs from 4.5.1 to v4.9.6 (#21918) 2026-04-28 13:51:55 +02:00
Marcelo Elizeche Landó
d56ff32732 core: bump requests from 2.33.0 to v2.33.1 (#21924) 2026-04-28 13:51:38 +02:00
Marcelo Elizeche Landó
95dd492555 core: bump trio from 0.32.0 to v0.33.0 (#21930) 2026-04-28 13:51:24 +02:00
Marcelo Elizeche Landó
033668373d core: bump multidict from 6.7.0 to v6.7.1 (#21914) 2026-04-28 13:51:21 +02:00
Marcelo Elizeche Landó
c358a4a6e5 core: bump s3transfer from 0.16.0 to v0.16.1 (#21926) 2026-04-28 13:51:07 +02:00
Marcelo Elizeche Landó
565f5cf9c1 core: bump zipp from 3.23.0 to v3.23.1 (#21937) 2026-04-28 13:50:54 +02:00
Marcelo Elizeche Landó
f4807135e5 core: bump opentelemetry-api from 1.39.1 to v1.41.1 (#21915) 2026-04-28 13:50:46 +02:00
Marcelo Elizeche Landó
a96445cdf8 core: bump orjson from 3.11.6 to v3.11.8 (#21916) 2026-04-28 13:50:43 +02:00
Marcelo Elizeche Landó
858ac8d5ff core: bump pathspec from 1.0.3 to v1.1.1 (#21917) 2026-04-28 13:50:38 +02:00
Marcelo Elizeche Landó
fb060d89af core: bump proto-plus from 1.27.0 to v1.27.2 (#21920) 2026-04-28 13:50:32 +02:00
Marcelo Elizeche Landó
682ed056dd core: bump protobuf from 6.33.5 to v6.33.6 (#21921) 2026-04-28 13:50:29 +02:00
Marcelo Elizeche Landó
be2cba2068 core: bump pycparser from 2.23 to v3.0 (#21922) 2026-04-28 13:50:26 +02:00
Marcelo Elizeche Landó
5bb8a1e341 core: bump pyparsing from 3.3.1 to v3.3.2 (#21923) 2026-04-28 13:50:22 +02:00
Marcelo Elizeche Landó
8cbe1bfdd7 core: bump rich from 14.2.0 to v15.0.0 (#21925) 2026-04-28 13:50:15 +02:00
Marcelo Elizeche Landó
93d615f0f4 core: bump setuptools from 80.9.0 to v82.0.1 (#21927) 2026-04-28 13:50:09 +02:00
Marcelo Elizeche Landó
17cdb82f15 core: bump stevedore from 5.6.0 to v5.7.0 (#21928) 2026-04-28 13:50:05 +02:00
Marcelo Elizeche Landó
007fa940d9 core: bump tenacity from 9.1.2 to v9.1.4 (#21929) 2026-04-28 13:50:02 +02:00
Marcelo Elizeche Landó
29e82d4985 core: bump types-paramiko from 4.0.0.20250822 to v4.0.0.20260408 (#21931) 2026-04-28 13:49:54 +02:00
Marcelo Elizeche Landó
f2fd092e8a core: bump types-pyasn1 from 0.6.0.20250914 to v0.6.0.20260408 (#21932) 2026-04-28 13:49:51 +02:00
Marcelo Elizeche Landó
3f179a25d7 core: bump types-pyyaml from 6.0.12.20250915 to v6.0.12.20260408 (#21933) 2026-04-28 13:49:48 +02:00
Marcelo Elizeche Landó
eba970dd03 core: bump ua-parser-builtins from 202601 to v202603 (#21934) 2026-04-28 13:49:44 +02:00
Marcelo Elizeche Landó
21447c461c core: bump wcwidth from 0.2.14 to v0.6.0 (#21935) 2026-04-28 13:49:41 +02:00
Marcelo Elizeche Landó
96c203757c core: bump yarl from 1.22.0 to v1.23.0 (#21936) 2026-04-28 13:49:38 +02:00
Marcelo Elizeche Landó
61b345e577 core: bump zope-interface from 8.2 to v8.4 (#21938) 2026-04-28 13:49:30 +02:00
Marc 'risson' Schmitt
2a027264b3 packages/ak-axum/accept/catch_panic: add acceptor to catch panics in lower acceptors, streams and services (#21860) 2026-04-27 16:40:50 +00:00
Félix MARQUET
fe4a7d2c5f website/integrations: update jellyseerr to seerr (#21855)
web: update jellyseerr doc to seerr
2026-04-27 15:08:08 +00:00
Marc 'risson' Schmitt
71af5e40a3 lifecycle/container: only mount required packages directories (#21859) 2026-04-27 17:00:05 +02:00
Marc 'risson' Schmitt
3e75278052 packages/ak-common/config: fix string load broken after previous fix (#21854) 2026-04-27 14:03:55 +00:00
Dominic R
620387f294 providers/scim: fix vCenter compatibility mode (#21830) 2026-04-27 12:00:00 +00:00
dependabot[bot]
9dfc4e76ee web: bump type-fest from 5.5.0 to 5.6.0 in /web (#21841)
Bumps [type-fest](https://github.com/sindresorhus/type-fest) from 5.5.0 to 5.6.0.
- [Release notes](https://github.com/sindresorhus/type-fest/releases)
- [Commits](https://github.com/sindresorhus/type-fest/compare/v5.5.0...v5.6.0)

---
updated-dependencies:
- dependency-name: type-fest
  dependency-version: 5.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 13:47:33 +02:00
dependabot[bot]
85f0ab899e web: bump the bundler group across 1 directory with 3 updates (#21839)
Bumps the bundler group with 1 update in the /web directory: [@vitest/browser](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser).


Updates `@vitest/browser` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/browser)

Updates `@vitest/browser-playwright` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/browser-playwright)

Updates `vitest` from 4.1.4 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest)

---
updated-dependencies:
- dependency-name: "@vitest/browser"
  dependency-version: 4.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: "@vitest/browser-playwright"
  dependency-version: 4.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: vitest
  dependency-version: 4.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 13:47:01 +02:00
dependabot[bot]
72c76bb95b core: bump msgraph-sdk from 1.55.0 to 1.56.0 (#21836)
Bumps [msgraph-sdk](https://github.com/microsoftgraph/msgraph-sdk-python) from 1.55.0 to 1.56.0.
- [Release notes](https://github.com/microsoftgraph/msgraph-sdk-python/releases)
- [Changelog](https://github.com/microsoftgraph/msgraph-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoftgraph/msgraph-sdk-python/compare/v1.55.0...v1.56.0)

---
updated-dependencies:
- dependency-name: msgraph-sdk
  dependency-version: 1.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 12:46:52 +01:00
dependabot[bot]
9ea465441b core: bump cryptography from 46.0.7 to 47.0.0 (#21837)
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.7 to 47.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.7...47.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 47.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 12:46:48 +01:00
dependabot[bot]
3c9d682eb3 core: bump mypy from 1.20.1 to 1.20.2 (#21838)
Bumps [mypy](https://github.com/python/mypy) from 1.20.1 to 1.20.2.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.20.1...v1.20.2)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.20.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 12:46:43 +01:00
dependabot[bot]
32de314485 core: bump library/golang from 982ae92 to 4a7137e in /lifecycle/container (#21840)
core: bump library/golang in /lifecycle/container

Bumps library/golang from `982ae92` to `4a7137e`.

---
updated-dependencies:
- dependency-name: library/golang
  dependency-version: 1.26.2-trixie
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 12:46:38 +01:00
dependabot[bot]
3a9211f248 web: bump dompurify from 3.4.0 to 3.4.1 in /web (#21843)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.0 to 3.4.1.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.0...3.4.1)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 12:46:34 +01:00
dependabot[bot]
26cfdf59c9 ci: bump int128/docker-manifest-create-action from 2.18.0 to 2.19.0 (#21844)
Bumps [int128/docker-manifest-create-action](https://github.com/int128/docker-manifest-create-action) from 2.18.0 to 2.19.0.
- [Release notes](https://github.com/int128/docker-manifest-create-action/releases)
- [Commits](3de37de96c...7df7f9e221)

---
updated-dependencies:
- dependency-name: int128/docker-manifest-create-action
  dependency-version: 2.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 12:46:30 +01:00
dependabot[bot]
b6f9013977 ci: bump taiki-e/install-action from 2.75.19 to 2.75.21 in /.github/actions/setup (#21845)
ci: bump taiki-e/install-action in /.github/actions/setup

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.19 to 2.75.21.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](5f57d6cb7c...787505cde8)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 12:46:26 +01:00
dependabot[bot]
3133f8cbda core: bump hyper-unix-socket from 0.3.0 to 0.6.1 (#21846)
Bumps [hyper-unix-socket](https://github.com/kristof-mattei/hyper-unix-socket) from 0.3.0 to 0.6.1.
- [Release notes](https://github.com/kristof-mattei/hyper-unix-socket/releases)
- [Changelog](https://github.com/kristof-mattei/hyper-unix-socket/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kristof-mattei/hyper-unix-socket/compare/v0.3.0...v0.6.1)

---
updated-dependencies:
- dependency-name: hyper-unix-socket
  dependency-version: 0.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 12:46:22 +01:00
Jens L.
b66024f26f web/packages: Rework SFE rendering (#21833)
* rework sfe to use old lit

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

* rework and cleanup some more

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-27 13:38:19 +02:00
Jens L.
8f1bdc01b6 providers/oauth2: Configure allowed grant types (#20363)
* naming cleanup

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

* add

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

* adjust defaults, start adding tests

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

* more tests

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

* fix tests

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

* fix tests

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

* gen

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

* fix proxy

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

* add UI

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

* attempt to fix e2e

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

* allow refresh token for conformance

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

* fix e2e

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-27 13:36:57 +02:00
Marc 'risson' Schmitt
5c3cd2c6ed packages/ak-common/config: fix boolean parsing from env variable (#21835) 2026-04-27 12:53:47 +02:00
Marc 'risson' Schmitt
97c9626bd4 root: init rust worker (#21324) 2026-04-27 01:08:32 +02:00
transifex-integration[bot]
3d7ff2cfef translate: Updates for project authentik and language de_DE (#21825)
* translate: Translate django.po in de_DE

100% translated source file: 'django.po'
on 'de_DE'.

* fix

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-26 12:49:59 +02:00
Jens L.
311954f920 providers/radius: fix message authenticator validation (#21824)
* providers/radius: fix message authenticator validation

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

* fix panic

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

* send message auth

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-25 20:53:29 +02:00
dependabot[bot]
ea4848c7c6 web: bump postcss from 8.5.8 to 8.5.10 in /web (#21819)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.10.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 11:28:06 +02:00
dependabot[bot]
2fd9a09055 web: bump brace-expansion from 1.1.13 to 1.1.14 (#21820)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.13 to 1.1.14.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.13...v1.1.14)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 11:27:27 +02:00
dependabot[bot]
b07b71f528 web: bump postcss from 8.5.8 to 8.5.10 (#21821)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.10.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 11:27:09 +02:00
Jens L.
c058363180 website/docs: improve social login docs titles (#21816)
* website/docs: improve social login docs titles

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

* sigh twitter

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-04-24 17:40:27 +02:00
Sai Asish Y
b5a92b783f providers/oauth2: require client_secret on device_code exchange for confidential clients (#21700)
* providers/oauth2: require client_secret on device_code exchange for confidential clients

TokenParams.__post_init__ only ran the client_secret check for the
authorization_code and refresh_token grant types:

	if self.grant_type in [GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_REFRESH_TOKEN]:
		if self.provider.client_type == ClientTypes.CONFIDENTIAL and not compare_digest(
			self.provider.client_secret, self.client_secret,
		):
			raise TokenError("invalid_client")

The device_code path (__post_init_device_code) then looked up the
DeviceToken solely by device_code and issued an access token if one
matched. A caller that knows the client_id and has stolen a
device_code (e.g. via the standard phishing flow: attacker starts
device authorization, sends user_code to a victim, victim completes
authorization, attacker redeems the device_code) did not have to
prove ownership of the confidential client.

RFC 6749 Section 2.3.1 requires confidential clients to authenticate
to the token endpoint, and RFC 8628 Section 3.4 inherits that: the
device_code is bearer-shaped but not a substitute for client
credentials. Keycloak and Okta both enforce client_secret on the
device token exchange for confidential clients; we didn't.

Add GRANT_TYPE_DEVICE_CODE to the list so the existing compare_digest
check runs for it too. Public clients are unaffected (the guard is
gated on ClientTypes.CONFIDENTIAL). client_credentials/password keep
their own client-auth path in __post_init_client_credentials, which
also enforces the secret (and supports client assertion).

Fixes #20828

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* Apply suggestion from @BeryJu

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

* update tests

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

---------

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Signed-off-by: Jens L. <jens@beryju.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>
Co-authored-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-24 17:23:36 +02:00
Marc 'risson' Schmitt
a4c60ece8b lifecycle/container: allow cross-compilation from arm64 to amd64 (#21817)
Co-authored-by: João C. Fernandes <jfernandes@cloudflare.com>
2026-04-24 17:00:46 +02:00
Jens L.
d1d38edb50 enterprise/endpoints/connectors: Fleet conditional access stage (#20978)
* rework mtls stage to be more modular

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

* sync fleet conditional access CA to authentik

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

* save host uuid

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

* initial stage impl

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

* add fixtures & tests

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

* add lookup

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

* migrate to parsing mobileconfig

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

* directly use stage_invalid

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

* add tests

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

* add more test

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

* test team mapping

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

* fix endpoint test

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

* cleanup

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

* Add document for this. Update sidebar.

* Doc improvement

* Add note about Fleet licensing

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

* re-fix tests after mtls traefik encoding change

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

* Add info about fleet and device config. Add link from fleet connector doc.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-04-24 16:17:00 +02:00
Jens L.
c6ee7b6881 core: complete rework to oobe and setup experience (#21753)
* initial

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

* use same startup template

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

* fix check not working

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

* unrelated: fix inspector auth

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

* add tests

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

* update docs

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

* ensure oobe flow can only accessed via correct url

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

* set setup flag when applying bootstrap blueprint when env is set

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

* add system visibility to flags to make them non-editable

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

* set setup flag for e2e tests

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

* fix tests and linting

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

* fix tests

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

* make github lint happy

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

* make tests have less assumptions

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

* Update docs

* include more heuristics in migration

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

* add management command to set any flag

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

* migrate worker command to signal

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

* improved api for setting flags

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

* short circuit

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-04-24 14:47:05 +02:00
dependabot[bot]
0459568a96 core: bump github.com/Azure/go-ntlmssp from 0.1.0 to 0.1.1 in the go_modules group across 1 directory (#21807)
core: bump github.com/Azure/go-ntlmssp

Bumps the go_modules group with 1 update in the / directory: [github.com/Azure/go-ntlmssp](https://github.com/Azure/go-ntlmssp).


Updates `github.com/Azure/go-ntlmssp` from 0.1.0 to 0.1.1
- [Release notes](https://github.com/Azure/go-ntlmssp/releases)
- [Commits](https://github.com/Azure/go-ntlmssp/compare/v0.1.0...v0.1.1)

---
updated-dependencies:
- dependency-name: github.com/Azure/go-ntlmssp
  dependency-version: 0.1.1
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:39:57 +01:00
dependabot[bot]
aa746e7585 lifecycle/aws: bump aws-cdk from 2.1118.3 to 2.1118.4 in /lifecycle/aws (#21808)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1118.3 to 2.1118.4.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1118.4/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1118.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:39:53 +01:00
dependabot[bot]
a4dcf097b3 core: bump pydantic from 2.13.2 to 2.13.3 (#21809)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.13.2 to 2.13.3.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.13.2...v2.13.3)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:39:48 +01:00
dependabot[bot]
c2ecff559c web: bump @sentry/browser from 10.48.0 to 10.49.0 in /web in the sentry group across 1 directory (#21810)
web: bump @sentry/browser in /web in the sentry group across 1 directory

Bumps the sentry group with 1 update in the /web directory: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 10.48.0 to 10.49.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.48.0...10.49.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-version: 10.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:39:43 +01:00
dependabot[bot]
c20ecb48f8 core: bump cachetools from 7.0.5 to 7.0.6 (#21811)
Bumps [cachetools](https://github.com/tkem/cachetools) from 7.0.5 to 7.0.6.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v7.0.5...v7.0.6)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-version: 7.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:39:39 +01:00
dependabot[bot]
34a50ad46e ci: bump calibreapp/image-actions from 4f7260f5dbd809ec86d03721c1ad71b8a841d3e0 to e2cc8db5d49c849e00844dfebf01438318e96fa2 (#21812)
ci: bump calibreapp/image-actions

Bumps [calibreapp/image-actions](https://github.com/calibreapp/image-actions) from 4f7260f5dbd809ec86d03721c1ad71b8a841d3e0 to e2cc8db5d49c849e00844dfebf01438318e96fa2.
- [Release notes](https://github.com/calibreapp/image-actions/releases)
- [Commits](4f7260f5db...e2cc8db5d4)

---
updated-dependencies:
- dependency-name: calibreapp/image-actions
  dependency-version: e2cc8db5d49c849e00844dfebf01438318e96fa2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:39:34 +01:00
dependabot[bot]
99410f3775 web: bump @patternfly/elements from 4.3.1 to 4.4.0 in /web (#21813)
Bumps [@patternfly/elements](https://github.com/patternfly/patternfly-elements/tree/HEAD/elements) from 4.3.1 to 4.4.0.
- [Release notes](https://github.com/patternfly/patternfly-elements/releases)
- [Changelog](https://github.com/patternfly/patternfly-elements/blob/main/elements/CHANGELOG.md)
- [Commits](https://github.com/patternfly/patternfly-elements/commits/@patternfly/elements@4.4.0/elements)

---
updated-dependencies:
- dependency-name: "@patternfly/elements"
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:39:30 +01:00
dependabot[bot]
86de4955aa ci: bump taiki-e/install-action from 2.75.18 to 2.75.19 in /.github/actions/setup (#21814)
ci: bump taiki-e/install-action in /.github/actions/setup

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.18 to 2.75.19.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](055f5df8c3...5f57d6cb7c)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 11:39:26 +01:00
dependabot[bot]
bea9b23555 lifecycle/aws: bump aws-cdk from 2.1118.2 to 2.1118.3 in /lifecycle/aws (#21801)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 18:09:55 +02:00
dependabot[bot]
9820ee1d67 core: bump rustls from 0.23.38 to 0.23.39 (#21802)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 14:18:04 +00:00
Marc 'risson' Schmitt
1379637389 ci: add rustls and aws-lc ecosystem crates to delay ignore list (#21800) 2026-04-23 13:42:25 +00:00
Dominic R
39e6c41566 admin/files: sign custom-domain S3 URLs for the final host (#21704) 2026-04-23 15:23:05 +02:00
Sai Asish Y
92a2d26c86 core: survive the empty-queryset race in chunked_queryset (#21666) 2026-04-23 15:21:57 +02:00
Simonyi Gergő
0f8d8c81d7 core: simplify boolean (#21790) 2026-04-23 14:47:23 +02:00
Sai Asish Y
cce646b132 providers/oauth2: clip device authorization scope against the provider's ScopeMapping set (#21701)
* providers/oauth2: clip device authorization scope against the provider's ScopeMapping set

DeviceView.parse_request stored the raw request scope straight onto the
DeviceToken:

	self.scopes = self.request.POST.get("scope", "").split(" ")
	...
	token = DeviceToken.objects.create(..., _scope=" ".join(self.scopes))

The token-exchange side then reads those scopes back directly:

	if SCOPE_OFFLINE_ACCESS in self.params.device_code.scope:
		refresh_token = RefreshToken(...)
		...

so a caller that adds offline_access to the device authorization
request body gets a refresh_token at the exchange, even when the
provider has no offline_access ScopeMapping configured. Every other
grant type clips scope against ScopeMapping for the provider inside
TokenParams.__check_scopes, but the device authorization endpoint
runs before TokenParams is ever constructed, so the clip never
happens for the device flow.

Combined with #20828 (missing client_secret verification on device
code exchange for confidential clients, now being fixed separately)
and the lack of per-app opt-out for the device flow, this gives any
caller that knows the client_id a path to an offline refresh token
against any OIDC application the deployment exposes.

Intersect the requested scope set with the provider's ScopeMapping
names before we ever persist the DeviceToken. offline_access that is
not configured is silently dropped, matching __check_scopes on the
other grant types. Configured offline_access still flows through
unchanged.

Fixes #20825

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* rework and add tests

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

---------

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-23 13:44:44 +02:00
dependabot[bot]
6d274d1e3d core: bump library/nginx from 3acc8b9 to 6e23479 in /website (#21794)
Bumps library/nginx from `3acc8b9` to `6e23479`.

---
updated-dependencies:
- dependency-name: library/nginx
  dependency-version: 1.29-trixie
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 11:20:09 +02:00
dependabot[bot]
8d5489e441 core: bump library/node from b272ff1 to 74ff139 in /website (#21795)
Bumps library/node from `b272ff1` to `74ff139`.

---
updated-dependencies:
- dependency-name: library/node
  dependency-version: 25.9.0-trixie
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 11:19:56 +02:00
dependabot[bot]
8ea9a48017 core: bump library/golang from cd8540d to 982ae92 in /lifecycle/container (#21793)
core: bump library/golang in /lifecycle/container

Bumps library/golang from `cd8540d` to `982ae92`.

---
updated-dependencies:
- dependency-name: library/golang
  dependency-version: 1.26.2-trixie
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 10:19:37 +01:00
Sai Asish Y
c6b5869b48 stages/user_write: refuse to write id/pk claims onto the user model (#21667)
* stages/user_write: refuse to write id/pk claims onto the user model

When an enrollment or source flow maps IdP-supplied attributes onto the
User model, update_user walks each key and, if the user already has an
attribute by that name, calls setattr(user, key, value) unconditionally.
"id" is always present on the User model (it is the Django PK), so a
SAML assertion that ships an "id" claim, e.g. a hex string from
mocksaml, was written straight into the PK field. Django then rejected
the save:

  ValueError: Field 'id' expected a number but got '<hex>'.

The log surfaced as "Failed to save user" and the enrollment flow
silently failed for every incoming user.

Treat "id" and "pk" the same way the existing "groups" entry is
treated: add them to disallowed_user_attributes so the walker logs and
skips them. IdP attributes can still be stored on user.attributes via
the dotted/underscored forms (e.g. attributes.id), which go through
write_attribute and land in the JSONField safely.

Added a regression test covering both id and pk in the prompt context.

Fixes #21580

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* fix lint

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

---------

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-23 11:03:12 +02:00
authentik-automation[bot]
e4971f9aa5 core, web: update translations (#21785)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-04-23 10:39:13 +02:00
Dominic R
028ec05a8b website: Merge branch (#21684)
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-04-23 01:46:10 +00:00
Ryan Pesek
b4c9ac57e0 core/applications: Optimize list applications when only_with_launch_url=true (#20428)
* Performance optimizations for the application list API endpoint when only_with_launch_url=true

* lint

---------

Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-04-23 03:15:16 +02:00
Dewi Roberts
80b93e1fbc website/docs: add authorization header info to all proxy configs (#21664)
Add authorization header info to all proxy configs
2026-04-23 02:35:02 +02:00
dependabot[bot]
dff6b48f53 web: bump @xmldom/xmldom from 0.8.12 to 0.8.13 in /web (#21784)
Bumps [@xmldom/xmldom](https://github.com/xmldom/xmldom) from 0.8.12 to 0.8.13.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.8.12...0.8.13)

---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
  dependency-version: 0.8.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 02:33:20 +02:00
gp-somni-labs
79473341d6 internal/outpost: serialize websocket writes to prevent panic (#21728)
The outpost API controller shares a single *websocket.Conn across
multiple goroutines: the event-handler loop, the 10s health ticker
(SendEventHello), the shutdown path (WriteMessage close), initEvent
writing the hello frame on (re)connect, and RAC session handlers that
also invoke SendEventHello. gorilla/websocket explicitly documents that
concurrent WriteMessage/WriteJSON calls are unsafe and will panic with
"concurrent write to websocket connection", which takes the outpost
(and embedded-outpost authentik-server) pod down.

Fix by adding a sync.Mutex on APIController guarding every write path
on eventConn (initEvent hello, Shutdown close message, SendEventHello).
Reads (ReadJSON in startEventHandler) are left unsynchronized as
gorilla permits a single concurrent reader alongside a writer.

Minimal, localized change: no API changes, no behavior changes, writes
are already infrequent so lock contention is negligible.

Refs #11090

Co-authored-by: curiosity <curiosity@somni.dev>
2026-04-23 02:33:10 +02:00
dependabot[bot]
99f9682d61 core: bump rand from 0.8.5 to 0.8.6 in the cargo group across 1 directory (#21783)
core: bump rand in the cargo group across 1 directory

Bumps the cargo group with 1 update in the / directory: [rand](https://github.com/rust-random/rand).


Updates `rand` from 0.8.5 to 0.8.6
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.8.6/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.5...0.8.6)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.8.6
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 02:02:24 +02:00
Bapuji Koraganti
987f367d7b web: merge MFA devices and tokens into unified Credentials tab (#21705)
* web: merge MFA devices and tokens into unified Credentials tab

Combines the separate "MFA Devices" and "Tokens and App passwords"
tabs into a single "Credentials" tab on the user settings page,
so users can manage all credentials from one place.

Fixes #21637

Signed-off-by: Bapuji Koraganti <bapuk.2008@gmail.com>

* add card title

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

---------

Signed-off-by: Bapuji Koraganti <bapuk.2008@gmail.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-23 02:02:00 +02:00
Jens L.
805ff9f1ab web/admin: fix policy/stage wizard label, fix connector create wizard, cleanup (#21781)
* update labels

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

* remove unused app wizard hint

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

* connector wizard should use grid

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-22 19:32:23 +02:00
dependabot[bot]
42fc9d537e website: bump the build group in /website with 6 updates (#21777)
* website: bump the build group in /website with 6 updates

Bumps the build group in /website with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@swc/core-darwin-arm64](https://github.com/swc-project/swc) | `1.15.26` | `1.15.30` |
| [@swc/core-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.15.26` | `1.15.30` |
| [@swc/core-linux-x64-gnu](https://github.com/swc-project/swc) | `1.15.26` | `1.15.30` |
| [@swc/html-darwin-arm64](https://github.com/swc-project/swc) | `1.15.26` | `1.15.30` |
| [@swc/html-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.15.26` | `1.15.30` |
| [@swc/html-linux-x64-gnu](https://github.com/swc-project/swc) | `1.15.26` | `1.15.30` |


Updates `@swc/core-darwin-arm64` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-linux-arm64-gnu` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-linux-x64-gnu` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/html-darwin-arm64` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/html-linux-arm64-gnu` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/html-linux-x64-gnu` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

---
updated-dependencies:
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-darwin-arm64"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-arm64-gnu"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-x64-gnu"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>

* sigh

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-22 17:38:32 +02:00
dependabot[bot]
3f4c0fb35d core: bump library/nginx from 7f0adca to 3acc8b9 in /website (#21775)
Bumps library/nginx from `7f0adca` to `3acc8b9`.

---
updated-dependencies:
- dependency-name: library/nginx
  dependency-version: 1.29-trixie
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 17:32:46 +02:00
dependabot[bot]
42d87072cf core: bump library/node from f57f0c7 to b272ff1 in /website (#21776)
core: bump library/node from `f57f0c7` to `7e77811` in /website

Bumps library/node from `f57f0c7` to `7e77811`.

---
updated-dependencies:
- dependency-name: library/node
  dependency-version: 25.9.0-trixie
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 17:32:36 +02:00
Jens L.
075a1f5875 web/admin: Allow binding users/groups in policy binding wizard and existing stage in stage binding wizard (#21697)
* web/admin: allow creating only binding for policies

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

* dont show type selector if only one is allowed

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

* do the same for stage wizard

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

* minor unrelated fix: alignment in table desc

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

* add option to bind existing policy

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

* adjust labels?

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

* Clean up post-type select state. Types.

* Clean up brand form.

* Flesh out parse.

* Tidy textarea.

* Fix table alignment when images are present.

* Simplify radio.

* Fix form group layout, styles.

* Flesh out plural helper.

* Flesh out formatted user display name.

* Allow slotted HTML in page description.

* Clean up transclusion types.

* Allow null.

* Flesh out user activation toggle.

* Clean up activation labeling.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-04-22 16:08:31 +02:00
Bapuji Koraganti
24edee3e78 flows: add warning message for expired password reset links (#21395)
* flows: add warning message for expired password reset links

Fixes #21306

* Replace token expiry check with REQUIRE_TOKEN authentication requirement

Incorporate review comments to move expired/invalid token handling from executor-level check to flow planner authentication requirement. This avoids disclosing whether a token ever existed and handles already-cleaned-up tokens.

* The fix was changing gettext_lazy to gettext

* remove unneeded migration

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

* update form

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-22 15:09:05 +02:00
dependabot[bot]
9d55b9a9b0 web: bump the swc group across 1 directory with 11 updates (#21778)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc/tree/HEAD/packages/core).


Updates `@swc/core` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/commits/v1.15.30/packages/core)

Updates `@swc/core-darwin-arm64` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-darwin-x64` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-linux-arm-gnueabihf` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-linux-arm64-gnu` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-linux-arm64-musl` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-linux-x64-gnu` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-linux-x64-musl` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-win32-arm64-msvc` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-win32-ia32-msvc` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

Updates `@swc/core-win32-x64-msvc` from 1.15.26 to 1.15.30
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.26...v1.15.30)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-version: 1.15.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 13:55:56 +02:00
dependabot[bot]
349be68d52 core: bump tokio from 1.52.0 to 1.52.1 (#21774)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.52.0 to 1.52.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.0...tokio-1.52.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.52.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 13:55:34 +02:00
dependabot[bot]
7dfb8d6129 core: bump library/node from a31ca31 to 735dd68 in /lifecycle/container (#21773)
core: bump library/node in /lifecycle/container

Bumps library/node from `a31ca31` to `735dd68`.

---
updated-dependencies:
- dependency-name: library/node
  dependency-version: '24'
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 13:55:24 +02:00
dependabot[bot]
7f7965e42c core: bump fido2 from 2.1.1 to 2.2.0 (#21772)
Bumps [fido2](https://github.com/Yubico/python-fido2) from 2.1.1 to 2.2.0.
- [Release notes](https://github.com/Yubico/python-fido2/releases)
- [Changelog](https://github.com/Yubico/python-fido2/blob/main/NEWS)
- [Commits](https://github.com/Yubico/python-fido2/compare/2.1.1...2.2.0)

---
updated-dependencies:
- dependency-name: fido2
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 13:55:13 +02:00
dependabot[bot]
2e2b471b94 core: bump library/golang from c0074c7 to cd8540d in /lifecycle/container (#21771)
core: bump library/golang in /lifecycle/container

Bumps library/golang from `c0074c7` to `cd8540d`.

---
updated-dependencies:
- dependency-name: library/golang
  dependency-version: 1.26.2-trixie
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 13:54:51 +02:00
dependabot[bot]
4d53cd0790 core: bump github.com/pires/go-proxyproto from 0.11.0 to 0.12.0 (#21770)
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 13:54:42 +02:00
Jens L.
7b913eaaa9 root: update rustls-webpki (#21769)
* root: update rustls-webpki

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

* allow earlier rustls-webpki updates since this is the second time this happened

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-22 13:00:11 +02:00
authentik-automation[bot]
880c1ec89a core, web: update translations (#21695)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-04-22 11:41:48 +02:00
dependabot[bot]
d7724a52f2 core: bump python-dotenv from 1.2.1 to 1.2.2 in the uv group across 1 directory (#21752)
core: bump python-dotenv in the uv group across 1 directory

Bumps the uv group with 1 update in the / directory: [python-dotenv](https://github.com/theskumar/python-dotenv).


Updates `python-dotenv` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-version: 1.2.2
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:41:23 +02:00
dependabot[bot]
508b45b6e3 core: bump github.com/jackc/pgx/v5 from 5.9.1 to 5.9.2 (#21755)
Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.9.1 to 5.9.2.
- [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v5.9.1...v5.9.2)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v5
  dependency-version: 5.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:41:05 +02:00
dependabot[bot]
2d52756761 core: bump github.com/go-openapi/runtime from 0.29.3 to 0.29.4 (#21756)
Bumps [github.com/go-openapi/runtime](https://github.com/go-openapi/runtime) from 0.29.3 to 0.29.4.
- [Release notes](https://github.com/go-openapi/runtime/releases)
- [Commits](https://github.com/go-openapi/runtime/compare/v0.29.3...v0.29.4)

---
updated-dependencies:
- dependency-name: github.com/go-openapi/runtime
  dependency-version: 0.29.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:40:56 +02:00
dependabot[bot]
6e84b74797 core: bump pydantic from 2.13.0 to 2.13.2 (#21757)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.13.0 to 2.13.2.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.13.0...v2.13.2)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:40:28 +02:00
dependabot[bot]
aff93d35ef core: bump django-stubs[compatible-mypy] from 6.0.2 to 6.0.3 (#21758)
Bumps [django-stubs[compatible-mypy]](https://github.com/typeddjango/django-stubs) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/typeddjango/django-stubs/releases)
- [Commits](https://github.com/typeddjango/django-stubs/compare/6.0.2...6.0.3)

---
updated-dependencies:
- dependency-name: django-stubs[compatible-mypy]
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:40:12 +02:00
dependabot[bot]
d995613212 core: bump aws-cdk-lib from 2.249.0 to 2.250.0 (#21759)
Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk) from 2.249.0 to 2.250.0.
- [Release notes](https://github.com/aws/aws-cdk/releases)
- [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.alpha.md)
- [Commits](https://github.com/aws/aws-cdk/compare/v2.249.0...v2.250.0)

---
updated-dependencies:
- dependency-name: aws-cdk-lib
  dependency-version: 2.250.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:40:03 +02:00
dependabot[bot]
194f04bb6f core: bump packaging from 26.0 to 26.1 (#21760)
Bumps [packaging](https://github.com/pypa/packaging) from 26.0 to 26.1.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/26.0...26.1)

---
updated-dependencies:
- dependency-name: packaging
  dependency-version: '26.1'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:39:50 +02:00
dependabot[bot]
ba14cac535 core: bump library/node from 28fd420 to a31ca31 in /lifecycle/container (#21761)
core: bump library/node in /lifecycle/container

Bumps library/node from `28fd420` to `a31ca31`.

---
updated-dependencies:
- dependency-name: library/node
  dependency-version: '24'
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:39:39 +02:00
dependabot[bot]
953c70f5fc ci: bump actions/setup-node from 6.3.0 to 6.4.0 (#21762)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.3.0 to 6.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](53b83947a5...48b55a011b)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:39:18 +02:00
dependabot[bot]
4c775b2258 ci: bump actions/setup-node from 6.3.0 to 6.4.0 in /.github/actions/setup (#21764)
ci: bump actions/setup-node in /.github/actions/setup

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.3.0 to 6.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](53b83947a5...48b55a011b)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:39:09 +02:00
dependabot[bot]
2c851f7cd0 ci: bump taiki-e/install-action from 2.75.17 to 2.75.18 in /.github/actions/setup (#21765)
ci: bump taiki-e/install-action in /.github/actions/setup

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.17 to 2.75.18.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](58e8625425...055f5df8c3)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:38:54 +02:00
dependabot[bot]
520f81966c core: bump tokio from 1.51.1 to 1.52.0 (#21766)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.51.1 to 1.52.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.51.1...tokio-1.52.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 11:38:41 +02:00
Jens L.
7f27ee3267 ci: fix postgres path for postgres 18 tests (#21767)
* ci: test migrations-from-stable failing

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

* fix postgres path

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-22 11:27:16 +02:00
Sai Asish Y
6d57854bff sources/oauth: pick a single pkce method from OIDC discovery, not the whole list (#21689)
* sources/oauth: pick a single pkce method from OIDC discovery, not the whole list

When an OAuth source is configured with `oidc_well_known_url`, the API
serializer fetches the upstream's OpenID configuration and merges the
selected endpoints into the source attrs. The merge used a straight
field_map that aliased the pkce TextField to
`code_challenge_methods_supported`:

    field_map = {
        ...
        "pkce": "code_challenge_methods_supported",
    }
    for ak_key, oidc_key in field_map.items():
        ...
        attrs[ak_key] = config.get(oidc_key, "")

`code_challenge_methods_supported` is a JSON array per RFC 8414
(e.g. ["plain", "S256"]), but attrs["pkce"] is backed by a TextField
with choices NONE / PLAIN / S256. Django does not validate choices on
plain assignment, so the list survives serialisation and is later
formatted by the client as
    str(pkce_mode) -> "['plain', 'S256']"
which ships as `code_challenge_method=%5B%27plain%27%2C+%27S256%27%5D`
on the /authorize request. The upstream rejects the subsequent /token
exchange with HTTP 400 because it has no PKCE state for that value.

Separate the pkce handling from the rest of the field_map loop: only
fill pkce when the user has not set it, and select one scalar method
from the advertised list (prefer S256, the RFC 7636 MUST for public
clients, then plain, then NONE as a last resort). Non-list / missing
values fall back to NONE. User-supplied pkce still wins, matching the
existing "don't overwrite user-set values" intent.

Fixes #21665

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

* update test

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

* simplify

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

---------

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-21 19:40:03 +02:00
Dominic R
f7871d726e website/integrations: grafana: migrate to entitlements (#21676)
* website/integrations: grafana: migrate to entitlements

* website/integrations: migrate Grafana role mappings to entitlements

* rm

* Add scope

* Add scope

* Update website/integrations/monitoring/grafana/index.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-04-21 14:08:22 +00:00
Jens L.
189056e19a providers/oauth2: don't auto-set redirect_uri (#21746)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-21 15:58:57 +02:00
Dominic R
24362625a9 website/integrations: forgejo: migrate to entitlements (#21682)
* website/integrations: forgejo: migrate to entitlements

* website/integrations: migrate Forgejo permissions to entitlements

* rm

* Add scope
2026-04-21 09:41:01 -04:00
dependabot[bot]
5266166d64 core: bump aws-lc-rs from 1.16.2 to 1.16.3 (#21740)
Bumps [aws-lc-rs](https://github.com/aws/aws-lc-rs) from 1.16.2 to 1.16.3.
- [Release notes](https://github.com/aws/aws-lc-rs/releases)
- [Commits](https://github.com/aws/aws-lc-rs/compare/v1.16.2...v1.16.3)

---
updated-dependencies:
- dependency-name: aws-lc-rs
  dependency-version: 1.16.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 15:16:11 +02:00
dependabot[bot]
44d13e3ea5 core: bump clap from 4.6.0 to 4.6.1 (#21744)
Bumps [clap](https://github.com/clap-rs/clap) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.0...clap_complete-v4.6.1)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 15:16:02 +02:00
dependabot[bot]
c7e8037ef7 website: bump docusaurus-plugin-openapi-docs from 5.0.0 to 5.0.1 in /website (#21711)
* website: bump docusaurus-plugin-openapi-docs in /website

Bumps [docusaurus-plugin-openapi-docs](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/tree/HEAD/packages/docusaurus-plugin-openapi-docs) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/releases)
- [Changelog](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/commits/v5.0.1/packages/docusaurus-plugin-openapi-docs)

---
updated-dependencies:
- dependency-name: docusaurus-plugin-openapi-docs
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* update both

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-21 14:32:02 +02:00
dependabot[bot]
a10769e60e core: bump sentry-sdk from 2.57.0 to 2.58.0 (#21733)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.57.0 to 2.58.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.57.0...2.58.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.58.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:54 +01:00
dependabot[bot]
1a1f752f28 core: bump pydantic from 2.12.5 to 2.13.0 (#21734)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.12.5 to 2.13.0.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.12.5...v2.13.0)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:49 +01:00
dependabot[bot]
081fe60ad7 core: bump aws-cdk-lib from 2.248.0 to 2.249.0 (#21735)
Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk) from 2.248.0 to 2.249.0.
- [Release notes](https://github.com/aws/aws-cdk/releases)
- [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.alpha.md)
- [Commits](https://github.com/aws/aws-cdk/compare/v2.248.0...v2.249.0)

---
updated-dependencies:
- dependency-name: aws-cdk-lib
  dependency-version: 2.249.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:45 +01:00
dependabot[bot]
8be14a6de4 ci: bump tj-actions/changed-files from 47.0.5 to 47.0.6 (#21737)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 47.0.5 to 47.0.6.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](22103cc46b...9426d40962)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 47.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:41 +01:00
dependabot[bot]
57c97d5318 ci: bump int128/docker-manifest-create-action from 2.17.0 to 2.18.0 (#21738)
Bumps [int128/docker-manifest-create-action](https://github.com/int128/docker-manifest-create-action) from 2.17.0 to 2.18.0.
- [Release notes](https://github.com/int128/docker-manifest-create-action/releases)
- [Commits](44422a4b04...3de37de96c)

---
updated-dependencies:
- dependency-name: int128/docker-manifest-create-action
  dependency-version: 2.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:36 +01:00
dependabot[bot]
d44cd63a52 web: bump prettier from 3.8.2 to 3.8.3 in /web (#21739)
Bumps [prettier](https://github.com/prettier/prettier) from 3.8.2 to 3.8.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.2...3.8.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:32 +01:00
dependabot[bot]
33e4f8beb2 core: bump axum from 0.8.8 to 0.8.9 (#21741)
Bumps [axum](https://github.com/tokio-rs/axum) from 0.8.8 to 0.8.9.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.8.8...axum-v0.8.9)

---
updated-dependencies:
- dependency-name: axum
  dependency-version: 0.8.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:26 +01:00
dependabot[bot]
1b6da073c8 core: bump rustls from 0.23.37 to 0.23.38 (#21742)
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.37 to 0.23.38.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.37...v/0.23.38)

---
updated-dependencies:
- dependency-name: rustls
  dependency-version: 0.23.38
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:22 +01:00
dependabot[bot]
c481a5c2f0 core: bump uuid from 1.23.0 to 1.23.1 (#21743)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.23.0 to 1.23.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.23.0...v1.23.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.23.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 13:15:18 +01:00
Dominic R
c300a5338e website/docs: reorganize SCIM provider docs (#21671) 2026-04-21 07:48:55 -04:00
Dominic R
742bbcc51f website/docs: update embedded outpost intro (#21669) 2026-04-21 07:41:37 -04:00
Dominic R
018c81178f website/integrations: elastic cloud: migrate to entitlements (#21683) 2026-04-21 07:04:12 -04:00
Dominic R
8bd601f91c website/integrations: ghe emu: migrate to entitlements (#21678) 2026-04-21 07:03:45 -04:00
Dominic R
6f1db505b5 website/integrations: ghe server: migrate to entitlements (#21677) 2026-04-21 07:03:31 -04:00
Dominic R
1e6d8aa5c4 website/docs: clarify branding file picker paths (#21687) 2026-04-21 07:03:08 -04:00
Dominic R
4893d3ef61 website/integrations: portainer: migrate to entitlements (#21679) 2026-04-21 07:00:48 -04:00
Dominic R
e99200c1a9 website/integrations: omada controller: migrate to entitlements (#21680) 2026-04-21 07:00:29 -04:00
dependabot[bot]
132417c3f0 core: bump selenium from 4.42.0 to 4.43.0 (#21714)
Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.42.0 to 4.43.0.
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.42.0...selenium-4.43.0)

---
updated-dependencies:
- dependency-name: selenium
  dependency-version: 4.43.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 00:50:45 +02:00
dependabot[bot]
9f1318f583 web: bump chromedriver from 147.0.2 to 147.0.4 in /web (#21721)
* web: bump chromedriver from 147.0.2 to 147.0.4 in /web

Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 147.0.2 to 147.0.4.
- [Commits](https://github.com/giggio/node-chromedriver/compare/147.0.2...147.0.4)

---
updated-dependencies:
- dependency-name: chromedriver
  dependency-version: 147.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* sigh

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-20 17:17:28 +01:00
dependabot[bot]
d7ca75024a core: bump lxml from 6.0.4 to 6.1.0 (#21713)
Bumps [lxml](https://github.com/lxml/lxml) from 6.0.4 to 6.1.0.
- [Release notes](https://github.com/lxml/lxml/releases)
- [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt)
- [Commits](https://github.com/lxml/lxml/compare/lxml-6.0.4...lxml-6.1.0)

---
updated-dependencies:
- dependency-name: lxml
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 17:12:37 +01:00
dependabot[bot]
addbf5a2f6 web: bump the swc group across 1 directory with 11 updates (#21718)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc/tree/HEAD/packages/core).


Updates `@swc/core` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/commits/v1.15.26/packages/core)

Updates `@swc/core-darwin-arm64` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-darwin-x64` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-linux-arm-gnueabihf` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-linux-arm64-gnu` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-linux-arm64-musl` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-linux-x64-gnu` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-linux-x64-musl` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-win32-arm64-msvc` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-win32-ia32-msvc` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-win32-x64-msvc` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:47:25 +01:00
dependabot[bot]
3dd05a4407 web: bump typescript from 6.0.2 to 6.0.3 in /web (#21719)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.2...v6.0.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:47:19 +01:00
dependabot[bot]
058af4504f web: bump knip from 6.3.1 to 6.4.1 in /web (#21720)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 6.3.1 to 6.4.1.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.4.1/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 6.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:47:06 +01:00
dependabot[bot]
5e88751516 core: bump github.com/getsentry/sentry-go from 0.45.0 to 0.45.1 (#21707)
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.45.0 to 0.45.1.
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.45.0...v0.45.1)

---
updated-dependencies:
- dependency-name: github.com/getsentry/sentry-go
  dependency-version: 0.45.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:46:46 +01:00
dependabot[bot]
87639eced4 website: bump the build group in /website with 6 updates (#21708)
Bumps the build group in /website with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@swc/core-darwin-arm64](https://github.com/swc-project/swc) | `1.15.24` | `1.15.26` |
| [@swc/core-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.15.24` | `1.15.26` |
| [@swc/core-linux-x64-gnu](https://github.com/swc-project/swc) | `1.15.24` | `1.15.26` |
| [@swc/html-darwin-arm64](https://github.com/swc-project/swc) | `1.15.24` | `1.15.26` |
| [@swc/html-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.15.24` | `1.15.26` |
| [@swc/html-linux-x64-gnu](https://github.com/swc-project/swc) | `1.15.24` | `1.15.26` |


Updates `@swc/core-darwin-arm64` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-linux-arm64-gnu` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/core-linux-x64-gnu` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/html-darwin-arm64` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/html-linux-arm64-gnu` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

Updates `@swc/html-linux-x64-gnu` from 1.15.24 to 1.15.26
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.15.24...v1.15.26)

---
updated-dependencies:
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-darwin-arm64"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-arm64-gnu"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-x64-gnu"
  dependency-version: 1.15.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:46:37 +01:00
dependabot[bot]
546f204c15 website: bump openapi-to-postmanv2 from 6.0.0 to 6.0.1 in /website (#21710)
Bumps [openapi-to-postmanv2](https://github.com/postmanlabs/openapi-to-postman) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/postmanlabs/openapi-to-postman/releases)
- [Changelog](https://github.com/postmanlabs/openapi-to-postman/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/postmanlabs/openapi-to-postman/compare/v6.0.0...v6.0.1)

---
updated-dependencies:
- dependency-name: openapi-to-postmanv2
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:46:33 +01:00
dependabot[bot]
0c6c5661a9 lifecycle/aws: bump aws-cdk from 2.1118.0 to 2.1118.2 in /lifecycle/aws (#21712)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1118.0 to 2.1118.2.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1118.2/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1118.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:46:28 +01:00
dependabot[bot]
fc9211220a core: bump ruff from 0.15.10 to 0.15.11 (#21715)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.10 to 0.15.11.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.10...0.15.11)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:46:19 +01:00
dependabot[bot]
7ad5c87e84 core: bump twilio from 9.10.4 to 9.10.5 (#21716)
Bumps [twilio](https://github.com/twilio/twilio-python) from 9.10.4 to 9.10.5.
- [Release notes](https://github.com/twilio/twilio-python/releases)
- [Changelog](https://github.com/twilio/twilio-python/blob/main/CHANGES.md)
- [Commits](https://github.com/twilio/twilio-python/compare/9.10.4...9.10.5)

---
updated-dependencies:
- dependency-name: twilio
  dependency-version: 9.10.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:46:14 +01:00
dependabot[bot]
05c2ec315a ci: bump taiki-e/install-action from 2.75.15 to 2.75.17 in /.github/actions/setup (#21722)
ci: bump taiki-e/install-action in /.github/actions/setup

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.15 to 2.75.17.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](5939f3337e...58e8625425)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:46:09 +01:00
dependabot[bot]
16731116ab ci: bump astral-sh/setup-uv from 8.0.0 to 8.1.0 in /.github/actions/setup (#21723)
ci: bump astral-sh/setup-uv in /.github/actions/setup

Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.0.0 to 8.1.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](cec208311d...08807647e7)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:46:01 +01:00
dependabot[bot]
8147b605e7 web: bump globals from 17.4.0 to 17.5.0 in /web (#21724)
Bumps [globals](https://github.com/sindresorhus/globals) from 17.4.0 to 17.5.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 08:45:48 +01:00
Jens L.
915b5a73fc enterprise/endpoints/connectors/agent: add independent secure enclave support for tap to login (#20766)
* enterprise/endpoints/connectors/agent: add independent secure enclave support for tap to login

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

* format

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

* fix API url

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

* remove optional settings

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

* add a missing text

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-18 20:29:17 +02:00
dependabot[bot]
08832b8520 web: bump basic-ftp from 5.2.2 to 5.3.0 in /web (#21654)
Bumps [basic-ftp](https://github.com/patrickjuchli/basic-ftp) from 5.2.2 to 5.3.0.
- [Release notes](https://github.com/patrickjuchli/basic-ftp/releases)
- [Changelog](https://github.com/patrickjuchli/basic-ftp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/patrickjuchli/basic-ftp/compare/v5.2.2...v5.3.0)

---
updated-dependencies:
- dependency-name: basic-ftp
  dependency-version: 5.3.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-04-17 23:50:31 +01:00
authentik-automation[bot]
d158fdb792 core, web: update translations (#21655)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-04-17 23:50:28 +01:00
dependabot[bot]
f86ca53309 core: bump github.com/getsentry/sentry-go from 0.44.1 to 0.45.0 (#21656)
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.44.1 to 0.45.0.
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.44.1...v0.45.0)

---
updated-dependencies:
- dependency-name: github.com/getsentry/sentry-go
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 23:50:24 +01:00
dependabot[bot]
19e406700e core: bump selenium from 4.41.0 to 4.42.0 (#21657)
Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.41.0 to 4.42.0.
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.41.0...selenium-4.42.0)

---
updated-dependencies:
- dependency-name: selenium
  dependency-version: 4.42.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 23:49:00 +01:00
dependabot[bot]
c74350145f web: bump @sentry/browser from 10.47.0 to 10.48.0 in /web in the sentry group across 1 directory (#21658)
web: bump @sentry/browser in /web in the sentry group across 1 directory

Bumps the sentry group with 1 update in the /web directory: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 10.47.0 to 10.48.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.47.0...10.48.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-version: 10.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 23:48:56 +01:00
dependabot[bot]
514ff57953 core: bump library/node from 9707cd4 to 28fd420 in /lifecycle/container (#21659)
core: bump library/node in /lifecycle/container

Bumps library/node from `9707cd4` to `28fd420`.

---
updated-dependencies:
- dependency-name: library/node
  dependency-version: '24'
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 23:48:52 +01:00
dependabot[bot]
680220f977 core: bump rust-toolchain from 1.94.1 to 1.95.0 (#21660)
Bumps [rust-toolchain](https://github.com/rust-lang/rust) from 1.94.1 to 1.95.0.
- [Release notes](https://github.com/rust-lang/rust/releases)
- [Changelog](https://github.com/rust-lang/rust/blob/main/RELEASES.md)
- [Commits](https://github.com/rust-lang/rust/compare/1.94.1...1.95.0)

---
updated-dependencies:
- dependency-name: rust-toolchain
  dependency-version: 1.95.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 23:48:49 +01:00
dependabot[bot]
27a3dc93e3 web: bump @types/node from 25.5.2 to 25.6.0 in /web (#21661)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.2 to 25.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 23:48:45 +01:00
dependabot[bot]
bd6102b59b web: bump @formatjs/intl-listformat from 8.3.1 to 8.3.2 in /web (#21662)
Bumps [@formatjs/intl-listformat](https://github.com/formatjs/formatjs) from 8.3.1 to 8.3.2.
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/@formatjs/intl-listformat@8.3.1...@formatjs/intl-listformat@8.3.2)

---
updated-dependencies:
- dependency-name: "@formatjs/intl-listformat"
  dependency-version: 8.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 23:48:41 +01:00
Dominic R
b41cb4817a website/docs: clarify Kubernetes JWT machine auth (#21650)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-04-17 17:38:01 -04:00
Dominic R
81bfcbb4e8 website/docs: clarify LDAP group attribute mappings (#21649)
* website/docs: clarify LDAP group attribute mappings

Explain that LDAP source property mappings can be assigned to groups, add an example for copying a custom LDAP group attribute into authentik group attributes, and note how to decode JSON-encoded values.

Closes: https://github.com/goauthentik/authentik/issues/5874

* Update website/docs/users-sources/sources/protocols/ldap/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/docs/users-sources/sources/protocols/ldap/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-04-17 12:33:29 +00:00
Dominic R
909d1335ad website/docs: clarify OAuth2 signing key behavior (#21652)
Clarify that the OAuth2 provider Signing Key field is optional and that authentik signs JWTs with the provider Client secret when no signing key is set.

Closes: https://github.com/goauthentik/authentik/issues/4824
2026-04-17 08:05:42 -04:00
Dominic R
8df67091d9 website/docs: clean up OAuth2 M2M documentation (#21651)
Refine the machine-to-machine authentication page, align examples and inline formatting with the docs style guide, and replace the small event logging table with a list.
2026-04-17 08:05:19 -04:00
Teffen Ellis
e4c7a8aded web/flows: Fix username autofocus. (#21646)
web: Fix username autofocus.
2026-04-17 04:43:12 +02:00
dependabot[bot]
75d14586d2 web: bump protocol-buffers-schema from 3.6.0 to 3.6.1 in /web (#21653)
Bumps [protocol-buffers-schema](https://github.com/mafintosh/protocol-buffers-schema) from 3.6.0 to 3.6.1.
- [Commits](https://github.com/mafintosh/protocol-buffers-schema/compare/v3.6.0...v3.6.1)

---
updated-dependencies:
- dependency-name: protocol-buffers-schema
  dependency-version: 3.6.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 00:13:13 +02:00
Teffen Ellis
6ed5cb5249 website/docs: Modal and wizard button labels (#21549)
* website/integrations: rename "Create with Provider" to "New Application"

The application list page now uses a split-button labeled
"New Application" instead of the old "Create with Provider" dropdown.
Update all 113 integration guides to match.

* website/docs: update flow, stage, and policy button labels

- "Create" → "New Flow", "New Stage", "New Policy" for trigger buttons
- "Finish" → "Create Flow", "Create Stage", "Create Policy" for submit
- "Create and bind stage" → "New Stage" / "Bind Existing Stage"
- "Create" (binding submit) → "Create Stage Binding"

* website/docs: update provider button labels

- "Create" → "New Provider" for trigger buttons
- "Create with Provider" → "New Application" in RAC docs
- "Create" → "New Property Mapping", "New RAC Endpoint", "New Prompt"
  for related entity creation

* website/docs: update directory button labels

- "Create" → "New Source" for federation/social login pages
- "Create" → "New Role", submit → "Create Role"
- "Create" → "New Invitation"
- Policy binding submit → "Create Policy Binding"

* website/docs: update endpoint device and system management button labels

- "Create" → "New Endpoint Connector", "New Enrollment Token",
  "New Device Access Group", "New Flow"
- Submit → "Create Device Access Group"
- "Create" → "New Notification Rule", "New Notification Transport"
- Binding submit → "Create Policy Binding"

* Reorganize policy documentation

* website/docs: address policy docs review feedback

* post-rebase

* website/docs: Reorganize policy documentation -- Revisions (#21601)

* apply suggestions

* Fix escaped.

* Fix whitespace.

* Update button label.

* Fix phrasing.

* Fix phrasing.

* Clean up stragglers.

* Format.

---------

Co-authored-by: Dominic R <dominic@sdko.org>
2026-04-16 17:35:38 +00:00
Teffen Ellis
b6496950bf web: Close modal on route navigation (#21622)
* Close dialog on navigation.

* web: update dialog, form, and sidebar styles with logical properties and scroll shadows

Migrate dialog padding CSS variables from physical (top/right/bottom/left) to logical
(block-start/inline-end/block-end/inline-start) naming. Add scroll shadow utility
class (ak-m-scroll-shadows) for scrollable regions. Rework radio input and form
control styles including transparent backgrounds, checkbox-style indicators, and
improved hover states. Refactor FormGroup marker to use CSS custom properties for
open/closed states. Move sidebar padding from nav container to scrollable list.

* web: refine elements and components for accessibility, type safety, and consistency

Add ARIA role and label to dialog body, apply scroll shadow classes to modal body,
sidebar nav, and wizard main. Update ak-status-label to support tri-state
(good/bad/null) rendering with ts-pattern matching and a neutral label. Simplify
FormGroup by removing wrapper div around default slot, adding part attributes for
header styling, and changing description to nullable type. Clean up LogViewer and
StaticTable with proper access modifiers, override annotations, and nullable item
types. Simplify ak-switch-input checked binding and remove unused slot attribute
from ak-radio-input help text.

* web: modernize application pages with modalInvoker and updated form patterns

Refactor ApplicationCheckAccessForm to use static form metadata properties
(verboseName, submitVerb, createLabel), formatAPISuccessMessage, and a private
CoreApi instance. Migrate ApplicationViewPage from ak-forms-modal slots to the
modalInvoker directive for both edit and check-access actions. Accept nullable
input in createPaginatedResponse for better null-safety. Fix casing of dropdown
menu items in ApplicationListPage.

* web: migrate remaining view pages to modalInvoker (#21592)

* Fix visibility check, search params.

* Add scroll shadow.

* Partial revert of input layout.

* Tidy groups.

* Fix check access form invoker, styles.

* Optional sizing.

* Lowercase

* Revise checkbox style.

* Close dialog on navigation.

* Fix padding.

* Touch up shadow heights.

* Migrate remaining view pages to modalInvoker, add e2e coverage.

* Fix alignment.

* Fix click handler, add placeholders.

* Fix issue where form field is not serialized.
2026-04-16 19:04:29 +02:00
Marc 'risson' Schmitt
05bb1d1fdd packages/ak-axum/server: fix unix socket cleanup when allow_failure is unset (#21645) 2026-04-16 16:20:16 +00:00
Marc 'risson' Schmitt
d51296cbb9 scripts/api_filter_schema: fix authentication (#21644) 2026-04-16 16:19:32 +00:00
Teffen Ellis
abb65d2682 web: Normalize use of .toJSON() over .json() (#21621)
* web: Normalize use of toJSON.

* fix checkbox group

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-16 15:01:35 +02:00
Teffen Ellis
4667deaefc web: Fix table visibility checks, search params. (#21623)
Fix visibility check, search params.
2026-04-16 15:01:02 +02:00
Marc 'risson' Schmitt
1b53426e2c packages/ak-common/tracing: get sentry config from API for outposts (#21625) 2026-04-16 14:00:01 +02:00
Marcus Pamelia
b3e7a01f10 sources/ldap: catch Google LDAP rate-limit errors during schema fetch (#21638)
When connecting to Google Secure LDAP, the ldap3 library fetches schema
info during bind() with get_info=ALL. Google rate-limits these schema
queries, raising LDAPAdminLimitExceededResult, and also returns
unsupported attributes, raising LDAPAttributeError.

The existing fallback logic retries with get_info=NONE but only catches
LDAPSchemaError and LDAPInsufficientAccessRightsResult. Add the two
missing exception types so the fallback works for Google Secure LDAP.

Fixes sync failures when using Google Secure LDAP as a federation source,
where every sync page task opens a new connection and the concurrent
schema fetches exhaust Google's rate budget.
2026-04-16 13:58:08 +02:00
Dominic R
404570a4d2 website/docs: Reorganize policy documentation (#21133)
* Reorganize policy documentation

* website/docs: address policy docs review feedback

* post-rebase

* website/docs: Reorganize policy documentation -- Revisions (#21601)

* apply suggestions

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-04-16 07:11:52 -04:00
dependabot[bot]
d2cbd82d7a web: bump dompurify from 3.3.3 to 3.4.0 in /web (#21632)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.3 to 3.4.0.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.3...3.4.0)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 12:59:33 +02:00
dependabot[bot]
b4f9bcc525 core: bump mypy from 1.20.0 to 1.20.1 (#21633)
Bumps [mypy](https://github.com/python/mypy) from 1.20.0 to 1.20.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.20.0...v1.20.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.20.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 06:54:28 +00:00
authentik-automation[bot]
067a2216a0 core, web: update translations (#21631)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-04-16 06:33:36 +00:00
dependabot[bot]
6d9eb2f133 ci: bump actions/cache from 5.0.4 to 5.0.5 (#21634)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.4 to 5.0.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](668228422a...27d5ce7f10)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 06:29:59 +00:00
dependabot[bot]
b2247c295b ci: bump taiki-e/install-action from 2.75.7 to 2.75.10 in /.github/actions/setup (#21635)
ci: bump taiki-e/install-action in /.github/actions/setup

Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.7 to 2.75.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](0abfcd587b...5939f3337e)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 06:29:23 +00:00
dependabot[bot]
53726eb140 ci: bump actions-rust-lang/setup-rust-toolchain from 1.15.4 to 1.16.0 in /.github/actions/setup (#21636)
ci: bump actions-rust-lang/setup-rust-toolchain

Bumps [actions-rust-lang/setup-rust-toolchain](https://github.com/actions-rust-lang/setup-rust-toolchain) from 1.15.4 to 1.16.0.
- [Release notes](https://github.com/actions-rust-lang/setup-rust-toolchain/releases)
- [Changelog](https://github.com/actions-rust-lang/setup-rust-toolchain/blob/main/CHANGELOG.md)
- [Commits](150fca883c...2b1f5e9b39)

---
updated-dependencies:
- dependency-name: actions-rust-lang/setup-rust-toolchain
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 06:28:50 +00:00
Jens L.
00639d9596 policies/event_matcher: Add query option to filter events (#21618)
* policies/event_matcher: support QL query

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

* fix lit dev warning

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

* cache autocomplete data if QL isn't setup yet

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

* add ui

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

* dont use ql input in modal

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

* cleanup

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

* fix codespell

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-16 01:52:11 +02:00
Dewi Roberts
10b39a3fb1 website/docs: remove broken version tag from oauth doc (#21628)
Remove broken tag
2026-04-15 15:24:25 -04:00
Ryan Pesek
af747c6c25 web/flows: prevent leader tab deadlock in continuous login flow (#21583)
* prevent leader tab deadlock in continuous login flow

* web: Continuous login tidy.

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-04-15 17:50:14 +02:00
Marc 'risson' Schmitt
668f37ea41 packages/clients: only generate needed endpoints (#21578)
* packages/clients: only generate needed endpoints

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* machete

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fixup

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-04-15 13:11:25 +00:00
Marc 'risson' Schmitt
012e0c504f core: bump rustls-webpki from 0.103.10 to 0.103.12 (#21620) 2026-04-15 12:20:53 +00:00
authentik-automation[bot]
b36ab0a0a7 stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#21612)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-04-15 09:13:52 +00:00
Marc 'risson' Schmitt
7912a51188 blueprints: fix reconcile calling @property (#21576)
Co-authored-by: João C. Fernandes <jfernandes@cloudflare.com>
2026-04-15 10:57:39 +02:00
dependabot[bot]
829999ce23 web: bump vite from 8.0.7 to 8.0.8 in /web (#21614)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.7 to 8.0.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 10:26:52 +02:00
dependabot[bot]
9e8bbd4c4c web: bump country-flag-icons from 1.6.15 to 1.6.16 in /web (#21615)
Bumps [country-flag-icons](https://gitlab.com/catamphetamine/country-flag-icons) from 1.6.15 to 1.6.16.
- [Changelog](https://gitlab.com/catamphetamine/country-flag-icons/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/country-flag-icons/compare/v1.6.15...v1.6.16)

---
updated-dependencies:
- dependency-name: country-flag-icons
  dependency-version: 1.6.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 10:26:17 +02:00
Dave Greene
26bfb6e52c outposts/controllers/k8s: add option to disable strict x509 checks (#21210)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-04-14 18:59:03 +02:00
Jens L.
c84c8d86f8 providers/oauth2: allow cross provider token introspection for federated providers (#21513)
* providers/oauth2: allow cross provider token introspection for federated providers

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

* better token revocation for federated providers

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

* cleanup

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

* remove superfluous types

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

* better test

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-14 15:51:06 +02:00
Erwan Hervé
cc62f2b3f8 lifecycle/container: fix OCI image labels (#21574)
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Erwan Hervé <erwan@herve.tech>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-04-14 14:47:16 +02:00
João C. Fernandes
bbd0cb2521 packages/django-dramatiq-postgres: reset db connections in raise_connection_error (#21577)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-04-14 12:20:23 +00:00
Dominic R
8327b4d177 core: refresh signed media URLs in flows (#21553) 2026-04-14 08:11:16 -04:00
Jens L.
426a9a036b events: don't log cacheentry events (#21597) 2026-04-14 13:49:47 +02:00
dependabot[bot]
aaed1ed601 web: bump prettier from 3.8.1 to 3.8.2 in /web (#21568)
* web: bump prettier from 3.8.1 to 3.8.2 in /web

Bumps [prettier](https://github.com/prettier/prettier) from 3.8.1 to 3.8.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.2)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* sigh

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-04-14 13:49:28 +02:00
Jens L.
09637b73cd providers/oauth2: fix time logic in refresh_token_threshold (#21537)
* providers/oauth2: fix time logic in refresh_token_threshold

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-14 13:26:23 +02:00
dependabot[bot]
679ff0e0f1 web: bump chromedriver from 147.0.1 to 147.0.2 in /web (#21564)
Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 147.0.1 to 147.0.2.
- [Commits](https://github.com/giggio/node-chromedriver/compare/147.0.1...147.0.2)

---
updated-dependencies:
- dependency-name: chromedriver
  dependency-version: 147.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 13:20:47 +02:00
dependabot[bot]
18dd32ad2d web: bump axios from 1.13.6 to 1.15.0 in /web (#21528)
Bumps [axios](https://github.com/axios/axios) from 1.13.6 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.6...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.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-04-14 13:19:00 +02:00
dependabot[bot]
379e609f35 web: bump the bundler group across 1 directory with 3 updates (#21562)
Bumps the bundler group with 1 update in the /web directory: [@vitest/browser](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser).


Updates `@vitest/browser` from 4.1.3 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/browser)

Updates `@vitest/browser-playwright` from 4.1.3 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/browser-playwright)

Updates `vitest` from 4.1.3 to 4.1.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: "@vitest/browser"
  dependency-version: 4.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: "@vitest/browser-playwright"
  dependency-version: 4.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
- dependency-name: vitest
  dependency-version: 4.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 13:16:56 +02:00
Dominic R
89afa97dc6 website/docs: clean up Kubernetes outpost integration page (#21587) 2026-04-14 07:16:49 -04:00
dependabot[bot]
1b5e6231b9 web: bump follow-redirects from 1.15.11 to 1.16.0 in /web (#21596)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.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-04-14 13:16:20 +02:00
dependabot[bot]
8ba2535b10 web: bump lodash-es from 4.17.23 to 4.18.1 in /web (#21595)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 13:16:00 +02:00
Jens L.
d7ae8405d6 web/admin: fix log viewer layout for application access check (#21594)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-04-14 13:15:50 +02:00
dependabot[bot]
2a6962c188 ci: bump softprops/action-gh-release from 2.6.1 to 2.6.2 (#21589)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.6.1 to 2.6.2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](153bb8e044...b430933298)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 12:38:42 +02:00
dependabot[bot]
a186813e71 ci: bump calibreapp/image-actions from 03c976c29803442fc4040a9de5509669e7759b81 to 4f7260f5dbd809ec86d03721c1ad71b8a841d3e0 (#21590)
ci: bump calibreapp/image-actions

Bumps [calibreapp/image-actions](https://github.com/calibreapp/image-actions) from 03c976c29803442fc4040a9de5509669e7759b81 to 4f7260f5dbd809ec86d03721c1ad71b8a841d3e0.
- [Release notes](https://github.com/calibreapp/image-actions/releases)
- [Commits](03c976c298...4f7260f5db)

---
updated-dependencies:
- dependency-name: calibreapp/image-actions
  dependency-version: 4f7260f5dbd809ec86d03721c1ad71b8a841d3e0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 12:32:49 +02:00
dependabot[bot]
d359921f95 ci: bump actions/create-github-app-token from 3.0.0 to 3.1.1 (#21591)
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 3.0.0 to 3.1.1.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](f8d387b68d...1b10c78c78)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: 3.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 12:21:36 +02:00
Dominic R
be85fbc9e7 website/integrations: fix Microsoft 365 federation guide (#21054) 2026-04-13 18:48:42 -04:00
2271 changed files with 36263 additions and 541519 deletions

View File

@@ -1,5 +1,5 @@
[alias]
t = ["nextest", "run"]
t = ["nextest", "run", "--workspace"]
[build]
rustflags = ["--cfg", "tokio_unstable"]

View File

@@ -1,5 +1,6 @@
[licenses]
allow = [
"Apache-2.0 WITH LLVM-exception",
"Apache-2.0",
"BSD-3-Clause",
"CC0-1.0",

View File

@@ -37,7 +37,7 @@ runs:
sudo rsync -a --delete /tmp/empty/ /usr/local/lib/android/
- name: Install uv
if: ${{ contains(inputs.dependencies, 'python') }}
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v5
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v5
with:
enable-cache: true
- name: Setup python
@@ -52,24 +52,24 @@ runs:
run: uv sync --all-extras --dev --frozen
- name: Setup rust (stable)
if: ${{ contains(inputs.dependencies, 'rust') && !contains(inputs.dependencies, 'rust-nightly') }}
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
with:
rustflags: ""
- name: Setup rust (nightly)
if: ${{ contains(inputs.dependencies, 'rust-nightly') }}
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1
with:
toolchain: nightly
components: rustfmt
rustflags: ""
- name: Setup rust dependencies
if: ${{ contains(inputs.dependencies, 'rust') }}
uses: taiki-e/install-action@0abfcd587b70a713fdaa7fb502c885e2112acb15 # v2
uses: taiki-e/install-action@cf525cb33f51aca27cd6fa02034117ab963ff9f1 # v2
with:
tool: cargo-deny cargo-machete cargo-llvm-cov nextest
- name: Setup node (web)
if: ${{ contains(inputs.dependencies, 'node') }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
with:
node-version-file: "${{ inputs.working-directory }}web/package.json"
cache: "npm"
@@ -77,7 +77,7 @@ runs:
registry-url: "https://registry.npmjs.org"
- name: Setup node (root)
if: ${{ contains(inputs.dependencies, 'node') }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4
with:
node-version-file: "${{ inputs.working-directory }}package.json"
cache: "npm"

View File

@@ -2,7 +2,7 @@ services:
postgresql:
image: docker.io/library/postgres:${PSQL_TAG:-16}
volumes:
- db-data:/var/lib/postgresql/data
- db-data:/var/lib/postgresql
command: "-c log_statement=all"
environment:
POSTGRES_USER: authentik

View File

@@ -66,6 +66,14 @@ updates:
default-days: 7
semver-major-days: 14
semver-patch-days: 3
exclude:
- aws-lc-fips-sys
- aws-lc-rs
- aws-lc-sys
- rustls
- rustls-pki-types
- rustls-platform-verifier
- rustls-webpki
- package-ecosystem: rust-toolchain
directory: "/"

View File

@@ -90,7 +90,7 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: int128/docker-manifest-create-action@44422a4b046d55dc036df622039ed3aec43c613c # v2
- uses: int128/docker-manifest-create-action@7df7f9e221d927eaadf87db231ddf728047308a4 # v2
id: build
with:
tags: ${{ matrix.tag }}

View File

@@ -33,7 +33,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
with:
node-version-file: website/package.json
cache: "npm"
@@ -41,7 +41,7 @@ jobs:
- working-directory: website/
name: Install Dependencies
run: npm ci
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
with:
path: |
${{ github.workspace }}/website/api/.docusaurus
@@ -71,7 +71,7 @@ jobs:
with:
name: api-docs
path: website/api/build
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
with:
node-version-file: website/package.json
cache: "npm"

View File

@@ -127,7 +127,10 @@ jobs:
with:
postgresql_version: ${{ matrix.psql }}
- name: run migrations to stable
run: uv run python -m lifecycle.migrate
run: |
docker ps
docker logs setup-postgresql-1
uv run python -m lifecycle.migrate
- name: checkout current code
run: |
set -x
@@ -250,7 +253,7 @@ jobs:
run: |
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
- id: cache-web
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
if: contains(matrix.job.profiles, 'selenium')
with:
path: web/dist
@@ -296,7 +299,7 @@ jobs:
run: |
docker compose -f tests/openid_conformance/compose.yml up -d --quiet-pull
- id: cache-web
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v4
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b

View File

@@ -145,7 +145,7 @@ jobs:
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: "go.mod"
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
with:
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
@@ -47,7 +47,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
with:
node-version-file: web/package.json
cache: "npm"
@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
with:
node-version-file: web/package.json
cache: "npm"

View File

@@ -29,7 +29,7 @@ jobs:
github.event.pull_request.head.repo.full_name == github.repository)
steps:
- id: generate_token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
@@ -38,7 +38,7 @@ jobs:
token: ${{ steps.generate_token.outputs.token }}
- name: Compress images
id: compress
uses: calibreapp/image-actions@03c976c29803442fc4040a9de5509669e7759b81 # main
uses: calibreapp/image-actions@e2cc8db5d49c849e00844dfebf01438318e96fa2 # main
with:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
compressOnly: ${{ github.event_name != 'pull_request' }}

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}

View File

@@ -10,7 +10,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
if: ${{ env.GH_APP_ID != '' }}
with:
app-id: ${{ secrets.GH_APP_ID }}

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}

View File

@@ -35,13 +35,13 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
fetch-depth: 2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # 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@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
with:
files: |
${{ matrix.package }}/package.json

View File

@@ -29,7 +29,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
@@ -57,7 +57,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}

View File

@@ -87,7 +87,7 @@ jobs:
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: "go.mod"
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
with:
node-version-file: web/package.json
cache: "npm"
@@ -151,7 +151,7 @@ jobs:
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: "go.mod"
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v5
with:
node-version-file: web/package.json
cache: "npm"

View File

@@ -67,7 +67,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
@@ -96,7 +96,7 @@ jobs:
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
git push --follow-tags
- name: Create Release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
token: "${{ steps.app-token.outputs.token }}"
tag_name: "version/${{ inputs.version }}"
@@ -115,7 +115,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
@@ -157,7 +157,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}

View File

@@ -21,7 +21,7 @@ jobs:
steps:
- id: generate_token
if: ${{ github.event_name != 'pull_request' }}
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}

View File

@@ -14,6 +14,7 @@ pyproject.toml @goauthentik/backend
uv.lock @goauthentik/backend
Cargo.toml @goauthentik/backend
Cargo.lock @goauthentik/backend
build.rs @goauthentik/backend
go.mod @goauthentik/backend
go.sum @goauthentik/backend
.cargo/ @goauthentik/backend

376
Cargo.lock generated
View File

@@ -17,6 +17,18 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -106,6 +118,37 @@ dependencies = [
"rustversion",
]
[[package]]
name = "argh"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "211818e820cda9ca6f167a64a5c808837366a6dfd807157c64c1304c486cd033"
dependencies = [
"argh_derive",
"argh_shared",
]
[[package]]
name = "argh_derive"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c442a9d18cef5dde467405d27d461d080d68972d6d0dfd0408265b6749ec427d"
dependencies = [
"argh_shared",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "argh_shared"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ade012bac4db278517a0132c8c10c6427025868dca16c801087c28d5a411f1"
dependencies = [
"serde",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
@@ -138,6 +181,30 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "authentik"
version = "2026.5.0-rc1"
dependencies = [
"arc-swap",
"argh",
"authentik-axum",
"authentik-common",
"axum",
"color-eyre",
"eyre",
"hyper-unix-socket",
"hyper-util",
"metrics",
"metrics-exporter-prometheus",
"nix 0.31.2",
"pyo3",
"pyo3-build-config",
"sqlx",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "authentik-axum"
version = "2026.5.0-rc1"
@@ -150,6 +217,7 @@ dependencies = [
"eyre",
"forwarded-header-value",
"futures",
"pin-project-lite",
"tokio",
"tokio-rustls",
"tower",
@@ -169,8 +237,6 @@ dependencies = [
"serde_json",
"serde_repr",
"serde_with",
"tokio",
"tokio-util",
"url",
"uuid",
]
@@ -180,6 +246,7 @@ name = "authentik-common"
version = "2026.5.0-rc1"
dependencies = [
"arc-swap",
"authentik-client",
"aws-lc-rs",
"axum-server",
"config",
@@ -191,6 +258,8 @@ dependencies = [
"nix 0.31.2",
"notify",
"pin-project-lite",
"reqwest",
"reqwest-middleware",
"rustls",
"sentry",
"serde",
@@ -200,6 +269,7 @@ dependencies = [
"thiserror 2.0.18",
"time",
"tokio",
"tokio-retry2",
"tokio-util",
"tracing",
"tracing-error",
@@ -229,9 +299,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.16.2"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
dependencies = [
"aws-lc-fips-sys",
"aws-lc-sys",
@@ -241,9 +311,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.39.0"
version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
dependencies = [
"cc",
"cmake",
@@ -253,9 +323,9 @@ dependencies = [
[[package]]
name = "axum"
version = "0.8.8"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
"axum-macros",
@@ -309,9 +379,9 @@ dependencies = [
[[package]]
name = "axum-macros"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"
dependencies = [
"proc-macro2",
"quote",
@@ -508,9 +578,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.0"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
@@ -530,9 +600,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.6.0"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
@@ -565,6 +635,33 @@ dependencies = [
"cc",
]
[[package]]
name = "color-eyre"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]]
name = "colorchoice"
version = "1.0.5"
@@ -726,6 +823,15 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
@@ -975,6 +1081,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -1207,7 +1319,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
"foldhash 0.1.5",
]
[[package]]
@@ -1215,6 +1327,9 @@ name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"foldhash 0.2.0",
]
[[package]]
name = "hashlink"
@@ -1341,9 +1456,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
[[package]]
name = "hyper"
version = "1.8.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
"atomic-waker",
"bytes",
@@ -1356,7 +1471,6 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
@@ -1391,6 +1505,20 @@ dependencies = [
"tower-service",
]
[[package]]
name = "hyper-unix-socket"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88978f1d73da0eb87d86555fcc40cbdd87bc86eb6525710b89db8c9278ec6a59"
dependencies = [
"bytes",
"hyper",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -1848,6 +1976,46 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "metrics"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8"
dependencies = [
"ahash",
"portable-atomic",
]
[[package]]
name = "metrics-exporter-prometheus"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda"
dependencies = [
"base64 0.22.1",
"indexmap",
"metrics",
"metrics-util",
"quanta",
"thiserror 2.0.18",
]
[[package]]
name = "metrics-util"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"hashbrown 0.16.1",
"metrics",
"quanta",
"rand 0.9.2",
"rand_xoshiro",
"sketches-ddsketch",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -1979,7 +2147,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
"smallvec",
"zeroize",
]
@@ -2231,6 +2399,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "owo-colors"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
[[package]]
name = "parking"
version = "2.2.1"
@@ -2307,12 +2481,6 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.7.5"
@@ -2346,6 +2514,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -2421,6 +2595,79 @@ dependencies = [
"prost",
]
[[package]]
name = "pyo3"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
dependencies = [
"libc",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
]
[[package]]
name = "pyo3-build-config"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
dependencies = [
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.28.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "quanta"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
dependencies = [
"crossbeam-utils",
"libc",
"once_cell",
"raw-cpuid",
"wasi",
"web-sys",
"winapi",
]
[[package]]
name = "quinn"
version = "0.11.9"
@@ -2500,9 +2747,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
@@ -2557,6 +2804,24 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_xoshiro"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
dependencies = [
"rand_core 0.9.5",
]
[[package]]
name = "raw-cpuid"
version = "11.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
dependencies = [
"bitflags 2.11.0",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -2735,9 +3000,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.37"
version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"aws-lc-rs",
"log",
@@ -2800,9 +3065,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.10"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
@@ -3149,6 +3414,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "sketches-ddsketch"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
[[package]]
name = "slab"
version = "0.4.12"
@@ -3313,7 +3584,7 @@ dependencies = [
"memchr",
"once_cell",
"percent-encoding",
"rand 0.8.5",
"rand 0.8.6",
"rsa",
"serde",
"sha1",
@@ -3354,7 +3625,7 @@ dependencies = [
"md-5",
"memchr",
"once_cell",
"rand 0.8.5",
"rand 0.8.6",
"serde",
"serde_json",
"sha2",
@@ -3474,6 +3745,12 @@ dependencies = [
"libc",
]
[[package]]
name = "target-lexicon"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -3596,9 +3873,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.51.1"
version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"bytes",
"libc",
@@ -3623,6 +3900,16 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-retry2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0266d56e57e6b29becbfce5daa6add8730941ca8192ddd7c24d25bf90c32a743"
dependencies = [
"pin-project",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
@@ -3646,9 +3933,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.28.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
dependencies = [
"futures-util",
"log",
@@ -3857,9 +4144,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.28.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
dependencies = [
"bytes",
"data-encoding",
@@ -3869,7 +4156,6 @@ dependencies = [
"rand 0.9.2",
"sha1",
"thiserror 2.0.18",
"utf-8",
]
[[package]]
@@ -3979,12 +4265,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-zero"
version = "0.8.1"
@@ -4005,9 +4285,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.23.0"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"getrandom 0.4.2",
"js-sys",

View File

@@ -20,11 +20,13 @@ publish = false
[workspace.dependencies]
arc-swap = "= 1.9.1"
argh = "= 0.1.19"
axum-server = { version = "= 0.8.0", features = ["tls-rustls-no-provider"] }
aws-lc-rs = { version = "= 1.16.2", features = ["fips"] }
axum = { version = "= 0.8.8", features = ["http2", "macros", "ws"] }
clap = { version = "= 4.6.0", features = ["derive", "env"] }
aws-lc-rs = { version = "= 1.16.3", features = ["fips"] }
axum = { version = "= 0.8.9", features = ["http2", "macros", "ws"] }
clap = { version = "= 4.6.1", features = ["derive", "env"] }
client-ip = { version = "0.2.1", features = ["forwarded-header"] }
color-eyre = "= 0.6.5"
colored = "= 3.1.1"
config-rs = { package = "config", version = "= 0.15.22", default-features = false, features = [
"json",
@@ -37,11 +39,17 @@ eyre = "= 0.6.12"
forwarded-header-value = "= 0.1.1"
futures = "= 0.3.32"
glob = "= 0.3.3"
hyper-unix-socket = "= 0.6.1"
hyper-util = "= 0.1.20"
ipnet = { version = "= 2.12.0", features = ["serde"] }
json-subscriber = "= 0.2.8"
nix = { version = "= 0.31.2", features = ["signal"] }
metrics = "= 0.24.3"
metrics-exporter-prometheus = { version = "= 0.18.1", default-features = false }
nix = { version = "= 0.31.2", features = ["hostname", "signal"] }
notify = "= 8.2.0"
pin-project-lite = "= 0.2.17"
pyo3 = "= 0.28.3"
pyo3-build-config = "= 0.28.3"
regex = "= 1.12.3"
reqwest = { version = "= 0.13.2", features = [
"form",
@@ -58,7 +66,7 @@ reqwest-middleware = { version = "= 0.5.1", features = [
"query",
"rustls",
] }
rustls = { version = "= 0.23.37", features = ["fips"] }
rustls = { version = "= 0.23.40", features = ["fips"] }
sentry = { version = "= 0.47.0", default-features = false, features = [
"backtrace",
"contexts",
@@ -89,7 +97,8 @@ sqlx = { version = "= 0.8.6", default-features = false, features = [
tempfile = "= 3.27.0"
thiserror = "= 2.0.18"
time = { version = "= 0.3.47", features = ["macros"] }
tokio = { version = "= 1.51.1", features = ["full", "tracing"] }
tokio = { version = "= 1.52.1", features = ["full", "tracing"] }
tokio-retry2 = "= 0.9.1"
tokio-rustls = "= 0.26.4"
tokio-util = { version = "= 0.7.18", features = ["full"] }
tower = "= 0.5.3"
@@ -103,17 +112,12 @@ tracing-subscriber = { version = "= 0.3.23", features = [
"tracing-log",
] }
url = "= 2.5.8"
uuid = { version = "= 1.23.0", features = ["serde", "v4"] }
uuid = { version = "= 1.23.1", features = ["serde", "v4"] }
ak-axum = { package = "authentik-axum", version = "2026.5.0-rc1", path = "./packages/ak-axum" }
ak-client = { package = "authentik-client", version = "2026.5.0-rc1", path = "./packages/client-rust" }
ak-common = { package = "authentik-common", version = "2026.5.0-rc1", path = "./packages/ak-common", default-features = false }
[profile.dev.package.backtrace]
opt-level = 3
[profile.release]
lto = true
debug = 2
[workspace.lints.rust]
ambiguous_negative_literals = "warn"
closure_returning_async_block = "warn"
@@ -227,3 +231,57 @@ unused_trait_names = "warn"
unwrap_in_result = "warn"
unwrap_used = "warn"
verbose_file_reads = "warn"
[profile.dev.package.backtrace]
opt-level = 3
[profile.dev]
panic = "abort"
[profile.release]
debug = 2
lto = "fat"
# Because of the async runtime, we want to die straightaway if we panic.
panic = "abort"
strip = true
[package]
name = "authentik"
version.workspace = true
authors.workspace = true
edition.workspace = true
readme.workspace = true
homepage.workspace = true
repository.workspace = true
license-file.workspace = true
publish.workspace = true
[features]
default = ["core", "proxy"]
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
proxy = ["ak-common/proxy"]
[build-dependencies]
pyo3-build-config.workspace = true
[dependencies]
ak-axum.workspace = true
ak-common.workspace = true
arc-swap.workspace = true
argh.workspace = true
axum.workspace = true
color-eyre.workspace = true
eyre.workspace = true
hyper-unix-socket.workspace = true
hyper-util.workspace = true
metrics.workspace = true
metrics-exporter-prometheus.workspace = true
nix.workspace = true
pyo3 = { workspace = true, optional = true }
sqlx = { workspace = true, optional = true }
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true
[lints]
workspace = true

View File

@@ -115,6 +115,9 @@ run-server: ## Run the main authentik server process
run-worker: ## Run the main authentik worker process
$(UV) run ak worker
run-worker-watch: ## Run the authentik worker, with auto reloading
watchexec --on-busy-update=restart --stop-signal=SIGINT --exts py,rs --no-meta --notify -- $(UV) run ak worker
core-i18n-extract:
$(UV) run ak makemessages \
--add-location file \
@@ -205,10 +208,10 @@ gen-diff: ## (Release) generate the changelog diff between the current schema a
npx prettier --write diff.md
gen-client-go: ## Build and install the authentik API for Golang
make -C "${PWD}/packages/client-go" build
$(UV) run make -C "${PWD}/packages/client-go" build
gen-client-rust: ## Build and install the authentik API for Rust
make -C "${PWD}/packages/client-rust" build version=${NPM_VERSION}
$(UV) run make -C "${PWD}/packages/client-rust" build version=${NPM_VERSION}
make lint-fix-rust
gen-client-ts: ## Build and install the authentik API for Typescript into the authentik UI Application

View File

@@ -106,6 +106,7 @@ class Backend:
self,
name: str,
request: HttpRequest | None = None,
use_cache: bool = True,
) -> dict[str, str] | None:
"""
Get URLs for each theme variant when filename contains %(theme)s.
@@ -121,7 +122,7 @@ class Backend:
return None
return {
theme: self.file_url(substitute_theme(name, theme), request, use_cache=True)
theme: self.file_url(substitute_theme(name, theme), request, use_cache=use_cache)
for theme in get_valid_themes()
}

View File

@@ -51,6 +51,7 @@ class PassthroughBackend(Backend):
self,
name: str,
request: HttpRequest | None = None,
use_cache: bool = True,
) -> dict[str, str] | None:
"""Support themed URLs for external URLs with %(theme)s placeholder.

View File

@@ -1,7 +1,7 @@
from collections.abc import Generator, Iterator
from contextlib import contextmanager
from tempfile import SpooledTemporaryFile
from urllib.parse import urlsplit
from urllib.parse import urlsplit, urlunsplit
import boto3
from botocore.config import Config
@@ -164,16 +164,19 @@ class S3Backend(ManageableBackend):
)
def _file_url(name: str, request: HttpRequest | None) -> str:
client = self.client
params = {
"Bucket": self.bucket_name,
"Key": f"{self.base_path}/{name}",
}
url = self.client.generate_presigned_url(
"get_object",
Params=params,
ExpiresIn=expires_in,
HttpMethod="GET",
operation_name = "GetObject"
operation_model = client.meta.service_model.operation_model(operation_name)
request_dict = client._convert_to_request_dict(
params,
operation_model,
endpoint_url=client.meta.endpoint_url,
context={"is_presign_request": True},
)
# Support custom domain for S3-compatible storage (so not AWS)
@@ -183,9 +186,8 @@ class S3Backend(ManageableBackend):
CONFIG.get(f"storage.{self.name}.custom_domain", None),
)
if custom_domain:
parsed = urlsplit(url)
scheme = "https" if use_https else "http"
path = parsed.path
path = request_dict["url_path"]
# When using path-style addressing, the presigned URL contains the bucket
# name in the path (e.g., /bucket-name/key). Since custom_domain must
@@ -200,9 +202,22 @@ class S3Backend(ManageableBackend):
if not path.startswith("/"):
path = f"/{path}"
url = f"{scheme}://{custom_domain}{path}?{parsed.query}"
custom_base = urlsplit(f"{scheme}://{custom_domain}")
return url
# Sign the final public URL instead of signing the internal S3 endpoint and
# rewriting it afterwards. Presigned SigV4 URLs include the host header in the
# canonical request, so post-sign host changes break strict backends like RustFS.
public_path = f"{custom_base.path.rstrip('/')}{path}" if custom_base.path else path
request_dict["url_path"] = public_path
request_dict["url"] = urlunsplit(
(custom_base.scheme, custom_base.netloc, public_path, "", "")
)
return client._request_signer.generate_presigned_url(
request_dict,
operation_name,
expires_in=expires_in,
)
if use_cache:
return self._cache_get_or_set(name, request, _file_url, expires_in)

View File

@@ -1,4 +1,5 @@
from unittest import skipUnless
from urllib.parse import parse_qs, urlsplit
from botocore.exceptions import UnsupportedSignatureVersionError
from django.test import TestCase
@@ -168,6 +169,44 @@ class TestS3Backend(FileTestS3BackendMixin, TestCase):
f"URL: {url}",
)
@CONFIG.patch("storage.s3.secure_urls", False)
@CONFIG.patch("storage.s3.addressing_style", "path")
def test_file_url_custom_domain_resigns_for_custom_host(self):
"""Test presigned URLs are signed for the custom domain host.
Host-changing custom domains must produce a signature query string for
the public host, not reuse the internal endpoint signature.
"""
bucket_name = self.media_s3_bucket_name
key_name = "application-icons/test.svg"
custom_domain = f"files.example.test:8020/{bucket_name}"
endpoint_signed_url = self.media_s3_backend.client.generate_presigned_url(
"get_object",
Params={
"Bucket": bucket_name,
"Key": f"{self.media_s3_backend.base_path}/{key_name}",
},
ExpiresIn=900,
HttpMethod="GET",
)
with CONFIG.patch("storage.media.s3.custom_domain", custom_domain):
custom_url = self.media_s3_backend.file_url(key_name, use_cache=False)
endpoint_parts = urlsplit(endpoint_signed_url)
custom_parts = urlsplit(custom_url)
self.assertEqual(custom_parts.scheme, "http")
self.assertEqual(custom_parts.netloc, "files.example.test:8020")
self.assertEqual(parse_qs(custom_parts.query)["X-Amz-SignedHeaders"], ["host"])
self.assertNotEqual(
custom_parts.query,
endpoint_parts.query,
"Custom-domain URLs must be signed for the public host, not reuse the endpoint "
"signature query string.",
)
def test_themed_urls_without_theme_variable(self):
"""Test themed_urls returns None when filename has no %(theme)s"""
result = self.media_s3_backend.themed_urls("logo.png")

View File

@@ -74,6 +74,10 @@ class FileManager:
) -> str:
"""
Get URL for accessing the file.
Set ``use_cache=False`` when the caller needs a fresh signed URL instead
of a cached one, for example when serializing flow/login payloads that
may be refreshed after the previous JWT has expired.
"""
if not name:
return ""
@@ -83,7 +87,7 @@ class FileManager:
for backend in self.backends:
if backend.supports_file(name):
return backend.file_url(name, request)
return backend.file_url(name, request, use_cache=use_cache)
LOGGER.warning(f"Could not find file backend for file: {name}")
return ""
@@ -92,10 +96,14 @@ class FileManager:
self,
name: str | None,
request: HttpRequest | Request | None = None,
use_cache: bool = True,
) -> dict[str, str] | None:
"""
Get URLs for each theme variant when filename contains %(theme)s.
``use_cache`` has the same semantics as ``file_url()`` and allows
callers to force regeneration of expiring signed URLs.
Returns dict mapping theme to URL if %(theme)s present, None otherwise.
"""
if not name:
@@ -106,7 +114,7 @@ class FileManager:
for backend in self.backends:
if backend.supports_file(name):
return backend.themed_urls(name, request)
return backend.themed_urls(name, request, use_cache=use_cache)
return None

View File

@@ -1,6 +1,7 @@
"""Test file service layer"""
from unittest import skipUnless
from unittest.mock import Mock
from urllib.parse import urlparse
from django.http import HttpRequest
@@ -53,6 +54,19 @@ class TestResolveFileUrlBasic(TestCase):
result = manager.file_url("/static/authentik/sources/icon.svg")
self.assertEqual(result, "/static/authentik/sources/icon.svg")
def test_file_url_forwards_use_cache(self):
"""Test file_url forwards use_cache to backend."""
manager = FileManager(FileUsage.MEDIA)
backend = Mock()
backend.supports_file.return_value = True
backend.file_url.return_value = "/files/media/public/test.png?token=fresh"
manager.backends = [backend]
result = manager.file_url("test.png", use_cache=False)
self.assertEqual(result, "/files/media/public/test.png?token=fresh")
backend.file_url.assert_called_once_with("test.png", None, use_cache=False)
class TestResolveFileUrlFileBackend(FileTestFileBackendMixin, TestCase):
def test_resolve_storage_file(self):

View File

@@ -3,7 +3,6 @@
import traceback
from collections.abc import Callable
from importlib import import_module
from inspect import ismethod
from django.apps import AppConfig
from django.conf import settings
@@ -72,12 +71,19 @@ class ManagedAppConfig(AppConfig):
def _reconcile(self, prefix: str) -> None:
for meth_name in dir(self):
meth = getattr(self, meth_name)
if not ismethod(meth):
# Check the attribute on the class to avoid evaluating @property descriptors.
# Using getattr(self, ...) on a @property would evaluate it, which can trigger
# expensive side effects (e.g. tenant_schedule_specs iterating all providers
# and running PolicyEngine queries for every user).
class_attr = getattr(type(self), meth_name, None)
if class_attr is None or isinstance(class_attr, property):
continue
category = getattr(meth, "_authentik_managed_reconcile", None)
if not callable(class_attr):
continue
category = getattr(class_attr, "_authentik_managed_reconcile", None)
if category != prefix:
continue
meth = getattr(self, meth_name)
name = meth_name.replace(prefix, "")
try:
self.logger.debug("Starting reconciler", name=name)

View File

@@ -1,5 +1,6 @@
"""Apply blueprint from commandline"""
from argparse import ArgumentParser
from sys import exit as sys_exit
from django.core.management.base import BaseCommand, no_translations
@@ -31,5 +32,5 @@ class Command(BaseCommand):
sys_exit(1)
importer.apply()
def add_arguments(self, parser):
def add_arguments(self, parser: ArgumentParser):
parser.add_argument("blueprints", nargs="+", type=str)

View File

@@ -101,13 +101,23 @@ class Brand(SerializerModel):
"""Get themed URLs for branding_favicon if it contains %(theme)s"""
return get_file_manager(FileUsage.MEDIA).themed_urls(self.branding_favicon)
def branding_default_flow_background_url(self) -> str:
def branding_default_flow_background_url(self, request=None, use_cache: bool = True) -> str:
"""Get branding_default_flow_background URL"""
return get_file_manager(FileUsage.MEDIA).file_url(self.branding_default_flow_background)
return get_file_manager(FileUsage.MEDIA).file_url(
self.branding_default_flow_background,
request,
use_cache=use_cache,
)
def branding_default_flow_background_themed_urls(self) -> dict[str, str] | None:
def branding_default_flow_background_themed_urls(
self, request=None, use_cache: bool = True
) -> dict[str, str] | None:
"""Get themed URLs for branding_default_flow_background if it contains %(theme)s"""
return get_file_manager(FileUsage.MEDIA).themed_urls(self.branding_default_flow_background)
return get_file_manager(FileUsage.MEDIA).themed_urls(
self.branding_default_flow_background,
request,
use_cache=use_cache,
)
@property
def serializer(self) -> type[Serializer]:

View File

@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"
GRANT_TYPE_IMPLICIT = "implicit"
GRANT_TYPE_HYBRID = "hybrid"
GRANT_TYPE_REFRESH_TOKEN = "refresh_token" # nosec
GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"
GRANT_TYPE_PASSWORD = "password" # nosec

View File

@@ -30,6 +30,8 @@ SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
SAML_STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
DEFAULT_ISSUER = "authentik"
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.2

View File

@@ -36,9 +36,13 @@ from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
def user_app_cache_key(
user_pk: str, page_number: int | None = None, only_with_launch_url: bool = False
) -> str:
"""Cache key where application list for user is saved"""
key = f"{CACHE_PREFIX}app_access/{user_pk}"
if only_with_launch_url:
key += "/launch"
if page_number:
key += f"/{page_number}"
return key
@@ -116,6 +120,7 @@ class ApplicationSerializer(ModelSerializer):
"meta_publisher",
"policy_engine_mode",
"group",
"meta_hide",
]
extra_kwargs = {
"backchannel_providers": {"required": False},
@@ -274,11 +279,17 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
if superuser_full_list and request.user.is_superuser:
return super().list(request)
only_with_launch_url = str(
request.query_params.get("only_with_launch_url", "false")
).lower()
only_with_launch_url = (
str(request.query_params.get("only_with_launch_url", "false")).lower()
) == "true"
queryset = self._filter_queryset_for_list(self.get_queryset())
queryset = queryset.exclude(meta_hide=True)
if only_with_launch_url:
# Pre-filter at DB level to skip expensive per-app policy evaluation
# for apps that can never appear in the launcher (no meta_launch_url
# and no provider, so no possible launch URL).
queryset = queryset.exclude(meta_launch_url="", provider__isnull=True)
paginator: Pagination = self.paginator
paginated_apps = paginator.paginate_queryset(queryset, request)
@@ -295,7 +306,6 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
except ValueError as exc:
raise ValidationError from exc
allowed_applications = self._get_allowed_applications(paginated_apps, user=for_user)
allowed_applications = self._expand_applications(allowed_applications)
serializer = self.get_serializer(allowed_applications, many=True)
return self.get_paginated_response(serializer.data)
@@ -305,19 +315,26 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
allowed_applications = self._get_allowed_applications(paginated_apps)
if should_cache:
allowed_applications = cache.get(
user_app_cache_key(self.request.user.pk, paginator.page.number)
user_app_cache_key(
self.request.user.pk, paginator.page.number, only_with_launch_url
)
)
if not allowed_applications:
if allowed_applications:
# Re-fetch cached applications since pickled instances lose prefetched
# relationships, causing N+1 queries during serialization
allowed_applications = self._expand_applications(allowed_applications)
else:
LOGGER.debug("Caching allowed application list", page=paginator.page.number)
allowed_applications = self._get_allowed_applications(paginated_apps)
cache.set(
user_app_cache_key(self.request.user.pk, paginator.page.number),
user_app_cache_key(
self.request.user.pk, paginator.page.number, only_with_launch_url
),
allowed_applications,
timeout=86400,
)
allowed_applications = self._expand_applications(allowed_applications)
if only_with_launch_url == "true":
if only_with_launch_url:
allowed_applications = self._filter_applications_with_launch_url(allowed_applications)
serializer = self.get_serializer(allowed_applications, many=True)

View File

@@ -19,7 +19,7 @@ from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.permissions import IsAuthenticated
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ValidationError
@@ -37,6 +37,77 @@ from authentik.endpoints.connectors.agent.auth import AgentAuth
from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
class BulkManyRelatedField(ManyRelatedField):
"""ManyRelatedField that validates all PKs in a single query instead of one per PK."""
def to_internal_value(self, data):
if isinstance(data, str) or not hasattr(data, "__iter__"):
self.fail("not_a_list", input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail("empty")
child = self.child_relation
pk_field = child.pk_field
# Coerce PKs through pk_field if defined
pk_map = {}
for item in data:
if isinstance(item, bool):
self.fail("incorrect_type", data_type=type(item).__name__)
pk = pk_field.to_internal_value(item) if pk_field else item
pk_map[pk] = item # map coerced PK -> original value for error reporting
queryset = child.get_queryset()
# Use count to validate all PKs exist in a single query
found_count = queryset.filter(pk__in=pk_map.keys()).count()
if found_count < len(pk_map):
# Some PKs not found — fall back to per-PK checks for error reporting.
# This only runs when there's an actual validation error (rare path).
for pk, original in pk_map.items():
if not queryset.filter(pk=pk).exists():
child.fail("does_not_exist", pk_value=original)
# Return raw PKs — Django's M2M set() accepts both objects and PKs,
# using get_prep_value() for type coercion. This avoids loading all
# objects into memory and avoids triggering post_init signals.
return list(pk_map.keys())
def to_representation(self, iterable):
# For non-prefetched querysets, get PKs directly without loading model instances.
# When prefetched, _result_cache is a list (possibly empty); when not, it's None.
if hasattr(iterable, "values_list") and getattr(iterable, "_result_cache", None) is None:
return list(iterable.values_list("pk", flat=True))
return super().to_representation(iterable)
class BulkPrimaryKeyRelatedField(PrimaryKeyRelatedField):
"""PrimaryKeyRelatedField that uses bulk validation when many=True."""
@classmethod
def many_init(cls, *args, **kwargs):
allow_empty = kwargs.pop("allow_empty", None)
max_length = kwargs.pop("max_length", None)
min_length = kwargs.pop("min_length", None)
child_relation = cls(*args, **kwargs)
list_kwargs = {
"child_relation": child_relation,
}
if allow_empty is not None:
list_kwargs["allow_empty"] = allow_empty
if max_length is not None:
list_kwargs["max_length"] = max_length
if min_length is not None:
list_kwargs["min_length"] = min_length
list_kwargs.update(
{
key: value
for key, value in kwargs.items()
if key in ("required", "default", "source")
}
)
return BulkManyRelatedField(**list_kwargs)
PARTIAL_USER_SERIALIZER_MODEL_FIELDS = [
"pk",
"username",
@@ -79,6 +150,7 @@ class GroupSerializer(ModelSerializer):
"""Group Serializer"""
attributes = JSONDictField(required=False)
users = BulkPrimaryKeyRelatedField(queryset=User.objects.all(), many=True, default=list)
parents = PrimaryKeyRelatedField(queryset=Group.objects.all(), many=True, required=False)
parents_obj = SerializerMethodField(allow_null=True)
children_obj = SerializerMethodField(allow_null=True)
@@ -193,9 +265,6 @@ class GroupSerializer(ModelSerializer):
"children_obj",
]
extra_kwargs = {
"users": {
"default": list,
},
"children": {
"required": False,
"default": list,
@@ -225,6 +294,7 @@ class GroupFilter(FilterSet):
members_by_pk = ModelMultipleChoiceFilter(
field_name="users",
queryset=User.objects.all(),
distinct=False,
)
def filter_attributes(self, queryset, name, value):
@@ -276,7 +346,8 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
]
def get_queryset(self):
base_qs = Group.objects.all().prefetch_related("roles")
# Always prefetch parents and children since their PKs are always serialized
base_qs = Group.objects.all().prefetch_related("roles", "parents", "children")
if self.serializer_class(context={"request": self.request})._should_include_users:
# Only fetch fields needed by PartialUserSerializer to reduce DB load and instantiation
@@ -287,16 +358,9 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
queryset=User.objects.all().only(*PARTIAL_USER_SERIALIZER_MODEL_FIELDS),
)
)
else:
base_qs = base_qs.prefetch_related(
Prefetch("users", queryset=User.objects.all().only("id"))
)
if self.serializer_class(context={"request": self.request})._should_include_children:
base_qs = base_qs.prefetch_related("children")
if self.serializer_class(context={"request": self.request})._should_include_parents:
base_qs = base_qs.prefetch_related("parents")
# When include_users=false, skip users prefetch entirely.
# BulkManyRelatedField.to_representation will use values_list to get PKs
# directly without loading User instances into memory.
return base_qs

View File

@@ -1,6 +1,5 @@
"""Tokens API Viewset"""
from datetime import timedelta
from typing import Any
from django.utils.timezone import now
@@ -19,15 +18,12 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_OWNER_PK,
USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token,
TokenIntents,
User,
UserTypes,
default_token_duration,
default_token_key,
)
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict
@@ -175,40 +171,6 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
Event.new(EventAction.SECRET_VIEW, secret=token).from_http(request) # noqa # nosec
return Response(TokenViewSerializer({"key": token.key}).data)
@extend_schema(
request=None,
responses={
200: TokenViewSerializer(many=False),
403: OpenApiResponse(description="Not the token owner, agent owner, or superuser"),
},
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
def rotate(self, request: Request, identifier: str) -> Response:
"""Rotate the token key and reset the expiry to 24 hours. Only callable by the token
owner, the owning agent's human owner, or a superuser."""
token = (
Token.objects.including_expired()
.select_related("user")
.filter(identifier=identifier)
.first()
)
if not token:
return Response(status=404)
if not request.user.is_superuser:
is_token_owner = token.user_id == request.user.pk
is_agent_owner = token.user.type == UserTypes.AGENT and str(
request.user.pk
) == token.user.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
if not is_token_owner and not is_agent_owner:
return Response(status=403)
token.key = default_token_key()
token.expires = now() + timedelta(hours=24)
token.save()
Event.new(EventAction.SECRET_ROTATE, secret=token).from_http(request) # noqa # nosec
return Response(TokenViewSerializer({"key": token.key}).data)
@permission_required("authentik_core.set_token_key")
@extend_schema(
request=TokenSetKeySerializer(),

View File

@@ -6,6 +6,7 @@ from typing import Any
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import AnonymousUser, Permission
from django.db.models import Exists, OuterRef, Prefetch, Q
from django.db.transaction import atomic
from django.db.utils import IntegrityError
from django.urls import reverse_lazy
@@ -13,6 +14,7 @@ from django.utils.http import urlencode
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from django_filters.filters import (
BooleanFilter,
CharFilter,
@@ -75,13 +77,9 @@ from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_USER,
)
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_ALLOWED_APPS,
USER_ATTRIBUTE_AGENT_OWNER_PK,
USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_PATH_AGENT,
USER_PATH_SERVICE_ACCOUNT,
USERNAME_MAX_LENGTH,
Application,
Group,
Session,
Token,
@@ -92,7 +90,6 @@ from authentik.core.models import (
)
from authentik.endpoints.connectors.agent.auth import AgentAuth
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict, sanitize_dict
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
@@ -110,6 +107,10 @@ from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger()
INVALID_PASSWORD_HASH_MESSAGE = gettext_lazy(
"Invalid password hash format. Must be a valid Django password hash."
)
class ParamUserSerializer(PassiveSerializer):
"""Partial serializer for query parameters to select a user"""
@@ -136,7 +137,7 @@ class PartialGroupSerializer(ModelSerializer):
class UserSerializer(ModelSerializer):
"""User Serializer"""
is_superuser = BooleanField(read_only=True)
is_superuser = SerializerMethodField()
avatar = SerializerMethodField()
attributes = JSONDictField(required=False)
groups = PrimaryKeyRelatedField(
@@ -173,6 +174,14 @@ class UserSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_roles", "true")).lower() == "true"
@extend_schema_field(BooleanField)
def get_is_superuser(self, instance: User) -> bool:
"""Use annotation if available to avoid N+1 query"""
ann = getattr(instance, "_annotated_is_superuser", None)
if ann is not None:
return ann
return instance.is_superuser
@extend_schema_field(PartialGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[PartialGroupSerializer] | None:
if not self._should_include_groups:
@@ -186,47 +195,79 @@ class UserSerializer(ModelSerializer):
return RoleSerializer(instance.roles, many=True).data
def __init__(self, *args, **kwargs):
"""Setting password and permissions directly is allowed only in blueprints."""
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["password"] = CharField(required=False, allow_null=True)
self.fields["password_hash"] = CharField(required=False, allow_null=True)
self.fields["permissions"] = ListField(
required=False,
child=ChoiceField(choices=get_permission_choices()),
)
def create(self, validated_data: dict) -> User:
"""If this serializer is used in the blueprint context, we allow for
directly setting a password. However should be done via the `set_password`
method instead of directly setting it like rest_framework."""
password = validated_data.pop("password", None)
perms_qs = Permission.objects.filter(
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
).values_list("content_type__app_label", "codename")
perms_list = [f"{ct}.{name}" for ct, name in list(perms_qs)]
"""Create a user, with blueprint-only password and permission writes."""
is_blueprint = SERIALIZER_CONTEXT_BLUEPRINT in self.context
if is_blueprint:
password = validated_data.pop("password", None)
password_hash = validated_data.pop("password_hash", None)
permissions = validated_data.pop("permissions", [])
self._validate_password_inputs(password, password_hash)
instance: User = super().create(validated_data)
self._set_password(instance, password)
instance.assign_perms_to_managed_role(perms_list)
if is_blueprint:
self._set_password(instance, password, password_hash)
perms_qs = Permission.objects.filter(
codename__in=[permission.split(".")[1] for permission in permissions]
).values_list("content_type__app_label", "codename")
perms_list = [f"{ct}.{name}" for ct, name in perms_qs]
instance.assign_perms_to_managed_role(perms_list)
self._ensure_password_not_empty(instance)
return instance
def update(self, instance: User, validated_data: dict) -> User:
"""Same as `create` above, set the password directly if we're in a blueprint
context"""
password = validated_data.pop("password", None)
perms_qs = Permission.objects.filter(
codename__in=[x.split(".")[1] for x in validated_data.pop("permissions", [])]
).values_list("content_type__app_label", "codename")
perms_list = [f"{ct}.{name}" for ct, name in list(perms_qs)]
"""Update a user, with blueprint-only password and permission writes."""
is_blueprint = SERIALIZER_CONTEXT_BLUEPRINT in self.context
if is_blueprint:
password = validated_data.pop("password", None)
password_hash = validated_data.pop("password_hash", None)
permissions = validated_data.pop("permissions", [])
self._validate_password_inputs(password, password_hash)
instance = super().update(instance, validated_data)
self._set_password(instance, password)
instance.assign_perms_to_managed_role(perms_list)
if is_blueprint:
self._set_password(instance, password, password_hash)
perms_qs = Permission.objects.filter(
codename__in=[permission.split(".")[1] for permission in permissions]
).values_list("content_type__app_label", "codename")
perms_list = [f"{ct}.{name}" for ct, name in perms_qs]
instance.assign_perms_to_managed_role(perms_list)
self._ensure_password_not_empty(instance)
return instance
def _set_password(self, instance: User, password: str | None):
"""Set password of user if we're in a blueprint context, and if it's an empty
string then use an unusable password"""
if SERIALIZER_CONTEXT_BLUEPRINT in self.context and password:
def _validate_password_inputs(self, password: str | None, password_hash: str | None):
"""Validate mutually-exclusive password inputs before any model mutation."""
if password is not None and password_hash is not None:
raise ValidationError(_("Cannot set both password and password_hash. Use only one."))
if password_hash is None:
return
try:
User.validate_password_hash(password_hash)
except ValueError as exc:
LOGGER.warning("Failed to identify password hash format", exc_info=exc)
raise ValidationError(INVALID_PASSWORD_HASH_MESSAGE) from exc
def _set_password(self, instance: User, password: str | None, password_hash: str | None = None):
"""Set password from plain text or hash."""
if password_hash is not None:
instance.set_password_from_hash(password_hash)
instance.save()
elif password:
instance.set_password(password)
instance.save()
def _ensure_password_not_empty(self, instance: User):
"""Store an explicit unusable password instead of an empty password field."""
if len(instance.password) == 0:
instance.set_unusable_password()
instance.save()
@@ -254,25 +295,8 @@ class UserSerializer(ModelSerializer):
raise ValidationError(_("Can't change internal service account to other user type."))
if not self.instance and user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT.value:
raise ValidationError(_("Setting a user to internal service account is not allowed."))
if (
self.instance
and self.instance.type == UserTypes.AGENT
and user_type != UserTypes.AGENT.value
):
raise ValidationError(_("Can't change agent user type."))
return user_type
def validate_attributes(self, attrs: dict) -> dict:
"""Prevent changes to agent owner"""
if not self.instance:
return attrs
if self.instance.type == UserTypes.AGENT:
existing_owner = self.instance.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
new_owner = attrs.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
if existing_owner is not None and new_owner != existing_owner:
raise ValidationError(_("Can't change owner of agent user."))
return attrs
def validate(self, attrs: dict) -> dict:
if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
raise ValidationError(_("Can't modify internal service account users"))
@@ -412,6 +436,12 @@ class UserPasswordSetSerializer(PassiveSerializer):
password = CharField(required=True)
class UserPasswordHashSetSerializer(PassiveSerializer):
"""Payload to set a users' password hash directly"""
password = CharField(required=True)
class UserServiceAccountSerializer(PassiveSerializer):
"""Payload to create a service account"""
@@ -427,26 +457,6 @@ class UserServiceAccountSerializer(PassiveSerializer):
)
class UserAgentSerializer(PassiveSerializer):
"""Payload to create an agent user"""
name = CharField(max_length=150)
owner = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False, default=None)
class UserAgentAllowedAppsSerializer(PassiveSerializer):
"""Payload to replace an agent's allowed applications"""
allowed_apps = ListField(child=UUIDField())
class UserAgentAllowedAppSerializer(PassiveSerializer):
"""Payload to add or remove a single allowed application"""
app = UUIDField()
action = ChoiceField(choices=[("add", "Add"), ("remove", "Remove")])
class UserRecoveryLinkSerializer(PassiveSerializer):
"""Payload to create a recovery link"""
@@ -583,10 +593,30 @@ class UserViewSet(
def get_queryset(self):
base_qs = User.objects.all().exclude_anonymous()
# Always prefetch groups since group PKs are always serialized.
# Use full prefetch when include_groups=true (for groups_obj), ID-only otherwise.
if self.serializer_class(context={"request": self.request})._should_include_groups:
base_qs = base_qs.prefetch_related("groups")
else:
base_qs = base_qs.prefetch_related(
Prefetch("groups", queryset=Group.objects.all().only("group_uuid"))
)
if self.serializer_class(context={"request": self.request})._should_include_roles:
base_qs = base_qs.prefetch_related("roles")
else:
base_qs = base_qs.prefetch_related(
Prefetch("roles", queryset=Role.objects.all().only("uuid"))
)
# Annotate is_superuser to avoid N+1 query per user
base_qs = base_qs.annotate(
_annotated_is_superuser=Exists(
Group.objects.filter(
is_superuser=True,
).filter(
Q(users=OuterRef("pk")) | Q(descendant_nodes__descendant__users=OuterRef("pk"))
)
)
)
return base_qs
@extend_schema(
@@ -733,260 +763,6 @@ class UserViewSet(
status=500,
)
@permission_required(
None,
["authentik_core.add_user", "authentik_core.add_token", "authentik_core.add_agent_user"],
)
@extend_schema(
request=UserAgentSerializer,
responses={
200: inline_serializer(
"UserAgentResponse",
{
"username": CharField(required=True),
"token": CharField(required=True),
"user_uid": CharField(required=True),
"user_pk": IntegerField(required=True),
},
)
},
)
@action(
detail=False,
methods=["POST"],
pagination_class=None,
filter_backends=[],
)
@validate(UserAgentSerializer)
def agent(self, request: Request, body: UserAgentSerializer) -> Response:
"""Create a new agent user. Enterprise only. Caller must be an internal user."""
from authentik.enterprise.license import LicenseKey
if not LicenseKey.cached_summary().status.is_valid:
raise ValidationError(_("Enterprise is required to use this endpoint."))
if request.user.type != UserTypes.INTERNAL:
raise ValidationError(_("Only internal users can create agent users."))
requested_owner = body.validated_data.get("owner")
if requested_owner and not request.user.is_superuser:
if requested_owner.pk != request.user.pk:
raise ValidationError(
_("Non-superusers can only create agents owned by themselves.")
)
owner = requested_owner or request.user
username = body.validated_data["name"]
with atomic():
try:
user: User = User.objects.create(
username=username,
name=username,
type=UserTypes.AGENT,
attributes={
USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk),
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [],
},
path=USER_PATH_AGENT,
)
user.set_unusable_password()
user.save()
token = Token.objects.create(
identifier=slugify(f"agent-{username}-token"),
intent=TokenIntents.INTENT_API,
user=user,
expires=now() + timedelta(hours=24),
expiring=True,
)
user.assign_perms_to_managed_role("authentik_core.view_token_key", token)
owner.assign_perms_to_managed_role("authentik_core.view_user", user)
owner.assign_perms_to_managed_role("authentik_core.change_user", user)
owner.assign_perms_to_managed_role("authentik_core.delete_user", user)
owner.assign_perms_to_managed_role("authentik_core.view_user_applications", user)
Event.new(
EventAction.MODEL_CREATED,
model=sanitize_dict(model_to_dict(user)),
agent_owner=sanitize_dict(model_to_dict(owner)),
).from_http(request)
return Response(
{
"username": user.username,
"user_uid": user.uid,
"user_pk": user.pk,
"token": token.key,
}
)
except IntegrityError as exc:
error_msg = str(exc).lower()
if "unique" in error_msg:
return Response(
data={"non_field_errors": [_("A user with this username already exists")]},
status=400,
)
else:
LOGGER.warning("Agent user creation failed", exc=exc)
return Response(
data={"non_field_errors": [_("Unable to create user")]},
status=400,
)
except (ValueError, TypeError) as exc:
LOGGER.error("Unexpected error during agent user creation", exc=exc)
return Response(
data={"non_field_errors": [_("Unknown error occurred")]},
status=500,
)
@extend_schema(
request=UserAgentAllowedAppsSerializer,
responses={
200: UserAgentAllowedAppsSerializer,
400: OpenApiResponse(description="Invalid app UUIDs or owner lacks access"),
403: OpenApiResponse(description="Not the agent's owner or superuser"),
},
)
@action(
detail=True,
methods=["PUT"],
url_path="agent_allowed_apps",
url_name="agent-allowed-apps",
pagination_class=None,
filter_backends=[],
)
@validate(UserAgentAllowedAppsSerializer)
def agent_allowed_apps(
self, request: Request, pk: int, body: UserAgentAllowedAppsSerializer
) -> Response:
"""Replace the allowed application list for an agent user.
Caller must be the agent's owner or a superuser."""
from authentik.core.apps import AppAccessWithoutBindings
from authentik.policies.engine import PolicyEngine
agent, owner = self._get_agent_and_owner(request)
app_uuids = body.validated_data["allowed_apps"]
errors = []
for app_uuid in app_uuids:
try:
app = Application.objects.get(pk=app_uuid)
except Application.DoesNotExist:
errors.append(str(app_uuid))
continue
engine = PolicyEngine(app, owner, request)
engine.empty_result = AppAccessWithoutBindings.get()
engine.use_cache = False
engine.build()
if not engine.passing:
errors.append(str(app_uuid))
if errors:
return Response(
data={
"allowed_apps": [
_(
"Owner does not have access to application %(uuid)s "
"or application does not exist."
)
% {"uuid": uuid}
for uuid in errors
]
},
status=400,
)
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = [str(u) for u in app_uuids]
agent.save(update_fields=["attributes"])
return Response({"allowed_apps": [str(u) for u in app_uuids]})
@extend_schema(
request=UserAgentAllowedAppSerializer,
responses={
200: UserAgentAllowedAppsSerializer,
204: OpenApiResponse(description="Application removed"),
400: OpenApiResponse(description="Invalid app UUID or owner lacks access"),
403: OpenApiResponse(description="Not the agent's owner or superuser"),
},
)
@action(
detail=True,
methods=["PATCH"],
url_path="agent_allowed_app",
url_name="agent-allowed-app",
pagination_class=None,
filter_backends=[],
)
@validate(UserAgentAllowedAppSerializer)
def agent_allowed_app(
self, request: Request, pk: int, body: UserAgentAllowedAppSerializer
) -> Response:
"""Add or remove a single application from an agent's allowed list.
Caller must be the agent's owner or a superuser."""
from authentik.core.apps import AppAccessWithoutBindings
from authentik.policies.engine import PolicyEngine
agent, owner = self._get_agent_and_owner(request)
app_uuid = str(body.validated_data["app"])
action = body.validated_data["action"]
current = agent.attributes.get(USER_ATTRIBUTE_AGENT_ALLOWED_APPS, [])
if action == "add":
try:
app = Application.objects.get(pk=app_uuid)
except Application.DoesNotExist:
return Response(
data={"app": [_("Application does not exist.")]},
status=400,
)
engine = PolicyEngine(app, owner, request)
engine.empty_result = AppAccessWithoutBindings.get()
engine.use_cache = False
engine.build()
if not engine.passing:
return Response(
data={"app": [_("Owner does not have access to this application.")]},
status=400,
)
if app_uuid not in current:
current.append(app_uuid)
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = current
agent.save(update_fields=["attributes"])
return Response({"allowed_apps": current})
if action == "remove":
if app_uuid in current:
current.remove(app_uuid)
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = current
agent.save(update_fields=["attributes"])
return Response(status=204)
return Response(
data={"action": [_("Invalid action.")]},
status=400,
)
def _get_agent_and_owner(self, request: Request) -> tuple[User, User]:
"""Validate that the target is an agent and the caller is authorized."""
agent: User = self.get_object()
if agent.type != UserTypes.AGENT:
raise ValidationError(_("User is not an agent user."))
owner_pk = agent.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
is_owner = str(request.user.pk) == owner_pk
if not request.user.is_superuser and not is_owner:
raise ValidationError(_("Not the agent's owner or superuser."))
try:
owner = User.objects.get(pk=owner_pk)
except User.DoesNotExist as exc:
raise ValidationError(_("Agent owner not found.")) from exc
return agent, owner
@extend_schema(responses={200: SessionUserSerializer(many=False)})
@action(
url_path="me",
@@ -1009,6 +785,11 @@ class UserViewSet(
self.request.session.modified = True
return Response(serializer.initial_data)
def _update_session_hash_after_password_change(self, request: Request, user: User):
if user.pk == request.user.pk and SESSION_KEY_IMPERSONATE_USER not in self.request.session:
LOGGER.debug("Updating session hash after password change")
update_session_auth_hash(self.request, user)
@permission_required("authentik_core.reset_user_password")
@extend_schema(
request=UserPasswordSetSerializer,
@@ -1032,9 +813,45 @@ class UserViewSet(
except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password", exc=exc)
return Response(status=400)
if user.pk == request.user.pk and SESSION_KEY_IMPERSONATE_USER not in self.request.session:
LOGGER.debug("Updating session hash after password change")
update_session_auth_hash(self.request, user)
self._update_session_hash_after_password_change(request, user)
return Response(status=204)
@permission_required("authentik_core.reset_user_password")
@extend_schema(
request=UserPasswordHashSetSerializer,
responses={
204: OpenApiResponse(description="Successfully changed password"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(
detail=True,
methods=["POST"],
permission_classes=[IsAuthenticated],
)
@validate(UserPasswordHashSetSerializer)
def set_password_hash(
self, request: Request, pk: int, body: UserPasswordHashSetSerializer
) -> Response:
"""Set a user's password from a pre-hashed Django password value.
Submit the Django password hash in the shared ``password`` request field.
This updates authentik's local password verifier only. It does not attempt
to propagate the password change to LDAP or Kerberos because no raw password
is available from the request payload.
"""
user: User = self.get_object()
try:
user.set_password_from_hash(body.validated_data["password"], request=request)
user.save()
except ValueError as exc:
LOGGER.debug("Failed to set password hash", exc=exc)
return Response(data={"password": [INVALID_PASSWORD_HASH_MESSAGE]}, status=400)
except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password hash", exc=exc)
return Response(status=400)
self._update_session_hash_after_password_change(request, user)
return Response(status=204)
@permission_required("authentik_core.reset_user_password")

View File

@@ -7,6 +7,12 @@ from authentik.tasks.schedules.common import ScheduleSpec
from authentik.tenants.flags import Flag
class Setup(Flag[bool], key="setup"):
default = False
visibility = "system"
class AppAccessWithoutBindings(Flag[bool], key="core_default_app_access"):
default = True
@@ -26,6 +32,10 @@ class AuthentikCoreConfig(ManagedAppConfig):
mountpoint = ""
default = True
def import_related(self):
super().import_related()
self.import_module("authentik.core.setup.signals")
@ManagedAppConfig.reconcile_tenant
def source_inbuilt(self):
"""Reconcile inbuilt source"""

View File

@@ -18,7 +18,6 @@ class Command(TenantCommand):
User.objects.exclude_anonymous()
.exclude(type=UserTypes.SERVICE_ACCOUNT)
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
.exclude(type=UserTypes.AGENT)
)
if options["usernames"] and options["all"]:
self.stderr.write("--all and usernames specified, only one can be specified")

View File

@@ -0,0 +1,28 @@
"""Hash password using Django's password hashers"""
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand):
"""Hash a password using Django's password hashers"""
help = "Hash a password for use with AUTHENTIK_BOOTSTRAP_PASSWORD_HASH"
def add_arguments(self, parser):
parser.add_argument(
"password",
type=str,
help="Password to hash",
)
def handle(self, *args, **options):
password = options["password"]
if not password:
raise CommandError("Password cannot be empty")
try:
hashed = make_password(password)
self.stdout.write(hashed)
except ValueError as exc:
raise CommandError(f"Error hashing password: {exc}") from exc

View File

@@ -1,41 +0,0 @@
# Generated by Django 5.2.13 on 2026-04-13 21:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0057_remove_user_groups_remove_user_user_permissions_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="user",
options={
"permissions": [
("reset_user_password", "Reset Password"),
("impersonate", "Can impersonate other users"),
("preview_user", "Can preview user data sent to providers"),
("view_user_applications", "View applications the user has access to"),
("add_agent_user", "Can create agent users"),
],
"verbose_name": "User",
"verbose_name_plural": "Users",
},
),
migrations.AlterField(
model_name="user",
name="type",
field=models.TextField(
choices=[
("internal", "Internal"),
("external", "External"),
("service_account", "Service Account"),
("internal_service_account", "Internal Service Account"),
("agent", "Agent"),
],
default="internal",
),
),
]

View File

@@ -0,0 +1,61 @@
# Generated by Django 5.2.13 on 2026-04-21 18:49
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db import migrations
def check_is_already_setup(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from django.conf import settings
from authentik.flows.models import FlowAuthenticationRequirement
VersionHistory = apps.get_model("authentik_admin", "VersionHistory")
Flow = apps.get_model("authentik_flows", "Flow")
User = apps.get_model("authentik_core", "User")
db_alias = schema_editor.connection.alias
# Upgrading from a previous version
if not settings.TEST and VersionHistory.objects.using(db_alias).count() > 1:
return True
# OOBE flow sets itself to this authentication requirement once finished
if (
Flow.objects.using(db_alias)
.filter(
slug="initial-setup", authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER
)
.exists()
):
return True
# non-akadmin and non-guardian anonymous user exist
if (
User.objects.using(db_alias)
.exclude(username="akadmin")
.exclude(username="AnonymousUser")
.exists()
):
return True
return False
def update_setup_flag(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.core.apps import Setup
from authentik.tenants.utils import get_current_tenant
is_already_setup = check_is_already_setup(apps, schema_editor)
if is_already_setup:
tenant = get_current_tenant()
tenant.flags[Setup().key] = True
tenant.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0057_remove_user_groups_remove_user_user_permissions_and_more"),
# 0024_flow_authentication adds the `authentication` field.
("authentik_flows", "0024_flow_authentication"),
]
operations = [migrations.RunPython(update_setup_flag, migrations.RunPython.noop)]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.12 on 2026-04-09 18:04
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_blank_launch_url(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Application = apps.get_model("authentik_core", "Application")
Application.objects.using(db_alias).filter(meta_launch_url="blank://blank").update(
meta_hide=True, meta_launch_url=""
)
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0058_setup"),
]
operations = [
migrations.AddField(
model_name="application",
name="meta_hide",
field=models.BooleanField(
default=False,
help_text="Hide this application from the user's My applications page.",
),
),
migrations.RunPython(migrate_blank_launch_url, migrations.RunPython.noop),
]

View File

@@ -10,7 +10,7 @@ from uuid import uuid4
import pgtrigger
from deepmerge import always_merger
from django.contrib.auth.hashers import check_password
from django.contrib.auth.hashers import check_password, identify_hasher
from django.contrib.auth.models import AbstractUser, Permission
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.contrib.sessions.base_session import AbstractBaseSession
@@ -67,10 +67,6 @@ USER_ATTRIBUTE_CHANGE_USERNAME = f"{_USER_ATTR_PREFIX}/can-change-username"
USER_ATTRIBUTE_CHANGE_NAME = f"{_USER_ATTR_PREFIX}/can-change-name"
USER_ATTRIBUTE_CHANGE_EMAIL = f"{_USER_ATTR_PREFIX}/can-change-email"
USER_PATH_SERVICE_ACCOUNT = f"{USER_PATH_SYSTEM_PREFIX}/service-accounts"
_USER_ATTR_AGENT_PREFIX = f"{USER_PATH_SYSTEM_PREFIX}/agent"
USER_ATTRIBUTE_AGENT_OWNER_PK = f"{_USER_ATTR_AGENT_PREFIX}/owner-pk"
USER_ATTRIBUTE_AGENT_ALLOWED_APPS = f"{_USER_ATTR_AGENT_PREFIX}/allowed-apps"
USER_PATH_AGENT = f"{USER_PATH_SYSTEM_PREFIX}/agents"
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
# used_by API that allows models to specify if they shadow an object
@@ -130,9 +126,6 @@ class UserTypes(models.TextChoices):
# accounts, such as outpost users
INTERNAL_SERVICE_ACCOUNT = "internal_service_account"
# Enterprise-gated agent users owned by an internal user
AGENT = "agent"
class AttributesMixin(models.Model):
"""Adds an attributes property to a model"""
@@ -392,7 +385,6 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
("impersonate", _("Can impersonate other users")),
("preview_user", _("Can preview user data sent to providers")),
("view_user_applications", _("View applications the user has access to")),
("add_agent_user", _("Can create agent users")),
]
indexes = [
models.Index(fields=["last_login"]),
@@ -568,6 +560,33 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
self.password_change_date = now()
return super().set_password(raw_password)
@staticmethod
def validate_password_hash(password_hash: str):
"""Validate that the value is a recognized Django password hash."""
identify_hasher(password_hash) # Raises ValueError if invalid
def set_password_from_hash(self, password_hash: str, signal=True, sender=None, request=None):
"""Set password directly from a pre-hashed value.
Unlike set_password(), this does not hash the input again. The provided value
must already be a valid Django password hash, and it is stored directly on the
user after validation.
Because no raw password is available, downstream password sync integrations
such as LDAP and Kerberos cannot be updated from this code path.
Raises ValueError if the hash format is not recognized.
"""
self.validate_password_hash(password_hash)
if self.pk and signal:
from authentik.core.signals import password_hash_changed
if not sender:
sender = self
password_hash_changed.send(sender=sender, user=self, request=request)
self.password = password_hash
self.password_change_date = now()
def check_password(self, raw_password: str) -> bool:
"""
Return a boolean of whether the raw_password was correct. Handles
@@ -743,6 +762,9 @@ class Application(SerializerModel, PolicyBindingModel):
meta_icon = FileField(default="", blank=True)
meta_description = models.TextField(default="", blank=True)
meta_publisher = models.TextField(default="", blank=True)
meta_hide = models.BooleanField(
default=False, help_text=_("Hide this application from the user's My applications page.")
)
objects = ApplicationQuerySet.as_manager()
@@ -798,9 +820,13 @@ class Application(SerializerModel, PolicyBindingModel):
def get_provider(self) -> Provider | None:
"""Get casted provider instance. Needs Application queryset with_provider"""
if hasattr(self, "_cached_provider"):
return self._cached_provider
if not self.provider:
self._cached_provider = None
return None
return get_deepest_child(self.provider)
self._cached_provider = get_deepest_child(self.provider)
return self._cached_provider
def backchannel_provider_for[T: Provider](self, provider_type: type[T], **kwargs) -> T | None:
"""Get Backchannel provider for a specific type"""
@@ -959,21 +985,34 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
objects = InheritanceManager()
def get_icon_url(self, request=None, use_cache: bool = True) -> str | None:
"""Get the URL to the source icon."""
if not self.icon:
return None
return get_file_manager(FileUsage.MEDIA).file_url(self.icon, request, use_cache=use_cache)
@property
def icon_url(self) -> str | None:
"""Get the URL to the source icon"""
return self.get_icon_url()
def get_icon_themed_urls(
self,
request=None,
use_cache: bool = True,
) -> dict[str, str] | None:
"""Get themed URLs for icon if it contains %(theme)s."""
if not self.icon:
return None
return get_file_manager(FileUsage.MEDIA).file_url(self.icon)
return get_file_manager(FileUsage.MEDIA).themed_urls(
self.icon,
request,
use_cache=use_cache,
)
@property
def icon_themed_urls(self) -> dict[str, str] | None:
"""Get themed URLs for icon if it contains %(theme)s"""
if not self.icon:
return None
return get_file_manager(FileUsage.MEDIA).themed_urls(self.icon)
return self.get_icon_themed_urls()
def get_user_path(self) -> str:
"""Get user path, fallback to default for formatting errors"""

View File

@@ -0,0 +1,42 @@
from os import getenv
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer
from authentik.core.apps import Setup
from authentik.root.signals import post_startup
from authentik.tenants.models import Tenant
BOOTSTRAP_BLUEPRINT = "system/bootstrap.yaml"
LOGGER = get_logger()
@receiver(post_startup)
def post_startup_setup_bootstrap(sender, **_):
if (
not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD")
and not getenv("AUTHENTIK_BOOTSTRAP_PASSWORD_HASH")
and not getenv("AUTHENTIK_BOOTSTRAP_TOKEN")
):
return
LOGGER.info("Configuring authentik through bootstrap environment variables")
content = BlueprintInstance(path=BOOTSTRAP_BLUEPRINT).retrieve()
# If we have bootstrap credentials set, run bootstrap tasks outside of main server
# sync, so that we can sure the first start actually has working bootstrap
# credentials
for tenant in Tenant.objects.filter(ready=True):
if Setup.get(tenant=tenant):
LOGGER.info("Tenant is already setup, skipping", tenant=tenant.schema_name)
continue
with tenant:
importer = Importer.from_string(content)
valid, logs = importer.validate()
if not valid:
LOGGER.warning("Blueprint invalid", tenant=tenant.schema_name)
for log in logs:
log.log()
importer.apply()
Setup.set(True, tenant=tenant)

View File

@@ -0,0 +1,80 @@
from functools import lru_cache
from http import HTTPMethod, HTTPStatus
from django.contrib.staticfiles import finders
from django.db import transaction
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.views import View
from structlog.stdlib import get_logger
from authentik.blueprints.models import BlueprintInstance
from authentik.core.apps import Setup
from authentik.flows.models import Flow, FlowAuthenticationRequirement, in_memory_stage
from authentik.flows.planner import FlowPlanner
from authentik.flows.stage import StageView
LOGGER = get_logger()
FLOW_CONTEXT_START_BY = "goauthentik.io/core/setup/started-by"
@lru_cache
def read_static(path: str) -> str | None:
result = finders.find(path)
if not result:
return None
with open(result, encoding="utf8") as _file:
return _file.read()
class SetupView(View):
setup_flow_slug = "initial-setup"
def dispatch(self, request: HttpRequest, *args, **kwargs):
if request.method != HTTPMethod.HEAD and Setup.get():
return redirect(reverse("authentik_core:root-redirect"))
return super().dispatch(request, *args, **kwargs)
def head(self, request: HttpRequest, *args, **kwargs):
if Setup.get():
return HttpResponse(status=HTTPStatus.SERVICE_UNAVAILABLE)
if not Flow.objects.filter(slug=self.setup_flow_slug).exists():
return HttpResponse(status=HTTPStatus.SERVICE_UNAVAILABLE)
return HttpResponse(status=HTTPStatus.OK)
def get(self, request: HttpRequest):
flow = Flow.objects.filter(slug=self.setup_flow_slug).first()
if not flow:
LOGGER.info("Setup flow does not exist yet, waiting for worker to finish")
return HttpResponse(
read_static("dist/standalone/loading/startup.html"),
status=HTTPStatus.SERVICE_UNAVAILABLE,
)
planner = FlowPlanner(flow)
plan = planner.plan(request, {FLOW_CONTEXT_START_BY: "setup"})
plan.append_stage(in_memory_stage(PostSetupStageView))
return plan.to_redirect(request, flow)
class PostSetupStageView(StageView):
"""Run post-setup tasks"""
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Wrapper when this stage gets hit with a post request"""
return self.get(request, *args, **kwargs)
def get(self, requeset: HttpRequest, *args, **kwargs):
with transaction.atomic():
# Remember we're setup
Setup.set(True)
# Disable OOBE Blueprints
BlueprintInstance.objects.filter(
**{"metadata__labels__blueprints.goauthentik.io/system-oobe": "true"}
).update(enabled=False)
# Make flow inaccessible
Flow.objects.filter(slug="initial-setup").update(
authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER
)
return self.executor.stage_ok()

View File

@@ -11,14 +11,12 @@ from django.http.request import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_OWNER_PK,
Application,
AuthenticatedSession,
BackchannelProvider,
ExpiringModel,
Session,
User,
UserTypes,
default_token_duration,
)
from authentik.flows.apps import RefreshOtherFlowsAfterAuthentication
@@ -26,6 +24,8 @@ from authentik.root.ws.consumer import build_device_group
# Arguments: user: User, password: str
password_changed = Signal()
# Arguments: user: User, request: HttpRequest | None
password_hash_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest,
# stage: Stage, context: dict[str, any]
login_failed = Signal()
@@ -71,35 +71,6 @@ def authenticated_session_delete(sender: type[Model], instance: AuthenticatedSes
Session.objects.filter(session_key=instance.pk).delete()
def _agent_qs_for_owner(owner_pk: int):
"""Return a queryset of agent users belonging to the given owner pk"""
return User.objects.filter(
type=UserTypes.AGENT,
attributes__contains={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner_pk)},
)
@receiver(post_delete, sender=User)
def user_delete_cascade_agents(sender: type[Model], instance: User, **_):
"""Delete agent users when their owner is deleted"""
_agent_qs_for_owner(instance.pk).delete()
@receiver(post_save, sender=User)
def user_save_propagate_agent_active(
sender: type[Model], instance: User, update_fields: frozenset[str] | None = None, **_
):
"""Propagate is_active changes to owned agent users"""
if update_fields is not None and "is_active" not in update_fields:
return
agents = _agent_qs_for_owner(instance.pk)
if not instance.is_active:
Session.objects.filter(
authenticatedsession__user__in=agents.filter(is_active=True)
).delete()
agents.update(is_active=instance.is_active)
@receiver(pre_save)
def backchannel_provider_pre_save(sender: type[Model], instance: Model, **_):
"""Ensure backchannel providers have is_backchannel set to true"""

View File

@@ -1,84 +0,0 @@
"""Test agent token-to-session exchange"""
from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_OWNER_PK,
USER_PATH_AGENT,
Token,
TokenIntents,
User,
UserTypes,
)
from authentik.core.tests.utils import create_test_user
from authentik.lib.generators import generate_id
class TestAgentSession(APITestCase):
"""Test agent token-to-session exchange"""
def _create_agent_with_token(self):
owner = create_test_user()
agent = User.objects.create(
username=generate_id(),
type=UserTypes.AGENT,
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
path=USER_PATH_AGENT,
)
agent.set_unusable_password()
agent.save()
token = Token.objects.create(
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
user=agent,
expiring=True,
)
return owner, agent, token
def test_session_exchange_success(self):
"""Valid agent token creates a session"""
_owner, _agent, token = self._create_agent_with_token()
response = self.client.post(
reverse("authentik_api:agent-session"),
data={"key": token.key},
content_type="application/json",
)
self.assertEqual(response.status_code, 204)
def test_session_exchange_invalid_token(self):
"""Invalid token key is rejected"""
response = self.client.post(
reverse("authentik_api:agent-session"),
data={"key": "nonexistent-key"},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
def test_session_exchange_non_agent(self):
"""Token belonging to a non-agent user is rejected"""
user = create_test_user()
token = Token.objects.create(
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
user=user,
expiring=True,
)
response = self.client.post(
reverse("authentik_api:agent-session"),
data={"key": token.key},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
def test_session_exchange_inactive_agent(self):
"""Inactive agent is rejected"""
_owner, agent, token = self._create_agent_with_token()
agent.is_active = False
agent.save(update_fields=["is_active"])
response = self.client.post(
reverse("authentik_api:agent-session"),
data={"key": token.key},
content_type="application/json",
)
self.assertEqual(response.status_code, 403)

View File

@@ -129,6 +129,7 @@ class TestApplicationsAPI(APITestCase):
"meta_icon_url": None,
"meta_icon_themed_urls": None,
"meta_description": "",
"meta_hide": False,
"meta_publisher": "",
"policy_engine_mode": "any",
},
@@ -187,12 +188,14 @@ class TestApplicationsAPI(APITestCase):
"meta_icon_url": None,
"meta_icon_themed_urls": None,
"meta_description": "",
"meta_hide": False,
"meta_publisher": "",
"policy_engine_mode": "any",
},
{
"launch_url": None,
"meta_description": "",
"meta_hide": False,
"meta_icon": "",
"meta_icon_url": None,
"meta_icon_themed_urls": None,

View File

@@ -0,0 +1,28 @@
"""Tests for hash_password management command."""
from io import StringIO
from django.contrib.auth.hashers import check_password
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import TestCase
class TestHashPasswordCommand(TestCase):
"""Test hash_password management command."""
def test_hash_password(self):
"""Test hashing a password."""
out = StringIO()
call_command("hash_password", "test123", stdout=out)
hashed = out.getvalue().strip()
self.assertTrue(hashed.startswith("pbkdf2_sha256$"))
self.assertTrue(check_password("test123", hashed))
def test_hash_password_empty_fails(self):
"""Test that empty password raises error."""
with self.assertRaises(CommandError) as ctx:
call_command("hash_password", "")
self.assertIn("Password cannot be empty", str(ctx.exception))

View File

@@ -4,6 +4,7 @@ from django.test import TestCase
from django.urls import reverse
from authentik.brands.models import Brand
from authentik.core.apps import Setup
from authentik.core.models import Application, UserTypes
from authentik.core.tests.utils import create_test_brand, create_test_user
@@ -12,6 +13,7 @@ class TestInterfaceRedirects(TestCase):
"""Test RootRedirectView and BrandDefaultRedirectView redirect logic by user type"""
def setUp(self):
Setup.set(True)
self.app = Application.objects.create(name="test-app", slug="test-app")
self.brand: Brand = create_test_brand(default_application=self.app)

View File

@@ -2,6 +2,7 @@
from collections.abc import Callable
from datetime import timedelta
from unittest.mock import patch
from django.test import RequestFactory, TestCase
from django.utils.timezone import now
@@ -10,6 +11,7 @@ from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Provider, Source, Token
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.lib.utils.reflection import all_subclasses
@@ -47,6 +49,58 @@ class TestModels(TestCase):
event.context["deprecation"], "authentik.core.models.Token.filter_not_expired"
)
@patch("authentik.core.models.get_file_manager")
def test_source_icon_url_can_bypass_cache(self, get_file_manager):
request = RequestFactory().get("/")
manager = get_file_manager.return_value
manager.file_url.return_value = "/files/media/public/source-icons/icon.svg?token=fresh"
source = Source(icon="source-icons/icon.svg")
self.assertEqual(
source.get_icon_url(request, use_cache=False),
"/files/media/public/source-icons/icon.svg?token=fresh",
)
manager.file_url.assert_called_once_with(
"source-icons/icon.svg",
request,
use_cache=False,
)
@patch("authentik.flows.models.get_file_manager")
def test_flow_background_urls_can_bypass_cache(self, get_file_manager):
request = RequestFactory().get("/")
manager = get_file_manager.return_value
manager.file_url.return_value = "/files/media/public/background.svg?token=fresh"
manager.themed_urls.return_value = {
"light": "/files/media/public/background-light.svg?token=fresh",
"dark": "/files/media/public/background-dark.svg?token=fresh",
}
flow = Flow(background="background-%(theme)s.svg")
self.assertEqual(
flow.background_url(request, use_cache=False),
"/files/media/public/background.svg?token=fresh",
)
self.assertEqual(
flow.background_themed_urls(request, use_cache=False),
{
"light": "/files/media/public/background-light.svg?token=fresh",
"dark": "/files/media/public/background-dark.svg?token=fresh",
},
)
manager.file_url.assert_called_once_with(
"background-%(theme)s.svg",
request,
use_cache=False,
)
manager.themed_urls.assert_called_once_with(
"background-%(theme)s.svg",
request,
use_cache=False,
)
def source_tester_factory(test_model: type[Source]) -> Callable:
"""Test source"""

View File

@@ -0,0 +1,174 @@
from http import HTTPStatus
from os import environ
from django.contrib.auth.hashers import make_password
from django.urls import reverse
from authentik.blueprints.tests import apply_blueprint
from authentik.core.apps import Setup
from authentik.core.models import Token, TokenIntents, User
from authentik.flows.models import Flow
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.root.signals import post_startup, pre_startup
from authentik.tenants.flags import patch_flag
class TestSetup(FlowTestCase):
def tearDown(self):
environ.pop("AUTHENTIK_BOOTSTRAP_PASSWORD", None)
environ.pop("AUTHENTIK_BOOTSTRAP_PASSWORD_HASH", None)
environ.pop("AUTHENTIK_BOOTSTRAP_TOKEN", None)
@patch_flag(Setup, True)
def test_setup(self):
"""Test existing instance"""
res = self.client.get(reverse("authentik_core:root-redirect"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(
res,
reverse("authentik_flows:default-authentication") + "?next=/",
fetch_redirect_response=False,
)
res = self.client.head(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.SERVICE_UNAVAILABLE)
res = self.client.get(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(
res,
reverse("authentik_core:root-redirect"),
fetch_redirect_response=False,
)
@patch_flag(Setup, False)
def test_not_setup_no_flow(self):
"""Test case on initial startup; setup flag is not set and oobe flow does
not exist yet"""
Flow.objects.filter(slug="initial-setup").delete()
res = self.client.get(reverse("authentik_core:root-redirect"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(res, reverse("authentik_core:setup"), fetch_redirect_response=False)
# Flow does not exist, hence 503
res = self.client.get(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.SERVICE_UNAVAILABLE)
res = self.client.head(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.SERVICE_UNAVAILABLE)
@patch_flag(Setup, False)
@apply_blueprint("default/flow-oobe.yaml")
def test_not_setup(self):
"""Test case for when worker comes up, and has created flow"""
res = self.client.get(reverse("authentik_core:root-redirect"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(res, reverse("authentik_core:setup"), fetch_redirect_response=False)
# Flow does not exist, hence 503
res = self.client.head(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.OK)
res = self.client.get(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(
res,
reverse("authentik_core:if-flow", kwargs={"flow_slug": "initial-setup"}),
fetch_redirect_response=False,
)
@apply_blueprint("default/flow-oobe.yaml")
@apply_blueprint("system/bootstrap.yaml")
def test_setup_flow_full(self):
"""Test full setup flow"""
Setup.set(False)
res = self.client.get(reverse("authentik_core:setup"))
self.assertEqual(res.status_code, HTTPStatus.FOUND)
self.assertRedirects(
res,
reverse("authentik_core:if-flow", kwargs={"flow_slug": "initial-setup"}),
fetch_redirect_response=False,
)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
)
self.assertEqual(res.status_code, HTTPStatus.OK)
self.assertStageResponse(res, component="ak-stage-prompt")
pw = generate_id()
res = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
{
"email": f"{generate_id()}@t.goauthentik.io",
"password": pw,
"password_repeat": pw,
"component": "ak-stage-prompt",
},
)
self.assertEqual(res.status_code, HTTPStatus.FOUND)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
)
self.assertEqual(res.status_code, HTTPStatus.FOUND)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
)
self.assertEqual(res.status_code, HTTPStatus.FOUND)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"}),
)
self.assertEqual(res.status_code, HTTPStatus.OK)
self.assertTrue(Setup.get())
user = User.objects.get(username="akadmin")
self.assertTrue(user.check_password(pw))
@patch_flag(Setup, False)
@apply_blueprint("default/flow-oobe.yaml")
@apply_blueprint("system/bootstrap.yaml")
def test_setup_flow_direct(self):
"""Test setup flow, directly accessing the flow"""
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": "initial-setup"})
)
self.assertStageResponse(
res,
component="ak-stage-access-denied",
error_message="Access the authentik setup by navigating to http://testserver/",
)
def test_setup_bootstrap_env(self):
"""Test setup with env vars"""
User.objects.filter(username="akadmin").delete()
Setup.set(False)
environ["AUTHENTIK_BOOTSTRAP_PASSWORD"] = generate_id()
environ["AUTHENTIK_BOOTSTRAP_TOKEN"] = generate_id()
pre_startup.send(sender=self)
post_startup.send(sender=self)
self.assertTrue(Setup.get())
user = User.objects.get(username="akadmin")
self.assertTrue(user.check_password(environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]))
token = Token.objects.filter(identifier="authentik-bootstrap-token").first()
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.key, environ["AUTHENTIK_BOOTSTRAP_TOKEN"])
def test_setup_bootstrap_env_password_hash(self):
"""Test setup with password hash env var"""
User.objects.filter(username="akadmin").delete()
Setup.set(False)
password = generate_id()
password_hash = make_password(password)
environ["AUTHENTIK_BOOTSTRAP_PASSWORD_HASH"] = password_hash
pre_startup.send(sender=self)
post_startup.send(sender=self)
self.assertTrue(Setup.get())
user = User.objects.get(username="akadmin")
self.assertEqual(user.password, password_hash)
self.assertTrue(user.check_password(password))

View File

@@ -199,50 +199,6 @@ class TestTokenAPI(APITestCase):
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
def test_token_rotate_by_owner(self):
"""Token owner can rotate their own token"""
token = Token.objects.create(
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
user=self.user,
expiring=True,
)
original_key = token.key
response = self.client.post(
reverse("authentik_api:token-rotate", kwargs={"identifier": token.identifier}),
)
self.assertEqual(response.status_code, 200)
token.refresh_from_db()
self.assertNotEqual(token.key, original_key)
self.assertEqual(token.key, loads(response.content)["key"])
def test_token_rotate_by_superuser(self):
"""Superuser can rotate any token"""
token = Token.objects.create(
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
user=self.user,
expiring=True,
)
self.client.force_login(self.admin)
response = self.client.post(
reverse("authentik_api:token-rotate", kwargs={"identifier": token.identifier}),
)
self.assertEqual(response.status_code, 200)
def test_token_rotate_unauthorized(self):
"""Non-owner cannot rotate another user's token"""
token = Token.objects.create(
identifier=generate_id(),
intent=TokenIntents.INTENT_API,
user=self.admin,
expiring=True,
)
response = self.client.post(
reverse("authentik_api:token-rotate", kwargs={"identifier": token.identifier}),
)
self.assertEqual(response.status_code, 403)
def test_serializer_no_request(self):
"""Test serializer without request"""
self.assertTrue(

View File

@@ -1,15 +1,15 @@
"""user tests"""
from django.test.testcases import TestCase
from unittest.mock import patch
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_OWNER_PK,
USER_PATH_AGENT,
AuthenticatedSession,
Session,
User,
UserTypes,
)
from django.contrib.auth.hashers import make_password
from django.test.testcases import TestCase
from rest_framework.exceptions import ValidationError
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.users import UserSerializer
from authentik.core.models import User
from authentik.core.signals import password_changed, password_hash_changed
from authentik.events.models import Event
from authentik.lib.generators import generate_id
@@ -41,92 +41,98 @@ class TestUsers(TestCase):
user.ak_groups.all()
self.assertEqual(Event.objects.count(), 1)
class TestAgentUserSignals(TestCase):
"""Test signals related to agent user lifecycle"""
def _create_owner(self):
owner = User.objects.create(username=generate_id())
owner.set_unusable_password()
owner.save()
return owner
def _create_agent(self, owner):
agent = User.objects.create(
def test_set_password_from_hash_signal_skips_source_sync_receivers(self):
"""Test hash password updates do not expose a raw password to sync receivers."""
user = User.objects.create(
username=generate_id(),
type=UserTypes.AGENT,
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
path=USER_PATH_AGENT,
attributes={"distinguishedName": "cn=test,ou=users,dc=example,dc=com"},
)
agent.set_unusable_password()
agent.save()
return agent
password_changed_captured = []
password_hash_changed_captured = []
dispatch_uid = generate_id()
hash_dispatch_uid = generate_id()
def test_delete_owner_cascades_to_agents(self):
"""Deleting an owner also deletes all their agent users"""
owner = self._create_owner()
agent1 = self._create_agent(owner)
agent2 = self._create_agent(owner)
other_owner = self._create_owner()
other_agent = self._create_agent(other_owner)
def password_changed_receiver(sender, **kwargs):
password_changed_captured.append(kwargs)
owner.delete()
def password_hash_changed_receiver(sender, **kwargs):
password_hash_changed_captured.append(kwargs)
self.assertFalse(User.objects.filter(pk=agent1.pk).exists())
self.assertFalse(User.objects.filter(pk=agent2.pk).exists())
self.assertTrue(User.objects.filter(pk=other_agent.pk).exists())
def test_deactivate_owner_deactivates_agents(self):
"""Setting an owner inactive also marks all their agents inactive"""
owner = self._create_owner()
agent = self._create_agent(owner)
owner.is_active = False
owner.save(update_fields=["is_active"])
agent.refresh_from_db()
self.assertFalse(agent.is_active)
def test_reactivate_owner_reactivates_agents(self):
"""Setting an owner active again also re-activates their agents"""
owner = self._create_owner()
owner.is_active = False
owner.save(update_fields=["is_active"])
agent = self._create_agent(owner)
agent.is_active = False
agent.save(update_fields=["is_active"])
owner.is_active = True
owner.save(update_fields=["is_active"])
agent.refresh_from_db()
self.assertTrue(agent.is_active)
def test_unrelated_owner_save_does_not_affect_agents(self):
"""Saving an owner without changing is_active does not touch agents"""
owner = self._create_owner()
agent = self._create_agent(owner)
agent.is_active = False
agent.save(update_fields=["is_active"])
owner.name = generate_id()
owner.save(update_fields=["name"])
agent.refresh_from_db()
self.assertFalse(agent.is_active)
def test_deactivate_owner_clears_agent_sessions(self):
"""Deactivating an owner removes authenticated sessions for their agents"""
owner = self._create_owner()
agent = self._create_agent(owner)
session = Session.objects.create(
session_key=generate_id(),
last_ip="255.255.255.255",
last_user_agent="",
password_changed.connect(password_changed_receiver, dispatch_uid=dispatch_uid)
password_hash_changed.connect(
password_hash_changed_receiver, dispatch_uid=hash_dispatch_uid
)
AuthenticatedSession.objects.create(user=agent, session=session)
try:
with (
patch(
"authentik.sources.ldap.signals.LDAPSource.objects.filter"
) as ldap_sources_filter,
patch(
"authentik.sources.kerberos.signals."
"UserKerberosSourceConnection.objects.select_related"
) as kerberos_connections_select,
):
user.set_password_from_hash(make_password("new-password")) # nosec
user.save()
finally:
password_changed.disconnect(dispatch_uid=dispatch_uid)
password_hash_changed.disconnect(dispatch_uid=hash_dispatch_uid)
owner.is_active = False
owner.save(update_fields=["is_active"])
self.assertEqual(password_changed_captured, [])
self.assertEqual(len(password_hash_changed_captured), 1)
ldap_sources_filter.assert_not_called()
kerberos_connections_select.assert_not_called()
self.assertFalse(Session.objects.filter(pk=session.pk).exists())
class TestUserSerializerPasswordHash(TestCase):
"""Test UserSerializer password_hash support in blueprint context."""
def test_password_hash_sets_password_directly(self):
"""Test a valid password hash is stored without re-hashing."""
password = "test-password-123" # nosec
password_hash = make_password(password)
serializer = UserSerializer(
data={
"username": generate_id(),
"name": "Test User",
"password_hash": password_hash,
},
context={SERIALIZER_CONTEXT_BLUEPRINT: True},
)
self.assertTrue(serializer.is_valid(), serializer.errors)
user = serializer.save()
self.assertEqual(user.password, password_hash)
self.assertTrue(user.check_password(password))
self.assertIsNotNone(user.password_change_date)
def test_password_hash_rejects_invalid_format(self):
"""Test invalid password hash values are rejected."""
serializer = UserSerializer(
data={
"username": generate_id(),
"name": "Test User",
"password_hash": "not-a-valid-hash",
},
context={SERIALIZER_CONTEXT_BLUEPRINT: True},
)
self.assertTrue(serializer.is_valid(), serializer.errors)
with self.assertRaises(ValidationError) as ctx:
serializer.save()
self.assertIn("Invalid password hash format", str(ctx.exception))
def test_password_hash_ignored_outside_blueprint_context(self):
"""Test password_hash is not accepted by the regular serializer."""
serializer = UserSerializer(
data={
"username": generate_id(),
"name": "Test User",
"password_hash": make_password("test"), # nosec
}
)
self.assertTrue(serializer.is_valid(), serializer.errors)
self.assertNotIn("password_hash", serializer.validated_data)

View File

@@ -2,23 +2,18 @@
from datetime import datetime, timedelta
from json import loads
from unittest.mock import MagicMock, patch
from django.contrib.auth.hashers import make_password
from django.urls.base import reverse
from django.utils.timezone import now
from rest_framework.test import APITestCase
from authentik.brands.models import Brand
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_ALLOWED_APPS,
USER_ATTRIBUTE_AGENT_OWNER_PK,
USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_PATH_AGENT,
Application,
AuthenticatedSession,
Session,
Token,
TokenIntents,
User,
UserTypes,
)
@@ -32,6 +27,9 @@ from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignatio
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.email.models import EmailStage
INVALID_PASSWORD_HASH = "not-a-valid-hash"
INVALID_PASSWORD_HASH_ERROR = "Invalid password hash format. Must be a valid Django password hash."
class TestUsersAPI(APITestCase):
"""Test Users API"""
@@ -40,6 +38,20 @@ class TestUsersAPI(APITestCase):
self.admin = create_test_admin_user()
self.user = create_test_user()
def _set_password_hash(self, user: User, password_hash: str, client=None):
return (client or self.client).post(
reverse("authentik_api:user-set-password-hash", kwargs={"pk": user.pk}),
data={"password": password_hash},
)
def _assert_password_hash_set(
self, user: User, password: str, password_hash: str, response
) -> None:
self.assertEqual(response.status_code, 204, response.data)
user.refresh_from_db()
self.assertEqual(user.password, password_hash)
self.assertTrue(user.check_password(password))
def test_filter_type(self):
"""Test API filtering by type"""
self.client.force_login(self.admin)
@@ -119,6 +131,26 @@ class TestUsersAPI(APITestCase):
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]})
def test_set_password_hash(self):
"""Test setting a user's password from a hash."""
self.client.force_login(self.admin)
password = generate_key()
password_hash = make_password(password)
response = self._set_password_hash(self.user, password_hash)
self._assert_password_hash_set(self.user, password, password_hash, response)
def test_set_password_hash_invalid(self):
"""Test invalid password hashes are rejected."""
self.client.force_login(self.admin)
response = self._set_password_hash(self.user, INVALID_PASSWORD_HASH)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"password": [INVALID_PASSWORD_HASH_ERROR]},
)
def test_recovery(self):
"""Test user recovery link"""
flow = create_test_flow(
@@ -267,6 +299,29 @@ class TestUsersAPI(APITestCase):
self.assertTrue(token_filter.exists())
self.assertTrue(token_filter.first().expiring)
def test_service_account_set_password_hash(self):
"""Service account password hash can be set through the API."""
self.client.force_login(self.admin)
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"create_group": False,
},
)
self.assertEqual(response.status_code, 200, response.data)
body = loads(response.content)
user = User.objects.get(pk=body["user_pk"])
self.assertEqual(user.type, UserTypes.SERVICE_ACCOUNT)
self.assertFalse(user.has_usable_password())
password = generate_key()
password_hash = make_password(password)
response = self._set_password_hash(user, password_hash)
self._assert_password_hash_set(user, password, password_hash, response)
def test_service_account_no_expire(self):
"""Service account creation without token expiration"""
self.client.force_login(self.admin)
@@ -884,249 +939,3 @@ class TestUsersAPI(APITestCase):
self.assertIn(user2.pk, pks)
# Verify user2 comes before user1 in descending order
self.assertLess(pks.index(user2.pk), pks.index(user1.pk))
class TestAgentUserAPI(APITestCase):
"""Test agent user API"""
def setUp(self) -> None:
self.admin = create_test_admin_user()
self.user = create_test_user()
self.owner = create_test_user()
self.owner.assign_perms_to_managed_role("authentik_core.add_agent_user")
self.owner.assign_perms_to_managed_role("authentik_core.add_user")
self.owner.assign_perms_to_managed_role("authentik_core.add_token")
def _create_agent(self, name="test-agent", owner=None):
owner = owner or self.admin
agent = User.objects.create(
username=name,
name=name,
type=UserTypes.AGENT,
attributes={
USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk),
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [],
},
path=USER_PATH_AGENT,
)
agent.set_unusable_password()
agent.save()
return agent
def test_agent_create(self):
"""Non-admin owner with correct permissions can create an agent"""
self.client.force_login(self.owner)
with patch(
"authentik.enterprise.license.LicenseKey.cached_summary",
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
):
response = self.client.post(
reverse("authentik_api:user-agent"),
data={"name": "test-agent"},
)
self.assertEqual(response.status_code, 200)
agent = User.objects.get(username="test-agent")
self.assertEqual(agent.type, UserTypes.AGENT)
self.assertEqual(agent.path, USER_PATH_AGENT)
self.assertEqual(agent.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK), str(self.owner.pk))
self.assertEqual(agent.attributes.get(USER_ATTRIBUTE_AGENT_ALLOWED_APPS), [])
self.assertFalse(agent.has_usable_password())
token = Token.objects.filter(user=agent, intent=TokenIntents.INTENT_API).first()
self.assertIsNotNone(token)
self.assertTrue(token.expiring)
def test_agent_create_no_license(self):
"""Agent creation is rejected without a valid enterprise license"""
self.client.force_login(self.owner)
with patch(
"authentik.enterprise.license.LicenseKey.cached_summary",
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=False))),
):
response = self.client.post(
reverse("authentik_api:user-agent"),
data={"name": "test-agent"},
)
self.assertEqual(response.status_code, 400)
def test_agent_create_non_internal_user(self):
"""Only internal users can create agent users"""
self.owner.type = UserTypes.EXTERNAL
self.owner.save(update_fields=["type"])
self.client.force_login(self.owner)
with patch(
"authentik.enterprise.license.LicenseKey.cached_summary",
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
):
response = self.client.post(
reverse("authentik_api:user-agent"),
data={"name": "test-agent"},
)
self.assertEqual(response.status_code, 400)
def test_agent_create_no_permission(self):
"""User without add_agent_user permission is rejected"""
self.client.force_login(self.user)
with patch(
"authentik.enterprise.license.LicenseKey.cached_summary",
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
):
response = self.client.post(
reverse("authentik_api:user-agent"),
data={"name": "test-agent"},
)
self.assertEqual(response.status_code, 403)
def test_agent_create_duplicate(self):
"""Duplicate agent username returns a user-friendly error"""
self._create_agent("test-agent-dup")
self.client.force_login(self.owner)
with patch(
"authentik.enterprise.license.LicenseKey.cached_summary",
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
):
response = self.client.post(
reverse("authentik_api:user-agent"),
data={"name": "test-agent-dup"},
)
self.assertEqual(response.status_code, 400)
def test_agent_type_cannot_be_changed(self):
"""Agent user type cannot be changed via the users API"""
agent = self._create_agent()
self.client.force_login(self.admin)
response = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": agent.pk}),
data={"type": UserTypes.INTERNAL},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
def test_agent_owner_cannot_be_changed(self):
"""Agent owner cannot be changed via the users API"""
agent = self._create_agent()
other = create_test_user()
self.client.force_login(self.admin)
new_attrs = dict(agent.attributes)
new_attrs[USER_ATTRIBUTE_AGENT_OWNER_PK] = str(other.pk)
response = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": agent.pk}),
data={"attributes": new_attrs},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
def test_agent_allowed_apps_update(self):
"""Owner can update the agent's allowed apps list"""
agent = self._create_agent(owner=self.admin)
app = Application.objects.create(name=generate_id(), slug=generate_id())
self.client.force_login(self.admin)
response = self.client.put(
reverse("authentik_api:user-agent-allowed-apps", kwargs={"pk": agent.pk}),
data={"allowed_apps": [str(app.pk)]},
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
agent.refresh_from_db()
self.assertIn(str(app.pk), agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS])
def test_agent_allowed_apps_update_unauthorized(self):
"""Non-owner, non-superuser is rejected when updating allowed apps"""
other = create_test_user()
agent = self._create_agent(owner=other)
self.client.force_login(self.user)
response = self.client.put(
reverse("authentik_api:user-agent-allowed-apps", kwargs={"pk": agent.pk}),
data={"allowed_apps": []},
content_type="application/json",
)
self.assertEqual(response.status_code, 403)
def test_agent_allowed_apps_update_non_agent(self):
"""Endpoint rejects non-agent users"""
self.client.force_login(self.admin)
response = self.client.put(
reverse("authentik_api:user-agent-allowed-apps", kwargs={"pk": self.user.pk}),
data={"allowed_apps": []},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
def test_agent_allowed_app_add(self):
"""PATCH add: owner can add a single app to agent's allowed list"""
agent = self._create_agent(owner=self.admin)
app = Application.objects.create(name=generate_id(), slug=generate_id())
self.client.force_login(self.admin)
response = self.client.patch(
reverse("authentik_api:user-agent-allowed-app", kwargs={"pk": agent.pk}),
data={"app": str(app.pk), "action": "add"},
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
agent.refresh_from_db()
self.assertIn(str(app.pk), agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS])
def test_agent_allowed_app_add_duplicate(self):
"""PATCH add: adding an already-allowed app is idempotent"""
agent = self._create_agent(owner=self.admin)
app = Application.objects.create(name=generate_id(), slug=generate_id())
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = [str(app.pk)]
agent.save(update_fields=["attributes"])
self.client.force_login(self.admin)
response = self.client.patch(
reverse("authentik_api:user-agent-allowed-app", kwargs={"pk": agent.pk}),
data={"app": str(app.pk), "action": "add"},
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
agent.refresh_from_db()
self.assertEqual(agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS].count(str(app.pk)), 1)
def test_agent_allowed_app_remove(self):
"""PATCH remove: owner can remove a single app from agent's allowed list"""
agent = self._create_agent(owner=self.admin)
app = Application.objects.create(name=generate_id(), slug=generate_id())
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = [str(app.pk)]
agent.save(update_fields=["attributes"])
self.client.force_login(self.admin)
response = self.client.patch(
reverse("authentik_api:user-agent-allowed-app", kwargs={"pk": agent.pk}),
data={"app": str(app.pk), "action": "remove"},
content_type="application/json",
)
self.assertEqual(response.status_code, 204)
agent.refresh_from_db()
self.assertNotIn(str(app.pk), agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS])
def test_agent_allowed_app_add_nonexistent(self):
"""PATCH add: nonexistent app UUID is rejected"""
agent = self._create_agent(owner=self.admin)
self.client.force_login(self.admin)
response = self.client.patch(
reverse("authentik_api:user-agent-allowed-app", kwargs={"pk": agent.pk}),
data={"app": "00000000-0000-0000-0000-000000000000", "action": "add"},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
def test_token_rotate_by_agent_owner(self):
"""Non-admin owner can rotate the agent's token"""
self.client.force_login(self.owner)
with patch(
"authentik.enterprise.license.LicenseKey.cached_summary",
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
):
response = self.client.post(
reverse("authentik_api:user-agent"),
data={"name": "rotate-test-agent"},
)
self.assertEqual(response.status_code, 200)
token = Token.objects.get(
user__username="rotate-test-agent", intent=TokenIntents.INTENT_API
)
original_key = token.key
response = self.client.post(
reverse("authentik_api:token-rotate", kwargs={"identifier": token.identifier}),
)
self.assertEqual(response.status_code, 200)
token.refresh_from_db()
self.assertNotEqual(token.key, original_key)

View File

@@ -1,7 +1,6 @@
"""authentik URL Configuration"""
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.urls import path
from authentik.core.api.application_entitlements import ApplicationEntitlementViewSet
@@ -19,7 +18,7 @@ from authentik.core.api.sources import (
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.transactional_applications import TransactionalApplicationView
from authentik.core.api.users import UserViewSet
from authentik.core.views.agent_session import AgentSessionView
from authentik.core.setup.views import SetupView
from authentik.core.views.apps import RedirectToAppLaunch
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import (
@@ -36,7 +35,7 @@ from authentik.tenants.channels import TenantsAwareMiddleware
urlpatterns = [
path(
"",
login_required(RootRedirectView.as_view()),
RootRedirectView.as_view(),
name="root-redirect",
),
path(
@@ -63,6 +62,11 @@ urlpatterns = [
FlowInterfaceView.as_view(),
name="if-flow",
),
path(
"setup",
SetupView.as_view(),
name="setup",
),
# Fallback for WS
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
path(
@@ -80,11 +84,6 @@ api_urlpatterns = [
TransactionalApplicationView.as_view(),
name="core-transactional-application",
),
path(
"core/agent/session/",
AgentSessionView.as_view(),
name="agent-session",
),
("core/groups", GroupViewSet),
("core/users", UserViewSet),
("core/tokens", TokenViewSet),

View File

@@ -1,50 +0,0 @@
"""Agent token-to-session exchange view"""
from django.contrib.auth import login
from rest_framework.authentication import BaseAuthentication
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.models import AuthenticatedSession, Token, TokenIntents, UserTypes
from authentik.stages.password import BACKEND_INBUILT
class NoAuthentication(BaseAuthentication):
"""Explicitly skip DRF authentication; the view authenticates via the request body."""
def authenticate(self, request):
return None
class AgentSessionView(APIView):
"""Exchange an agent's API token for an authenticated session."""
authentication_classes = [NoAuthentication]
permission_classes = [AllowAny]
def post(self, request: Request) -> Response:
key = request.data.get("key")
if not key:
return Response({"detail": "Key is required."}, status=400)
token = (
Token.objects.filter(key=key, intent=TokenIntents.INTENT_API)
.select_related("user")
.first()
)
if not token:
return Response({"detail": "Invalid token."}, status=400)
if token.is_expired:
return Response({"detail": "Token has expired."}, status=403)
if token.user.type != UserTypes.AGENT:
return Response({"detail": "Token does not belong to an agent user."}, status=400)
if not token.user.is_active:
return Response({"detail": "Agent user is inactive."}, status=403)
login(request._request, token.user, backend=BACKEND_INBUILT)
session = AuthenticatedSession.from_request(request._request, token.user)
if session:
session.save()
return Response(status=204)

View File

@@ -3,6 +3,7 @@
from json import dumps
from typing import Any
from django.contrib.auth.mixins import AccessMixin
from django.http import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import redirect
@@ -14,12 +15,13 @@ from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer
from authentik.brands.models import Brand
from authentik.core.apps import Setup
from authentik.core.models import UserTypes
from authentik.lib.config import CONFIG
from authentik.policies.denied import AccessDeniedResponse
class RootRedirectView(RedirectView):
class RootRedirectView(AccessMixin, RedirectView):
"""Root redirect view, redirect to brand's default application if set"""
pattern_name = "authentik_core:if-user"
@@ -30,7 +32,6 @@ class RootRedirectView(RedirectView):
UserTypes.EXTERNAL,
UserTypes.SERVICE_ACCOUNT,
UserTypes.INTERNAL_SERVICE_ACCOUNT,
UserTypes.AGENT,
):
brand: Brand = request.brand
if brand.default_application:
@@ -41,6 +42,10 @@ class RootRedirectView(RedirectView):
return None
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if not Setup.get():
return redirect("authentik_core:setup")
if not request.user.is_authenticated:
return self.handle_no_permission()
if redirect_response := RootRedirectView().redirect_to_app(request):
return redirect_response
return super().dispatch(request, *args, **kwargs)
@@ -71,7 +76,6 @@ class BrandDefaultRedirectView(InterfaceView):
UserTypes.EXTERNAL,
UserTypes.SERVICE_ACCOUNT,
UserTypes.INTERNAL_SERVICE_ACCOUNT,
UserTypes.AGENT,
):
brand: Brand = request.brand
if brand.default_application:

View File

@@ -138,13 +138,7 @@ class AgentConnectorController(BaseController[AgentConnector]):
"AllowDeviceIdentifiersInAttestation": True,
"AuthenticationMethod": "UserSecureEnclaveKey",
"EnableAuthorization": True,
"EnableCreateUserAtLogin": True,
"FileVaultPolicy": ["RequireAuthentication"],
"LoginPolicy": ["RequireAuthentication"],
"NewUserAuthorizationMode": "Standard",
"UnlockPolicy": ["RequireAuthentication"],
"UseSharedDeviceKeys": True,
"UserAuthorizationMode": "Standard",
},
},
],

View File

@@ -0,0 +1,54 @@
# Generated by Django 5.2.12 on 2026-03-06 14:38
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_endpoints_connectors_agent",
"0004_agentconnector_challenge_idle_timeout_and_more",
),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="AppleIndependentSecureEnclave",
fields=[
("created", models.DateTimeField(auto_now_add=True)),
("last_updated", models.DateTimeField(auto_now=True)),
(
"name",
models.CharField(
help_text="The human-readable name of this device.", max_length=64
),
),
(
"confirmed",
models.BooleanField(default=True, help_text="Is this device ready for use?"),
),
("last_used", models.DateTimeField(null=True)),
("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
("apple_secure_enclave_key", models.TextField()),
("apple_enclave_key_id", models.TextField()),
("device_type", models.TextField()),
(
"user",
models.ForeignKey(
help_text="The user that this device belongs to.",
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Apple Independent Secure Enclave",
"verbose_name_plural": "Apple Independent Secure Enclaves",
},
),
]

View File

@@ -19,6 +19,7 @@ from authentik.flows.stage import StageView
from authentik.lib.generators import generate_key
from authentik.lib.models import InternallyManagedMixin, SerializerModel
from authentik.lib.utils.time import timedelta_string_validator
from authentik.stages.authenticator.models import Device as Authenticator
if TYPE_CHECKING:
from authentik.endpoints.connectors.agent.controller import AgentConnectorController
@@ -172,3 +173,17 @@ class AppleNonce(InternallyManagedMixin, ExpiringModel):
class Meta(ExpiringModel.Meta):
verbose_name = _("Apple Nonce")
verbose_name_plural = _("Apple Nonces")
class AppleIndependentSecureEnclave(Authenticator):
"""A device-independent secure enclave key, used by Tap-to-login"""
uuid = models.UUIDField(primary_key=True, default=uuid4)
apple_secure_enclave_key = models.TextField()
apple_enclave_key_id = models.TextField()
device_type = models.TextField()
class Meta:
verbose_name = _("Apple Independent Secure Enclave")
verbose_name_plural = _("Apple Independent Secure Enclaves")

View File

@@ -1,10 +1,12 @@
from unittest.mock import PropertyMock, patch
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
from authentik.endpoints.connectors.agent.models import AgentConnector
from authentik.endpoints.controller import BaseController
from authentik.endpoints.models import StageMode
from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector
from authentik.lib.generators import generate_id
@@ -25,16 +27,22 @@ class TestAPI(APITestCase):
)
self.assertEqual(res.status_code, 201)
def test_endpoint_stage_fleet(self):
connector = FleetConnector.objects.create(name=generate_id())
res = self.client.post(
reverse("authentik_api:stages-endpoint-list"),
data={
"name": generate_id(),
"connector": str(connector.pk),
"mode": StageMode.REQUIRED,
},
)
def test_endpoint_stage_agent_no_stage(self):
connector = AgentConnector.objects.create(name=generate_id())
class controller(BaseController):
def capabilities(self):
return []
with patch.object(AgentConnector, "controller", PropertyMock(return_value=controller)):
res = self.client.post(
reverse("authentik_api:stages-endpoint-list"),
data={
"name": generate_id(),
"connector": str(connector.pk),
"mode": StageMode.REQUIRED,
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(
res.content, {"connector": ["Selected connector is not compatible with this stage."]}

View File

@@ -0,0 +1,28 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.connectors.agent.models import AppleIndependentSecureEnclave
class AppleIndependentSecureEnclaveSerializer(ModelSerializer):
class Meta:
model = AppleIndependentSecureEnclave
fields = [
"uuid",
"user",
"apple_secure_enclave_key",
"apple_enclave_key_id",
"device_type",
]
class AppleIndependentSecureEnclaveViewSet(UsedByMixin, ModelViewSet):
queryset = AppleIndependentSecureEnclave.objects.all()
serializer_class = AppleIndependentSecureEnclaveSerializer
search_fields = [
"name",
"user__name",
]
ordering = ["uuid"]
filterset_fields = ["user", "apple_enclave_key_id"]

View File

@@ -11,6 +11,7 @@ from authentik.endpoints.connectors.agent.models import (
AgentConnector,
AgentDeviceConnection,
AgentDeviceUserBinding,
AppleIndependentSecureEnclave,
AppleNonce,
DeviceToken,
EnrollmentToken,
@@ -25,7 +26,7 @@ class TestAppleToken(TestCase):
def setUp(self):
self.apple_sign_key = create_test_cert(PrivateKeyAlg.ECDSA)
sign_key_pem = self.apple_sign_key.public_key.public_bytes(
self.sign_key_pem = self.apple_sign_key.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
).decode()
@@ -50,7 +51,7 @@ class TestAppleToken(TestCase):
device=self.device,
connector=self.connector,
apple_sign_key_id=self.apple_sign_key.kid,
apple_signing_key=sign_key_pem,
apple_signing_key=self.sign_key_pem,
apple_encryption_key=self.enc_pub,
)
self.user = create_test_user()
@@ -59,7 +60,7 @@ class TestAppleToken(TestCase):
user=self.user,
order=0,
apple_enclave_key_id=self.apple_sign_key.kid,
apple_secure_enclave_key=sign_key_pem,
apple_secure_enclave_key=self.sign_key_pem,
)
self.device_token = DeviceToken.objects.create(device=self.connection)
@@ -113,3 +114,62 @@ class TestAppleToken(TestCase):
).first()
self.assertIsNotNone(event)
self.assertEqual(event.context["device"]["name"], self.device.name)
@reconcile_app("authentik_crypto")
def test_token_independent(self):
nonce = generate_id()
AgentDeviceUserBinding.objects.all().delete()
AppleIndependentSecureEnclave.objects.create(
user=self.user,
apple_enclave_key_id=self.apple_sign_key.kid,
apple_secure_enclave_key=self.sign_key_pem,
)
AppleNonce.objects.create(
device_token=self.device_token,
nonce=nonce,
)
embedded = encode(
{"iss": str(self.connector.pk), "aud": str(self.device.pk), "request_nonce": nonce},
self.apple_sign_key.private_key,
headers={
"kid": self.apple_sign_key.kid,
},
algorithm=JWTAlgorithms.from_private_key(self.apple_sign_key.private_key),
)
assertion = encode(
{
"iss": str(self.connector.pk),
"aud": "http://testserver/endpoints/agent/psso/token/",
"request_nonce": nonce,
"assertion": embedded,
"jwe_crypto": {
"apv": (
"AAAABUFwcGxlAAAAQQTFgZOospN6KbkhXhx1lfa-AKYxjEfJhTJrkpdEY_srMmkPzS7VN0Bzt2AtNBEXE"
"aphDONiP2Mq6Oxytv5JKOxHAAAAJDgyOThERkY5LTVFMUUtNEUwMS04OEUwLUI3QkQzOUM4QjA3Qw"
)
},
},
self.apple_sign_key.private_key,
headers={
"kid": self.apple_sign_key.kid,
},
algorithm=JWTAlgorithms.from_private_key(self.apple_sign_key.private_key),
)
res = self.client.post(
reverse("authentik_enterprise_endpoints_connectors_agent:psso-token"),
data={
"assertion": assertion,
"platform_sso_version": "1.0",
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
},
)
self.assertEqual(res.status_code, 200)
event = Event.objects.filter(
action=EventAction.LOGIN,
app="authentik.endpoints.connectors.agent",
).first()
self.assertIsNotNone(event)
self.assertEqual(event.context["device"]["name"], self.device.name)

View File

@@ -1,5 +1,8 @@
from django.urls import path
from authentik.enterprise.endpoints.connectors.agent.api.secure_enclave import (
AppleIndependentSecureEnclaveViewSet,
)
from authentik.enterprise.endpoints.connectors.agent.views.apple_jwks import AppleJWKSView
from authentik.enterprise.endpoints.connectors.agent.views.apple_nonce import NonceView
from authentik.enterprise.endpoints.connectors.agent.views.apple_register import (
@@ -23,6 +26,7 @@ urlpatterns = [
]
api_urlpatterns = [
("endpoints/agents/psso/ise", AppleIndependentSecureEnclaveViewSet),
path(
"endpoints/agents/psso/register/device/",
RegisterDeviceView.as_view(),

View File

@@ -19,6 +19,7 @@ from authentik.endpoints.connectors.agent.models import (
AgentConnector,
AgentDeviceConnection,
AgentDeviceUserBinding,
AppleIndependentSecureEnclave,
AppleNonce,
DeviceAuthenticationToken,
)
@@ -103,7 +104,9 @@ class TokenView(View):
nonce.delete()
return decoded
def validate_embedded_assertion(self, assertion: str) -> tuple[AgentDeviceUserBinding, dict]:
def validate_embedded_assertion(
self, assertion: str
) -> tuple[AgentDeviceUserBinding | AppleIndependentSecureEnclave, dict]:
"""Decode an embedded assertion and validate it by looking up the matching device user"""
decode_unvalidated = get_unverified_header(assertion)
expected_kid = decode_unvalidated["kid"]
@@ -112,8 +115,13 @@ class TokenView(View):
target=self.device_connection.device, apple_enclave_key_id=expected_kid
).first()
if not device_user:
LOGGER.warning("Could not find device user binding for user")
raise ValidationError("Invalid request")
independent_user = AppleIndependentSecureEnclave.objects.filter(
apple_enclave_key_id=expected_kid
).first()
if not independent_user:
LOGGER.warning("Could not find device user binding or independent enclave for user")
raise ValidationError("Invalid request")
device_user = independent_user
decoded: dict[str, Any] = decode(
assertion,
device_user.apple_secure_enclave_key,

View File

@@ -1,11 +1,15 @@
import re
from plistlib import loads
from typing import Any
from cryptography.hazmat.primitives import serialization
from cryptography.x509 import load_der_x509_certificate
from django.db import transaction
from requests import RequestException
from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.crypto.models import CertificateKeyPair
from authentik.endpoints.controller import BaseController, Capabilities, ConnectorSyncException
from authentik.endpoints.facts import (
DeviceFacts,
@@ -44,7 +48,7 @@ class FleetController(BaseController[DBC]):
return "fleetdm.com"
def capabilities(self) -> list[Capabilities]:
return [Capabilities.ENROLL_AUTOMATIC_API]
return [Capabilities.STAGE_ENDPOINTS, Capabilities.ENROLL_AUTOMATIC_API]
def _url(self, path: str) -> str:
return f"{self.connector.url}{path}"
@@ -76,8 +80,44 @@ class FleetController(BaseController[DBC]):
except RequestException as exc:
raise ConnectorSyncException(exc) from exc
@property
def mtls_ca_managed(self) -> str:
return f"goauthentik.io/endpoints/connectors/fleet/{self.connector.pk}"
def _sync_mtls_ca(self):
"""Sync conditional access Root CA for mTLS"""
try:
# Fleet doesn't have an API to just get the Conditional Access Root CA Cert (yet),
# hence we fetch the apple config profile and extract it
res = self._session.get(self._url("/api/v1/fleet/conditional_access/idp/apple/profile"))
res.raise_for_status()
profile = loads(res.text).get("PayloadContent", [])
raw_cert = None
for payload in profile:
if payload.get("PayloadIdentifier", "") != "com.fleetdm.conditional-access-ca":
continue
raw_cert = payload.get("PayloadContent")
if not raw_cert:
raise ConnectorSyncException("Failed to get conditional acccess CA")
except RequestException as exc:
raise ConnectorSyncException(exc) from exc
cert = load_der_x509_certificate(raw_cert)
CertificateKeyPair.objects.update_or_create(
managed=self.mtls_ca_managed,
defaults={
"name": f"Fleet Endpoint connector {self.connector.name}",
"certificate_data": cert.public_bytes(
encoding=serialization.Encoding.PEM,
).decode("utf-8"),
},
)
@transaction.atomic
def sync_endpoints(self) -> None:
try:
self._sync_mtls_ca()
except ConnectorSyncException as exc:
self.logger.warning("Failed to sync conditional access CA", exc=exc)
for host in self._paginate_hosts():
serial = host["hardware_serial"]
device, _ = Device.objects.get_or_create(
@@ -198,6 +238,8 @@ class FleetController(BaseController[DBC]):
for policy in host.get("policies", [])
],
"agent_version": fleet_version,
# Host UUID is required for conditional access matching
"uuid": host.get("uuid", "").lower(),
},
},
}

View File

@@ -51,6 +51,12 @@ class FleetConnector(Connector):
def component(self) -> str:
return "ak-endpoints-connector-fleet-form"
@property
def stage(self):
from authentik.enterprise.endpoints.connectors.fleet.stage import FleetStageView
return FleetStageView
class Meta:
verbose_name = _("Fleet Connector")
verbose_name_plural = _("Fleet Connectors")

View File

@@ -0,0 +1,51 @@
from cryptography.x509 import (
Certificate,
Extension,
SubjectAlternativeName,
UniformResourceIdentifier,
)
from rest_framework.exceptions import PermissionDenied
from authentik.crypto.models import CertificateKeyPair, fingerprint_sha256
from authentik.endpoints.models import Device, EndpointStage, StageMode
from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector
from authentik.enterprise.stages.mtls.stage import PLAN_CONTEXT_CERTIFICATE, MTLSStageView
from authentik.flows.planner import PLAN_CONTEXT_DEVICE
FLEET_CONDITIONAL_ACCESS_URI_PREFIX = "urn:device:apple:uuid:"
class FleetStageView(MTLSStageView):
def get_authorities(self):
stage: EndpointStage = self.executor.current_stage
connector = FleetConnector.objects.filter(pk=stage.connector_id).first()
controller = connector.controller(connector)
kp = CertificateKeyPair.objects.filter(managed=controller.mtls_ca_managed).first()
return [kp] if kp else None
def lookup_device(self, cert: Certificate, mode: StageMode):
san_ext: Extension[SubjectAlternativeName] = cert.extensions.get_extension_for_oid(
SubjectAlternativeName.oid
)
raw_values = san_ext.value.get_values_for_type(UniformResourceIdentifier)
values = [x.removeprefix(FLEET_CONDITIONAL_ACCESS_URI_PREFIX).lower() for x in raw_values]
self.logger.debug("Looking for devices with uuid", fleet_device_uuid=values)
device = Device.objects.filter(
**{"deviceconnection__devicefactsnapshot__data__vendor__fleetdm.com__uuid__in": values}
).first()
if not device and mode == StageMode.REQUIRED:
raise PermissionDenied("Failed to find device")
self.executor.plan.context[PLAN_CONTEXT_DEVICE] = device
self.executor.plan.context[PLAN_CONTEXT_CERTIFICATE] = self._cert_to_dict(cert)
return self.executor.stage_ok()
def dispatch(self, request, *args, **kwargs):
stage: EndpointStage = self.executor.current_stage
try:
cert = self.get_cert(stage.mode)
if not cert:
return self.executor.stage_ok()
self.logger.debug("Received certificate", cert=fingerprint_sha256(cert))
return self.lookup_device(cert, stage.mode)
except PermissionDenied as exc:
return self.executor.stage_invalid(error_message=exc.detail)

View File

@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDwDCCAqigAwIBAgIBBDANBgkqhkiG9w0BAQsFADBpMQkwBwYDVQQGEwAxJDAi
BgNVBAoTG0xvY2FsIGNlcnRpZmljYXRlIGF1dGhvcml0eTEQMA4GA1UECxMHU0NF
UCBDQTEkMCIGA1UEAxMbRmxlZXQgY29uZGl0aW9uYWwgYWNjZXNzIENBMB4XDTI2
MDMxODExMTc1NFoXDTI3MDQyMDExMjc1NFowLDEqMCgGA1UEAxMhRmxlZXQgY29u
ZGl0aW9uYWwgYWNjZXNzIGZvciBPa3RhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA3xuKxQQ8JSA4qCJ6RfOB7tbQurhwXiaJSLUDG7R5ncdRcd9LH/9y
5ZyI5kQACOwfICHmv02zR4/CrurfzXabo3CCpvcMdS7JI/FzP1GIIZ5RsR7oPFC6
JJg3m5BHuoHsUtCD7w0D52WiE7XVfbw47h2ChKmGMhkSrBvQnp3dHFEt8ntbl1/q
zCSuQaLeR2sQFurBDVBdinEgsvb1YHaYHi4tdFx5joG64Q/nJXyA2OM4hO9uBF+G
c4UVTzubx5sxwONcPhC9H+eLMpF1VHeU9gAGBlruVusUEYDmlqYQuA+bW5fTr4Zd
ZmJ5e+CzzUBYHduAML9a5S+1jbxSPZFBSwIDAQABo4GvMIGsMA4GA1UdDwEB/wQE
AwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUPrc1+LvbR9WoJIWZ
7YQa/3IX2w8wHwYDVR0jBBgwFoAUfl92kU2qcH4e+hypez4kEnqMbk4wRQYDVR0R
BD4wPIY6dXJuOmRldmljZTphcHBsZTp1dWlkOjVCRjQyMkQ2LTZFQUItNTE1Ni1B
QzVBLTlFQURDOTUyNDcxMzANBgkqhkiG9w0BAQsFAAOCAQEAGfxJ/u4271tnUUTB
J39YU6z2Ciav+9G3BtbvxBXI57Po7zCE6Z1sVkvYq6Xd0CcItPWRjbSPEy78ZzS0
By+gPy5fkKc8HHJ5I1wK890xbLBUS1P4EbdVBzI9ggouEa3B2asE10asnzLoKE4C
0FYWQwrzCsso8yxsJj1S8RKtd6MMbCis/9OQSC8om2tu6cLO+OftVn5DHtNWFidw
tAl/oHn2cZPUfZGpJGrHNZlp5w1c1dYfQeiPayoQIbsF+8eMV424G76z/8UPhMBs
R23LByv4TlUOPAGn2TRa2WtLIXs7FgqXRIFW4CjsPsEpXSVNlkYcn/VHY7Jl13zz
CRQ1Pg==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<!-- Trusted CA certificate -->
<dict>
<key>PayloadCertificateFileName</key>
<string>conditional_access_ca.der</string>
<key>PayloadContent</key>
<data>MIIDjzCCAnegAwIBAgIBATANBgkqhkiG9w0BAQsFADBpMQkwBwYDVQQGEwAxJDAiBgNVBAoTG0xvY2FsIGNlcnRpZmljYXRlIGF1dGhvcml0eTEQMA4GA1UECxMHU0NFUCBDQTEkMCIGA1UEAxMbRmxlZXQgY29uZGl0aW9uYWwgYWNjZXNzIENBMB4XDTI1MTIwOTEyMjI1MVoXDTM1MTIwOTEyMjI1MVowaTEJMAcGA1UEBhMAMSQwIgYDVQQKExtMb2NhbCBjZXJ0aWZpY2F0ZSBhdXRob3JpdHkxEDAOBgNVBAsTB1NDRVAgQ0ExJDAiBgNVBAMTG0ZsZWV0IGNvbmRpdGlvbmFsIGFjY2VzcyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANrgCcpzQci2UhH+Dn0eHopnnbx3HbMabMCHXm6xteMVFLrdQJDTFrZCQzcexUgbpPJ0az6mn4szo+E3stn0y2PPWsiAiVhFwp5M9HwNg18rPgDmITv2pM3l/hlEsfggjq6TEVO2gRcq4NujEGagcYX6kp6nWxh6bbRngQ/hlK6mXItWV3x0G9eTcbFObwZhbuC2dNbccytdqbVEIpBjp6fftQnQwAaUVjoyZBFlf1C1cDV4+1jpaVsIj11U1olA33GJCHcZQ4CJEsgh8yiSsvkH5RNf94CGINB5ixsMfppjSXV/vNkWDKEfmUXW2q4ft7KK/L/SRq8QSB4VqTAp2GsCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH5fdpFNqnB+HvocqXs+JBJ6jG5OMA0GCSqGSIb3DQEBCwUAA4IBAQAJr4bTGlrANoHStu4Y+OXjGbEQjZOe546Bcln4eWrEB16eaVzfKuZgjJYdcOmp36/v34QY/OCXEIsixrBU5aW/Sr53IK6UQSZV3O3xbBc4Aert7AbeJ4NVGZyelfVQo/5G0qM6k9p0+zpIZqNAzFbhcSPIzuE7ig2OGsFoQU+bXhzk09bsZ+u4BXibzVNfMuMG+DHNv0PRjll272nEPI3bGwHF5tdrnfJG6e9t+qK9j9UqmSlBknHQJNeU5o8IDcmWYjWtOuBzecYsg8pZzXabJqlHTBIz/h7waRe7jtrK+XopK3jghRf9JTL+i0Y8NbVjoNkIoS3xMeRhnNbR9lw1</data>
<key>PayloadDescription</key>
<string>Fleet conditional access CA certificate</string>
<key>PayloadDisplayName</key>
<string>Fleet conditional access CA</string>
<key>PayloadIdentifier</key>
<string>com.fleetdm.conditional-access-ca</string>
<key>PayloadType</key>
<string>com.apple.security.root</string>
<key>PayloadUUID</key>
<string>ef1b2231-ad80-5511-9893-1f9838295147</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDescription</key>
<string>Configures SCEP enrollment for Okta conditional access</string>
<key>PayloadDisplayName</key>
<string>Fleet conditional access for Okta</string>
<key>PayloadIdentifier</key>
<string>com.fleetdm.conditional-access-okta</string>
<key>PayloadOrganization</key>
<string>Fleet Device Management</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadScope</key>
<string>User</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>6fa509a3-feca-56f7-a283-d6a81c733ed2</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>

View File

@@ -1,27 +1,27 @@
{
"created_at": "2025-06-25T22:21:35Z",
"updated_at": "2025-12-20T11:42:09Z",
"created_at": "2026-02-18T16:31:34Z",
"updated_at": "2026-03-18T11:29:18Z",
"software": null,
"software_updated_at": "2025-10-22T02:24:25Z",
"id": 1,
"detail_updated_at": "2025-10-23T23:30:31Z",
"label_updated_at": "2025-10-23T23:30:31Z",
"policy_updated_at": "2025-10-23T23:02:11Z",
"last_enrolled_at": "2025-06-25T22:21:37Z",
"seen_time": "2025-10-23T23:59:08Z",
"software_updated_at": "2026-03-18T11:29:17Z",
"id": 19,
"detail_updated_at": "2026-03-18T11:29:18Z",
"label_updated_at": "2026-03-18T11:29:18Z",
"policy_updated_at": "2026-03-18T11:29:18Z",
"last_enrolled_at": "2026-02-18T16:31:45Z",
"seen_time": "2026-03-18T11:31:34Z",
"refetch_requested": false,
"hostname": "jens-mac-vm.local",
"uuid": "C8B98348-A0A6-5838-A321-57B59D788269",
"uuid": "5BF422D6-6EAB-5156-AC5A-9EADC9524713",
"platform": "darwin",
"osquery_version": "5.19.0",
"osquery_version": "5.21.0",
"orbit_version": null,
"fleet_desktop_version": null,
"scripts_enabled": null,
"os_version": "macOS 26.0.1",
"build": "25A362",
"os_version": "macOS 26.3",
"build": "25D125",
"platform_like": "darwin",
"code_name": "",
"uptime": 256356000000000,
"uptime": 653014000000000,
"memory": 4294967296,
"cpu_type": "arm64e",
"cpu_subtype": "ARM64E",
@@ -31,38 +31,41 @@
"hardware_vendor": "Apple Inc.",
"hardware_model": "VirtualMac2,1",
"hardware_version": "",
"hardware_serial": "Z5DDF07GK6",
"hardware_serial": "ZV35VFDD50",
"computer_name": "jens-mac-vm",
"timezone": null,
"public_ip": "92.116.179.252",
"primary_ip": "192.168.85.3",
"primary_mac": "e6:9d:21:c2:2f:19",
"primary_ip": "192.168.64.7",
"primary_mac": "5e:72:1c:89:98:29",
"distributed_interval": 10,
"config_tls_refresh": 60,
"logger_tls_period": 10,
"team_id": 2,
"team_id": 5,
"pack_stats": null,
"team_name": "prod",
"gigs_disk_space_available": 23.82,
"percent_disk_space_available": 37,
"team_name": "dev",
"gigs_disk_space_available": 16.52,
"percent_disk_space_available": 26,
"gigs_total_disk_space": 62.83,
"gigs_all_disk_space": null,
"issues": {
"failing_policies_count": 1,
"critical_vulnerabilities_count": 2,
"total_issues_count": 3
"critical_vulnerabilities_count": 0,
"total_issues_count": 1
},
"device_mapping": null,
"mdm": {
"enrollment_status": "On (manual)",
"dep_profile_error": false,
"server_url": "https://fleet.beryjuio-home.k8s.beryju.io/mdm/apple/mdm",
"server_url": "https://fleet.beryjuio-prod.k8s.beryju.io/mdm/apple/mdm",
"name": "Fleet",
"encryption_key_available": false,
"connected_to_fleet": true
},
"refetch_critical_queries_until": null,
"last_restarted_at": "2025-10-21T00:17:55Z",
"status": "offline",
"last_restarted_at": "2026-03-10T22:05:44.00887Z",
"status": "online",
"display_text": "jens-mac-vm.local",
"display_name": "jens-mac-vm"
"display_name": "jens-mac-vm",
"fleet_id": 5,
"fleet_name": "dev"
}

View File

@@ -21,12 +21,19 @@ TEST_HOST = {"hosts": [TEST_HOST_UBUNTU, TEST_HOST_MACOS, TEST_HOST_WINDOWS, TES
class TestFleetConnector(APITestCase):
def setUp(self):
self.connector = FleetConnector.objects.create(
name=generate_id(), url="http://localhost", token=generate_id()
name=generate_id(),
url="http://localhost",
token=generate_id(),
map_teams_access_group=True,
)
def test_sync(self):
controller = self.connector.controller(self.connector)
with Mocker() as mock:
mock.get(
"http://localhost/api/v1/fleet/conditional_access/idp/apple/profile",
text=load_fixture("fixtures/cond_acc_profile.mobileconfig"),
)
mock.get(
"http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=0&per_page=50&device_mapping=true&populate_software=true&populate_users=true",
json=TEST_HOST,
@@ -40,6 +47,9 @@ class TestFleetConnector(APITestCase):
identifier="VMware-56 4d 4a 5a b0 22 7b d7-9b a5 0b dc 8f f2 3b 60"
).first()
self.assertIsNotNone(device)
group = device.access_group
self.assertIsNotNone(group)
self.assertEqual(group.name, "prod")
self.assertEqual(
device.cached_facts.data,
{
@@ -50,7 +60,13 @@ class TestFleetConnector(APITestCase):
"version": "24.04.3 LTS",
},
"disks": [],
"vendor": {"fleetdm.com": {"policies": [], "agent_version": ""}},
"vendor": {
"fleetdm.com": {
"policies": [],
"agent_version": "",
"uuid": "5a4a4d56-22b0-d77b-9ba5-0bdc8ff23b60",
}
},
"network": {"hostname": "ubuntu-desktop", "interfaces": []},
"hardware": {
"model": "VMware20,1",
@@ -72,6 +88,10 @@ class TestFleetConnector(APITestCase):
self.connector.save()
controller = self.connector.controller(self.connector)
with Mocker() as mock:
mock.get(
"http://localhost/api/v1/fleet/conditional_access/idp/apple/profile",
text=load_fixture("fixtures/cond_acc_profile.mobileconfig"),
)
mock.get(
"http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=0&per_page=50&device_mapping=true&populate_software=true&populate_users=true",
json=TEST_HOST,
@@ -81,11 +101,13 @@ class TestFleetConnector(APITestCase):
json={"hosts": []},
)
controller.sync_endpoints()
self.assertEqual(mock.call_count, 2)
self.assertEqual(mock.call_count, 3)
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[0].headers["foo"], "bar")
self.assertEqual(mock.request_history[1].method, "GET")
self.assertEqual(mock.request_history[1].headers["foo"], "bar")
self.assertEqual(mock.request_history[2].method, "GET")
self.assertEqual(mock.request_history[2].headers["foo"], "bar")
def test_map_host_linux(self):
controller = self.connector.controller(self.connector)
@@ -128,6 +150,6 @@ class TestFleetConnector(APITestCase):
"arch": "arm64e",
"family": OSFamily.macOS,
"name": "macOS",
"version": "26.0.1",
"version": "26.3",
},
)

View File

@@ -0,0 +1,84 @@
from json import loads
from ssl import PEM_FOOTER, PEM_HEADER
from django.urls import reverse
from requests_mock import Mocker
from authentik.core.tests.utils import (
create_test_flow,
)
from authentik.endpoints.models import Device, EndpointStage, StageMode
from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector
from authentik.enterprise.stages.mtls.stage import PLAN_CONTEXT_CERTIFICATE
from authentik.flows.models import FlowDesignation, 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 FleetConnectorStageTests(FlowTestCase):
def setUp(self):
super().setUp()
self.connector = FleetConnector.objects.create(
name=generate_id(), url="http://localhost", token=generate_id()
)
controller = self.connector.controller(self.connector)
with Mocker() as mock:
mock.get(
"http://localhost/api/v1/fleet/conditional_access/idp/apple/profile",
text=load_fixture("fixtures/cond_acc_profile.mobileconfig"),
)
mock.get(
"http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=0&per_page=50&device_mapping=true&populate_software=true&populate_users=true",
json={"hosts": [loads(load_fixture("fixtures/host_macos.json"))]},
)
mock.get(
"http://localhost/api/v1/fleet/hosts?order_key=hardware_serial&page=1&per_page=50&device_mapping=true&populate_software=true&populate_users=true",
json={"hosts": []},
)
controller.sync_endpoints()
self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.stage = EndpointStage.objects.create(
name=generate_id(),
mode=StageMode.REQUIRED,
connector=self.connector,
)
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=0)
self.host_cert = load_fixture("fixtures/cond_acc_host.pem")
def _format_traefik(self, cert: str | None = None):
cert = cert if cert else self.host_cert
return cert.replace(PEM_HEADER, "").replace(PEM_FOOTER, "").replace("\n", "")
def test_assoc(self):
dev = Device.objects.get(identifier="ZV35VFDD50")
with self.assertFlowFinishes() as plan:
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
headers={"X-Forwarded-TLS-Client-Cert": self._format_traefik()},
)
self.assertEqual(res.status_code, 200)
plan = plan()
self.assertEqual(plan.context[PLAN_CONTEXT_DEVICE], dev)
self.assertEqual(
plan.context[PLAN_CONTEXT_CERTIFICATE]["subject"],
"CN=Fleet conditional access for Okta",
)
def test_assoc_not_found(self):
dev = Device.objects.get(identifier="ZV35VFDD50")
dev.delete()
with self.assertFlowFinishes() as plan:
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
headers={"X-Forwarded-TLS-Client-Cert": self._format_traefik()},
)
self.assertEqual(res.status_code, 200)
self.assertStageResponse(res, self.flow, component="ak-stage-access-denied")
plan = plan()
self.assertNotIn(PLAN_CONTEXT_DEVICE, plan.context)

View File

@@ -141,10 +141,8 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
# according to the provider's settings
base = User.objects.all().exclude_anonymous().filter(**kwargs)
if self.exclude_users_service_account:
base = (
base.exclude(type=UserTypes.SERVICE_ACCOUNT)
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
.exclude(type=UserTypes.AGENT)
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
)
if self.filter_group:
base = base.filter(groups__in=[self.filter_group])

View File

@@ -130,10 +130,8 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
# according to the provider's settings
base = User.objects.all().exclude_anonymous().filter(**kwargs)
if self.exclude_users_service_account:
base = (
base.exclude(type=UserTypes.SERVICE_ACCOUNT)
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
.exclude(type=UserTypes.AGENT)
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
)
if self.filter_group:
base = base.filter(groups__in=[self.filter_group])

View File

@@ -12,7 +12,7 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.core.signals import password_changed
from authentik.core.signals import password_changed, password_hash_changed
from authentik.enterprise.providers.ssf.models import (
EventTypes,
SSFProvider,
@@ -84,14 +84,13 @@ def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSessi
)
@receiver(password_changed)
def ssf_password_changed_cred_change(sender, user: User, password: str | None, **_):
def _send_password_credential_change(user: User, change_type: str):
"""Credential change trigger (password changed)"""
send_ssf_events(
EventTypes.CAEP_CREDENTIAL_CHANGE,
{
"credential_type": "password",
"change_type": "revoke" if password is None else "update",
"change_type": change_type,
},
sub_id={
"format": "complex",
@@ -103,6 +102,16 @@ def ssf_password_changed_cred_change(sender, user: User, password: str | None, *
)
@receiver(password_hash_changed)
@receiver(password_changed)
def ssf_password_changed_cred_change(signal, sender, user: User, password: str | None = None, **_):
"""Credential change trigger (password changed)"""
if signal is password_hash_changed:
_send_password_credential_change(user, "update")
return
_send_password_credential_change(user, "revoke" if password is None else "update")
device_type_map = {
StaticDevice: "pin",
TOTPDevice: "pin",

View File

@@ -1,5 +1,6 @@
from uuid import uuid4
from django.contrib.auth.hashers import make_password
from django.urls import reverse
from rest_framework.test import APITestCase
@@ -52,6 +53,21 @@ class TestSignals(APITestCase):
)
self.assertEqual(res.status_code, 201, res.content)
def _assert_password_credential_change(self, user, change_type: str):
stream = Stream.objects.filter(provider=self.provider).first()
self.assertIsNotNone(stream)
event = StreamEvent.objects.filter(stream=stream).first()
self.assertIsNotNone(event)
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
event_payload = event.payload["events"][
"https://schemas.openid.net/secevent/caep/event-type/credential-change"
]
self.assertEqual(event_payload["change_type"], change_type)
self.assertEqual(event_payload["credential_type"], "password")
self.assertEqual(event.payload["sub_id"]["format"], "complex")
self.assertEqual(event.payload["sub_id"]["user"]["format"], "email")
self.assertEqual(event.payload["sub_id"]["user"]["email"], user.email)
def test_signal_logout(self):
"""Test user logout"""
user = create_test_user()
@@ -79,19 +95,25 @@ class TestSignals(APITestCase):
user.set_password(generate_id())
user.save()
stream = Stream.objects.filter(provider=self.provider).first()
self.assertIsNotNone(stream)
event = StreamEvent.objects.filter(stream=stream).first()
self.assertIsNotNone(event)
self.assertEqual(event.status, SSFEventStatus.PENDING_FAILED)
event_payload = event.payload["events"][
"https://schemas.openid.net/secevent/caep/event-type/credential-change"
]
self.assertEqual(event_payload["change_type"], "update")
self.assertEqual(event_payload["credential_type"], "password")
self.assertEqual(event.payload["sub_id"]["format"], "complex")
self.assertEqual(event.payload["sub_id"]["user"]["format"], "email")
self.assertEqual(event.payload["sub_id"]["user"]["email"], user.email)
self._assert_password_credential_change(user, "update")
def test_signal_password_change_from_hash(self):
"""Test user password change from a pre-hashed password."""
user = create_test_user()
self.client.force_login(user)
user.set_password_from_hash(make_password(generate_id()))
user.save()
self._assert_password_credential_change(user, "update")
def test_signal_password_revoke(self):
"""Test explicit password revoke."""
user = create_test_user()
self.client.force_login(user)
user.set_password(None)
user.save()
self._assert_password_credential_change(user, "revoke")
def test_signal_authenticator_added(self):
"""Test authenticator creation signal"""

View File

@@ -15,6 +15,7 @@ from cryptography.x509 import (
)
from cryptography.x509.verification import PolicyBuilder, Store, VerificationError
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import PermissionDenied
from authentik.brands.models import Brand
from authentik.core.models import User
@@ -25,7 +26,6 @@ from authentik.enterprise.stages.mtls.models import (
MutualTLSStage,
UserAttributes,
)
from authentik.flows.challenge import AccessDeniedChallenge
from authentik.flows.models import FlowDesignation
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
@@ -217,8 +217,7 @@ class MTLSStageView(ChallengeStageView):
return None
return str(_cert_attr[0])
def dispatch(self, request, *args, **kwargs):
stage: MutualTLSStage = self.executor.current_stage
def get_cert(self, mode: StageMode):
certs = [
*self._parse_cert_xfcc(),
*self._parse_cert_nginx(),
@@ -228,21 +227,26 @@ class MTLSStageView(ChallengeStageView):
authorities = self.get_authorities()
if not authorities:
self.logger.warning("No Certificate authority found")
if stage.mode == StageMode.OPTIONAL:
return self.executor.stage_ok()
if stage.mode == StageMode.REQUIRED:
return super().dispatch(request, *args, **kwargs)
if mode == StageMode.OPTIONAL:
return None
if mode == StageMode.REQUIRED:
raise PermissionDenied("Unknown error")
cert = self.validate_cert(authorities, certs)
if not cert and stage.mode == StageMode.REQUIRED:
if not cert and mode == StageMode.REQUIRED:
self.logger.warning("Client certificate required but no certificates given")
return super().dispatch(
request,
*args,
error_message=_("Certificate required but no certificate was given."),
**kwargs,
)
if not cert and stage.mode == StageMode.OPTIONAL:
raise PermissionDenied(str(_("Certificate required but no certificate was given.")))
if not cert and mode == StageMode.OPTIONAL:
self.logger.info("No certificate given, continuing")
return None
return cert
def dispatch(self, request, *args, **kwargs):
stage: MutualTLSStage = self.executor.current_stage
try:
cert = self.get_cert(stage.mode)
except PermissionDenied as exc:
return self.executor.stage_invalid(error_message=exc.detail)
if not cert:
return self.executor.stage_ok()
self.logger.debug("Received certificate", cert=fingerprint_sha256(cert))
existing_user = self.check_if_user(cert)
@@ -251,15 +255,5 @@ class MTLSStageView(ChallengeStageView):
elif existing_user:
self.auth_user(existing_user, cert)
else:
return super().dispatch(
request, *args, error_message=_("No user found for certificate."), **kwargs
)
return self.executor.stage_invalid(_("No user found for certificate."))
return self.executor.stage_ok()
def get_challenge(self, *args, error_message: str | None = None, **kwargs):
return AccessDeniedChallenge(
data={
"component": "ak-stage-access-denied",
"error_message": str(error_message or "Unknown error"),
}
)

View File

@@ -6,18 +6,6 @@ from dramatiq.actor import actor
from authentik.enterprise.license import LicenseKey
def _deactivate_agent_users():
"""Mark all active agent users inactive and remove their sessions when the enterprise
license is not valid. Called after each license usage recording."""
from authentik.core.models import Session, User, UserTypes
agents = User.objects.filter(type=UserTypes.AGENT, is_active=True)
Session.objects.filter(authenticatedsession__user__in=agents).delete()
agents.update(is_active=False)
@actor(description=_("Update enterprise license status."))
def enterprise_update_usage():
usage = LicenseKey.get_total().record_usage()
if not usage.status.is_valid:
_deactivate_agent_users()
LicenseKey.get_total().record_usage()

View File

@@ -1,64 +0,0 @@
"""Enterprise task tests"""
from django.test import TestCase
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_OWNER_PK,
USER_PATH_AGENT,
User,
UserTypes,
)
from authentik.lib.generators import generate_id
class TestDeactivateAgentUsers(TestCase):
"""Tests for _deactivate_agent_users enterprise task"""
def _create_agent(self, owner):
agent = User.objects.create(
username=generate_id(),
type=UserTypes.AGENT,
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
path=USER_PATH_AGENT,
is_active=True,
)
agent.set_unusable_password()
agent.save()
return agent
def test_deactivates_all_active_agents(self):
"""_deactivate_agent_users marks all active agent users inactive"""
from authentik.enterprise.tasks import _deactivate_agent_users
owner = User.objects.create(username=generate_id())
agent1 = self._create_agent(owner)
agent2 = self._create_agent(owner)
_deactivate_agent_users()
agent1.refresh_from_db()
agent2.refresh_from_db()
self.assertFalse(agent1.is_active)
self.assertFalse(agent2.is_active)
def test_does_not_deactivate_non_agents(self):
"""_deactivate_agent_users does not affect non-agent service accounts"""
from authentik.enterprise.tasks import _deactivate_agent_users
sa = User.objects.create(
username=generate_id(),
type=UserTypes.SERVICE_ACCOUNT,
is_active=True,
)
internal = User.objects.create(
username=generate_id(),
type=UserTypes.INTERNAL,
is_active=True,
)
_deactivate_agent_users()
sa.refresh_from_db()
internal.refresh_from_db()
self.assertTrue(sa.is_active)
self.assertTrue(internal.is_active)

View File

@@ -13,6 +13,7 @@ from django.core.exceptions import SuspiciousOperation
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.http import HttpRequest, HttpResponse
from django_postgres_cache.models import CacheEntry
from structlog.stdlib import BoundLogger, get_logger
from authentik.blueprints.v1.importer import excluded_models
@@ -31,6 +32,7 @@ IGNORED_MODELS = tuple(
Notification,
StaticToken,
Session,
CacheEntry,
)
)

View File

@@ -11,7 +11,7 @@ from django.http import HttpRequest
from rest_framework.request import Request
from authentik.core.models import AuthenticatedSession, User
from authentik.core.signals import login_failed, password_changed
from authentik.core.signals import login_failed, password_changed, password_hash_changed
from authentik.events.models import Event, EventAction
from authentik.flows.models import Stage
from authentik.flows.planner import (
@@ -112,8 +112,15 @@ def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_
)
@receiver(password_hash_changed)
@receiver(password_changed)
def on_password_changed(sender, user: User, password: str, request: HttpRequest | None, **_):
def on_password_changed(
sender,
user: User,
password: str | None = None,
request: HttpRequest | None = None,
**_,
):
"""Log password change"""
Event.new(EventAction.PASSWORD_SET).from_http(request, user=user)

View File

@@ -2,6 +2,7 @@
from urllib.parse import urlencode
from django.contrib.auth.hashers import make_password
from django.contrib.contenttypes.models import ContentType
from django.test import RequestFactory, TestCase
from django.views.debug import SafeExceptionReporterFilter
@@ -10,7 +11,7 @@ from guardian.shortcuts import get_anonymous_user
from authentik.brands.models import Brand
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_user
from authentik.events.models import Event
from authentik.events.models import Event, EventAction
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views.executor import QS_QUERY, SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
@@ -213,3 +214,14 @@ class TestEvents(TestCase):
event = Event.new("unittest", foo="foo bar \u0000 baz")
event.save()
self.assertEqual(event.context["foo"], "foo bar baz")
def test_password_set_signal_on_set_password_from_hash(self):
"""Changing password from hash should still emit an audit event."""
user = create_test_user()
old_count = Event.objects.filter(action=EventAction.PASSWORD_SET, user__pk=user.pk).count()
user.set_password_from_hash(make_password(generate_id()))
user.save()
new_count = Event.objects.filter(action=EventAction.PASSWORD_SET, user__pk=user.pk).count()
self.assertEqual(new_count, old_count + 1)

View File

@@ -31,7 +31,7 @@ from authentik.policies.types import PolicyRequest
# Special keys which are *not* cleaned, even when the default filter
# is matched
ALLOWED_SPECIAL_KEYS = re.compile(
r"passing|password_change_date|^auth_method(_args)?$|^goauthentik\.io/agent/",
r"passing|password_change_date|^auth_method(_args)?$",
flags=re.I,
)

View File

@@ -11,6 +11,10 @@ class FlowNonApplicableException(SentryIgnoredException):
policy_result: PolicyResult | None = None
def __init__(self, policy_result: PolicyResult | None = None, *args):
super().__init__(*args)
self.policy_result = policy_result
@property
def messages(self) -> str:
"""Get messages from policy result, fallback to generic reason"""

View File

@@ -42,6 +42,7 @@ class Migration(migrations.Migration):
("require_superuser", "Require Superuser"),
("require_redirect", "Require Redirect"),
("require_outpost", "Require Outpost"),
("require_token", "Require Token"),
],
default="none",
help_text="Required level of authentication and authorization to access a flow.",

View File

@@ -40,6 +40,7 @@ class FlowAuthenticationRequirement(models.TextChoices):
REQUIRE_SUPERUSER = "require_superuser"
REQUIRE_REDIRECT = "require_redirect"
REQUIRE_OUTPOST = "require_outpost"
REQUIRE_TOKEN = "require_token"
class NotConfiguredAction(models.TextChoices):
@@ -185,25 +186,47 @@ class Flow(SerializerModel, PolicyBindingModel):
help_text=_("Required level of authentication and authorization to access a flow."),
)
def background_url(self, request: HttpRequest | None = None) -> str:
def background_url(
self,
request: HttpRequest | None = None,
use_cache: bool = True,
) -> str:
"""Get the URL to the background image"""
if not self.background:
if request:
return request.brand.branding_default_flow_background_url()
return request.brand.branding_default_flow_background_url(
request,
use_cache=use_cache,
)
return (
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
)
return get_file_manager(FileUsage.MEDIA).file_url(self.background, request)
return get_file_manager(FileUsage.MEDIA).file_url(
self.background,
request,
use_cache=use_cache,
)
def background_themed_urls(self, request: HttpRequest | None = None) -> dict[str, str] | None:
def background_themed_urls(
self,
request: HttpRequest | None = None,
use_cache: bool = True,
) -> dict[str, str] | None:
"""Get themed URLs for background if it contains %(theme)s"""
if not self.background:
if request:
return request.brand.branding_default_flow_background_themed_urls()
return request.brand.branding_default_flow_background_themed_urls(
request,
use_cache=use_cache,
)
return None
return get_file_manager(FileUsage.MEDIA).themed_urls(self.background, request)
return get_file_manager(FileUsage.MEDIA).themed_urls(
self.background,
request,
use_cache=use_cache,
)
stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)

View File

@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any
from django.core.cache import cache
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from sentry_sdk import start_span
from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
@@ -26,6 +27,7 @@ from authentik.lib.config import CONFIG
from authentik.lib.utils.urls import redirect_with_qs
from authentik.outposts.models import Outpost
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
from authentik.root.middleware import ClientIPMiddleware
if TYPE_CHECKING:
@@ -226,6 +228,15 @@ class FlowPlanner:
and context.get(PLAN_CONTEXT_IS_REDIRECTED) is None
):
raise FlowNonApplicableException()
if (
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_TOKEN
and context.get(PLAN_CONTEXT_IS_RESTORED) is None
):
raise FlowNonApplicableException(
PolicyResult(
False, _("This link is invalid or has expired. Please request a new one.")
)
)
outpost_user = ClientIPMiddleware.get_outpost_user(request)
if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
if not outpost_user:
@@ -273,9 +284,7 @@ class FlowPlanner:
engine.build()
result = engine.result
if not result.passing:
exc = FlowNonApplicableException()
exc.policy_result = result
raise exc
raise FlowNonApplicableException(result)
# User is passing so far, check if we have a cached plan
cached_plan_key = cache_key(self.flow, user)
cached_plan = cache.get(cached_plan_key, None)

View File

@@ -194,12 +194,14 @@ class ChallengeStageView(StageView):
if not hasattr(challenge, "initial_data"):
challenge.initial_data = {}
if "flow_info" not in challenge.initial_data:
# Flow payloads can outlive the previous signed media JWT, so
# refreshes must mint fresh URLs instead of reusing cached ones.
flow_info = ContextualFlowInfo(
data={
"title": self.format_title(),
"background": self.executor.flow.background_url(self.request),
"background": self.executor.flow.background_url(use_cache=False),
"background_themed_urls": self.executor.flow.background_themed_urls(
self.request
use_cache=False,
),
"cancel_url": self.cancel_url,
"layout": self.executor.flow.layout,

View File

@@ -1,5 +1,6 @@
"""flow views tests"""
from datetime import timedelta
from unittest.mock import MagicMock, PropertyMock, patch
from urllib.parse import urlencode
@@ -7,6 +8,7 @@ from django.http import HttpRequest, HttpResponse
from django.test import override_settings
from django.test.client import RequestFactory
from django.urls import reverse
from django.utils.timezone import now
from rest_framework.exceptions import ParseError
from authentik.core.models import Group, User
@@ -17,6 +19,7 @@ from authentik.flows.models import (
FlowDeniedAction,
FlowDesignation,
FlowStageBinding,
FlowToken,
InvalidResponseAction,
)
from authentik.flows.planner import FlowPlan, FlowPlanner
@@ -24,6 +27,7 @@ from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageVie
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import (
NEXT_ARG_NAME,
QS_KEY_TOKEN,
QS_QUERY,
SESSION_KEY_PLAN,
FlowExecutorView,
@@ -740,3 +744,77 @@ class TestFlowExecutor(FlowTestCase):
"title": flow.title,
},
)
@patch(
"authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_expired_flow_token(self):
"""Test that an expired flow token shows an appropriate error message"""
flow = create_test_flow(
FlowDesignation.RECOVERY,
authentication=FlowAuthenticationRequirement.REQUIRE_TOKEN,
)
user = create_test_user()
plan = FlowPlan(flow_pk=flow.pk.hex, bindings=[], markers=[])
token = FlowToken.objects.create(
user=user,
identifier=generate_id(),
flow=flow,
_plan=FlowToken.pickle(plan),
expires=now() - timedelta(hours=1),
)
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(
url + f"?{urlencode({QS_QUERY: urlencode({QS_KEY_TOKEN: token.key})})}"
)
self.assertStageResponse(
response,
flow,
component="ak-stage-access-denied",
error_message="This link is invalid or has expired. Please request a new one.",
)
@patch(
"authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_invalid_flow_token_require_token(self):
"""Test that an invalid/nonexistent token on a REQUIRE_TOKEN flow shows error"""
flow = create_test_flow(
FlowDesignation.RECOVERY,
authentication=FlowAuthenticationRequirement.REQUIRE_TOKEN,
)
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(
url + f"?{urlencode({QS_QUERY: urlencode({QS_KEY_TOKEN: 'invalid-token'})})}"
)
self.assertStageResponse(
response,
flow,
component="ak-stage-access-denied",
error_message="This link is invalid or has expired. Please request a new one.",
)
@patch(
"authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_no_token_require_token(self):
"""Test that accessing a REQUIRE_TOKEN flow without any token shows error"""
flow = create_test_flow(
FlowDesignation.RECOVERY,
authentication=FlowAuthenticationRequirement.REQUIRE_TOKEN,
)
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url)
self.assertStageResponse(
response,
flow,
component="ak-stage-access-denied",
error_message="This link is invalid or has expired. Please request a new one.",
)

View File

@@ -33,7 +33,7 @@ class TestFlowInspector(APITestCase):
FlowStageBinding.objects.create(
target=flow,
stage=ident_stage,
order=1,
order=0,
invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
)
dummy_stage = DummyStage.objects.create(name=generate_id())

View File

@@ -26,6 +26,7 @@ from authentik.flows.models import (
)
from authentik.flows.planner import (
PLAN_CONTEXT_IS_REDIRECTED,
PLAN_CONTEXT_IS_RESTORED,
PLAN_CONTEXT_PENDING_USER,
FlowPlanner,
cache_key,
@@ -129,6 +130,22 @@ class TestFlowPlanner(TestCase):
planner.allow_empty_flows = True
planner.plan(request)
def test_authentication_require_token(self):
"""Test flow authentication (require_token)"""
flow = create_test_flow()
flow.authentication = FlowAuthenticationRequirement.REQUIRE_TOKEN
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
with self.assertRaises(FlowNonApplicableException):
planner.plan(request)
context = {PLAN_CONTEXT_IS_RESTORED: True}
planner.plan(request, context)
@patch(
"authentik.policies.engine.PolicyEngine.result",
POLICY_RETURN_FALSE,

View File

@@ -4,9 +4,14 @@ from collections.abc import Callable
from django.test import RequestFactory, TestCase
from authentik.core.tests.utils import RequestFactory as AuthentikRequestFactory
from authentik.core.tests.utils import create_test_flow
from authentik.flows.models import FlowStageBinding
from authentik.flows.stage import StageView
from authentik.flows.views.executor import FlowExecutorView
from authentik.lib.utils.reflection import all_subclasses
from authentik.stages.dummy.models import DummyStage
from authentik.stages.dummy.stage import DummyStageView
class TestViews(TestCase):
@@ -15,6 +20,27 @@ class TestViews(TestCase):
def setUp(self) -> None:
self.factory = RequestFactory()
self.exec = FlowExecutorView(request=self.factory.get("/"))
self.authentik_factory = AuthentikRequestFactory()
def test_challenge_stage_flow_info_uses_relative_background(self):
"""Test challenge flow info keeps background URLs app-relative."""
flow = create_test_flow()
stage = DummyStage.objects.create(name="dummy")
FlowStageBinding.objects.create(target=flow, stage=stage, order=0)
request = self.authentik_factory.get("/")
executor = FlowExecutorView(flow=flow, request=request)
executor.current_stage = stage
view = DummyStageView(executor)
view.request = request
challenge = view._get_challenge()
self.assertEqual(
challenge.initial_data["flow_info"]["background"],
"/static/dist/assets/images/flow_background.jpg",
)
def view_tester_factory(view_class: type[StageView]) -> Callable:

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