Compare commits

...

88 Commits

Author SHA1 Message Date
authentik-automation[bot]
0dccbd4193 release: 2026.2.1 2026-03-03 19:49:59 +00:00
authentik-automation[bot]
6a70894e01 website/docs: add release notes for 2026.2.1 (cherry-pick #20659 to version-2026.2) (#20695)
website/docs: add release notes for `2026.2.1` (#20659)

* add release notes for `2026.2.1`

* Update release notes for version 2026.2



---------

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

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

fix identification stage styling in compatibility mode

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

* Start

* Add links

* Links

* sidebar

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




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




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




* Update 2025-09-includesec.md



* Apply suggestions from code review





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




* Add link

---------

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

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

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

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

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

* WIP

* WIP

* Apply suggestions from code review




---------

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

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

fix worker startup on macos

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

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

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

* web/flows: fix inverted source icons



* fix actually



---------

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

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

* internal: make http timeouts configurable



* Changed formatting to match the rest of the doc

---------

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

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

This cherry-pick has conflicts that need manual resolution.

Original PR: #20550
Original commit: 4c8916adde

* Veeam conflict fix

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

---------

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

* Begin

* WIP

* WIP

* WIP

* Fix link

* Fix spellig and links

* Enterprise vs enterprise plus

* Changes based on Tana's comment

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




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




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




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




* Apply suggestions

* Apply suggestion from Eric

* Update doc title after discussion with Tana

* Fix links

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




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




* Apply suggestions

* US dollars

* Apply Fletcher's suggestions

---------

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

* Remove bad redirect

* Remove space

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

fix upgrade link in `2026.2` release notes

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

update supported versions

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

This cherry-pick has conflicts that need manual resolution.

Original PR: #20527
Original commit: 884e662277

* fix conflicts

---------

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

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

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

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

This cherry-pick has conflicts that need manual resolution.

Original PR: #20489
Original commit: 9da1014271

* Update index.mdx

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

---------

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

* docs: add auth config steps

* tweak



* Changed wording

* Fix broken link

---------

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

* stages: fix infinite recursion

* respect mode



* add tests



---------

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

fix Edit Policy button on Flow view page

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

This cherry-pick has conflicts that need manual resolution.

Original PR: #20429
Original commit: ab981dec86

* revert miscellaneous changes

These don't need to be in 2026.2

---------

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

add `ES384` to enterprise license algorithms

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

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

* Update documentation to reflect allow sending client id via header

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

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

* add info about make install and recovery key

* fix formatting on troubleshooting tip

* Apply suggestion from @dominic-r



* tweak to bump

* tweak

* tweaked words abouot make install per jens

* build

---------

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

* web: Center footer links.

* Refine track resizing behavior.

* Fix odd scenario.

* Tidy padding.

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

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

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

* policies: measure policy process from manager



* fix constructor



---------

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

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

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




* enterprise/lifecycle: replace extend_schema_field with type annotations

---------

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

* monkey patch pyjwt to accept mismatching key

* restore `_validate_curve` after monkeypatch

* add explanatory comment

* next year is 2027, dummy

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

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

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

## What

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

## Why

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

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

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

* web: Fix element property names with custom attributes.

---------

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

docs: correct reference to overriden S3 variable

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

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

add cause to `ak_groups` deprecation event and logs

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

Fix broken link

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

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

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

* Fix footer alignment.

* Fix loading position in compatibility mode.

* Apply min height only when placeholder content is present.

* Fix alignment in compatibility mode.

* Add compatibility mode host selectors.

* Fix nullish challenge height. Clarify selector behavior.

* Add type defintion

* Fix padding.

* Fix misapplication of pf-* class to container.

* Fix huge base64 encoded attribute.

* Clean up layering issues, order of styles.

* Disable dev override.

* Document parts.

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

fix object permission request

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

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

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

This cherry-pick has conflicts that need manual resolution.

Original PR: #20338
Original commit: e056dbdadd

* Fix conflict

* Fix conflicts

---------

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

* WIP

* Sentence

* Delete image

* WIP

* adjust wording

---------

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

* Begin

* Add steps

* Apply suggestions

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




* Apply suggestion from @dominic-r



---------

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

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

* add affine to release notes

* use built-in github linking

* add missing credits to Arcane integration

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

* web: fix italic formatting in lifecycle rule help text

* r

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

Make wording more generic

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

* website/docs: Custom CSS

* Revise.

* Fix paths.

* Update links.

* Update header capitalization



---------

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

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

* first draft

* add table of parms

* tweak

* add section about certs

* a little more content

* more info on wa

* new procedurla file and edit sidebar

* tweaks

* dewi and jens edits

* tweak to remove bullet

* add docs link to the Rel Notes

* dewi edits thx

* ooops missed that last edit

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

* Update SSF documentation

* Fix tags

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




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




* Apply suggestions from code review




---------

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

* WIP

* Add link to 2025.10 release notes

* Apply suggestions from code review




---------

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

Fixes the property mapping formatting

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

Broken by 646a0d3692

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

View File

@@ -58,7 +58,7 @@ runs:
run: |
export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/compose.yml up -d
cd web && npm i
cd web && npm ci
- name: Generate config
if: ${{ contains(inputs.dependencies, 'python') }}
shell: uv run python {0}

View File

@@ -160,10 +160,17 @@ jobs:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Build web
- name: Install web dependencies
working-directory: web/
run: |
npm ci
- name: Generate API Clients
run: |
make gen-client-ts
make gen-client-go
- name: Build web
working-directory: web/
run: |
npm run build-proxy
- name: Build outpost
run: |
@@ -210,12 +217,12 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- name: Run test suite in final docker images
run: |
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql
docker compose run -u root server test-all
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> lifecycle/container/.env
docker compose -f lifecycle/container/compose.yml pull -q
docker compose -f lifecycle/container/compose.yml up --no-start
docker compose -f lifecycle/container/compose.yml start postgresql
docker compose -f lifecycle/container/compose.yml run -u root server test-all
sentry-release:
needs:
- build-server

View File

@@ -91,6 +91,7 @@ jobs:
# ID from https://api.github.com/users/authentik-automation[bot]
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
git pull
git commit -a -m "release: ${{ inputs.version }}" --allow-empty
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
git push --follow-tags
@@ -174,21 +175,25 @@ jobs:
if: "${{ inputs.release_reason == 'feature' }}"
run: |
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}"
reason="{{ inputs.release_reason }}"
jq \
--arg version "${{ inputs.version }}" \
--arg changelog "See ${changelog_url}" \
--arg changelog_url "${changelog_url}" \
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url' version.json > version.new.json
--arg reason "${reason}" \
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url | .stable.reason = $reason' version.json > version.new.json
mv version.new.json version.json
- name: Bump version
if: "${{ inputs.release_reason != 'feature' }}"
run: |
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}#fixed-in-$(echo -n ${{ inputs.version}} | sed 's/\.//g')"
reason="{{ inputs.release_reason }}"
jq \
--arg version "${{ inputs.version }}" \
--arg changelog "See ${changelog_url}" \
--arg changelog_url "${changelog_url}" \
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url' version.json > version.new.json
--arg reason "${reason}" \
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url | .stable.reason = $reason' version.json > version.new.json
mv version.new.json version.json
- name: Create pull request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7

View File

@@ -148,11 +148,11 @@ bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
ifndef version
$(error Usage: make bump version=20xx.xx.xx )
endif
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' pyproject.toml
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' authentik/__init__.py
$(eval current_version := $(shell cat ${PWD}/internal/constants/VERSION))
$(SED_INPLACE) 's/^version = ".*"/version = "$(version)"/' ${PWD}/pyproject.toml
$(SED_INPLACE) 's/^VERSION = ".*"/VERSION = "$(version)"/' ${PWD}/authentik/__init__.py
$(MAKE) gen-build gen-compose aws-cfn
npm version --no-git-tag-version --allow-same-version $(version)
cd ${PWD}/web && npm version --no-git-tag-version --allow-same-version $(version)
$(SED_INPLACE) "s/\"${current_version}\"/\"$(version)\"/" ${PWD}/package.json ${PWD}/package-lock.json ${PWD}/web/package.json ${PWD}/web/package-lock.json
echo -n $(version) > ${PWD}/internal/constants/VERSION
#########################

View File

@@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
| Version | Supported |
| ---------- | ---------- |
| 2025.10.x | ✅ |
| 2025.12.x | ✅ |
| 2026.2.x | ✅ |
## Reporting a Vulnerability

View File

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

View File

@@ -1,6 +1,8 @@
"""Schema generation tests"""
from pathlib import Path
from tempfile import gettempdir
from uuid import uuid4
from django.core.management import call_command
from django.urls import reverse
@@ -29,15 +31,14 @@ class TestSchemaGeneration(APITestCase):
def test_build_schema(self):
"""Test schema build command"""
blueprint_file = Path("blueprints/schema.json")
api_file = Path("schema.yml")
blueprint_file.unlink()
api_file.unlink()
tmp = Path(gettempdir())
blueprint_file = tmp / f"{str(uuid4())}.json"
api_file = tmp / f"{str(uuid4())}.yml"
with (
CONFIG.patch("debug", True),
CONFIG.patch("tenants.enabled", True),
CONFIG.patch("outposts.disable_embedded_outpost", True),
):
call_command("build_schema")
call_command("build_schema", blueprint_file=blueprint_file, api_file=api_file)
self.assertTrue(blueprint_file.exists())
self.assertTrue(api_file.exists())

View File

@@ -1,5 +1,7 @@
"""authentik core models"""
import re
import traceback
from datetime import datetime, timedelta
from enum import StrEnum
from hashlib import sha256
@@ -15,7 +17,6 @@ from django.contrib.sessions.base_session import AbstractBaseSession
from django.core.validators import validate_slug
from django.db import models
from django.db.models import Q, QuerySet, options
from django.db.models.constants import LOOKUP_SEP
from django.http import HttpRequest
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -43,6 +44,7 @@ from authentik.lib.models import (
DomainlessFormattedURLValidator,
SerializerModel,
)
from authentik.lib.utils.inheritance import get_deepest_child
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import PolicyBindingModel
from authentik.rbac.models import Role
@@ -528,23 +530,35 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
"default: in 30 days). See authentik logs for every will invocation of this "
"deprecation."
)
stacktrace = traceback.format_stack()
# The last line is this function, the next-to-last line is its caller
cause = stacktrace[-2] if len(stacktrace) > 1 else "Unknown, see stacktrace in logs"
if search := re.search(r'"(.*?)"', cause):
cause = f"Property mapping or Expression policy named {search.group(1)}"
LOGGER.warning(
"deprecation used",
message=message_logger,
deprecation=deprecation,
replacement=replacement,
cause=cause,
stacktrace=stacktrace,
)
if not Event.filter_not_expired(
action=EventAction.CONFIGURATION_WARNING, context__deprecation=deprecation
action=EventAction.CONFIGURATION_WARNING,
context__deprecation=deprecation,
context__cause=cause,
).exists():
event = Event.new(
EventAction.CONFIGURATION_WARNING,
deprecation=deprecation,
replacement=replacement,
message=message_event,
cause=cause,
)
event.expires = datetime.now() + timedelta(days=30)
event.save()
return self.groups
def set_password(self, raw_password, signal=True, sender=None, request=None):
@@ -789,25 +803,7 @@ class Application(SerializerModel, PolicyBindingModel):
"""Get casted provider instance. Needs Application queryset with_provider"""
if not self.provider:
return None
candidates = []
base_class = Provider
for subclass in base_class.objects.get_queryset()._get_subclasses_recurse(base_class):
parent = self.provider
for level in subclass.split(LOOKUP_SEP):
try:
parent = getattr(parent, level)
except AttributeError:
break
if parent in candidates:
continue
idx = subclass.count(LOOKUP_SEP)
if type(parent) is not base_class:
idx += 1
candidates.insert(idx, parent)
if not candidates:
return None
return candidates[-1]
return get_deepest_child(self.provider)
def backchannel_provider_for[T: Provider](self, provider_type: type[T], **kwargs) -> T | None:
"""Get Backchannel provider for a specific type"""

View File

@@ -44,19 +44,24 @@
{% endblock %}
</div>
</main>
<footer aria-label="Site footer" class="pf-c-login__footer pf-m-dark">
<ul class="pf-c-list pf-m-inline">
{% for link in footer_links %}
<li>
<a href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
<li>
<span>
{% trans 'Powered by authentik' %}
</span>
</li>
</ul>
<footer
name="site-footer"
aria-label="{% trans 'Site footer' %}"
class="pf-c-login__footer pf-m-dark">
<div name="flow-links" aria-label="{% trans 'Flow links' %}">
<ul class="pf-c-list pf-m-inline" part="list">
{% for link in footer_links %}
<li part="list-item">
<a part="list-item-link" href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
<li part="list-item">
<span>
{% trans 'Powered by authentik' %}
</span>
</li>
</ul>
</div>
</footer>
</div>
</div>

View File

@@ -78,7 +78,7 @@ def generate_key_id_legacy(key_data: str) -> str:
"""Generate Key ID using MD5 (legacy format for backwards compatibility)."""
if not key_data:
return ""
return md5(key_data.encode("utf-8")).hexdigest() # nosec
return md5(key_data.encode("utf-8"), usedforsecurity=False).hexdigest() # nosec
class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):

View File

@@ -1,5 +1,6 @@
from hashlib import sha256
from json import loads
from unittest.mock import PropertyMock, patch
from django.urls import reverse
from jwt import encode
@@ -232,3 +233,43 @@ class TestEndpointStage(FlowTestCase):
plan = plan()
self.assertNotIn(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, plan.context)
self.assertEqual(plan.context[PLAN_CONTEXT_DEVICE], self.device)
def test_endpoint_stage_connector_no_stage_optional(self):
flow = create_test_flow()
stage = EndpointStage.objects.create(connector=self.connector, mode=StageMode.OPTIONAL)
FlowStageBinding.objects.create(stage=stage, target=flow, order=0)
with patch(
"authentik.endpoints.connectors.agent.models.AgentConnector.stage",
PropertyMock(return_value=None),
):
with self.assertFlowFinishes() as plan:
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertStageRedirects(res, reverse("authentik_core:root-redirect"))
plan = plan()
self.assertNotIn(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, plan.context)
self.assertNotIn(PLAN_CONTEXT_DEVICE, plan.context)
def test_endpoint_stage_connector_no_stage_required(self):
flow = create_test_flow()
stage = EndpointStage.objects.create(connector=self.connector, mode=StageMode.REQUIRED)
FlowStageBinding.objects.create(stage=stage, target=flow, order=0)
with patch(
"authentik.endpoints.connectors.agent.models.AgentConnector.stage",
PropertyMock(return_value=None),
):
with self.assertFlowFinishes() as plan:
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertStageResponse(
res,
component="ak-stage-access-denied",
error_message="Invalid stage configuration",
)
plan = plan()
self.assertNotIn(PLAN_CONTEXT_AGENT_ENDPOINT_CHALLENGE, plan.context)
self.assertNotIn(PLAN_CONTEXT_DEVICE, plan.context)

View File

@@ -1,4 +1,4 @@
from authentik.endpoints.models import EndpointStage
from authentik.endpoints.models import EndpointStage, StageMode
from authentik.flows.stage import StageView
PLAN_CONTEXT_ENDPOINT_CONNECTOR = "endpoint_connector"
@@ -6,15 +6,24 @@ PLAN_CONTEXT_ENDPOINT_CONNECTOR = "endpoint_connector"
class EndpointStageView(StageView):
def _get_inner(self):
def _get_inner(self) -> StageView | None:
stage: EndpointStage = self.executor.current_stage
inner_stage: type[StageView] | None = stage.connector.stage
if not inner_stage:
return self.executor.stage_ok()
return None
return inner_stage(self.executor, request=self.request)
def dispatch(self, request, *args, **kwargs):
return self._get_inner().dispatch(request, *args, **kwargs)
inner = self._get_inner()
if inner is None:
stage: EndpointStage = self.executor.current_stage
if stage.mode == StageMode.OPTIONAL:
return self.executor.stage_ok()
else:
return self.executor.stage_invalid("Invalid stage configuration")
return inner.dispatch(request, *args, **kwargs)
def cleanup(self):
return self._get_inner().cleanup()
inner = self._get_inner()
if inner is not None:
return inner.cleanup()

View File

@@ -15,6 +15,7 @@ from django.core.cache import cache
from django.db.models.query import QuerySet
from django.utils.timezone import now
from jwt import PyJWTError, decode, get_unverified_header
from jwt.algorithms import ECAlgorithm
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
ChoiceField,
@@ -109,13 +110,20 @@ class LicenseKey:
intermediate.verify_directly_issued_by(get_licensing_key())
except InvalidSignature, TypeError, ValueError, Error:
raise ValidationError("Unable to verify license") from None
_validate_curve_original = ECAlgorithm._validate_curve
try:
# authentik's license are generated with `algorithm="ES512"` and signed with
# a key of curve `secp384r1`. Starting with version 2.11.0, pyjwt enforces the spec, see
# https://github.com/jpadilla/pyjwt/commit/5b8622773358e56d3d3c0a9acf404809ff34433a
# authentik will change its license generation to `algorithm="ES384"` in 2026.
# TODO: remove this when the last incompatible license runs out.
ECAlgorithm._validate_curve = lambda *_: True
body = from_dict(
LicenseKey,
decode(
jwt,
our_cert.public_key(),
algorithms=["ES512"],
algorithms=["ES384", "ES512"],
audience=get_license_aud(),
options={"verify_exp": check_expiry, "verify_signature": check_expiry},
),
@@ -125,6 +133,8 @@ class LicenseKey:
if unverified["aud"] != get_license_aud():
raise ValidationError("Invalid Install ID in license") from None
raise ValidationError("Unable to verify license") from None
finally:
ECAlgorithm._validate_curve = _validate_curve_original
return body
@staticmethod

View File

@@ -1,11 +1,11 @@
from datetime import date
from datetime import datetime
from django.db.models import BooleanField as ModelBooleanField
from django.db.models import Case, Q, Value, When
from django_filters.rest_framework import BooleanFilter, FilterSet
from drf_spectacular.utils import extend_schema, extend_schema_field
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.fields import DateField, IntegerField, SerializerMethodField
from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.mixins import CreateModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
@@ -21,6 +21,7 @@ from authentik.enterprise.lifecycle.utils import (
ReviewerUserSerializer,
admin_link_for_model,
parse_content_type,
start_of_day,
)
from authentik.lib.utils.time import timedelta_from_string
@@ -67,13 +68,13 @@ class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
def get_object_admin_url(self, iteration: LifecycleIteration) -> str:
return admin_link_for_model(iteration.object)
@extend_schema_field(DateField())
def get_grace_period_end(self, iteration: LifecycleIteration) -> date:
return iteration.opened_on + timedelta_from_string(iteration.rule.grace_period)
def get_grace_period_end(self, iteration: LifecycleIteration) -> datetime:
return start_of_day(
iteration.opened_on + timedelta_from_string(iteration.rule.grace_period)
)
@extend_schema_field(DateField())
def get_next_review_date(self, iteration: LifecycleIteration):
return iteration.opened_on + timedelta_from_string(iteration.rule.interval)
def get_next_review_date(self, iteration: LifecycleIteration) -> datetime:
return start_of_day(iteration.opened_on + timedelta_from_string(iteration.rule.interval))
def get_user_can_review(self, iteration: LifecycleIteration) -> bool:
return iteration.user_can_review(self.context["request"].user)
@@ -102,7 +103,7 @@ class IterationViewSet(EnterpriseRequiredMixin, CreateModelMixin, GenericViewSet
default=Value(False),
output_field=ModelBooleanField(),
)
)
).distinct()
@action(
detail=False,

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.11 on 2026-02-13 09:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_lifecycle", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="lifecycleiteration",
name="opened_on",
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@@ -1,3 +1,4 @@
from datetime import timedelta
from uuid import uuid4
from django.contrib.contenttypes.fields import GenericForeignKey
@@ -13,7 +14,7 @@ from rest_framework.serializers import BaseSerializer
from authentik.blueprints.models import ManagedModel
from authentik.core.models import Group, User
from authentik.enterprise.lifecycle.utils import link_for_model
from authentik.enterprise.lifecycle.utils import link_for_model, start_of_day
from authentik.events.models import Event, EventAction, NotificationSeverity, NotificationTransport
from authentik.lib.models import SerializerModel
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
@@ -98,7 +99,9 @@ class LifecycleRule(SerializerModel):
def _get_newly_overdue_iterations(self) -> QuerySet[LifecycleIteration]:
return self.lifecycleiteration_set.filter(
opened_on__lte=timezone.now() - timedelta_from_string(self.grace_period),
opened_on__lt=start_of_day(
timezone.now() + timedelta(days=1) - timedelta_from_string(self.grace_period)
),
state=ReviewState.PENDING,
)
@@ -106,7 +109,9 @@ class LifecycleRule(SerializerModel):
recent_iteration_ids = LifecycleIteration.objects.filter(
content_type=self.content_type,
object_id__isnull=False,
opened_on__gte=timezone.now() - timedelta_from_string(self.interval),
opened_on__gte=start_of_day(
timezone.now() + timedelta(days=1) - timedelta_from_string(self.interval)
),
).values_list(Cast("object_id", output_field=self._get_pk_field()), flat=True)
return self.get_objects().exclude(pk__in=recent_iteration_ids)
@@ -186,7 +191,7 @@ class LifecycleIteration(SerializerModel, ManagedModel):
rule = models.ForeignKey(LifecycleRule, null=True, on_delete=models.SET_NULL)
state = models.CharField(max_length=10, choices=ReviewState, default=ReviewState.PENDING)
opened_on = models.DateField(auto_now_add=True)
opened_on = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [models.Index(fields=["content_type", "opened_on"])]

View File

@@ -1,3 +1,4 @@
import datetime as dt
from datetime import timedelta
from unittest.mock import patch
@@ -319,7 +320,7 @@ class TestLifecycleModels(TestCase):
content_type=content_type, object_id=str(app_one.pk), rule=rule_overdue
)
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=(timezone.now().date() - timedelta(days=20))
opened_on=(timezone.now() - timedelta(days=20))
)
# Apply again to trigger overdue logic
@@ -383,7 +384,7 @@ class TestLifecycleModels(TestCase):
content_type=content_type, object_id=str(app_overdue.pk), rule=rule_overdue
)
LifecycleIteration.objects.filter(pk=overdue_iteration.pk).update(
opened_on=(timezone.now().date() - timedelta(days=20))
opened_on=(timezone.now() - timedelta(days=20))
)
# Apply overdue rule to mark iteration as overdue
@@ -667,3 +668,178 @@ class TestLifecycleModels(TestCase):
reviewers = list(rule.get_reviewers())
self.assertIn(explicit_reviewer, reviewers)
self.assertIn(group_member, reviewers)
class TestLifecycleDateBoundaries(TestCase):
"""Verify that start_of_day normalization ensures correct overdue/due
detection regardless of exact task execution time within a day.
The daily task may run at any point during the day. The start_of_day
normalization in _get_newly_overdue_iterations and _get_newly_due_objects
ensures that the boundary is always at midnight, so millisecond variations
in task execution time do not affect results."""
def _create_rule_and_iteration(self, grace_period="days=1", interval="days=365"):
app = Application.objects.create(name=generate_id(), slug=generate_id())
content_type = ContentType.objects.get_for_model(Application)
rule = LifecycleRule.objects.create(
name=generate_id(),
content_type=content_type,
object_id=str(app.pk),
interval=interval,
grace_period=grace_period,
)
iteration = LifecycleIteration.objects.get(
content_type=content_type, object_id=str(app.pk), rule=rule
)
return app, rule, iteration
def test_overdue_iteration_opened_yesterday(self):
"""grace_period=1 day: iteration opened yesterday at any time is overdue today."""
_, rule, iteration = self._create_rule_and_iteration(grace_period="days=1")
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
for opened_on in [
dt.datetime(2025, 6, 14, 0, 0, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 14, 12, 0, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 14, 23, 59, 59, 999999, tzinfo=dt.UTC),
]:
with self.subTest(opened_on=opened_on):
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=opened_on, state=ReviewState.PENDING
)
with patch("django.utils.timezone.now", return_value=fixed_now):
self.assertIn(iteration, list(rule._get_newly_overdue_iterations()))
def test_not_overdue_iteration_opened_today(self):
"""grace_period=1 day: iteration opened today at any time is NOT overdue."""
_, rule, iteration = self._create_rule_and_iteration(grace_period="days=1")
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
for opened_on in [
dt.datetime(2025, 6, 15, 0, 0, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 23, 59, 59, 999999, tzinfo=dt.UTC),
]:
with self.subTest(opened_on=opened_on):
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=opened_on, state=ReviewState.PENDING
)
with patch("django.utils.timezone.now", return_value=fixed_now):
self.assertNotIn(iteration, list(rule._get_newly_overdue_iterations()))
def test_overdue_independent_of_task_execution_time(self):
"""Overdue detection gives the same result whether the task runs at 00:00:01 or 23:59:59."""
_, rule, iteration = self._create_rule_and_iteration(grace_period="days=1")
opened_on = dt.datetime(2025, 6, 14, 18, 0, 0, tzinfo=dt.UTC)
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=opened_on, state=ReviewState.PENDING
)
for task_time in [
dt.datetime(2025, 6, 15, 0, 0, 1, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 12, 0, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 23, 59, 59, tzinfo=dt.UTC),
]:
with self.subTest(task_time=task_time):
with patch("django.utils.timezone.now", return_value=task_time):
self.assertIn(iteration, list(rule._get_newly_overdue_iterations()))
def test_overdue_boundary_multi_day_grace_period(self):
"""grace_period=30 days: overdue after 30 full days, not after 29."""
_, rule, iteration = self._create_rule_and_iteration(grace_period="days=30")
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
# Opened 30 days ago (May 16), should go overdue
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=dt.datetime(2025, 5, 16, 12, 0, 0, tzinfo=dt.UTC),
state=ReviewState.PENDING,
)
with patch("django.utils.timezone.now", return_value=fixed_now):
self.assertIn(iteration, list(rule._get_newly_overdue_iterations()))
# Opened 29 days ago (May 17), should NOT go overdue
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=dt.datetime(2025, 5, 17, 12, 0, 0, tzinfo=dt.UTC),
state=ReviewState.PENDING,
)
with patch("django.utils.timezone.now", return_value=fixed_now):
self.assertNotIn(iteration, list(rule._get_newly_overdue_iterations()))
def test_due_object_iteration_opened_yesterday(self):
"""interval=1 day: object with iteration opened yesterday is due for a new review."""
app, rule, iteration = self._create_rule_and_iteration(interval="days=1")
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
for opened_on in [
dt.datetime(2025, 6, 14, 0, 0, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 14, 12, 0, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 14, 23, 59, 59, 999999, tzinfo=dt.UTC),
]:
with self.subTest(opened_on=opened_on):
LifecycleIteration.objects.filter(pk=iteration.pk).update(opened_on=opened_on)
with patch("django.utils.timezone.now", return_value=fixed_now):
self.assertIn(app, list(rule._get_newly_due_objects()))
def test_not_due_object_iteration_opened_today(self):
"""interval=1 day: object with iteration opened today is NOT due."""
app, rule, iteration = self._create_rule_and_iteration(interval="days=1")
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
for opened_on in [
dt.datetime(2025, 6, 15, 0, 0, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 23, 59, 59, 999999, tzinfo=dt.UTC),
]:
with self.subTest(opened_on=opened_on):
LifecycleIteration.objects.filter(pk=iteration.pk).update(opened_on=opened_on)
with patch("django.utils.timezone.now", return_value=fixed_now):
self.assertNotIn(app, list(rule._get_newly_due_objects()))
def test_due_independent_of_task_execution_time(self):
"""Due detection gives the same result whether the task runs at 00:00:01 or 23:59:59."""
app, rule, iteration = self._create_rule_and_iteration(interval="days=1")
opened_on = dt.datetime(2025, 6, 14, 18, 0, 0, tzinfo=dt.UTC)
LifecycleIteration.objects.filter(pk=iteration.pk).update(opened_on=opened_on)
for task_time in [
dt.datetime(2025, 6, 15, 0, 0, 1, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 12, 0, 0, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 23, 59, 59, tzinfo=dt.UTC),
]:
with self.subTest(task_time=task_time):
with patch("django.utils.timezone.now", return_value=task_time):
self.assertIn(app, list(rule._get_newly_due_objects()))
def test_due_boundary_multi_day_interval(self):
"""interval=30 days: due after 30 full days, not after 29."""
app, rule, iteration = self._create_rule_and_iteration(interval="days=30")
fixed_now = dt.datetime(2025, 6, 15, 14, 30, 0, tzinfo=dt.UTC)
# Previous review opened 30 days ago (May 16), review is due for the object
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=dt.datetime(2025, 5, 16, 12, 0, 0, tzinfo=dt.UTC)
)
with patch("django.utils.timezone.now", return_value=fixed_now):
self.assertIn(app, list(rule._get_newly_due_objects()))
# Previous review opened 29 days ago (May 17), new review is NOT due
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=dt.datetime(2025, 5, 17, 12, 0, 0, tzinfo=dt.UTC)
)
with patch("django.utils.timezone.now", return_value=fixed_now):
self.assertNotIn(app, list(rule._get_newly_due_objects()))
def test_apply_overdue_at_boundary(self):
"""apply() marks iteration overdue when grace period just expired,
regardless of what time the daily task runs."""
_, rule, iteration = self._create_rule_and_iteration(
grace_period="days=1", interval="days=365"
)
opened_on = dt.datetime(2025, 6, 14, 20, 0, 0, tzinfo=dt.UTC)
for task_time in [
dt.datetime(2025, 6, 15, 0, 0, 1, tzinfo=dt.UTC),
dt.datetime(2025, 6, 15, 23, 59, 59, tzinfo=dt.UTC),
]:
with self.subTest(task_time=task_time):
LifecycleIteration.objects.filter(pk=iteration.pk).update(
opened_on=opened_on, state=ReviewState.PENDING
)
with patch("django.utils.timezone.now", return_value=task_time):
rule.apply()
iteration.refresh_from_db()
self.assertEqual(iteration.state, ReviewState.OVERDUE)

View File

@@ -1,3 +1,4 @@
from datetime import datetime
from urllib import parse
from django.contrib.contenttypes.models import ContentType
@@ -39,6 +40,10 @@ def link_for_model(model: Model) -> str:
return f"{reverse("authentik_core:if-admin")}#{admin_link_for_model(model)}"
def start_of_day(dt: datetime) -> datetime:
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
class ContentTypeField(ChoiceField):
def __init__(self, **kwargs):
super().__init__(choices=model_choices(), **kwargs)

View File

@@ -78,7 +78,8 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
def create(self, user: User):
"""Create user from scratch and create a connection object"""
microsoft_user = self.to_schema(user, None)
self.check_email_valid(microsoft_user.user_principal_name)
if microsoft_user.user_principal_name:
self.check_email_valid(microsoft_user.user_principal_name)
with transaction.atomic():
try:
response = self._request(self.client.users.post(microsoft_user))
@@ -118,7 +119,8 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
def update(self, user: User, connection: MicrosoftEntraProviderUser):
"""Update existing user"""
microsoft_user = self.to_schema(user, connection)
self.check_email_valid(microsoft_user.user_principal_name)
if microsoft_user.user_principal_name:
self.check_email_valid(microsoft_user.user_principal_name)
response = self._request(
self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user)
)

View File

@@ -5,6 +5,7 @@ from django.urls import reverse
from rest_framework.fields import CharField, SerializerMethodField, URLField
from authentik.core.api.providers import ProviderSerializer
from authentik.core.models import Provider
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.ws_federation.models import WSFederationProvider
from authentik.enterprise.providers.ws_federation.processors.metadata import MetadataProcessor
@@ -18,6 +19,29 @@ class WSFederationProviderSerializer(EnterpriseRequiredMixin, SAMLProviderSerial
wtrealm = CharField(source="audience")
url_wsfed = SerializerMethodField()
def get_url_download_metadata(self, instance: WSFederationProvider) -> str:
"""Get metadata download URL"""
if "request" not in self._context:
return ""
request: HttpRequest = self._context["request"]._request
try:
return request.build_absolute_uri(
reverse(
"authentik_providers_ws_federation:metadata-download",
kwargs={"application_slug": instance.application.slug},
)
)
except Provider.application.RelatedObjectDoesNotExist:
return request.build_absolute_uri(
reverse(
"authentik_api:wsfederationprovider-metadata",
kwargs={
"pk": instance.pk,
},
)
+ "?download"
)
def get_url_wsfed(self, instance: WSFederationProvider) -> str:
"""Get WS-Fed url"""
if "request" not in self._context:

View File

@@ -81,6 +81,8 @@ class SignInProcessor:
self.sign_in_request = sign_in_request
self.saml_processor = AssertionProcessor(self.provider, self.request, AuthNRequest())
self.saml_processor.provider.audience = self.sign_in_request.wtrealm
if self.provider.signing_kp:
self.saml_processor.provider.sign_assertion = True
def create_response_token(self):
root = Element(f"{{{NS_WS_FED_TRUST}}}RequestSecurityTokenResponse", nsmap=NS_MAP)
@@ -148,7 +150,8 @@ class SignInProcessor:
def response(self) -> dict[str, str]:
root = self.create_response_token()
assertion = root.xpath("//saml:Assertion", namespaces=NS_MAP)[0]
self.saml_processor._sign(assertion)
if self.provider.signing_kp:
self.saml_processor._sign(assertion)
str_token = etree.tostring(root).decode("utf-8") # nosec
return delete_none_values(
{

View File

@@ -3,7 +3,7 @@
from django.urls import path
from authentik.enterprise.providers.ws_federation.api.providers import WSFederationProviderViewSet
from authentik.enterprise.providers.ws_federation.views import WSFedEntryView
from authentik.enterprise.providers.ws_federation.views import MetadataDownload, WSFedEntryView
urlpatterns = [
path(
@@ -11,6 +11,12 @@ urlpatterns = [
WSFedEntryView.as_view(),
name="wsfed",
),
# Metadata
path(
"<slug:application_slug>/metadata/",
MetadataDownload.as_view(),
name="metadata-download",
),
]
api_urlpatterns = [

View File

@@ -1,6 +1,8 @@
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views import View
from structlog.stdlib import get_logger
from authentik.core.models import Application, AuthenticatedSession
@@ -160,3 +162,24 @@ class WSFedFlowFinalView(ChallengeStageView):
"attrs": response,
},
)
class MetadataDownload(View):
"""Redirect to metadata download"""
def dispatch(self, request: HttpRequest, application_slug: str) -> HttpResponse:
app = Application.objects.filter(slug=application_slug).with_provider().first()
if not app:
raise Http404
provider = app.get_provider()
if not provider:
raise Http404
return redirect(
reverse(
"authentik_api:wsfederationprovider-metadata",
kwargs={
"pk": provider.pk,
},
)
+ "?download"
)

View File

@@ -9,7 +9,15 @@
{{ block.super }}
<link rel="prefetch" href="{{ flow_background_url }}" />
{% if flow.compatibility_mode and not inspector %}
<script data-id="shady-dom">ShadyDOM = { force: true };</script>
{% comment %}
@see {@link web/types/webcomponents.d.ts} for type definitions.
{% endcomment %}
<script data-id="shady-dom">
"use strict";
window.ShadyDOM = window.ShadyDOM || {}
window.ShadyDOM.force = true
</script>
{% endif %}
{% include "base/header_js.html" %}
<script data-id="flow-config">
@@ -45,16 +53,11 @@
slug="{{ flow.slug }}"
class="pf-c-login"
data-layout="{{ flow.layout|default:'stacked' }}"
loading
>
{% include "base/placeholder.html" %}
<ak-brand-links
slot="footer"
exportparts="list:brand-links-list, list-item:brand-links-list-item"
role="contentinfo"
aria-label="{% trans 'Site footer' %}"
class="pf-c-login__footer {% if flow.layout == 'stacked' %}pf-m-dark{% endif %}"
></ak-brand-links>
<ak-brand-links name="flow-links" slot="footer"></ak-brand-links>
</ak-flow-executor>
</div>
</div>

View File

@@ -141,6 +141,10 @@ web:
# workers: 2
threads: 4
path: /
timeout_http_read_header: 5s
timeout_http_read: 30s
timeout_http_write: 60s
timeout_http_idle: 120s
worker:
processes: 1

View File

@@ -42,7 +42,7 @@ ARG_SANITIZE = re.compile(r"[:.-]")
def sanitize_arg(arg_name: str) -> str:
return re.sub(ARG_SANITIZE, "_", arg_name)
return re.sub(ARG_SANITIZE, "_", slugify(arg_name))
class BaseEvaluator:
@@ -311,7 +311,9 @@ class BaseEvaluator:
def wrap_expression(self, expression: str) -> str:
"""Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())
handler_signature = ",".join(
[x for x in [sanitize_arg(x) for x in self._context.keys()] if x]
)
full_expression = ""
full_expression += f"def handler({handler_signature}):\n"
full_expression += indent(expression, " ")

View File

@@ -1,5 +1,6 @@
"""Test Evaluator base functions"""
from pathlib import Path
from unittest.mock import patch
from django.test import RequestFactory, TestCase
@@ -353,3 +354,18 @@ class TestEvaluator(TestCase):
self.assertEqual(message.to, ["to@example.com"])
self.assertEqual(message.cc, ["cc1@example.com", "cc2@example.com"])
self.assertEqual(message.bcc, ["bcc1@example.com", "bcc2@example.com"])
def test_expr_arg_escape(self):
"""Test escaping of arguments"""
eval = BaseEvaluator()
eval._context = {
'z=getattr(getattr(__import__("os"), "popen")("id > /tmp/test"), "read")()': "bar",
"@@": "baz",
"{{": "baz",
"aa@@": "baz",
}
res = eval.evaluate("return locals()")
self.assertEqual(
res, {"zgetattrgetattr__import__os_popenid_tmptest_read": "bar", "aa": "baz"}
)
self.assertFalse(Path("/tmp/test").exists())

View File

@@ -0,0 +1,119 @@
"""Tests for inheritance helpers."""
from contextlib import contextmanager
from django.db import connection, models
from django.test import TransactionTestCase
from django.test.utils import isolate_apps
from authentik.lib.utils.inheritance import get_deepest_child
@contextmanager
def temporary_inheritance_models():
"""Create a temporary multi-table inheritance graph for testing."""
with isolate_apps("authentik.lib.tests"):
class GrandParent(models.Model):
class Meta:
app_label = "tests"
def __str__(self) -> str:
return f"GrandParent({self.pk})"
class Parent(GrandParent):
class Meta:
app_label = "tests"
def __str__(self) -> str:
return f"Parent({self.pk})"
class Child(Parent):
class Meta:
app_label = "tests"
def __str__(self) -> str:
return f"Child({self.pk})"
class GrandChild(Child):
class Meta:
app_label = "tests"
def __str__(self) -> str:
return f"GrandChild({self.pk})"
with connection.schema_editor() as schema_editor:
schema_editor.create_model(GrandParent)
schema_editor.create_model(Parent)
schema_editor.create_model(Child)
schema_editor.create_model(GrandChild)
try:
yield GrandParent, Parent, Child, GrandChild
finally:
with connection.schema_editor() as schema_editor:
schema_editor.delete_model(GrandChild)
schema_editor.delete_model(Child)
schema_editor.delete_model(Parent)
schema_editor.delete_model(GrandParent)
class TestInheritanceUtils(TransactionTestCase):
"""Tests for helper functions in authentik.lib.utils.inheritance."""
def test_get_deepest_child_grandparent_to_parent(self):
"""GrandParent -> Parent."""
with temporary_inheritance_models() as (GrandParent, Parent, _Child, _GrandChild):
parent = Parent.objects.create()
grandparent = GrandParent.objects.get(pk=parent.pk)
resolved = get_deepest_child(grandparent)
self.assertIsInstance(resolved, Parent)
self.assertEqual(resolved.pk, parent.pk)
def test_get_deepest_child_grandparent_to_child(self):
"""GrandParent -> Child."""
with temporary_inheritance_models() as (GrandParent, _Parent, Child, _GrandChild):
child = Child.objects.create()
grandparent = GrandParent.objects.get(pk=child.pk)
resolved = get_deepest_child(grandparent)
self.assertIsInstance(resolved, Child)
self.assertEqual(resolved.pk, child.pk)
def test_get_deepest_child_grandparent_to_grandchild(self):
"""GrandParent -> GrandChild."""
with temporary_inheritance_models() as (GrandParent, _Parent, _Child, GrandChild):
grandchild = GrandChild.objects.create()
grandparent = GrandParent.objects.get(pk=grandchild.pk)
resolved = get_deepest_child(grandparent)
self.assertIsInstance(resolved, GrandChild)
self.assertEqual(resolved.pk, grandchild.pk)
def test_get_deepest_child_parent_to_child(self):
"""Parent -> Child (start from non-root)."""
with temporary_inheritance_models() as (_GrandParent, Parent, Child, _GrandChild):
child = Child.objects.create()
parent = Parent.objects.get(pk=child.pk)
resolved = get_deepest_child(parent)
self.assertIsInstance(resolved, Child)
self.assertEqual(resolved.pk, child.pk)
def test_get_deepest_child_no_queries_with_preloaded_relations(self):
"""No extra queries when the inheritance chain is fully select_related."""
with temporary_inheritance_models() as (GrandParent, _Parent, _Child, GrandChild):
grandchild = GrandChild.objects.create()
grandparent = GrandParent.objects.select_related("parent__child__grandchild").get(
pk=grandchild.pk
)
with self.assertNumQueries(0):
resolved = get_deepest_child(grandparent)
self.assertIsInstance(resolved, GrandChild)

View File

@@ -0,0 +1,41 @@
from django.db.models import Model, OneToOneField, OneToOneRel
def get_deepest_child(parent: Model) -> Model:
"""
In multiple table inheritance, given any ancestor object, get the deepest child object.
See https://docs.djangoproject.com/en/dev/topics/db/models/#multi-table-inheritance
This function does not query the database if `select_related` has been performed on all
subclasses of `parent`'s model.
"""
# Almost verbatim copy from django-model-utils, see
# https://github.com/jazzband/django-model-utils/blob/5.0.0/model_utils/managers.py#L132
one_to_one_rels = [
field for field in parent._meta.get_fields() if isinstance(field, OneToOneRel)
]
submodel_fields = [
rel
for rel in one_to_one_rels
if isinstance(rel.field, OneToOneField)
and issubclass(rel.field.model, parent._meta.model)
and parent._meta.model is not rel.field.model
and rel.parent_link
]
submodel_accessors = [submodel_field.get_accessor_name() for submodel_field in submodel_fields]
# End Copy
child = None
for submodel in submodel_accessors:
try:
child = getattr(parent, submodel)
break
except AttributeError:
continue
if not child:
return parent
return get_deepest_child(child)

View File

@@ -132,9 +132,14 @@ class PolicyEngine:
# If we didn't find any static bindings, do nothing
return
self.logger.debug("P_ENG: Found static bindings", **matched_bindings)
if matched_bindings.get("passing", 0) > 0:
# Any passing static binding -> passing
passing = True
if self.mode == PolicyEngineMode.MODE_ANY:
if matched_bindings.get("passing", 0) > 0:
# Any passing static binding -> passing
passing = True
elif self.mode == PolicyEngineMode.MODE_ALL:
if matched_bindings.get("passing", 0) == matched_bindings["total"]:
# All static bindings are passing -> passing
passing = True
elif matched_bindings["total"] > 0 and matched_bindings.get("passing", 0) < 1:
# No matching static bindings but at least one is configured -> not passing
passing = False
@@ -185,6 +190,16 @@ class PolicyEngine:
# Only call .recv() if no result is saved, otherwise we just deadlock here
if not proc_info.result:
proc_info.result = proc_info.connection.recv()
if proc_info.result and proc_info.result._exec_time:
HIST_POLICIES_EXECUTION_TIME.labels(
binding_order=proc_info.binding.order,
binding_target_type=proc_info.binding.target_type,
binding_target_name=proc_info.binding.target_name,
object_type=(
class_to_path(self.request.obj.__class__) if self.request.obj else ""
),
mode="execute_process",
).observe(proc_info.result._exec_time)
return self
@property

View File

@@ -2,6 +2,7 @@
from multiprocessing import get_context
from multiprocessing.connection import Connection
from time import perf_counter
from django.core.cache import cache
from sentry_sdk import start_span
@@ -11,8 +12,6 @@ from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
from authentik.lib.utils.errors import exception_to_dict
from authentik.lib.utils.reflection import class_to_path
from authentik.policies.apps import HIST_POLICIES_EXECUTION_TIME
from authentik.policies.exceptions import PolicyException
from authentik.policies.models import PolicyBinding
from authentik.policies.types import CACHE_PREFIX, PolicyRequest, PolicyResult
@@ -123,18 +122,9 @@ class PolicyProcess(PROCESS_CLASS):
def profiling_wrapper(self):
"""Run with profiling enabled"""
with (
start_span(
op="authentik.policy.process.execute",
) as span,
HIST_POLICIES_EXECUTION_TIME.labels(
binding_order=self.binding.order,
binding_target_type=self.binding.target_type,
binding_target_name=self.binding.target_name,
object_type=class_to_path(self.request.obj.__class__) if self.request.obj else "",
mode="execute_process",
).time(),
):
with start_span(
op="authentik.policy.process.execute",
) as span:
span: Span
span.set_data("policy", self.binding.policy)
span.set_data("request", self.request)
@@ -142,8 +132,14 @@ class PolicyProcess(PROCESS_CLASS):
def run(self): # pragma: no cover
"""Task wrapper to run policy checking"""
result = None
try:
self.connection.send(self.profiling_wrapper())
start = perf_counter()
result = self.profiling_wrapper()
end = perf_counter()
result._exec_time = max((end - start), 0)
except Exception as exc: # noqa
LOGGER.warning("Policy failed to run", exc=exc)
self.connection.send(PolicyResult(False, str(exc)))
result = PolicyResult(False, str(exc))
finally:
self.connection.send(result)

View File

@@ -33,6 +33,9 @@ class TestPolicyEngine(TestCase):
self.policy_raises = ExpressionPolicy.objects.create(
name=generate_id(), expression="{{ 0/0 }}"
)
self.group_member = Group.objects.create(name=generate_id())
self.user.groups.add(self.group_member)
self.group_non_member = Group.objects.create(name=generate_id())
def test_engine_empty(self):
"""Ensure empty policy list passes"""
@@ -51,7 +54,7 @@ class TestPolicyEngine(TestCase):
self.assertEqual(result.passing, True)
self.assertEqual(result.messages, ("dummy",))
def test_engine_mode_all(self):
def test_engine_mode_all_dyn(self):
"""Ensure all policies passes with AND mode (false and true -> false)"""
pbm = PolicyBindingModel.objects.create(policy_engine_mode=PolicyEngineMode.MODE_ALL)
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
@@ -67,7 +70,7 @@ class TestPolicyEngine(TestCase):
),
)
def test_engine_mode_any(self):
def test_engine_mode_any_dyn(self):
"""Ensure all policies passes with OR mode (false and true -> true)"""
pbm = PolicyBindingModel.objects.create(policy_engine_mode=PolicyEngineMode.MODE_ANY)
PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
@@ -83,6 +86,26 @@ class TestPolicyEngine(TestCase):
),
)
def test_engine_mode_all_static(self):
"""Ensure all policies passes with OR mode (false and true -> true)"""
pbm = PolicyBindingModel.objects.create(policy_engine_mode=PolicyEngineMode.MODE_ALL)
PolicyBinding.objects.create(target=pbm, group=self.group_member, order=0)
PolicyBinding.objects.create(target=pbm, group=self.group_non_member, order=1)
engine = PolicyEngine(pbm, self.user)
result = engine.build().result
self.assertEqual(result.passing, False)
self.assertEqual(result.messages, ())
def test_engine_mode_any_static(self):
"""Ensure all policies passes with OR mode (false and true -> true)"""
pbm = PolicyBindingModel.objects.create(policy_engine_mode=PolicyEngineMode.MODE_ANY)
PolicyBinding.objects.create(target=pbm, group=self.group_member, order=0)
PolicyBinding.objects.create(target=pbm, group=self.group_non_member, order=1)
engine = PolicyEngine(pbm, self.user)
result = engine.build().result
self.assertEqual(result.passing, True)
self.assertEqual(result.messages, ())
def test_engine_negate(self):
"""Test negate flag"""
pbm = PolicyBindingModel.objects.create()

View File

@@ -77,6 +77,8 @@ class PolicyResult:
log_messages: list[LogEvent] | None
_exec_time: int | None
def __init__(self, passing: bool, *messages: str):
self.passing = passing
self.messages = messages
@@ -84,6 +86,7 @@ class PolicyResult:
self.source_binding = None
self.source_results = []
self.log_messages = []
self._exec_time = None
def __repr__(self):
return self.__str__()

View File

@@ -68,6 +68,8 @@ class IDToken:
at_hash: str | None = None
# Session ID, https://openid.net/specs/openid-connect-frontchannel-1_0.html#ClaimsContents
sid: str | None = None
# JWT ID, https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.7
jti: str | None = None
claims: dict[str, Any] = field(default_factory=dict)
@@ -81,6 +83,7 @@ class IDToken:
(token.expires if token.expires is not None else default_token_duration()).timestamp()
)
id_token.iss = provider.get_issuer(request)
id_token.jti = generate_id()
id_token.aud = provider.client_id
id_token.claims = {}

View File

@@ -5,6 +5,7 @@ from urllib.parse import parse_qs, urlparse
from django.test import RequestFactory
from django.urls import reverse
from django.utils import translation
from django.utils.timezone import now
from authentik.blueprints.tests import apply_blueprint
@@ -690,18 +691,21 @@ class TestAuthorize(OAuthTestCase):
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
self.client.logout()
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"client_id": "test",
"state": state,
"redirect_uri": "foo://localhost",
"ui_locales": "invalid fr",
},
)
parsed = parse_qs(urlparse(response.url).query)
self.assertEqual(parsed["locale"], ["fr"])
try:
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"client_id": "test",
"state": state,
"redirect_uri": "foo://localhost",
"ui_locales": "invalid fr",
},
)
parsed = parse_qs(urlparse(response.url).query)
self.assertEqual(parsed["locale"], ["fr"])
finally:
translation.deactivate()
@apply_blueprint("default/flow-default-authentication-flow.yaml")
def test_ui_locales_invalid(self):

View File

@@ -1,5 +1,6 @@
"""Device backchannel tests"""
from base64 import b64encode
from json import loads
from django.urls import reverse
@@ -26,7 +27,7 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase):
provider=self.provider,
)
def test_backchannel_invalid(self):
def test_backchannel_invalid_client_id_via_post_body(self):
"""Test backchannel"""
res = self.client.post(
reverse("authentik_providers_oauth2:device"),
@@ -50,7 +51,7 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase):
)
self.assertEqual(res.status_code, 400)
def test_backchannel(self):
def test_backchannel_client_id_via_post_body(self):
"""Test backchannel"""
res = self.client.post(
reverse("authentik_providers_oauth2:device"),
@@ -61,3 +62,37 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase):
self.assertEqual(res.status_code, 200)
body = loads(res.content.decode())
self.assertEqual(body["expires_in"], 60)
def test_backchannel_invalid_client_id_via_auth_header(self):
"""Test backchannel"""
creds = b64encode(b"foo:").decode()
res = self.client.post(
reverse("authentik_providers_oauth2:device"),
HTTP_AUTHORIZATION=f"Basic {creds}",
)
self.assertEqual(res.status_code, 400)
res = self.client.post(
reverse("authentik_providers_oauth2:device"),
)
self.assertEqual(res.status_code, 400)
# test without application
self.application.provider = None
self.application.save()
res = self.client.post(
reverse("authentik_providers_oauth2:device"),
data={
"client_id": "test",
},
)
self.assertEqual(res.status_code, 400)
def test_backchannel_client_id_via_auth_header(self):
"""Test backchannel"""
creds = b64encode(f"{self.provider.client_id}:".encode()).decode()
res = self.client.post(
reverse("authentik_providers_oauth2:device"),
HTTP_AUTHORIZATION=f"Basic {creds}",
)
self.assertEqual(res.status_code, 200)
body = loads(res.content.decode())
self.assertEqual(body["expires_in"], 60)

View File

@@ -16,7 +16,7 @@ from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.errors import DeviceCodeError
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.utils import TokenResponse
from authentik.providers.oauth2.utils import TokenResponse, extract_client_auth
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
LOGGER = get_logger()
@@ -32,7 +32,7 @@ class DeviceView(View):
def parse_request(self):
"""Parse incoming request"""
client_id = self.request.POST.get("client_id", None)
client_id, _ = extract_client_auth(self.request)
if not client_id:
raise DeviceCodeError("invalid_client")
provider = OAuth2Provider.objects.filter(client_id=client_id).first()

View File

@@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _
from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
from ldap3.core.exceptions import LDAPException, LDAPInsufficientAccessRightsResult, LDAPSchemaError
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.core.models import (
Group,
@@ -31,6 +32,7 @@ from authentik.tasks.schedules.common import ScheduleSpec
LDAP_TIMEOUT = 15
LDAP_UNIQUENESS = "ldap_uniq"
LDAP_DISTINGUISHED_NAME = "distinguishedName"
LOGGER = get_logger()
def flatten(value: Any) -> Any:
@@ -268,6 +270,7 @@ class LDAPSource(IncomingSyncSource):
)
if self.start_tls:
LOGGER.debug("Connection StartTLS", source=self)
conn.start_tls(read_server_info=False)
try:
successful = conn.bind()
@@ -278,7 +281,9 @@ class LDAPSource(IncomingSyncSource):
# See https://github.com/goauthentik/authentik/issues/4590
# See also https://github.com/goauthentik/authentik/issues/3399
if server_kwargs.get("get_info", ALL) == NONE:
LOGGER.warning("Failed to connect after schema downgrade", source=self, exc=exc)
raise exc
LOGGER.warning("Downgrading connection to no schema info", source=self, exc=exc)
server_kwargs["get_info"] = NONE
return self.connection(server, server_kwargs, connection_kwargs)
finally:

View File

@@ -7,6 +7,7 @@ from django.http import HttpRequest
from django.templatetags.static import static
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from lxml.etree import _Element # nosec
from rest_framework.serializers import Serializer
from authentik.common.saml.constants import (
@@ -217,9 +218,8 @@ class SAMLSource(Source):
def property_mapping_type(self) -> type[PropertyMapping]:
return SAMLSourcePropertyMapping
def get_base_user_properties(self, root: Any, name_id: Any, **kwargs):
def get_base_user_properties(self, root: _Element, assertion: _Element, name_id: Any, **kwargs):
attributes = {}
assertion = root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
if assertion is None:
raise ValueError("Assertion element not found")
attribute_statement = assertion.find(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")

View File

@@ -66,6 +66,8 @@ class ResponseProcessor:
_http_request: HttpRequest
_assertion: _Element | None = None
def __init__(self, source: SAMLSource, request: HttpRequest):
self._source = source
self._http_request = request
@@ -122,6 +124,7 @@ class ResponseProcessor:
index_of,
decrypted_assertion,
)
self._assertion = decrypted_assertion
def _verify_signature(self, signature_node: _Element):
"""Verify a single signature node"""
@@ -162,6 +165,10 @@ class ResponseProcessor:
raise InvalidSignature("No Signature exists in the Assertion element.")
self._verify_signature(signature_nodes[0])
parent = signature_nodes[0].getparent()
if parent is None or parent.tag != f"{{{NS_SAML_ASSERTION}}}Assertion":
raise InvalidSignature("No Signature exists in the Assertion element.")
self._assertion = parent
def _verify_request_id(self):
if self._source.allow_idp_initiated:
@@ -239,14 +246,21 @@ class ResponseProcessor:
identifier=str(name_id.text),
user_info={
"root": self._root,
"assertion": self.get_assertion(),
"name_id": name_id,
},
policy_context={},
)
def get_assertion(self) -> Element | None:
"""Get assertion element, if we have a signed assertion"""
if self._assertion is not None:
return self._assertion
return self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
def _get_name_id(self) -> Element:
"""Get NameID Element"""
assertion = self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
assertion = self.get_assertion()
if assertion is None:
raise ValueError("Assertion element not found")
subject = assertion.find(f"{{{NS_SAML_ASSERTION}}}Subject")
@@ -299,6 +313,7 @@ class ResponseProcessor:
identifier=str(name_id.text),
user_info={
"root": self._root,
"assertion": self.get_assertion(),
"name_id": name_id,
},
policy_context={

View File

@@ -0,0 +1,68 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_other_id_pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">bad</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">bad</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">bad</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#pfxa06693ef-cec7-f4a6-cb7f-ad074445a1a3"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>zNDuGxwP4gVkv/Dzt7kiKo/4gzk=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>GLP/vE8uxerB0uDpPslUgLPBL6ePQB619MoQ0I2Y5lAtFE6CB1zh8BnzChRx/bFjNy4byfOe8mFfM0r7WUi1PJOFWyUPoatdLl7wHHBIRTnPpYmu3Tb2Gz0sOP0F8wW7JkBft5gJfVw49nk5si9/3Q3o52jnJZ7dPtqfIOh8uNeopikK0HLF6sU05qCCtjcXfniEnLQFNBFMo9uY5GQqmR5n3nqPz1wYyyfFOAbVmGgBIoO2PfGX2GVLQhltc9qf2JMhks4jgZsZ8iLUIiH1lcLGWZEEs94k8k0P6gSv1uZ7Vbhksd/N9Jq9pCVuEJ/jRPcAdVjzbxqKQAj6ELwr8O6fepTzA+CAdwEolBnx/C6TmSbVZ+IWk6QUGe4x4+IAukC+0hkKENlO0ELOScksvyhpgHbxNA4rp+DhGupCaO/I2RrsQkmvavbqm+wSEspK7scK112SDunjDvqPHsPYgukD33T/97PxTLorg2kKP9HHJwPJKoXXeyOGcA6vwK+RqrAlZ2dLGAgcXo+sJcdCLuvxDNz9VXofBjBZIKVKdmYhm0QJaPYHtuQsAyFavQhdOBOmGHb7QX3YE3Xy4dX4LymtT+Jlb1I4FJSht/9HUIHW1FdhfDak4f7gUgjuMamMddLD0jVgeESupSREzFv/gj2IrctkbgjAO0iuuiBgKMg=</ds:SignatureValue>
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloXDTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLlyrbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5EFUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8uvm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq614ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOpmTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCqlDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5kyPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/GcpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE28Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusOLT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDRUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OIAVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/OnS7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HAC8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1ZpoMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YEpF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTuvr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMClpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>

View File

@@ -36,7 +36,9 @@ class TestPropertyMappings(TestCase):
def test_user_base_properties(self):
"""Test user base properties"""
properties = self.source.get_base_user_properties(root=ROOT, name_id=NAME_ID)
properties = self.source.get_base_user_properties(
root=ROOT, assertion=ROOT.find(f"{{{NS_SAML_ASSERTION}}}Assertion"), name_id=NAME_ID
)
self.assertEqual(
properties,
{
@@ -49,7 +51,11 @@ class TestPropertyMappings(TestCase):
def test_group_base_properties(self):
"""Test group base properties"""
properties = self.source.get_base_user_properties(root=ROOT_GROUPS, name_id=NAME_ID)
properties = self.source.get_base_user_properties(
root=ROOT_GROUPS,
assertion=ROOT_GROUPS.find(f"{{{NS_SAML_ASSERTION}}}Assertion"),
name_id=NAME_ID,
)
self.assertEqual(properties["groups"], ["group 1", "group 2"])
for group_id in ["group 1", "group 2"]:
properties = self.source.get_base_group_properties(root=ROOT, group_id=group_id)

View File

@@ -164,6 +164,31 @@ class TestResponseProcessor(TestCase):
parser = ResponseProcessor(self.source, request)
parser.parse()
def test_verification_assertion_duplicate(self):
"""Test verifying signature inside assertion, where the response has another assertion
before our signed assertion"""
key = load_fixture("fixtures/signature_cert.pem")
kp = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=key,
)
self.source.verification_kp = kp
self.source.signed_assertion = True
self.source.signed_response = False
request = self.factory.post(
"/",
data={
"SAMLResponse": b64encode(
load_fixture("fixtures/response_signed_assertion_dup.xml").encode()
).decode()
},
)
parser = ResponseProcessor(self.source, request)
parser.parse()
self.assertNotEqual(parser._get_name_id().text, "bad")
self.assertEqual(parser._get_name_id().text, "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
def test_verification_response(self):
"""Test verifying signature inside response"""
key = load_fixture("fixtures/signature_cert.pem")

View File

@@ -6,6 +6,7 @@ from django.contrib.auth.views import redirect_to_login
from django.http.request import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.middleware import get_user
from authentik.core.models import Session
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
@@ -54,11 +55,13 @@ class SessionBindingBroken(SentryIgnoredException):
def logout_extra(request: HttpRequest, exc: SessionBindingBroken):
"""Similar to django's logout method, but able to carry more info to the signal"""
# Dispatch the signal before the user is logged out so the receivers have a
# chance to find out *who* logged out.
user = getattr(request, "user", None)
# Since this middleware runs before the AuthenticationMiddleware, we can't use `request.user`
# as it hasn't been populated yet.
user = get_user(request)
if not getattr(user, "is_authenticated", True):
user = None
# Dispatch the signal before the user is logged out so the receivers have a
# chance to find out *who* logged out.
user_logged_out.send(
sender=user.__class__, request=request, user=user, event_extra=exc.to_event()
)

View File

@@ -10,6 +10,8 @@ from django.utils.timezone import now
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import AuthenticatedSession, Session
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.events.models import Event, EventAction
from authentik.events.utils import get_user
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
@@ -270,6 +272,7 @@ class TestUserLoginStage(FlowTestCase):
def test_session_binding_broken(self):
"""Test session binding"""
Event.objects.all().delete()
self.client.force_login(self.user)
session = self.client.session
session[Session.Keys.LAST_IP] = "192.0.2.1"
@@ -285,3 +288,5 @@ class TestUserLoginStage(FlowTestCase):
)
+ f"?{NEXT_ARG_NAME}={reverse("authentik_api:user-me")}",
)
event = Event.objects.filter(action=EventAction.LOGOUT).first()
self.assertEqual(event.user, get_user(self.user))

View File

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

View File

@@ -29,7 +29,7 @@ entries:
password=request.user.password
)
# ...otherwise we set an immutable ID based on the user's UID
user["on_premises_immutable_id"] = request.user.uid,
user["on_premises_immutable_id"] = request.user.uid
return user
- identifiers:
managed: goauthentik.io/providers/microsoft_entra/group

View File

@@ -104,7 +104,11 @@ type OutpostConfig struct {
}
type WebConfig struct {
Path string `yaml:"path" env:"PATH, overwrite"`
Path string `yaml:"path" env:"PATH, overwrite"`
TimeoutHttpReadHeader string `yaml:"timeout_http_read_header" env:"TIMEOUT_HTTP_READ_HEADER, overwrite"`
TimeoutHttpRead string `yaml:"timeout_http_read" env:"TIMEOUT_HTTP_READ, overwrite"`
TimeoutHttpWrite string `yaml:"timeout_http_write" env:"TIMEOUT_HTTP_WRITE, overwrite"`
TimeoutHttpIdle string `yaml:"timeout_http_idle" env:"TIMEOUT_HTTP_IDLE, overwrite"`
}
type LogConfig struct {

View File

@@ -1 +1 @@
2026.2.0-rc1
2026.2.1

View File

@@ -26,7 +26,6 @@ import (
"goauthentik.io/api/v3"
"goauthentik.io/internal/config"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/proxyv2/constants"
"goauthentik.io/internal/outpost/proxyv2/hs256"
"goauthentik.io/internal/outpost/proxyv2/metrics"
"goauthentik.io/internal/outpost/proxyv2/templates"
@@ -294,22 +293,16 @@ func (a *Application) Stop() {
func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
redirect := a.endpoint.EndSessionEndpoint
s, err := a.sessions.Get(r, a.SessionName())
if err != nil {
cc := a.getClaimsFromSession(rw, r)
if cc == nil {
a.redirectToStart(rw, r)
return
}
c, exists := s.Values[constants.SessionClaims]
if c == nil && !exists {
a.redirectToStart(rw, r)
return
}
cc := c.(types.Claims)
uv := url.Values{
"id_token_hint": []string{cc.RawToken},
}
redirect += "?" + uv.Encode()
err = a.Logout(r.Context(), func(c types.Claims) bool {
err := a.Logout(r.Context(), func(c types.Claims) bool {
return c.Sub == cc.Sub
})
if err != nil {

View File

@@ -76,7 +76,7 @@ func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
}
}
redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.Path)
redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.EscapedPath())
if a.Mode() == api.PROXYMODE_FORWARD_DOMAIN {
dom := strings.TrimPrefix(*a.proxyConfig.CookieDomain, ".")

View File

@@ -27,6 +27,24 @@ func TestRedirectToStart_Proxy(t *testing.T) {
assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect])
}
func TestRedirectToStart_Proxy_EncodedSlash(t *testing.T) {
a := newTestApplication()
a.proxyConfig.Mode = api.PROXYMODE_PROXY.Ptr()
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
// %2F is a URL-encoded forward slash, used by apps like RabbitMQ in queue paths
req, _ := http.NewRequest("GET", "/api/queues/%2F/MYChannelCreated", nil)
rr := httptest.NewRecorder()
a.redirectToStart(rr, req)
assert.Equal(t, http.StatusFound, rr.Code)
loc, _ := rr.Result().Location()
assert.Contains(t, loc.String(), "%252F", "encoded slash %2F must be preserved in redirect URL")
s, _ := a.sessions.Get(req, a.SessionName())
assert.Contains(t, s.Values[constants.SessionRedirect].(string), "%2F", "encoded slash %2F must be preserved in session redirect")
}
func TestRedirectToStart_Forward(t *testing.T) {
a := newTestApplication()
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_SINGLE.Ptr()

View File

@@ -187,10 +187,7 @@ func BuildConnConfig(cfg config.PostgreSQLConfig) (*pgx.ConnConfig, error) {
if connConfig.RuntimeParams == nil {
connConfig.RuntimeParams = make(map[string]string)
}
if cfg.DefaultSchema != "" {
connConfig.RuntimeParams["search_path"] = cfg.DefaultSchema
}
effectiveSearchPath := cfg.DefaultSchema
// Parse and apply connection options if specified
if cfg.ConnOptions != "" {
@@ -198,12 +195,39 @@ func BuildConnConfig(cfg config.PostgreSQLConfig) (*pgx.ConnConfig, error) {
if err != nil {
return nil, fmt.Errorf("failed to parse connection options: %w", err)
}
// search_path from ConnOptions is not supported here; Django controls schema selection.
// Always remove it so it cannot end up in startup RuntimeParams via applyConnOptions.
delete(connOpts, "search_path")
if err := applyConnOptions(connConfig, connOpts); err != nil {
return nil, fmt.Errorf("failed to apply connection options: %w", err)
}
}
// search_path may already be present via pgx/libpq inherited defaults (e.g. service files).
// Always remove it from startup RuntimeParams; apply it via AfterConnect instead.
if inheritedSearchPath, hasInheritedSearchPath := connConfig.RuntimeParams["search_path"]; hasInheritedSearchPath {
if effectiveSearchPath == "" {
effectiveSearchPath = inheritedSearchPath
}
delete(connConfig.RuntimeParams, "search_path")
}
// Set search_path after connection startup to avoid startup-parameter issues with PgBouncer.
if effectiveSearchPath != "" {
connConfig.AfterConnect = func(ctx context.Context, pgConn *pgconn.PgConn) error {
result := pgConn.ExecParams(
ctx,
"select pg_catalog.set_config('search_path', $1, false)",
[][]byte{[]byte(effectiveSearchPath)},
nil,
nil,
nil,
).Read()
return result.Err
}
}
return connConfig, nil
}

View File

@@ -704,7 +704,7 @@ func TestBuildConnConfig(t *testing.T) {
DefaultSchema: "custom_schema",
},
validate: func(t *testing.T, cc *pgx.ConnConfig) {
assert.Equal(t, "custom_schema", cc.RuntimeParams["search_path"])
assert.NotNil(t, cc.AfterConnect)
},
},
{
@@ -760,7 +760,7 @@ func TestBuildConnConfig(t *testing.T) {
assert.Equal(t, "admin", cc.User)
assert.Equal(t, "my super secret password!@#", cc.Password)
assert.Equal(t, "production", cc.Database)
assert.Equal(t, "app_schema", cc.RuntimeParams["search_path"])
assert.NotNil(t, cc.AfterConnect)
assert.Equal(t, "authentik", cc.RuntimeParams["application_name"])
},
},
@@ -867,7 +867,7 @@ func TestBuildConnConfig_WithSSLCertificates(t *testing.T) {
assert.Equal(t, "db.example.com", cc.TLSConfig.ServerName)
assert.NotNil(t, cc.TLSConfig.RootCAs)
assert.Len(t, cc.TLSConfig.Certificates, 1)
assert.Equal(t, "app_schema", cc.RuntimeParams["search_path"])
assert.NotNil(t, cc.AfterConnect)
assert.Equal(t, "authentik", cc.RuntimeParams["application_name"])
},
},
@@ -1361,6 +1361,83 @@ func TestBuildConnConfig_WithBase64EncodedConnOptions(t *testing.T) {
}
}
// Verifies DefaultSchema is applied via AfterConnect and never via startup RuntimeParams.
func TestBuildConnConfig_SearchPath_DefaultSchema(t *testing.T) {
cfg := config.PostgreSQLConfig{
Host: "localhost",
Port: "5432",
User: "authentik",
Name: "authentik",
DefaultSchema: "default_schema",
}
connConfig, err := BuildConnConfig(cfg)
require.NoError(t, err)
require.NotNil(t, connConfig.AfterConnect)
_, hasSearchPath := connConfig.RuntimeParams["search_path"]
assert.False(t, hasSearchPath, "search_path should not appear in RuntimeParams")
}
// Verifies ConnOptions search_path is ignored and excluded from startup RuntimeParams.
func TestBuildConnConfig_SearchPath_ConnOptions(t *testing.T) {
cfg := config.PostgreSQLConfig{
Host: "localhost",
Port: "5432",
User: "authentik",
Name: "authentik",
ConnOptions: base64.StdEncoding.EncodeToString([]byte(`{"search_path":"connopt_schema"}`)),
}
connConfig, err := BuildConnConfig(cfg)
require.NoError(t, err)
assert.Nil(t, connConfig.AfterConnect)
_, hasSearchPath := connConfig.RuntimeParams["search_path"]
assert.False(t, hasSearchPath, "search_path should not appear in RuntimeParams")
}
// Verifies ConnOptions search_path does not override DefaultSchema and other conn options still apply.
func TestBuildConnConfig_SearchPath_ConnOptionsIgnoredWhenDefaultSchemaSet(t *testing.T) {
cfg := config.PostgreSQLConfig{
Host: "localhost",
Port: "5432",
User: "authentik",
Name: "authentik",
DefaultSchema: "default_schema",
ConnOptions: base64.StdEncoding.EncodeToString([]byte(`{"search_path":"override_schema","application_name":"authentik-proxy"}`)),
}
connConfig, err := BuildConnConfig(cfg)
require.NoError(t, err)
require.NotNil(t, connConfig.AfterConnect)
assert.Equal(t, "authentik-proxy", connConfig.RuntimeParams["application_name"])
_, hasSearchPath := connConfig.RuntimeParams["search_path"]
assert.False(t, hasSearchPath, "search_path should not appear in RuntimeParams")
}
// Verifies inherited search_path from pgx/libpq defaults is removed from startup RuntimeParams.
func TestBuildConnConfig_SearchPath_InheritedServiceSetting(t *testing.T) {
serviceFile := filepath.Join(t.TempDir(), "pg_service.conf")
err := os.WriteFile(serviceFile, []byte("[authentik-test]\nsearch_path=service_schema\n"), 0o600)
require.NoError(t, err)
t.Setenv("PGSERVICE", "authentik-test")
t.Setenv("PGSERVICEFILE", serviceFile)
cfg := config.PostgreSQLConfig{
Host: "localhost",
Port: "5432",
User: "authentik",
Name: "authentik",
}
connConfig, err := BuildConnConfig(cfg)
require.NoError(t, err)
require.NotNil(t, connConfig.AfterConnect)
_, hasSearchPath := connConfig.RuntimeParams["search_path"]
assert.False(t, hasSearchPath, "search_path should not appear in RuntimeParams")
}
// TestBuildConnConfig_TargetSessionAttrs demonstrates how target_session_attrs
// should be properly handled using pgx's ValidateConnect callback
func TestBuildConnConfig_TargetSessionAttrs(t *testing.T) {

View File

@@ -3,15 +3,26 @@ package web
import (
"net/http"
"time"
"goauthentik.io/internal/config"
)
func durationOrFallback(raw string, fallback time.Duration) time.Duration {
p, err := time.ParseDuration(raw)
if err != nil {
return fallback
}
return p
}
func Server(h http.Handler) *http.Server {
c := config.Get()
return &http.Server{
Handler: h,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 120 * time.Second,
ReadHeaderTimeout: durationOrFallback(c.Web.TimeoutHttpReadHeader, 5*time.Second),
ReadTimeout: durationOrFallback(c.Web.TimeoutHttpRead, 30*time.Second),
WriteTimeout: durationOrFallback(c.Web.TimeoutHttpWrite, 60*time.Second),
IdleTimeout: durationOrFallback(c.Web.TimeoutHttpIdle, 120*time.Second),
MaxHeaderBytes: http.DefaultMaxHeaderBytes,
}
}

View File

@@ -18,7 +18,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2026.2.0-rc1
Default: 2026.2.1
Description: authentik Docker image tag
AuthentikServerCPU:
Type: Number

View File

@@ -31,7 +31,7 @@ services:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.1}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
@@ -53,7 +53,7 @@ services:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.0-rc1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.2.1}
restart: unless-stopped
shm_size: 512mb
user: root

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2026.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2026.2.1",
"dependencies": {
"@eslint/js": "^9.39.1",
"@goauthentik/eslint-config": "./packages/eslint-config",

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2026.2.1",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -235,15 +235,16 @@ class PostgresChannelLoopLayer(BaseChannelLayer):
try:
while True:
message_id, message = await q.get()
if message is None:
async with await self.connection() as conn:
async with conn.cursor() as cursor:
async with await self.connection() as conn:
async with conn.cursor() as cursor:
if message is None:
await cursor.execute(
sql.SQL("""
SELECT {table}.{message}
DELETE
FROM {table}
WHERE {table}.{id} = %s
""").format(
RETURNING {table}.{message}
""").format(
table=sql.Identifier(MESSAGE_TABLE),
id=sql.Identifier("id"),
message=sql.Identifier("message"),
@@ -254,6 +255,18 @@ class PostgresChannelLoopLayer(BaseChannelLayer):
if row is None:
continue
message = row[0]
else:
await cursor.execute(
sql.SQL("""
DELETE
FROM {table}
WHERE {table}.{id} = %s
""").format(
table=sql.Identifier(MESSAGE_TABLE),
id=sql.Identifier("id"),
),
(message_id,),
)
break
except asyncio.CancelledError, TimeoutError, GeneratorExit:
# We assume here that the reason we are cancelled is because the consumer

View File

@@ -1,5 +1,7 @@
import platform
import sys
from argparse import Namespace
from multiprocessing import set_start_method
from typing import Any
from django.apps.registry import apps
@@ -69,7 +71,10 @@ class Command(BaseCommand):
args.pid_file = pid_file
args.verbose = verbosity - 1
# > On macOS [...] the fork start method should be considered unsafe
# https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods
if not platform.system() == "Darwin":
set_start_method("fork")
connections.close_all()
sys.exit(main(args)) # type: ignore[no-untyped-call]

View File

@@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2026.2.0-rc1"
version = "2026.2.1"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.14.*"
@@ -9,7 +9,7 @@ dependencies = [
"argon2-cffi==25.1.0",
"cachetools==7.0.0",
"channels==4.3.2",
"cryptography==46.0.4",
"cryptography==46.0.5",
"dacite==1.9.2",
"deepmerge==2.0",
"defusedxml==0.7.1",

View File

@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2026.2.0-rc1
version: 2026.2.1
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@@ -42144,15 +42144,15 @@ components:
readOnly: true
opened_on:
type: string
format: date
format: date-time
readOnly: true
grace_period_end:
type: string
format: date
format: date-time
readOnly: true
next_review_date:
type: string
format: date
format: date-time
readOnly: true
reviews:
type: array

View File

@@ -2,7 +2,7 @@
set -e -x -o pipefail
hash="$(git rev-parse HEAD || openssl rand -base64 36 | sha256sum)"
AUTHENTIK_IMAGE="xghcr.io/goauthentik/server"
AUTHENTIK_IMAGE="authentik.invalid/goauthentik/server"
AUTHENTIK_TAG="$(echo "$hash" | cut -c1-15)"
if [ -f lifecycle/container/.env ]; then
@@ -24,7 +24,7 @@ if [[ -v BUILD ]]; then
make gen-client-go
touch lifecycle/container/.env
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" .
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" -f lifecycle/container/Dockerfile .
fi
docker compose -f lifecycle/container/compose.yml up --no-start

92
uv.lock generated
View File

@@ -221,7 +221,7 @@ wheels = [
[[package]]
name = "authentik"
version = "2026.2.0rc1"
version = "2026.2.1"
source = { editable = "." }
dependencies = [
{ name = "ak-guardian" },
@@ -338,7 +338,7 @@ requires-dist = [
{ name = "argon2-cffi", specifier = "==25.1.0" },
{ name = "cachetools", specifier = "==7.0.0" },
{ name = "channels", specifier = "==4.3.2" },
{ name = "cryptography", specifier = "==46.0.4" },
{ name = "cryptography", specifier = "==46.0.5" },
{ name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" },
@@ -932,55 +932,55 @@ wheels = [
[[package]]
name = "cryptography"
version = "46.0.4"
version = "46.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" }
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" },
{ url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" },
{ url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" },
{ url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" },
{ url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" },
{ url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" },
{ url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" },
{ url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" },
{ url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" },
{ url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" },
{ url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
{ url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
{ url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
{ url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" },
{ url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" },
{ url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" },
{ url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" },
{ url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" },
{ url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" },
{ url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" },
{ url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" },
{ url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" },
{ url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" },
{ url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" },
{ url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" },
{ url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
{ url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
{ url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
{ url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" },
{ url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" },
{ url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" },
{ url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" },
{ url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" },
{ url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" },
{ url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" },
{ url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" },
{ url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
{ url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
{ url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
{ url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
{ url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
{ url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
{ url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
{ url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
{ url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
{ url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
{ url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
]
[[package]]

77
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/web",
"version": "2026.2.0-rc1",
"version": "2026.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/web",
"version": "2026.2.0-rc1",
"version": "2026.2.1",
"license": "MIT",
"workspaces": [
"./packages/*"
@@ -188,7 +188,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -2610,6 +2609,7 @@
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
@@ -3879,6 +3879,18 @@
}
}
},
"node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": {
"version": "0.22.4",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz",
"integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
}
},
"node_modules/@swagger-api/apidom-reference": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.0.1.tgz",
@@ -4017,7 +4029,6 @@
"integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.25"
@@ -4347,7 +4358,8 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/chai": {
"version": "5.2.3",
@@ -4722,7 +4734,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz",
"integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -4741,7 +4752,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4831,7 +4841,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
@@ -5072,7 +5081,6 @@
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz",
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/browser": "4.0.18",
"@vitest/mocker": "4.0.18",
@@ -5538,7 +5546,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6081,7 +6088,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6370,7 +6376,6 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -6402,7 +6407,6 @@
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
@@ -6673,7 +6677,6 @@
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
@@ -7068,7 +7071,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -7229,7 +7231,6 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@@ -7497,6 +7498,7 @@
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz",
"integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.20"
},
@@ -7509,6 +7511,7 @@
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz",
"integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
@@ -7559,7 +7562,8 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/dompurify": {
"version": "3.3.1",
@@ -7854,7 +7858,6 @@
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -7948,7 +7951,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9240,6 +9242,7 @@
"resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.1.1.tgz",
"integrity": "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/fisker/git-hooks-list?sponsor=1"
}
@@ -10849,7 +10852,6 @@
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz",
"integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@lit/reactive-element": "^2.1.0",
"lit-element": "^4.2.0",
@@ -11112,6 +11114,7 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -13555,7 +13558,6 @@
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright-core": "1.58.2"
},
@@ -13657,7 +13659,6 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -13692,6 +13693,7 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -13706,6 +13708,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -13717,7 +13720,8 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/prismjs": {
"version": "1.30.0",
@@ -13925,7 +13929,6 @@
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
"integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ramda"
@@ -14005,7 +14008,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14015,7 +14017,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -14552,7 +14553,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -15153,13 +15153,15 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-2.0.1.tgz",
"integrity": "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/sort-package-json": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-3.5.0.tgz",
"integrity": "sha512-moY4UtptUuP5sPuu9H9dp8xHNel7eP5/Kz/7+90jTvC0IOiPH2LigtRM/aSFSxreaWoToHUVUpEV4a2tAs2oKQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"detect-indent": "^7.0.1",
"detect-newline": "^4.0.1",
@@ -15294,7 +15296,6 @@
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.7.tgz",
"integrity": "sha512-LFKSuZyF6EW2/Kkl5d7CvqgwhXXfuWv+aLBuoc616boLKJ3mxXuea+GxIgfk02NEyTKctJ0QsnSh5pAomf6Qkg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^2.0.1",
@@ -15695,6 +15696,7 @@
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@pkgr/core": "^0.2.9"
},
@@ -15900,6 +15902,18 @@
"node": ">=6"
}
},
"node_modules/tree-sitter": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
"integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.0.0",
"node-gyp-build": "^4.8.0"
}
},
"node_modules/tree-sitter-json": {
"version": "0.24.8",
"resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz",
@@ -16142,7 +16156,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16156,7 +16169,6 @@
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
"integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
@@ -16575,7 +16587,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -16664,7 +16675,6 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",
@@ -17357,7 +17367,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/web",
"version": "2026.2.0-rc1",
"version": "2026.2.1",
"license": "MIT",
"private": true,
"scripts": {

View File

@@ -89,7 +89,7 @@ export class BoundStagesList extends Table<FlowStageBinding> {
html` <ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.stageObj?.component, {
slot: "form",
instancePk: item.pk,
instancePk: item.stageObj?.pk,
actionLabel: msg("Update"),
headline: msg(str`Update ${item.stageObj?.verboseName}`, {
id: "form.headline.update",

View File

@@ -243,8 +243,10 @@ export class LifecycleRuleForm extends ModelForm<LifecycleRule, string> {
name="minReviewersIsPerGroup"
?checked=${this.instance?.minReviewersIsPerGroup ?? false}
label=${msg("Min reviewers is per-group")}
help=${msg(
"If checked, approving a review will require at least that many users from _each_ of the selected groups. When disabled, the value is a total across all groups.",
.help=${msg(
html`If checked, approving a review will require at least that many users from
<em>each</em> of the selected groups. When disabled, the value is a total
across all groups.`,
)}
>
</ak-switch-input>

View File

@@ -112,7 +112,7 @@ export class BoundPoliciesList<T extends PolicyBinding = PolicyBinding> extends
return html`<ak-forms-modal>
${StrictUnsafe<CustomFormElementTagName>(item.policyObj?.component, {
slot: "form",
instancePk: item.pk,
instancePk: item.policyObj?.pk,
actionLabel: msg("Update"),
headline: msg(str`Update ${item.policyObj?.name}`, {
id: "form.headline.update",

View File

@@ -63,10 +63,13 @@ export class RoleObjectPermissionForm extends ModelForm<RoleAssignData, number>
}
send(data: RoleAssignData): Promise<unknown> {
const [app, _model] = this.model?.split(".") || "";
return new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByRolesAssign({
uuid: data.role,
permissionAssignRequest: {
permissions: Object.keys(data.permissions).filter((key) => data.permissions[key]),
permissions: Object.keys(data.permissions)
.filter((key) => data.permissions[key])
.map((permission) => `${app}.${permission}`),
model: this.model!,
objectPk: this.objectPk,
},

View File

@@ -53,6 +53,10 @@ export const eventActionToLabel = new Map<EventActions | undefined, string>([
[EventActions.EmailSent, msg("Email sent")],
[EventActions.UpdateAvailable, msg("Update available")],
[EventActions.ExportReady, msg("Data export ready")],
[EventActions.ReviewInitiated, msg("Review initiated")],
[EventActions.ReviewOverdue, msg("Review overdue")],
[EventActions.ReviewAttested, msg("Review attested")],
[EventActions.ReviewCompleted, msg("Review completed")],
]);
export const actionToLabel = (action?: EventActions): string =>

View File

@@ -33,7 +33,7 @@ export class AkSwitchInput extends AKElement {
required = false;
@property({ type: String })
help = "";
help: string | TemplateResult = "";
/**
* For more complex help instructions, provide a template result.
@@ -47,11 +47,13 @@ export class AkSwitchInput extends AKElement {
#fieldID: string = IDGenerator.randomID();
protected renderHelp() {
const helpText = this.help.trim();
const helpContent = typeof this.help === "string" ? this.help.trim() : this.help;
return [
helpText
? html`<p id="${this.#fieldID}-help" class="pf-c-form__helper-text">${helpText}</p>`
helpContent
? html`<p id="${this.#fieldID}-help" class="pf-c-form__helper-text">
${helpContent}
</p>`
: nothing,
this.bighelp ? this.bighelp : nothing,
];

View File

@@ -1,4 +1,5 @@
:host {
:host,
ak-loading-overlay.style-scope {
position: absolute;
inset: 0;
z-index: 1;
@@ -11,6 +12,7 @@
);
}
:host([topmost]) {
:host([topmost]),
ak-loading-overlay[topmost].style-scope {
z-index: var(--pf-global--ZIndex--2xl);
}

View File

@@ -8,7 +8,7 @@ import {
import { spread } from "@open-wc/lit-helpers";
import { LitElement, nothing } from "lit";
import { LitElement, nothing, PropertyDeclaration } from "lit";
import { html as staticHTML, unsafeStatic } from "lit-html/static.js";
import { guard } from "lit/directives/guard.js";
@@ -35,21 +35,57 @@ export function isAKElementConstructor(input: CustomElementConstructor): input i
return Object.prototype.isPrototypeOf.call(AKElement, input);
}
function getPrefix(type: unknown, isProperty: boolean) {
if (isProperty) {
return ".";
export const Prefix = {
Property: ".",
BooleanAttribute: "?",
Attribute: "",
} as const;
export type Prefix = (typeof Prefix)[keyof typeof Prefix];
/**
* Given a Lit property declaration, determine the appropriate prefix for rendering the property as either a property or an attribute, based on the declaration's type and attribute configuration.
*
* @param propDeclaration The Lit property declaration to analyze.
* @returns The determined prefix for rendering the property.
*/
function resolvePrefix<T extends PropertyDeclaration<unknown, unknown>>(
propDeclaration: T,
): Prefix {
if (!propDeclaration.attribute) {
return Prefix.Property;
}
switch (type) {
switch (propDeclaration.type) {
case String:
return "";
return Prefix.Attribute;
case Boolean:
return "?";
return Prefix.BooleanAttribute;
default:
return ".";
return Prefix.Property;
}
}
/**
* Given a Lit property declaration, a resolved prefix, and the original property key,
* determine the appropriate name to use for rendering the property,
* taking into account any custom attribute name specified in the declaration.
*/
function resolvePropertyName<T extends PropertyDeclaration<unknown, unknown>>(
propDeclaration: T,
prefix: Prefix,
key: string,
): string {
if (prefix === Prefix.Property) {
return key;
}
if ("attribute" in propDeclaration && typeof propDeclaration.attribute === "string") {
return propDeclaration.attribute;
}
return key;
}
/**
* Given a pre-registered custom element tag name and a record of properties,
* render the element with the given properties applied.
@@ -63,16 +99,19 @@ export function StrictUnsafe<T extends CustomElementTagName>(
tagName: T,
props?: LitPropertyRecord<HTMLElementTagNameMap[T]>,
): SlottedTemplateResult;
export function StrictUnsafe<T extends AKElement>(
tagName: string,
props?: LitPropertyRecord<T>,
): SlottedTemplateResult;
export function StrictUnsafe<T extends string>(
tagName: string,
props?: T extends CustomElementTagName
? LitPropertyRecord<HTMLElementTagNameMap[T]>
: LitPropertyRecord<LitElement>,
): SlottedTemplateResult;
export function StrictUnsafe<T extends string>(
tagName: string,
props?: T extends CustomElementTagName
@@ -103,17 +142,20 @@ export function StrictUnsafe<T extends string>(
const filteredProps: Record<string, unknown> = {};
for (const [key, value] of Object.entries(props || {})) {
const propDeclaration = elementProperties.get(key);
for (const [propName, propValue] of Object.entries(props || {})) {
const propDeclaration = elementProperties.get(propName);
if (propDeclaration) {
const prefix = getPrefix(propDeclaration.type, !propDeclaration.attribute);
filteredProps[`${prefix}${key}`] = value;
const prefix = resolvePrefix(propDeclaration);
const name = resolvePropertyName(propDeclaration, prefix, propName);
filteredProps[`${prefix}${name}`] = propValue;
continue;
}
if (observedAttributes.has(key) || key in ElementConstructor.prototype) {
filteredProps[key] = String(value);
if (observedAttributes.has(propName) || propName in ElementConstructor.prototype) {
filteredProps[propName] = String(propValue);
}
}

View File

@@ -1,6 +1,7 @@
@import "../styles/authentik/components/Login/login.css";
:host {
:host,
ak-flow-executor.style-scope {
display: flex;
min-height: 100dvh;
flex-flow: column nowrap;
@@ -49,14 +50,14 @@
}
}
filter: var(--ak-global--background-contrast-Filter);
filter: var(--ak-global--BackgroundContrastFilter);
grid-area: header;
/* At least a third of the card cut-off is available. */
@media (width <= 61.25rem) and (height <= 61.25rem) {
--ak-global--background-contrast-Filter: none;
--ak-c-flow-executor__locale-select--Color: var(--ak-global--background-contrast);
--ak-global--BackgroundContrastFilter: none;
--ak-c-flow-executor__locale-select--Color: var(--ak-c-login__main--Color);
grid-area: main;
}
@@ -79,7 +80,7 @@
@media (min-width: 70rem) and (min-height: 17.5rem) {
:host([data-layout^="sidebar"]),
[data-layout^="sidebar"] /* Compatibility mode */ {
--ak-global--background-contrast-Filter: none !important;
--ak-global--BackgroundContrastFilter: none !important;
[part="locale-select"] {
--ak-c-flow-executor__locale-select--Color: inherit !important;

View File

@@ -68,6 +68,18 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
*
* @attr {string} slug - The slug of the flow to execute.
* @prop {ChallengeTypes | null} challenge - The current challenge to render.
*
* @part main - The main container for the flow content.
* @part content - The container for the stage content.
* @part content-iframe - The iframe element when using a frame background layout.
* @part footer - The footer container.
* @part locale-select - The locale select component.
* @part branding - The branding element, used for the background image in some layouts.
* @part loading-overlay - The loading overlay element.
* @part challenge-additional-actions - Container in stages which have additional actions.
* @part challenge-footer-band - Container for the stage footer, used for additional actions in some stages.
* @part locale-select-label - The label of the locale select component.
* @part locale-select-select - The select element of the locale select component.
*/
@customElement("ak-flow-executor")
export class FlowExecutor
@@ -538,7 +550,7 @@ export class FlowExecutor
//#region Render
protected renderLoading(): SlottedTemplateResult {
return html`<slot class="slotted-content" name="placeholder"></slot>`;
return html`<slot name="placeholder"></slot>`;
}
protected renderFrameBackground(): SlottedTemplateResult {
@@ -567,6 +579,21 @@ export class FlowExecutor
});
}
protected renderFooter(): SlottedTemplateResult {
return guard([this.layout], () => {
return html`<footer
aria-label=${msg("Site footer")}
name="site-footer"
part="footer"
class="pf-c-login__footer ${this.layout === FlowLayoutEnum.Stacked
? "pf-m-dark"
: ""}"
>
<slot name="footer"></slot>
</footer>`;
});
}
protected override render(): SlottedTemplateResult {
const { component } = this.challenge || {};
@@ -593,11 +620,11 @@ export class FlowExecutor
})}
</div>
${this.loading && this.challenge
? html`<ak-loading-overlay></ak-loading-overlay>`
? html`<ak-loading-overlay part="loading-overlay"></ak-loading-overlay>`
: nothing}
${component ? until(this.renderChallenge(component)) : this.renderLoading()}
</main>
<slot name="footer"></slot>`;
${this.renderFooter()}`;
}
//#endregion

View File

@@ -3,7 +3,8 @@
width: 100%;
}
:host {
:host,
ak-flow-inspector.style-scope {
background-color: var(--pf-c-notification-drawer--BackgroundColor);
--pf-c-drawer__panel--BackgroundColor: var(--pf-global--BackgroundColor--150) !important;
}

View File

@@ -0,0 +1,41 @@
:host,
ak-form-static.style-scope {
margin-block-start: var(--pf-global--spacer--sm);
display: flex;
align-items: center;
justify-content: space-between;
flex-flow: wrap;
gap: var(--pf-global--spacer--sm);
}
.pf-c-avatar {
flex: 0 0 auto;
}
.primary-content {
display: flex;
align-items: center;
flex: 1 1 auto;
gap: var(--pf-global--spacer--md);
}
.username {
flex: 1 1 auto;
text-align: left;
max-width: 20rem;
text-overflow: ellipsis;
overflow-wrap: break-word;
display: box;
display: -webkit-box;
line-clamp: 3;
-webkit-line-clamp: 3;
box-orient: vertical;
-webkit-box-orient: vertical;
overflow: hidden;
}
.links {
flex: 0 0 auto;
text-align: right;
}

View File

@@ -3,6 +3,8 @@ import { LitFC } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { isDefaultAvatar } from "#elements/utils/images";
import Styles from "#flow/FormStatic.css";
import {
AccessDeniedChallenge,
AuthenticatorDuoChallenge,
@@ -18,7 +20,7 @@ import {
} from "@goauthentik/api";
import { msg, str } from "@lit/localize";
import { css, CSSResult, html, nothing } from "lit";
import { CSSResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { guard } from "lit/directives/guard.js";
@@ -26,6 +28,8 @@ import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
@customElement("ak-form-static")
export class AKFormStatic extends AKElement {
static styles: CSSResult[] = [PFAvatar, Styles];
public override role = "banner";
public override ariaLabel = msg("User information");
@@ -35,59 +39,12 @@ export class AKFormStatic extends AKElement {
@property({ type: String })
public username: string = "";
static styles: CSSResult[] = [
PFAvatar,
css`
:host {
margin-block-start: var(--pf-global--spacer--sm);
display: flex;
align-items: center;
justify-content: space-between;
flex-flow: wrap;
gap: var(--pf-global--spacer--sm);
}
.pf-c-avatar {
flex: 0 0 auto;
}
.primary-content {
display: flex;
align-items: center;
flex: 1 1 auto;
gap: var(--pf-global--spacer--md);
}
.username {
flex: 1 1 auto;
text-align: left;
max-width: 20rem;
text-overflow: ellipsis;
overflow-wrap: break-word;
display: box;
display: -webkit-box;
line-clamp: 3;
-webkit-line-clamp: 3;
box-orient: vertical;
-webkit-box-orient: vertical;
overflow: hidden;
}
.links {
flex: 0 0 auto;
text-align: right;
}
`,
];
protected override render() {
if (!this.username) {
return nothing;
}
return html`
<div class="primary-content">
return html`<div class="primary-content">
${this.avatar && !isDefaultAvatar(this.avatar)
? html`<img
class="pf-c-avatar"
@@ -105,8 +62,7 @@ export class AKFormStatic extends AKElement {
</div>
<div class="links">
<slot name="link"></slot>
</div>
`;
</div>`;
}
}
@@ -138,8 +94,7 @@ export const FlowUserDetails: LitFC<FlowUserDetailsProps> = ({ challenge }) => {
[pendingUserAvatar, pendingUser, flowInfo],
() =>
html`<ak-form-static
class="pf-c-form__group"
avatar=${ifPresent(pendingUserAvatar)}
.avatar=${ifPresent(pendingUserAvatar)}
username=${ifPresent(pendingUser)}
>
${flowInfo?.cancelUrl

View File

@@ -6,45 +6,60 @@ import { AKElement } from "#elements/Base";
import { FooterLink } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, html } from "lit";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import PFList from "@patternfly/patternfly/components/List/list.css";
const styles = css`
.pf-c-list a {
color: unset;
}
ul.pf-c-list.pf-m-inline {
justify-content: center;
padding: 0;
column-gap: var(--pf-global--spacer--xl);
row-gap: var(--pf-global--spacer--md);
}
`;
/**
* @part list - The list element containing the links
* @part list-item - Each item in the list, including the "Powered by authentik" item
* @part list-item-link - The link element for each item, if applicable
*/
@customElement("ak-brand-links")
export class BrandLinks extends AKElement {
static styles = [PFList, styles];
/**
* Rendering in the light DOM ensures consistent styling across some of the
* more complex flow environments, such as...
*
* - When JavaScript is not available, such as on error pages.
* - During the initial loading of the page, before the web components are fully initialized.
* - After the flow executor has initialized, to avoid repaint issues.
*/
protected createRenderRoot(): HTMLElement | DocumentFragment {
return this;
}
@property({ type: Array, attribute: false })
public links: FooterLink[] = globalAK().brand.uiFooterLinks || [];
render() {
return html`<ul aria-label=${msg("Site links")} class="pf-c-list pf-m-inline" part="list">
${map(this.links, (link) => {
const links = [
...this.links,
{
name: msg("Powered by authentik"),
href: null,
},
];
return html`<ul
aria-label=${msg("Site links")}
class="pf-c-list pf-m-inline"
part="list"
data-count=${links.length}
>
${map(links, (link, idx) => {
const children = sanitizeHTML(BrandedHTMLPolicy, link.name);
if (link.href) {
return html`<li><a href="${link.href}">${children}</a></li>`;
}
return html`<li part="list-item">
<span>${children}</span>
return html`<li
part="list-item"
data-index=${idx}
data-kind=${link.href ? "link" : "text"}
data-track-name=${idx === 0 ? "start" : idx === links.length - 1 ? "end" : idx}
>
${link.href
? html`<a part="list-item-link" href=${link.href}>${children}</a>`
: children}
</li>`;
})}
<li part="list-item"><span>${msg("Powered by authentik")}</span></li>
</ul>`;
}
}

View File

@@ -42,9 +42,13 @@ export class FlowCard extends AKElement {
// No title if the challenge doesn't provide a title and no custom title is set
let title: null | SlottedTemplateResult = null;
if (this.hasSlotted("title")) {
title = html`<h1 class="pf-c-title pf-m-3xl"><slot name="title"></slot></h1>`;
title = html`<h1 class="pf-c-title pf-m-3xl ak-m-clamped">
<slot name="title"></slot>
</h1>`;
} else if (this.challenge?.flowInfo?.title) {
title = html`<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo.title}</h1>`;
title = html`<h1 class="pf-c-title pf-m-3xl ak-m-clamped">
${this.challenge.flowInfo.title}
</h1>`;
}
const footer = this.hasSlotted("footer") ? html`<slot name="footer"></slot>` : null;
const footerBand = this.hasSlotted("footer-band")

View File

@@ -1,22 +1,20 @@
.authenticator-button {
/* compatibility-mode-fix */
& {
align-items: center;
width: 100%;
display: grid;
grid-template-columns: minmax(auto, 2rem) minmax(33%, max-content);
gap: var(--pf-global--spacer--lg);
}
.authenticator-button,
ak-stage-authenticator-validate.style-scope .authenticator-button {
align-items: center;
width: 100%;
display: grid;
grid-template-columns: minmax(auto, 2rem) minmax(33%, max-content);
gap: var(--pf-global--spacer--lg);
&:hover {
background-color: var(--pf-global--BackgroundColor--200);
}
}
i {
font-size: var(--pf-global--icon--FontSize--lg);
}
i {
font-size: var(--pf-global--icon--FontSize--lg);
}
.content {
text-align: left;
.content {
text-align: left;
}
}

View File

@@ -1,9 +1,11 @@
:host {
:host,
ak-stage-captcha.style-scope {
--captcha-background-to: var(--pf-global--BackgroundColor--light-100);
--captcha-background-from: var(--pf-global--BackgroundColor--light-300);
}
:host([theme="dark"]) {
:host([theme="dark"]),
ak-stage-captcha[theme="dark"].style-scope {
--captcha-background-to: var(--ak-dark-background-light);
--captcha-background-from: var(--pf-global--BackgroundColor--300);
}

View File

@@ -421,7 +421,7 @@ export class IdentificationStage extends BaseStage<
? html`
<p>
${msg(
"Enter the email associated with your account, and we'll send you a link to reset your password.",
"Enter the email address or username associated with your account.",
)}
</p>
`

View File

@@ -1,9 +1,8 @@
fieldset[name="login-sources"] {
--ak-c-login-sources-padding-inline: var(--pf-global--spacer--xl);
/* compatibility-mode-fix */
fieldset[name="login-sources"],
ak-stage-identification.style-scope fieldset[name="login-sources"] {
& {
--ak-c-login-sources-padding-inline: var(--pf-global--spacer--xl);
flex: 1 1 auto;
display: flex;
flex-flow: row wrap;
@@ -65,9 +64,12 @@ fieldset[name="login-sources"] {
}
}
:host([theme="dark"]) fieldset[name="login-sources"] .pf-c-button__icon {
img,
.pf-c-button__icon .fas {
filter: invert(1);
:host([theme="dark"]),
ak-stage-identification[theme="dark"].style-scope {
fieldset[name="login-sources"] .pf-c-button__icon {
img,
.pf-c-button__icon .fas {
filter: invert(1);
}
}
}

View File

@@ -33,6 +33,12 @@
--pf-global--BackgroundColor--100: var(--pf-global--BackgroundColor--light-100);
}
.pf-m-title {
.pf-m-3xl.ak-m-clamped {
--pf-c-title--m-3xl--FontSize: clamp(1rem, var(--pf-global--FontSize--3xl), 7dvw);
}
}
.pf-m-monospace {
font-family: var(--pf-global--FontFamily--monospace);

View File

@@ -24,6 +24,7 @@
/* #region authentik extensions */
/* #region Root */
:root {
--ak-accent: #fd4b2d;
@@ -32,8 +33,9 @@
--ak-dark-background-light: #1c1e21;
--ak-dark-background-lighter: #2b2e33;
--ak-global--background-contrast: var(--pf-global--Color--100);
--ak-global--background-contrast-Filter: drop-shadow(
--ak-global--BackgroundColorContrast--100: var(--pf-global--Color--light-100);
--ak-global--BackgroundContrastFilter: drop-shadow(
0 0 2px
var(--ak-locale-select--ShadowBlendColor, var(--pf-global--BackgroundColor--dark-200))
);
@@ -42,4 +44,8 @@
--ak-sidebar--minimum-auto-width: 80rem;
}
html[data-theme="dark"] {
--ak-global--BackgroundColorContrast--100: var(--pf-global--palette--black-150);
}
/* #endregion */

View File

@@ -1,41 +1,39 @@
/**
* @file Patternfly Login component overrides and customizations.
*
* These styles are not as simple as they may seem at first glance.
* The overlap between the concept of the login page, the flow executor,
* and Django-provided templates means that these styles need to be flexible.
*
* - The initial render of the login page is server-side rendered.
* - The use of ShadyDOM in the flow executor means that styles need to be compatible with both Shadow DOM and Light DOM contexts.
* - The layout must adapt for mobile, tablet, and desktop.
* - And dark and light themes must work while allowing for user provided style overrides.
* - These styles have a unique relationship with the styles in `FlowExecutor.css` and `static.global.css`.
*
* All that said, we generally follow Patternfly's structure, save for the mobile layout which is unique to our implementation.
*/
/* #region Login Component */
/* compatibility-mode-fix */
.pf-c-login.pf-c-login {
--ak-c-login--PaddingMax: 8dvw;
--ak-c-login--padding: clamp(
var(--pf-global--spacer--md),
var(--pf-global--spacer--2xl),
var(--ak-c-login--PaddingMax)
);
--ak-c-login__main--brand-PaddingMin: var(--pf-global--spacer--xs);
--ak-c-login__main--brand-PaddingIdeal: 5rem;
--ak-c-login__main--brand-PaddingMax: 15dvh;
--ak-c-login__footer--PaddingBlock: var(--pf-global--spacer--md);
--ak-c-login--MaxWidth: 35rem;
--ak-c-login__main-ColumnWidth: minmax(
min(100%, var(--ak-c-login--MaxWidth)),
var(--ak-c-login--MaxWidth)
);
--pf-c-login__main-body--PaddingBottom: 0;
--ak-c-login__main--footer-PaddingMin: var(--pf-global--spacer--xs);
--ak-c-login__main--footer-PaddingIdeal: 3rem;
--ak-c-login__main--footer-PaddingMax: 9dvh;
--pf-c-login__main-footer--PaddingBottom: clamp(
var(--ak-c-login__main--footer-PaddingMin),
var(--ak-c-login__main--footer-PaddingIdeal),
var(--ak-c-login__main--footer-PaddingMax)
);
--pf-c-login__main-footer-band--BackgroundColor: transparent;
/**
* Take note, we avoid applying Patternfly styles to custom elements directly:
*
* ```html
* <ak-button class="pf-c-button pf-m-primary">Click me</ak-button>
* ```
*
* However, the flow executor requires that the `.pf-c-login` class be applied to the host element.
* This allows for some careful enhancements to the login page without depending on the Shadow DOM,
* with some caveats:
*
* - Custom variables should be defined in static.global.css and used here to allow the user to override them as needed.
* - The data-layout attribute is applied to this element and the .pf-c-login__main element,
* allowing for some layout-specific styles to be applied.
* - The pf-c-login__footer is slotted into the flow executor, and requires a
* delicate balance of inheriting styles from the login page while ensuring sufficient contrast against the background.
*/
.pf-c-login {
flex: 1 1 auto;
padding: 0;
@@ -63,7 +61,7 @@
&::before {
display: block;
content: "";
background-color: var(--ak-c-login--BackgroundColorOverlay, transparent);
background-color: var(--ak-c-login--BackgroundColorOverlay, transparent) !important;
z-index: -1;
height: 100%;
pointer-events: none;
@@ -81,7 +79,7 @@
}
@media (max-width: 35rem) or (max-height: 17.5rem) {
--ak-c-login--BackgroundColorOverlay: var(--pf-c-login__main--BackgroundColor);
--ak-c-login--BackgroundColorOverlay: var(--ak-c-login__main--BackgroundColor);
}
}
@@ -89,16 +87,11 @@
grid-area: main;
}
[data-theme="dark"] .pf-c-login,
:host([theme="dark"]) .pf-c-login {
--pf-c-login__main--BackgroundColor: var(--pf-global--BackgroundColor--100);
}
/* #region Page Header */
.pf-c-login__header {
grid-area: header;
padding-inline: calc(var(--ak-c-login--padding) / 2);
padding-inline: calc(var(--ak-c-login--spacer) / 2);
align-self: start;
display: grid;
@@ -107,7 +100,7 @@
/* #endregion */
/* #region Page Footer */
/* #region Main Footer */
/* compatibility-mode-fix */
.pf-c-login__main-footer .pf-c-button__icon {
@@ -125,10 +118,6 @@
/* #region Card Main */
.pf-c-login__main {
--pf-c-login__container--PaddingLeft: 0 !important;
--pf-c-login__container--PaddingRight: 0 !important;
--ak-c-login__main--BoxShadow: var(--pf-global--BoxShadow--md);
box-shadow: var(--ak-c-login__main--BoxShadow) !important;
grid-area: main;
@@ -136,17 +125,36 @@
position: relative;
max-width: var(--ak-c-login--MaxWidth);
min-height: calc(var(--ak-c-login--MaxWidth) * 0.8);
min-height: var(--ak-c-login__main--MinHeight, unset);
display: flex;
flex-flow: column;
justify-content: space-between;
.slotted-content {
slot[name="placeholder"] {
position: relative;
flex: 1 1 auto;
}
/**
* Note that the use of `slot` as attribute selector is intentional.
* We're checking for the presence of an element attempting to slot itself as the placeholder.
*
* This approach allows us to handle some of the lesser-intuitive combinations
* of whether we're in a shadow DOM and how to gracefully transition to a
* post-JavaScript state without any awkward repaints.
* We're also interested in whether the slot itself is within the main element,
* as it indicates that the placeholder content is slotted and a shadow DOM is present.
*
* This ensures the height remains consistent from the initial render,
* preventing layout shifts when the placeholder is replaced with the actual content.
*/
&:has([slot="placeholder"]),
&:has(slot[name="placeholder"]) {
--ak-c-login__main--MinHeight: calc(var(--ak-c-login--MaxWidth) * 0.8);
}
@media (max-width: 35rem) or (max-height: 17.5rem) {
--ak-c-login__main--BoxShadow: none;
}
@@ -156,15 +164,6 @@
/* #region Main Header */
.pf-c-login__main-header {
padding-inline: var(--ak-c-login--padding);
padding-block: clamp(var(--pf-global--spacer--xs), 6dvw, var(--pf-global--spacer--lg));
.pf-c-title {
font-size: clamp(1rem, var(--pf-c-title--m-3xl--FontSize), 7dvw);
}
}
.pf-c-login__main-header.pf-c-brand {
--ak-c-login__main-padding-block-start: clamp(
var(--ak-c-login__main--brand-PaddingMin),
@@ -172,13 +171,13 @@
var(--ak-c-login__main--brand-PaddingMax)
);
padding-inline: calc(var(--ak-c-login--padding) / 4);
padding-inline: calc(var(--ak-c-login--spacer) / 4);
padding-block-start: calc(
var(--ak-c-login__main-padding-block-start) - var(--ak-c-login__footer--PaddingBlock)
);
padding-bottom: var(--pf-global--spacer--xs);
padding-block-end: calc(var(--ak-c-login--padding) / 2);
padding-block-end: calc(var(--ak-c-login--spacer) / 2);
display: flex;
justify-content: center;
@@ -203,7 +202,6 @@
.pf-c-login__main-body {
flex: 1 1 auto;
padding-inline: var(--ak-c-login--padding);
}
/* #endregion */
@@ -242,7 +240,7 @@
/* #region Layout variations */
.pf-c-login[data-layout$="frame_background"] {
--ak-c-login--BackgroundColorOverlay: var(--pf-c-login__main--BackgroundColor);
--ak-c-login--BackgroundColorOverlay: var(--ak-c-login__main--BackgroundColor);
}
.pf-c-login[data-layout^="sidebar_left"] {
@@ -292,19 +290,13 @@
.pf-c-login[data-layout^="sidebar"] {
--ak-c-login--MaxWidth: 36rem;
--ak-c-login--BackgroundColorOverlay: var(--pf-c-login__main--BackgroundColor);
--ak-c-login--BackgroundColorOverlay: var(--ak-c-login__main--BackgroundColor);
--ak-c-login__footer--Color: var(--ak-c-login__main--Color);
.pf-c-login__main {
height: 100%;
justify-content: normal;
}
.pf-c-login__footer {
color: inherit;
flex: 1 1 auto;
justify-content: end;
width: 100%;
}
}
.pf-c-login[data-layout^="sidebar_left"] {
@@ -328,28 +320,55 @@
/* #endregion */
/* #region Page Footer */
/**
* The footer must respect a few constraints to ensure it remains legible::after
*
* - The mobile layout should have the same background color as the login main content.
* - Aside from CSS variables footer styles should not be applied in the static.global.css file,
* This may seem unnecessary, but PatternFly's own base styles for `pf-c-*` elements
*. will override styles in an uphill battle against user overrides.
*/
.pf-c-login__footer {
--pf-global--Color--100: var(--pf-global--Color--light-100) !important;
grid-area: footer;
flex: 0 0 auto;
padding-block: var(--ak-c-login__footer--PaddingBlock);
display: flex;
flex-direction: column;
align-self: end;
justify-content: center;
padding-inline: var(--pf-global--spacer--xl) !important;
padding-block: var(--ak-c-login__footer--PaddingBlock) !important;
align-self: end;
flex: 0 0 auto;
min-height: calc((var(--ak-c-login__footer--PaddingBlock) * 2) + 1rem);
line-height: var(--pf-global--LineHeight--md);
min-height: calc(
(var(--ak-c-login__footer--PaddingBlock) * 2) + (var(--pf-global--LineHeight--md) * 1rem)
);
/* Only applicable to the smallest of mobile viewports. */
max-width: 100dvw;
overflow: hidden;
color: var(--ak-c-login__footer--Color);
@media (max-width: 35rem) {
color: var(--pf-global--Color--200);
--ak-c-login__footer--Color: var(--ak-c-login__main--Color);
}
@media (min-width: 35rem) and (min-height: 17.5rem) {
filter: var(--ak-global--background-contrast-Filter);
filter: var(--ak-global--BackgroundContrastFilter);
}
}
/**
* The dark modifier is used in stacked layout to ensure sufficient contrast against the darker background.
* This may appear unnecessary, but PF4's own login footer styles are not designed
* with our mobile layout in mind. this ensures that the footer remains legible
* even when the card is reduced in size and the background contrast is removed.
*/
.pf-c-login__footer {
@media (max-width: 35rem) {
--pf-global--Color--100: var(--pf-global--Color--dark-100) !important;
--pf-global--Color--200: var(--pf-global--Color--dark-200) !important;
}
}

View File

@@ -15,18 +15,126 @@
@import "#elements/locale/ak-locale-select.css";
@import "#flow/FlowExecutor.css";
.pf-c-login__main-body {
display: flex;
flex-flow: column;
/**
* @file Static global styles for authentik.
*
* Similar the base/globals.css file, this file is only injected in server templates
* that may not have the full web component support.
* If you're deciding on where to put a style, prefer a more specific file
* to avoid unnecessarily increasing the global scope of the style.
*/
.pf-c-form {
display: flex;
flex-flow: column;
flex: 1 1 auto;
justify-content: end;
/* #region Custom login variables */
:root {
--ak-c-login--PaddingMax: 8dvw;
--ak-c-login--spacer: clamp(
var(--pf-global--spacer--md),
var(--pf-global--spacer--2xl),
var(--ak-c-login--PaddingMax)
);
--ak-c-login--MaxWidth: 35rem;
--ak-c-login__main--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
--ak-c-login__main--Color: var(--pf-global--Color--dark-100);
--ak-c-login__main--brand-PaddingMin: var(--pf-global--spacer--xs);
--ak-c-login__main--brand-PaddingIdeal: 5rem;
--ak-c-login__main--brand-PaddingMax: 15dvh;
--ak-c-login__main-ColumnWidth: minmax(
min(100%, var(--ak-c-login--MaxWidth)),
var(--ak-c-login--MaxWidth)
);
--ak-c-login__main-header-PaddingBlock: clamp(
var(--pf-global--spacer--xs),
6dvw,
var(--pf-global--spacer--lg)
);
--ak-c-login__main-header-PaddingInline: var(--ak-c-login--spacer);
--ak-c-login__main--footer-PaddingMin: var(--pf-global--spacer--xs);
--ak-c-login__main--footer-PaddingIdeal: 3rem;
--ak-c-login__main--footer-PaddingMax: 9dvh;
--ak-c-login__main--BoxShadow: var(--pf-global--BoxShadow--md);
--ak-c-login__footer--PaddingBlock: var(--pf-global--spacer--md);
--ak-c-login__footer--Color: var(--ak-global--BackgroundColorContrast--100);
--ak-c-login__footer--ColumnGap: min(var(--pf-global--spacer--2xl), 2dvw);
--ak-c-login__footer--RowGap: var(--pf-global--spacer--md);
--ak-c-login__footer--Display: grid;
--ak-c-login__footer--MaxWidth: var(--ak-c-login--MaxWidth);
/* Gracefully degrade to the login max width if CSS size functions are not supported. */
--ak-c-login__footer--MaxWidth: min(100dvw, var(--ak-c-login--MaxWidth));
--ak-c-login__footer--TrackMin: max-content;
--ak-c-login__footer--TrackWidth: minmax(
var(--ak-c-login__footer--TrackMin),
var(--ak-c-login__footer--TrackMax)
);
--ak-c-login__footer--ItemMaxWidth: calc(
var(--ak-c-login__footer--MaxWidth) - var(--ak-c-login__footer--ColumnGap)
);
--ak-c-login__footer--ColumnCount: 4;
--ak-c-login__footer--TrackMax: calc(
(var(--ak-c-login__footer--MaxWidth) / var(--ak-c-login__footer--ColumnCount)) -
var(--ak-c-login__footer--ColumnGap)
);
@media (width <= 35rem) {
--ak-c-login__footer--TrackWidth: 1fr;
--ak-c-login__footer__list-item--FlexBasis: 100%;
}
}
[data-theme="dark"] .pf-c-login {
--ak-c-login__main--BackgroundColor: var(--pf-global--BackgroundColor--dark-100);
}
/* #endregion */
/* #region PF4 Login */
.pf-c-login {
--pf-c-login__main-header--PaddingTop: var(--ak-c-login__main-header-PaddingBlock);
--pf-c-login__main-header--PaddingBottom: var(--ak-c-login__main-header-PaddingBlock);
--pf-c-login__main-header--PaddingLeft: var(--ak-c-login__main-header-PaddingInline);
--pf-c-login__main-header--PaddingRight: var(--ak-c-login__main-header-PaddingInline);
--pf-c-login__main--BackgroundColor: var(--ak-c-login__main--BackgroundColor);
--pf-c-login__main-body--PaddingLeft: var(--ak-c-login--spacer);
--pf-c-login__main-body--PaddingRight: var(--ak-c-login--spacer);
--pf-c-login__main-body--PaddingBottom: 0;
--pf-c-login__main-footer--PaddingBottom: clamp(
var(--ak-c-login__main--footer-PaddingMin),
var(--ak-c-login__main--footer-PaddingIdeal),
var(--ak-c-login__main--footer-PaddingMax)
);
--pf-c-login__main-footer-band--BackgroundColor: transparent;
--pf-c-login__footer--c-list--xl--PaddingTop: 0;
--pf-c-login__footer--PaddingLeft: var(--pf-global--spacer--lg);
--pf-c-login__footer--PaddingRight: var(--pf-global--spacer--lg);
--pf-c-login__container--PaddingLeft: 0 !important;
--pf-c-login__container--PaddingRight: 0 !important;
}
/* #endregion */
/* #region Form */
/* Fallback form controls with minimal runtime expectations. */
.form-control-static {
margin-top: var(--pf-global--spacer--sm);
display: flex;
@@ -48,3 +156,61 @@
line-height: var(--pf-global--spacer--xl);
}
}
/* #endregion */
/* #region Flow Links */
[name="flow-links"] {
[part="list"],
&::part(list) {
--pf-c-list--m-inline--li--MarginRight: 0;
/* 3 entries is a unique scenario where 2 columns is visually balanced. */
&[data-count="3"] {
--ak-c-login__footer--ColumnCount: 2;
}
justify-content: center;
column-gap: var(--ak-c-login__footer--ColumnGap);
row-gap: var(--ak-c-login__footer--RowGap);
max-width: var(--ak-c-login__footer--MaxWidth);
place-items: center;
display: var(--ak-c-login__footer--Display);
grid-template-columns: repeat(
var(--ak-c-login__footer--ColumnCount),
var(--ak-c-login__footer--TrackWidth)
);
grid-template-rows:
[header] max-content
[main] max-content
[footer];
}
[part="list-item"],
&::part(list-item) {
/* CSS grid is preferred, but if the custom CSS overrides this, default to something reasonable. */
flex: 1 1 var(--ak-c-login__footer__list-item--FlexBasis, auto);
text-align: center;
max-width: var(--ak-c-login__footer--ItemMaxWidth);
&[data-kind="text"] {
&[data-track-name="start"] {
grid-column: 1 / -1;
}
&[data-track-name="end"] {
grid-column: 1 / -1;
}
}
}
[part="list-item-link"],
&::part(list-item-link) {
color: unset;
}
}
/* #endregion */

65
web/types/webcomponents.d.ts vendored Normal file
View File

@@ -0,0 +1,65 @@
/**
* @file Web component globals applied to the Window object.
*
* @see https://www.npmjs.com/package/@webcomponents/webcomponentsjs
*/
export {};
declare global {
type Booleanish = "true" | "false";
type WebComponentFlags = Record<string, Booleanish | boolean | Record<string, boolean>>;
interface WebComponents {
/**
* Flags that can be set on the `WebComponents` global to control the behavior of web components in the application.
* Typically, this is limited to the `webcomponents-loader`.
*/
flags?: WebComponentFlags;
}
interface ShadyDOM {
/**
* Forces the use of the Shady DOM polyfill, even in browsers that support native Shadow DOM.
* This can be useful for testing or to work around specific issues with native Shadow DOM in certain browsers.
*/
force?: boolean | Booleanish;
/**
* Prevents the patching of native Shadow DOM APIs when the Shady DOM polyfill is in use.
* This can be useful for debugging or to avoid conflicts with other libraries that also patch these APIs.
*/
noPatch?: boolean | Booleanish;
}
interface CustomElementRegistry {
/**
* An indication of whether the polyfill for web components is in use.
*/
readonly forcePolyfill?: Booleanish | boolean;
}
interface Window {
/**
* An object representing the state of web component support and configuration in the application.
*/
WebComponents?: Readonly<WebComponents>;
/**
* An object representing the configuration for the Shady DOM polyfill,
* which provides support for Shadow DOM in browsers that do not natively support it.
*/
ShadyDOM?: Readonly<ShadyDOM>;
/**
* A root path for loading web component polyfills. This is only applicable
*
* @remarks
* If you're using the loader on a page that enforces the `trusted-types`
* Content Security Policy, you'll need to allow the `webcomponents-loader`
* policy name so that the loader can dynamically create and insert a `<script>`
* for the polyfill bundle it selects based on feature detection. I
* f you set `WebComponents.root` (which is rare), it should be set to a {@linkcode TrustedScriptURL}
* for Trusted Types compatibility.
*/
root?: string | TrustedScriptURL;
}
}

View File

@@ -31,7 +31,7 @@ Keys prefixed with `goauthentik.io` are used internally by authentik and are sub
`pending_user` is used by multiple stages. In the context of most flow executions, it represents the data of the user that is executing the flow. This value is not set automatically, it is set via the [Identification stage](../../stages/identification/index.mdx).
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx) and others will use this value if it is set, and fallback to the request's users when possible.
Stages that require a user, such as the [Password stage](../../stages/password/index.md), the [Authenticator validation stage](../../stages/authenticator_validate/index.mdx), and others will use this value if it is set, and fall back to the request's user when possible.
#### `prompt_data` (Dictionary)
@@ -55,8 +55,6 @@ Stores the final redirect URL that the user's browser will be sent to after the
If _Show matched user_ is disabled, this key will hold the user identifier entered by the user in the identification stage.
Stores the final redirect URL that the user's browser will be sent to after the flow is finished executing successfully. This is set when an un-authenticated user attempts to access a secured application, and when a user authenticates/enrolls with an external source.
#### `application` (Application object)
When an unauthenticated user attempts to access a secured resource, they are redirected to an authentication flow. The application they attempted to access will be stored in the key attached to this object. For example: `application.github`, with `application` being the key and `github` the value.
@@ -151,7 +149,7 @@ Type the `pending_user` will be created as. Must be one of `internal`, `external
##### `user_backend` (string)
Set by the [Password stage](../../stages/password/index.md) after successfully authenticating in the user. Contains a dot-notation to the authentication backend that was used to successfully authenticate the user.
Set by the [Password stage](../../stages/password/index.md) after successfully authenticating the user. Contains a dot-notation to the authentication backend that was used to successfully authenticate the user.
##### `auth_method` (string)

View File

@@ -2,9 +2,9 @@
title: Default flows
---
When you create a new provider, you can select certain default flows that will be used with the provider and its associated application. For example, you can [create a custom flow](../index.md#create-a-custom-flow) that override the defaults configured on the brand.
When you create a new provider, you can select certain default flows that will be used with the provider and its associated application. For example, you can [create a custom flow](../index.md#create-a-custom-flow) that overrides the defaults configured on the brand.
If no default flow is selected when the provider is created, to determine which flow should be used authentik will first check if there is a default flow configured in the active [**Brand**](../../../../sys-mgmt/brands.md). If no default is configured there, authentik will go through all flows with the matching designation, sorted by `slug` and evaluate policies bound directly to the flows, and the first flow whose policies allow access will be picked.
If no default flow is selected when the provider is created, authentik will first check if there is a default flow configured in the active [**Brand**](../../../../sys-mgmt/brands/index.md). If no default is configured there, authentik will go through all flows with the matching designation, sorted by `slug`, evaluate policies bound directly to the flows, and pick the first flow whose policies allow access.
import DefaultFlowList from "../../flow/flow_list/\_defaultflowlist.mdx";

View File

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

View File

@@ -6,4 +6,4 @@ The user interface (/if/user/) uses a specialized flow executor to allow individ
Because the stages in a flow can change during its execution, be aware that configuring this executor to use any stage type other than Prompt or User Write will automatically trigger a redirect to the standard executor.
An admin can customize which fields can be changed by the user by updating the default-user-settings-flow, or copying it to create a new flow with a Prompt Stage and a User Write Stage. Different variants of your flow can be applied to different [Brands](../../../../sys-mgmt/brands.md) on the same authentik instance.
An admin can customize which fields can be changed by the user by updating the default-user-settings-flow, or copying it to create a new flow with a Prompt Stage and a User Write Stage. Different variants of your flow can be applied to different [Brands](../../../../sys-mgmt/brands/index.md) on the same authentik instance.

View File

@@ -4,7 +4,7 @@ title: Flows
Flows are a major component in authentik. In conjunction with stages and [policies](../../../customize/policies/index.md), flows are at the heart of our system of building blocks, used to define and execute the workflows of authentication, authorization, enrollment, and user settings.
There are over a dozen default, out-of-the box flows available in authentik. Users can decide if they already have everything they need with the [default flows](../flow/examples/default_flows.md) or if they want to [create](#create-a-custom-flow) their own custom flow, using the Admin interface, Terraform, or via the API.
There are over a dozen default, out-of-the-box flows available in authentik. Users can decide if they already have everything they need with the [default flows](../flow/examples/default_flows.md) or if they want to [create](#create-a-custom-flow) their own custom flow, using the Admin interface, Terraform, or via the API.
A flow is a method of describing a sequence of stages. A stage represents a single verification or logic step. By connecting a series of stages within a flow (and optionally attaching policies as needed) you can build a highly flexible process for authenticating users, enrolling them, and more.
@@ -54,7 +54,7 @@ To create a flow, follow these steps:
After creating the flow, you can then [bind specific stages](../stages/index.md#bind-a-stage-to-a-flow) to the flow and [bind policies](../../../customize/policies/working_with_policies.md) to the flow to further customize the user's log in and authentication process.
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../sys-mgmt/brands.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
To determine which flow should be used, authentik will first check which default authentication flow is configured in the active [**Brand**](../../../sys-mgmt/brands/index.md). If no default is configured there, the policies in all flows with the matching designation are checked, and the first flow with matching policies sorted by `slug` will be used.
## Flow configuration options
@@ -78,9 +78,9 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
**Behavior settings**:
- **Compatibility mode**: Toggle this option on to increase compatibility with password managers and mobile devices. Password managers like [1Password](https://1password.com/), for example, don't need this setting to be enabled, when accessing the flow from a desktop browser. However accessing the flow from a mobile device might necessitate this setting to be enabled.
- **Compatibility mode**: Toggle this option on to increase compatibility with password managers and mobile devices. Password managers like [1Password](https://1password.com/), for example, don't need this setting to be enabled when accessing the flow from a desktop browser. However, accessing the flow from a mobile device might necessitate this setting to be enabled.
The technical reasons for this settings' existence is due to the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
The technical reason for this setting's existence is the JavaScript libraries we're using for the default flow interface. These interfaces are implemented using [Lit](https://lit.dev/), which is a modern web development library. It uses a web standard called ["Shadow DOMs"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which makes encapsulating styles simpler. Due to differences in Browser APIs, many password managers are not compatible with this technology.
When the compatibility mode is enabled, authentik uses a polyfill which emulates the Shadow DOM APIs without actually using the feature, and instead a traditional DOM is rendered. This increases support for password managers, especially on mobile devices.
@@ -95,7 +95,7 @@ import Defaultflowlist from "../flow/flow_list/\_defaultflowlist.mdx";
- **Layout**: select how the UI displays the flow when it is executed; with stacked elements, content left or right, and sidebar left or right.
- **Background**: optionally, select a background image for the UI presentation of the flow. This overrides any default background image configured in the [Branding settings](../../../sys-mgmt/brands.md#branding-settings).
- **Background**: optionally, select a background image for the UI presentation of the flow. This overrides any default background image configured in the [Branding settings](../../../sys-mgmt/brands/index.md#branding-settings).
## Edit or delete a flow

View File

@@ -80,4 +80,4 @@ For detailed instructions, refer to Google documentation.
4. Click **Finish**.
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the users' browser.
After creating the stage, it can be used in any flow. Compared to other Authenticator stages, this stage does not require enrollment. Instead of adding an [Authenticator Validation Stage](../authenticator_validate/index.mdx), this stage only verifies the user's browser.

View File

@@ -66,7 +66,7 @@ return {
## Verify only
To only verify the validity of a users' phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
To only verify the validity of a user's phone number, without saving it in an easily accessible way, you can enable this option. Phone numbers from devices enrolled through this stage will only have their hashed phone number saved. These devices can also not be used with the [Authenticator validation](../authenticator_validate/index.mdx) stage.
## Limiting phone numbers

View File

@@ -26,7 +26,7 @@ Keep in mind that when using Code-based devices (TOTP, Static and SMS), values l
#### Less-frequent validation
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the users devices within the selected classes are checked.
You can configure this stage to only ask for MFA validation if the user hasn't authenticated themselves within a defined time period. To configure this, set _Last validation threshold_ to any non-zero value. Any of the user's devices within the selected classes are checked.
#### Passwordless authentication

View File

@@ -97,7 +97,7 @@ See the [Envoy mTLS documentation](https://www.envoyproxy.io/docs/envoy/latest/s
#### No reverse proxy
When using authentik without a reverse proxy, select the certificate authorities in the corresponding [brand](../../../../sys-mgmt/brands.md#client-certificates) for the domain, under **Other global settings**.
When using authentik without a reverse proxy, select the certificate authorities in the corresponding [brand](../../../../sys-mgmt/brands/index.md#client-certificates) for the domain, under **Other global settings**.
## Stage configuration

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