Compare commits

...

53 Commits

Author SHA1 Message Date
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
212 changed files with 2470 additions and 990 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

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

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2026.2.0-rc1"
VERSION = "2026.2.0-rc5"
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
@@ -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):

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

@@ -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,7 +110,15 @@ 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 used to be 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
# New licenses are generated with `algorithm="ES384"` and signed with `secp384r1`.
# The last license will run out by March 2027.
# TODO: remove this in March 2027.
ECAlgorithm._validate_curve = lambda *_: True
body = from_dict(
LicenseKey,
decode(
@@ -125,6 +134,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

@@ -4,6 +4,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.providers.saml.views.metadata import MetadataDownload
urlpatterns = [
path(
@@ -11,6 +12,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

@@ -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

@@ -185,6 +185,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

@@ -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

@@ -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

@@ -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.0-rc5 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

@@ -1 +1 @@
2026.2.0-rc1
2026.2.0-rc5

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

@@ -18,7 +18,7 @@ Parameters:
Description: authentik Docker image
AuthentikVersion:
Type: String
Default: 2026.2.0-rc1
Default: 2026.2.0-rc5
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.0-rc5}
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.0-rc5}
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.0-rc5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2026.2.0-rc1",
"version": "2026.2.0-rc5",
"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.0-rc5",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2026.2.0-rc1"
version = "2026.2.0-rc5"
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.0-rc5
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.0rc5"
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.0-rc5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/web",
"version": "2026.2.0-rc1",
"version": "2026.2.0-rc5",
"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.0-rc5",
"license": "MIT",
"private": true,
"scripts": {

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

@@ -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,18 +1,15 @@
fieldset[name="login-sources"] {
fieldset[name="login-sources"],
ak-stage-identification.style-scope fieldset[name="login-sources"] {
--ak-c-login-sources-padding-inline: var(--pf-global--spacer--xl);
/* compatibility-mode-fix */
flex: 1 1 auto;
display: flex;
flex-flow: row wrap;
justify-content: center;
gap: var(--pf-global--spacer--sm);
& {
flex: 1 1 auto;
display: flex;
flex-flow: row wrap;
justify-content: center;
gap: var(--pf-global--spacer--sm);
padding-inline: var(--ak-c-login-sources-padding-inline) !important;
padding-block-start: var(--pf-global--spacer--md) !important;
}
padding-inline: var(--ak-c-login-sources-padding-inline) !important;
padding-block-start: var(--pf-global--spacer--md) !important;
.source-button {
display: flex;
@@ -65,9 +62,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 {
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

View File

@@ -6,7 +6,7 @@ This is a generic password prompt which authenticates the current `pending_user`
## Passwordless login
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the users device, which is documented here.
There are two different ways to configure passwordless authentication; you can follow the instructions [here](../authenticator_validate/index.mdx#passwordless-authentication) to allow users to directly authenticate with their authenticator (only supported for WebAuthn devices), or dynamically skip the password stage depending on the user's device, which is documented here.
If you want users to be able to pick a passkey from the browser's passkey/autofill UI without entering a username first, configure **Passkey autofill (WebAuthn conditional UI)** in the [Identification stage](../identification/index.mdx#passkey-autofill-webauthn-conditional-ui). This is separate from configuring a dedicated passwordless flow, and can be used alongside normal identification flows.

View File

@@ -8,7 +8,7 @@ The device code flow is also known as _device flow_ or _device authorization gra
### Requirements
This device flow is only possible if the active [brand](../../../sys-mgmt/brands.md) has a device code flow configured. This flow is run _after_ the user logs in, and before the user authenticates.
This device flow is only possible if the active [brand](../../../sys-mgmt/brands/index.md) has a device code flow configured. This flow is run _after_ the user logs in, and before the user authenticates.
authentik does not include a default flow for this use case, so it is necessary to create a new one with a **Designation** of `Stage Configuration`.
@@ -25,6 +25,17 @@ client_id=application_client_id&
scope=openid email my-other-scope
```
Alternatively the client id may be sent via the HTTP Authorization header:
```http
POST /application/o/device/ HTTP/1.1
Host: authentik.company
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer YXBwbGljYXRpb25fY2xpZW50X2lkOg==
scope=openid email my-other-scope
```
The response contains the following fields:
- `device_code`: Device code, which is the code kept on the device

View File

@@ -1,5 +1,5 @@
---
title: OAuth2/OpenID Connect front-channel and back-channel logout
title: Front-channel and back-channel logout
description: Configure front-channel and back-channel logout for OAuth2/OpenID Connect providers
authentik_version: "2025.8.0"
authentik_preview: true

View File

@@ -23,7 +23,7 @@ OAuth 2.0 is an authorization protocol that allows an application (the RP) to de
1. An authorization request is prepared by the RP and contains parameters for its implementation of OAuth and which data it requires, and then the User's browser is redirected to that URL.
2. The RP sends a request to authentik in the background to exchange the access code for an access token (and optionally a refresh token).
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the users's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorised (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
In detail, with OAuth2 when a user accesses the application (the RP) via their browser, the RP then prepares a URL with parameters for the OpenID Provider (OP), which the user's browser is redirected to. The OP authenticates the user and generates an authorization code. The OP then redirects the client (the user's browser) back to the RP, along with that authorization code. In the background, the RP then sends that same authorization code in a request authenticated by the `client_id` and `client_secret` to the OP. Finally, the OP responds by sending an Access Token saying this user has been authorized (the RP is recommended to validate this token using cryptography) and optionally a Refresh Token.
The image below shows a typical authorization code flow.
@@ -183,6 +183,28 @@ This does _not_ apply to special scopes, as those are not configurable in the pr
- `user:email`: Allows read-only access to `/user`, including email address
- `read:org`: Allows read-only access to `/user/teams`, listing all the user's groups as teams.
### Email scope verification
In authentik releases prior to 2025.10, the email scope always set the `email_verified` claim to `True`. Since authentik does not have a single authoritative source to determine whether a user's email is actually verified, asserting this claim could have security implications. As of 2025.10, `email_verified` now defaults to `False`.
Some applications require this claim to be `True` in order to authenticate users. In those cases, you can create a custom email scope mapping (**Customization** > **Property Mappings**) that always returns `email_verified` as `True`:
```python
return {
"email": request.user.email,
"email_verified": True
}
```
For greater security guarantees, verify users' email addresses and store the verification status as a user attribute (for example, `email_verified` set to `True` or `False`). You can then configure the scope mapping to return this value dynamically:
```python
return {
"email": request.user.email,
"email_verified": request.user.attributes.get("email_verified", False)
}
```
## Signing & Encryption
[JWTs](https://jwt.io/introduction) created by authentik will always be signed.

View File

@@ -8,6 +8,6 @@ The [WebFinger protocol](https://webfinger.net/) allows for the discovery of inf
## authentik WebFinger support
authentik provides a WebFinger endpoint when the **Default application** setting uses an OIDC provider. Instructions on how to set a **Default application** can be found in the [authentik Branding documentation](../../../sys-mgmt/brands.md#external-user-settings).
authentik provides a WebFinger endpoint when the **Default application** setting uses an OIDC provider. Instructions on how to set a **Default application** can be found in the [authentik Branding documentation](../../../sys-mgmt/brands/index.md#external-user-settings).
The WebFinger endpoint is available at: `https://authentik.company/.well-known/webfinger` (where authentik.company is the FQDN of your authentik instance)

View File

@@ -15,15 +15,7 @@ Scope mappings are used by the OAuth2 provider to map information from authentik
:::info Default value for `email_verified`
By default, authentik sets the `email_verified` claim to `False`, since it has no way to confirm whether a user's email is verified. Setting this claim to `True` by default could introduce unintended security risks.
Be aware that some applications might require this claim to be true to successfully authenticate users. In this case you should create a custom email scope mapping that returns `email_verified` as `True`, using the following expression:
```
return {
"email": user.email,
"email_verified": True,
}
```
Be aware that some applications might require this claim to be true to successfully authenticate users. See [Email scope verification](../oauth2/index.mdx#email-scope-verification) for more information.
:::
## Skip objects during synchronization

View File

@@ -0,0 +1,87 @@
---
title: Create a Remote Access Control (RAC) provider
---
For an overview of Remote Access Control (RAC), see the [RAC provider](./index.md) documentation.
You can also watch our video on YouTube for setting up RAC:
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts?start=22" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
## Workflow to create an RAC provider
Follow this workflow to create and configure an RAC provider:
1. Create a RAC provider and application pair.
2. Create RAC property mappings (that define the access credentials to each remote machine).
3. Create endpoints for each remote machine you want to connect to.
4. Create an RAC outpost to service the provider.
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
### Create a RAC provider and application pair
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **Create with Provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to create only the provider by navigating to **Applications** > **Providers** and clicking **Create**.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications** > **Applications** and click **Create with Provider** to create an application and provider pair.
3. On the **New application** page, define the application details, and then click **Next**.
4. Select the **RAC** provider type, and then click **Next**.
5. On the **Configure Remote Access Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
### Create RAC property mappings
Next, you need to add property mappings for each remote machine you want to access. RAC property mappings can be used to pass the access credentials and connection settings of the remote machine.
Refer to the [RAC Credentials Prompt](./rac_credentials_prompt.md) and [RAC SSH Public Key Authentication](./rac-public-key.md) documentation for alternative methods of handling RAC authentication.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Customization** > **Property Mappings**, and click **Create**.
3. Select **RAC Provider Property Mapping** as the property mapping type, and then click **Next**.
4. On the **Create RAC Provider Property Mapping** page, provide the following configuration settings:
- **Name**: provide a name for the property mapping
- Under **General settings**:
- **Username**: the username for the remote machine
- **Password**: the password for the remote machine
- Under **Advanced settings**:
- **Expression _(optional)_**: define other connection settings to be used, such as an SSH key. For more information, refer to the [Connection settings](./index.md#connection-settings) documentation.
5. Click **Finish**.
### Create endpoints for the provider
Then, you need to create an endpoint corresponding to each remote machine you want to connect to. Endpoints define the IP address, port, protocol, and other settings used for connecting to a remote machine.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers**.
3. Click the **Edit** button on the RAC provider that you previously created.
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
- **Provider Name** (endpoint name): define a name for the endpoint
- **Protocol**: select the appropriate protocol
- **Host**: enter the host name or IP address of the remote machine. Optionally include the port.
- **Maximum concurrent connections**: select a value or use `-1` to disable the limitation
- **Property mappings**: select either the property mapping that you previously created, or use one of the default RAC property mappings
- **Advanced settings _(optional)_**: define other connection settings to be used. For more information, refer to the [Connection settings](./index.md#connection-settings) documentation
5. Click **Create**.
### Create an RAC outpost
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Outposts**.
3. Click **Create** and set the following values:
- **Name**: define a name for the outpost.
- **Type**: `RAC`
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
- **Applications**: select the RAC application that you previously created.
- **Advanced settings _(optional)_**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
4. Click **Create** to save your new outpost.
## Access the remote machine
To verify your configuration and access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page, click the **Remote Access** application to start a secure session on the remote machine in your web browser.
If you defined multiple endpoints, click the endpoint for the remote machine that you want to access.

View File

@@ -1,88 +0,0 @@
---
title: Create a Remote Access Control (RAC) provider
---
The Remote Access Control (RAC) provider is a highly flexible feature for accessing remote machines.
For overview information, see the [RAC provider](./index.md) documentation. You can also view our video on YouTube for setting up RAC.
<iframe width="560" height="315" src="https://www.youtube.com/embed/9wahIBRV6Ts?start=22" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>
## Overview workflow to create an RAC provider
The typical workflow to create and configure a RAC provider is:
1. Create an application and provider.
2. Create property mappings (that define the access credentials to each remote machine).
3. Create an endpoint for each remote machine you want to connect to.
4. Create an RAC outpost to service the provider.
Depending on whether you are connecting using RDP, SSH, or VNC, the exact configuration choices will differ, but the overall workflow applies to all RAC connections.
### Create an application and RAC provider
The first step is to create the RAC application and provider pair.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Applications** and click **Create with provider**.
3. Follow these [instructions](../../applications/manage_apps.mdx#create-an-application-and-provider-pair) to create your RAC application and provider.
### Create RAC property mappings
Next, you need to add property mappings for each remote machine you want to access. Property mappings allow you to pass information to external applications, and with RAC they are used to pass the host name, IP address, and access credentials of the remote machine.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Customization** > **Property Mappings** and click **Create**.
- **Select Type**: `RAC Provider Property Mapping`
- **Create RAC Property Mapping**:
- **Name**s: define a name for the property mapping, perhaps include the type of connection (RDP, SSH, VNC)
- **General settings**:
- **Username**: the username for the remote machine
- **Password**: the password for the remote machine
- **RDP settings**:
- **Ignore server certificate**: select **Enabled** (Depending on the setup of your RDP Server, it might be required to enable this setting.)
- **Enable wallpaper**: optional
- **Enable font smoothing**: optional
- **Enable full window dragging**: optional
- Advanced settings:
- **Expressions**: optional, using Python you can define custom [expressions](../property-mappings/expression.mdx).
3. Click **Finish**.
### Create endpoints for the provider
Then, you need to create an endpoint for each remote machine. Endpoints are defined within providers; connections between the remote machine and authentik are enabled through communication between the provider's endpoint and the remote machine.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers**.
3. Click the **Edit** button on the RAC provider that you previously created.
4. On the Provider page, under **Endpoints**, click **Create**, and provide the following settings:
- **Name**: define a name for the endpoint, perhaps include the type of connection (RDP, SSH, VNC).
- **Protocol**: select the appropriate protocol.
- **Host**: enter the host name or IP address of the remote machine.
- **Maximum concurrent connections**: select a value or use `-1` to disable the limitation.
- **Property mapping**: select either the property mapping that you previously created, or use one of the default settings.
- **Advance settings**: (_optional_)
5. Click **Create**.
### Create an RAC outpost
The RAC provider requires the deployment of an [RAC Outpost](../../outposts/index.mdx).
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Outposts**.
3. Click **Create** and set the following values:
- **Name**: define a name for the outpost.
- **Type**: `RAC`
- **Integration**: select either Docker or Kubernetes, or optionally [manually deploy the outpost](../../outposts/index.mdx#outpost-integrations).
- **Applications**: select the RAC application that you previously created.
- **Advanced settings (optional)**: for further optional configuration settings, refer to [RAC Configuration](../../outposts/index.mdx#configuration).
4. Click Create to save your new outpost.
## Access the remote machine
To verify your configuration and then access the remote machine, go to the **User interface** of your authentik instance. On the **My applications** page click the **Remote Access** application and authentik then connects you to a secure session on the remote machine, in your web browser.
If you defined multiple endpoints, click the endpoint for the remote machine that you want to access.

View File

@@ -2,67 +2,85 @@
title: Remote Access Control (RAC) Provider
---
:::info
This provider requires the deployment of the [RAC Outpost](../../outposts/index.mdx).
:::
## About the Remote Access Control (RAC) Provider
The RAC provider allows users to access remote Windows, macOS, and Linux machines via [RDP](https://en.wikipedia.org/wiki/Remote_Desktop_Protocol)/[SSH](https://en.wikipedia.org/wiki/Secure_Shell)/[VNC](https://en.wikipedia.org/wiki/Virtual_Network_Computing). Just like other providers in authentik, the RAC provider is associated with an application that appears on a user's **My applications** page.
:::info
Note that with RAC, you create a single application and associated provider that serves to connect with _all remote machines_ that you want to configure for access via RAC.
:::
For instructions on creating a RAC provider, refer to the [Create a Remote Access Control (RAC) provider](./create-rac-provider.md) documentation. Alternatively, watch our ["Remote Access Control (RAC) in authentik" video on YouTube](https://www.youtube.com/watch?v=9wahIBRV6Ts).
For instructions on creating a RAC provider, refer to the [Managing RAC providers](./how-to-rac.md) documentation. You can also view our [video on YouTube](https://www.youtube.com/watch?v=9wahIBRV6Ts) for setting up a RAC.
## RAC components
For an example of how to configure RAC connections settings, refer to the [RAC SSH Public Key Authentication](./rac-public-key.md) documentation.
A RAC provider uses several components:
There are several components used with a RAC provider; let's take a closer look at the high-level configuration layout of these components and how they are managed using endpoints and connections.
```mermaid
architecture-beta
service application(mdi:application-outline)[Application]
service provider(mdi:application-cog-outline)[Provider]
service endpoint(mdi:network-pos)[Endpoint Settings]
service server(mdi:server)[authentik Server]
service outpost(mdi:server-plus)[RAC Outpost]
![](./rac-v3.png)
service machine(mdi:desktop-classic)[Remote Machine]
The provider-application pair, the authentik server, and the authentik API are typical to all configurations. With RAC, there are some new components, namely the endpoints, the outpost, and of course the target remote machines.
application:R --> L:provider
provider:B -- T:endpoint
provider:R --> L:server
server:R <--> L:outpost
outpost:B <--> T:machine
```
When a user starts the RAC application, the app communicates with the authentik server, which then connects to an instance of the outpost (the exact instance is selected dynamically based on connection load). After the outpost is selected, then the authentik server sends the outpost the instructions (based on the data you defined in the endpoint) required to connect to the remote machine.
When a user starts the RAC application, it communicates with the authentik server, which then connects to the RAC outpost and sends instructions (based on the endpoint data you defined) on how to connect to the remote machine.
After the connection to the remote machine is made, the outpost sends a message back to the authentik server (via websockets), and the web browser opens the websocket connection to the remote machine.
After connecting to the remote machine, the outpost sends a message back to the authentik server (via WebSockets), and the web browser opens the WebSocket connection to the remote machine.
### Endpoints
## Endpoints
Unlike other providers, where one provider-application pair must be created for each resource you wish to access, the RAC provider handles this slightly differently. For each remote machine (computer/server) that should be accessible, you create an _Endpoint_ object within a single RAC provider. (And as mentioned above, a single provider-application pair is used for all remote connections.)
Unlike other providers, where an application-provider pair is created for each resource you wish to access, RAC works differently. RAC uses a single application connected to one RAC provider. The RAC provider then has an _Endpoint_ object for each remote machine (computer/server) you want to connect to.
The _Endpoint_ object specifies the hostname/IP of the machine to connect to, as well as the protocol to use. Additionally it is possible to bind policies to _endpoint_ objects to restrict access. Users must have access to both the application that the RAC Provider is using as well as the individual endpoint.
The _Endpoint_ object specifies:
Configuration details such as credentials can be specified through _settings_, which can be specified on different levels and are all merged together when connecting:
- Hostname, IP address, and port of the remote machine
- Protocol to use: SSH, RDP, or VNC
- RDP connection settings
- [RAC Property mappings](#rac-property-mappings) to apply
- [Connection settings](#connection-settings) to apply
1. Default settings
2. Provider settings
3. Endpoint settings
4. Provider property mapping settings
5. Endpoint property mapping settings
6. Connection settings
Additionally, it is possible to bind policies to _Endpoint_ objects to restrict user access. To connect to a remote machine, users must have access to both the application that the RAC provider is using and the corresponding endpoint.
### Connection settings
## Connection management
Each connection is authorized through authentik policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
A new connection is created every time an RAC application/endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, you can configure connection expiry in the RAC provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
## RAC Property Mappings
Additionally, it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
You can create RAC property mappings via **Customization** > **Property Mappings**.
RAC property mappings allow you to configure the following settings:
- **Username**: the username for the remote machine
- **Password**: the password for the remote machine
- **Ignore Server certificate**: set whether the validity of the returned RDP server certificate will be ignored
- **Enable wallpaper**: enable/disable the desktop wallpaper of the RDP server
- **Enable font-smoothing**: enable/disable font-smoothing (anti-aliasing) on the RDP server
- **Enable full window dragging**: enable/disable whether the full content of a window is visible while moving it on the RDP server
- **Advanced settings**: set [connection settings](#connection-settings) via a Python expression
## Connection settings
The RAC provider utilises [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
For a full list of possible connection configurations, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).
Connection settings can include `username`, `password`, `domain`, `private-key`, `security`, `enable-audio`, and more.
RAC connection settings can be set via several methods:
For a full list of possible connection settings, see the [Apache Guacamole connection configuration documentation](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#configuring-connections).
1. The settings of the RAC provider
2. RAC endpoint settings
3. RAC property mappings
4. Retrieved from user or group attributes via RAC property mappings
RAC connection settings can be set via several methods and are all merged together when connecting:
For an example of how to set a connection setting see the [RAC SSH public key authentication](./rac-public-key.md) page.
1. Default settings
2. RAC Provider settings
3. RAC Endpoint settings
4. RAC Provider property mapping settings
5. RAC Endpoint property mapping settings
6. The `connection_settings` object in the flow plan
For examples of how to configure connection settings, see the [RAC SSH public key authentication](./rac-public-key.md) and [RAC Credentials Prompt](./rac_credentials_prompt.md) documentation.
## Capabilities

View File

@@ -66,8 +66,10 @@ The pipe character (`|`) is required to preserve linebreaks in the YAML text. Se
- **Expression**:
```python
return {
"private-key": "-----BEGIN SSH PRIVATE KEY-----
import textwrap
private_key = textwrap.dedent("""
-----BEGIN SSH PRIVATE KEY-----
SAMPLEgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
@@ -75,7 +77,12 @@ The pipe character (`|`) is required to preserve linebreaks in the YAML text. Se
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
-----END SSH PRIVATE KEY-----",
-----END SSH PRIVATE KEY-----
""")
return {
"username": "<your_username>",
"private-key": private_key
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -9,7 +9,7 @@ authentik SAML providers can be created either from scratch or by using SAML met
To create a provider along with the corresponding application that uses it for authentication, navigate to **Applications** > **Applications** and click **Create with provider**. We recommend this combined approach for most common use cases. Alternatively, you can use the legacy method to solely create the provider by navigating to **Applications** > **Providers** and clicking **Create**.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
2. Navigate to **Applications** > **Applications** and click **Create with provider** to create an application and provider pair.
3. On the **New application** page, define the application details, and then click **Next**.
4. Select **SAML Provider** as the **Provider Type**, and then click **Next**.
5. On the **Configure SAML Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
@@ -19,7 +19,7 @@ To create a provider along with the corresponding application that uses it for a
If you have exported SAML metadata from your SP, you can optionally create the authentik SAML provider by importing this metadata.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers** and click **Create** to create a provider.
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
3. Select **SAML Provider from Metadata** as the **Provider Type**, and then click **Next**.
4. On the **Create SAML Provider from Metadata** page, provide the configuration settings along with an SP metadata file and then click **Finish** to create the provider.
5. (Optional) Edit the created SAML provider and configure any further settings.
@@ -33,7 +33,7 @@ After an authentik SAML provider has been created via any of the above methods,
To download the metadata of an authentik SAML provider, follow these steps:
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers**.
2. Navigate to **Applications** > **Providers**.
3. Click the name of the provider you want metadata from to open its overview tab.
4. In the **Related objects** section, under **Metadata** click on **Download**. This will download the metadata XML file for that provider.
@@ -42,7 +42,7 @@ To download the metadata of an authentik SAML provider, follow these steps:
To view and optionally download the metadata of an authentik SAML provider, follow these steps:
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers**.
2. Navigate to **Applications** > **Providers**.
3. Click the name of the provider you want metadata from to open its overview tab.
4. Navigate to the **Metadata** tab.
5. The metadata for the provider will be shown in a codebox. You can optionally use the **Download** button to obtain the metadata as a file.

View File

@@ -25,7 +25,7 @@ When you create a new SCIM provider, select which **Authentication Mode** the ap
![Creating a SCIM provider](./scim_oauth.png)
Whichever mode you select you'll need to enter a SCIM base **URL**, for the endpoint.
Whichever mode you select, you'll need to enter a SCIM base **URL** for the endpoint.
### Default authentication method for a SCIM provider
@@ -54,7 +54,7 @@ Data is synchronized in multiple ways:
- When a user/group is created/modified/deleted, that action is sent to all SCIM providers
- Periodically (once an hour), all SCIM providers are fully synchronized
The actual synchronization process is run in the authentik worker. To allow this process to better to scale, a task is started for each 100 users and groups, so when multiple workers are available the workload will be distributed.
The actual synchronization process is run in the authentik worker. To allow this process to scale better, a task is started for each 100 users and groups, so when multiple workers are available the workload will be distributed.
### Attribute mapping
@@ -70,7 +70,7 @@ By default, service accounts are excluded from being synchronized. This can be c
Users can be filtered using application policies.
Only users who can view the scim provider's application are synced by the scim provider.
Only users who can view the SCIM provider's application are synced by the SCIM provider.
#### Group Filters
@@ -94,7 +94,7 @@ To configure a compatibility mode, select the appropriate option in the **SCIM C
### Filtering users
By default service accounts are excluded from being synchronized. This can be configured in the SCIM provider. Additionally, an optional group can be configured to only synchronize the users that are members of the selected group. Changing this group selection does _not_ remove members outside of the group that might have been created previously.
By default, service accounts are excluded from being synchronized. This can be configured in the SCIM provider. Additionally, an optional group can be configured to only synchronize the users that are members of the selected group. Changing this group selection does _not_ remove members outside of the group that might have been created previously.
### Supported options
@@ -118,7 +118,7 @@ The `ServiceProviderConfig` is cached for 1 hour after it is fetched. The cache
### Using in conjunction with other providers
A lot of applications support SCIM in conjunction with another SSO protocol like OAuth/OIDC or SAML. With default settings, the unique user IDs in SCIM and other protocols are identical, which should easily allow applications to link users the are provisioned with users that are logging in.
A lot of applications support SCIM in conjunction with another SSO protocol like OAuth/OIDC or SAML. With default settings, the unique user IDs in SCIM and other protocols are identical, which should easily allow applications to link users that are provisioned with users that are logging in.
Applications can either match users on a unique ID sent by authentik called `externalId`, by their email or username.

View File

@@ -46,17 +46,17 @@ Front-channel logout sends logout requests through the user's browser. authentik
#### iframe mode (default for OIDC)
- Loads all provider logout URLs simultaneously in hidden iframes
- Provides fast, parallel logout across multiple providers
- Required by the OIDC front-channel logout specification
- Most SAML providers also support iframe-based logout
- Loads all provider logout URLs simultaneously in hidden iframes
- Provides fast, parallel logout across multiple providers
- Required by the OIDC front-channel logout specification
- Most SAML providers also support iframe-based logout
#### Native Mode (SAML Only)
- Uses the active browser tab to chain redirects and POST requests sequentially
- Provides better compatibility with SAML providers that have iframe restrictions
- Each provider redirects the user back to authentik before proceeding to the next provider
- Not available for OIDC providers as the specification requires iframe support
- Uses the active browser tab to chain redirects and POST requests sequentially
- Provides better compatibility with SAML providers that have iframe restrictions
- Each provider redirects the user back to authentik before proceeding to the next provider
- Not available for OIDC providers as the specification requires iframe support
:::info
Use native front-channel mode for SAML providers if you encounter iframe compatibility issues, such as Content Security Policy (CSP) restrictions or cookie handling problems.
@@ -66,10 +66,10 @@ Use native front-channel mode for SAML providers if you encounter iframe compati
Back-channel logout sends logout requests directly from the authentik server to each provider's logout endpoint via HTTP POST.
- Does not require user browser interaction
- Works even when the user is offline or their browser is closed
- Is automatically triggered by administrators terminating a user session (user deactivation or session deletion)
- Requires the provider to accept server-to-server POST requests
- Does not require user browser interaction
- Works even when the user is offline or their browser is closed
- Is automatically triggered by administrators terminating a user session (user deactivation or session deletion)
- Requires the provider to accept server-to-server POST requests
**For SAML**: Requires POST SLS binding.
**For OIDC**: Requires a `logout_uri` configured for back-channel that accepts logout tokens.
@@ -125,8 +125,8 @@ See the [OIDC Front-channel and Back-channel logout documentation](../oauth2/fro
authentik tracks provider sessions to enable single logout:
- **SAML**: Creates `SAMLSession` records containing the `SessionIndex`, `NameID`, and `NameID format` for each successful authentication.
- **OIDC**: Tracks session identifiers (`sid`) and ID tokens required for logout requests.
- **SAML**: Creates `SAMLSession` records containing the `SessionIndex`, `NameID`, and `NameID format` for each successful authentication.
- **OIDC**: Tracks session identifiers (`sid`) and ID tokens required for logout requests.
These session records are automatically created during authentication and deleted after logout or expiration.
@@ -134,8 +134,8 @@ These session records are automatically created during authentication and delete
Back-channel logout is always triggered when a user session is terminated via administrative actions:
- **Session Deletion**: When an administrator manually deletes a user's session through the Admin interface or API, authentik sends back-channel logout requests to all configured providers.
- **User Deactivation**: When a user account is deactivated, authentik automatically sends back-channel logout requests to terminate all active sessions across all providers.
- **Session Deletion**: When an administrator manually deletes a user's session through the Admin interface or API, authentik sends back-channel logout requests to all configured providers.
- **User Deactivation**: When a user account is deactivated, authentik automatically sends back-channel logout requests to terminate all active sessions across all providers.
These requests are processed asynchronously to avoid blocking administrative operations.

View File

@@ -1,56 +1,42 @@
---
title: Configure an SSF provider
authentik_version: "2025.2.0"
description: "How to create and configure an SSF provider in authentik"
authentik_enterprise: true
authentik_preview: true
tags:
- backchannel
- provider
tags: [Shared Signals Framework, SSF, Apple Business Manager, backchannel]
---
The workflow to implement an SSF provider as a [backchannel provider](../../applications/manage_apps.mdx#backchannel-providers) for an application/provider pair is as follows:
Follow this workflow to create and configure an SSF provider for an application:
1. Create the SSF provider (which serves as the backchannel provider).
1. Create the SSF provider (which serves as the [backchannel provider](../../applications/manage_apps.mdx#backchannel-providers)).
2. Create an OIDC provider (which serves as the protocol provider for the application).
3. Create the application, and assign both the OIDC provider and the SSF provider.
## Create the SSF provider
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
2. Click **Create**.
3. In the modal, select the **Provider Type** of **SSF**, and then click **Next**.
4. On the **New provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
5. Click **Finish** to create and save the provider.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
3. Select **Shared Signals Framework Provider** as the **Provider Type**, and then click **Next**.
4. On the **Create SSF Provider** page, provide the configuration settings. Be sure to select a **Signing Key**.
5. Click **Finish** to create the provider.
## Create the OIDC provider
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
2. Click **Create**.
3. In the modal, select the **Provider Type** of **OIDC**, and then click **Next**.
4. Define the settings for the provider, and then click **Finish** to save the new provider.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications** > **Providers** and click **Create** to create a provider.
3. Select **OAuth2/OpenID Provider** as the **Provider Type**, and then click **Next**.
4. On the **Create OAuth2/OpenID Provider** page, provide the configuration settings and then click **Finish** to create the provider.
## Create the application
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Applications**.
2. Click **Create**.
3. Define the settings for the application:
- **Name**: define a descriptive name of the application.
- **Slug**: optionally define the internal application name used in URLs.
- **Group**: optionally select a group that you want to have access to this application.
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications** > **Applications** and click **Create** to create an application.
3. Configure the following required settings for the application:
- **Name**: provide a descriptive name of the application.
- **Slug**: provide the application slug used in URLs.
- **Provider**: select the OIDC provider that you created.
- **Backchannel Providers**: select the SSF provider you created.
- **Policy engine mode**: define policy-based access.
- **UI Settings**: optionally define a launch URL, an icon, and other UI elements.
- **Backchannel Providers**: select the SSF provider that you created.
4. Click **Create** to save the new application.
The new application, with its OIDC provider and the backchannel SFF provider, should now appear in your list of Applications.
The new application, with its OIDC provider and the backchannel SSF provider, should now appear in your application list.

View File

@@ -1,49 +1,53 @@
---
title: Shared Signals Framework (SSF) Provider
sidebar_label: SSF Provider
description: "Overview of SSF and the authentik SSF provider"
authentik_version: "2025.2.0"
authentik_enterprise: true
authentik_preview: true
tags: [Shared Signals Framework, SSF, Apple Business Manager]
---
Shared Signals Framework (SSF) is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected, secure webhooks.
The Shared Signals Framework (SSF) provider allows you to integrate applications with the Shared Signals Framework protocol.
## About Shared Signals Framework
SSF is a common standard for sharing asynchronous real-time security signals and events across multiple applications and an identity provider. The framework is a collection of standards and communication processes, documented in a [specification](https://openid.net/specs/openid-sharedsignals-framework-1_0-ID3.html). SSF leverages the APIs of the application and the IdP, using privacy-protected and secure webhooks.
In authentik, an SSF provider allows applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
The authentik SSF provider allows OIDC applications to subscribe to certain types of security signals (which are then translated into SETs, or Security Event Tokens) that are captured by authentik (the IdP), and then the application can respond to each event. In this scenario, authentik acts as the _transmitter_ and the application acts as the _receiver_ of the events.
Events in authentik that are tracked via SSF include when an MFA device is added or removed, logouts, sessions being revoked by Admin or user clicking logout, or credentials changed.
Refer to our documentation to learn how to [create a SSF provider](./create-ssf-provider.md).
## Example use cases
One important use case for SFF is to [integrate Apple Business Manager](https://integrations.goauthentik.io/device-management/apple/) or any of the Apple device management platforms with authentik, so that users can enroll their Apple devices using their authentik credentials. When a user signs in with their email address, Apple redirects them to authentik for authentication. Once authenticated, Apple enrolls the user's device and grants access to Apple services.
One important use case for SSF is to [integrate Apple Business Manager](https://integrations.goauthentik.io/device-management/apple/) or any of the Apple device management platforms with authentik, so that users can enroll their Apple devices using their authentik credentials. When a user signs in with their email address, Apple redirects them to authentik for authentication. Once authenticated, Apple enrolls the user's device and grants access to Apple services.
Another use case for SSF is when an Admin wants to know if a user logs out of authentik, so that the user is then also automatically logged out of all other work-focused applications.
Another use case for SSF is when an administrator wants to know when a user logs out of authentik, so that the user is then also automatically logged out of all other work-focused applications.
Another example use case is when an application uses SSF to subscribe to authorization events because the application needs to know if a user changed their password in authentik. If a user did change their password, then the application receives a POST request to write the fact that the password was changed.
## About using SSF in authentik
## Using the authentik SSF provider
Let's look at a few details about using SSF in authentik.
The SSF provider serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application.
The SSF provider in authentik serves as a [backchannel provider](../../applications/manage_apps#backchannel-providers). Backchannel providers are used to augment the functionality of the main provider for an application. Thus you will still need to [create a typical application/provider pair](../../applications/manage_apps#create-an-application-and-provider-pair) (using an OIDC provider), and when creating the application, assign the SSF provider as a backchannel provider.
Therefore you still need to [create a typical OIDC application/provider pair](../../applications/manage_apps#create-an-application-and-provider-pair), and when creating the application, assign the SSF provider as a [backchannel provider](../../applications/manage_apps#backchannel-providers).
When an authentik Admin [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
When an authentik administrator [creates an SSF provider](./create-ssf-provider), they need to configure both the application (the receiver) and authentik (the IdP and the transmitter).
### The application (the receiver)
Within the application, the admin creates an SSF stream (which comprises all the signals that the app wants to subscribe to) and defines the audience, called `aud` in the specification (the URL that identifies the stream). A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can change or delete the stream.
Within the application, the administrator creates an SSF stream which lists all the signals that the application wants to subscribe to, and defines the audience (`aud`), which is the URL that identifies the stream. A stream is basically an API request to authentik, which asks for a POST of all events. How that request is sent varies from application to application. An application can also change or delete the stream.
Note that authentik doesn't specify which events to subscribe to; instead the application defines which they want to listen for.
authentik does not specify which events to subscribe to; instead the application defines which events they want to listen for.
### authentik (the transmitter)
To configure authentik as a shared signals transmitter, the authentik Admin [creates a new provider](./create-ssf-provider), selecting the type "SSF", to serve as the backchannelprovider for the application.
To configure authentik as a shared signals transmitter, the authentik administrator [creates a new SSF provider](./create-ssf-provider), to serve as the backchannel provider for the application.
When creating the SSF provider you will need to select a signing key. This is the key that the Security Event Tokens (SET) is signed with.
When creating the SSF provider you will need to select a signing key that is used to sign the Security Event Tokens (SET).
Optionally, you can specify a event retention time period: this value determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
Optionally, you can specify an event retention time period, which determines how long events are stored for. If an event could not be sent correctly, and retries occur, the event's expiration is also increased by this duration.
:::info
:::note SET events
Be aware that the SET events are different events than those displayed in the authentik Admin interface under **Events**.
:::

View File

@@ -0,0 +1,38 @@
---
title: Create a WS-Federation provider
---
An authentik WS-Federation provider is typically created as part of an application/provider pair, using the steps below. You can also create a standalone provider, and then later assign an application to use it.
## Create a WS-Federation provider and application pair
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Applications** and click **Create with provider** to create an application and provider pair.
3. On the **New application** page, define the application details, and then click **Next**.
4. Select **WS-Federation Provider** as the **Provider Type**, and then click **Next**.
5. On the **Configure WS-Federation Provider** page, provide a name for the provider, select an authorization flow, and the two required configuration settings:
- **Reply URL**: Enter the application callback URL, where the token should be sent. This is the specific endpoint on an RP (application) where an Identity Provider (STS) sends the security token and authentication response after a successful log in.
- **Realm**: Enter the identifier (string) of the requesting realm; that is, the Relying Party (RP) or application receiving the token. Realm is similar to the SAML 2.0 Entity ID.
6. Click **Submit** to create both the application and the provider.
## Export authentik WS-Federation provider metadata
After an authentik WS-Federation provider has been created via any of the above methods, you can access its metadata in one of two ways:
### Download authentik metadata for a WS-Federation provider
To download the metadata of an authentik WS-Federation provider, follow these steps:
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers**.
3. Click the name of the provider you want metadata for.
4. On the **Overview** tab, in the **Related objects** section, click on **Download** under **Metadata**. This will download the metadata XML file for that provider.
### Access the Metadata tab for a WS-Federation provider
To view and optionally download the metadata of an authentik WS-Federation provider, follow these steps:
1. Log in to authentik as an administrator, and open the authentik Admin interface.
2. Navigate to **Applications > Providers**.
3. Click the name of the provider you want metadata for, and then click the **Metadata** tab.
4. The metadata for the provider will be shown in a code box. You can optionally use the **Download** button to obtain the metadata as a file.

View File

@@ -0,0 +1,53 @@
---
title: WS-Federation Provider
---
The WS-Federation provider is used to integrate with applications and service providers that use [WS-Federation protocol](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adfsod/204de335-ea34-4f9b-ae73-8b7d4c8152d1). WS-Federation is an XML-based identity federation protocol that uses token exchange for federated Single Sign-On (SSO) and IdP authentication, specifically for Windows applications such as SharePoint.
There are similarities between WS-Federation and SAML protocols, but there are several key differences in terminology, most importantly:
- WS-Federation term: **STS (Security Token Service)**
- SAML term: **IdP (Identity Provider)**
:::info SAML2 token support
Note that we only support the SAML2 token type within WS-Federation providers, and that using the WS-Federation provider with Entra ID is not supported because Entra ID requires a SAML 1.0 token.
:::
## Supported URL request parameters
The following URL request parameters are supported in the authentik WS-Federation provider:
- **wa**: This is a required parameter that represents the action being requested, typically wsignin1.0 for signing in. The parameter's value tells the Security Token Service (STS) which operation to execute.
- **wtrealm**: The unique identifier (realm) of the Relying Party (RP) or application requesting the security token, for example, urn:my-app:rp. It defines the trust relationship between the RP and the Identity Provider (IdP) and indicates which application is initiating the WS-Federation request. This is a required query parameter that tells the Security Token Service (STS) which relying party the token is intended for.
- **wreply**: The target URL to which the Identity Provider (IdP) sends the WS-Federation response containing the security token. This URL is supplied by the Service Provider (SP). authentik verifies that the received `wreply` parameter matches the URL configured by the administrator and stored in the database.
- **wctx**: A context value that is used to maintain state between the Relying Party (RP) and the Identity Provider (IdP) across redirects. It serves the same purpose as the `RelayState` parameter in SAML. The RP includes this value in the authentication request, and the IdP returns it unchanged in the response, allowing the RP to validate and restore the original session or request context.
## WS-Federation bindings and endpoints
_Bindings_ define how an Identity Provider (IdP) and the WS-Federation STS (Security Token Service), or IdP in SAML terms, communicate; how messages are transported over network protocols, specifying transport (like HTTP), encoding, and security detail that allow WS-Federation to facilitate secure identity sharing across systems. Both the IdP and STS define various endpoints in their metadata, each associated with a specific WS-Federation binding.
| Endpoint | URL |
| -------- | --------------------- |
| SSO/SLO | `/application/wsfed/` |
## WS-Federation metadata
Using metadata ensures that WS-Federation single sign-on works reliably by exchanging and maintaining identity and connection information. WS-Federation metadata is an XML document that defines how IdPs and SPs securely interact for authentication. It includes information such as endpoints, bindings, certificates, and unique identifiers. The metadata is what you provide the application to configure it for authenticating with authentik.
You can [export WS-Federation metadata](./create-wsfed-provider.md#export-authentik-ws-federation-provider-metadata) from an authentik WS-Federation provider to an STS to automatically provide important endpoint and certificate information to the SP.
## Certificates
The certificates used with WS-Federation to sign Request Security Token Response (RSTR), which contains the assertion, are the same certificates that are used by SAML.
For details, refer to our [SAML certificates documentation](../saml/index.md#certificates).
## WS-Federation property mappings
Property mappings are used during the authentication process to align, or "map", user attributes values between the SP and STS (Security Token Service), the latter being the equivalent of SAML's IdP.
The same property mappings that are used in WS-Federation are used in SAML. For details, refer to our [SAML property mapping documentation](../saml/index.md#certificates).
## Attributes for WS-Federation
WS-Federation and SAML also share the use of the [NameID](../saml/index.md#nameid) and the [AuthnContextClassRef](../saml/index.md#authncontextclassref) attributes.

View File

@@ -35,8 +35,8 @@ This container executes background tasks, such as sending emails, the event noti
#### Persistence
- `/certs` is used for authentik to import external certs, which in most cases shouldn't be used for SAML, but rather if you use authentik without a reverse proxy, this can be used for example for the [Let's Encrypt integration](../sys-mgmt/certificates.md#lets-encrypt-integration)
- `/templates` is used for [custom email templates](../add-secure-apps/flows-stages/stages/email/index.mdx#custom-templates), and as with the other ones fully optional
- `/certs` is used for authentik to import external certs, which in most cases shouldn't be used for SAML, but if you use authentik without a reverse proxy, this can be used, for example, for the [Let's Encrypt integration](../sys-mgmt/certificates.md#lets-encrypt-integration).
- `/templates` is used for [custom email templates](../add-secure-apps/flows-stages/stages/email/index.mdx#custom-templates), and as with the others is fully optional.
### PostgreSQL

View File

@@ -4,7 +4,7 @@ sidebar_custom_props:
termName: Application
tags:
- Core Concepts
shortDescription: An application is what you authenticate into with authentik, and they are displayed on the "My applications" page on the User interface.
shortDescription: An application is what you authenticate into with authentik and is displayed on the "My applications" page in the User interface.
authentikSpecific: true
longDescription: An application is paired with a provider, and with defined policies and other configurations controls user access. It also holds information like UI name, icon, and more.
---

View File

@@ -6,5 +6,5 @@ sidebar_custom_props:
- Core Concepts
shortDescription: A yes/no gate evaluated by type and settings.
authentikSpecific: true
longDescription: At a base level a policy is a yes/no gate. It evaluates to True or False depending on the policy kind and settings. For example, a Group Membership policy evaluates to True if the user is a member of the specified group and False if not. Policies can conditionally apply stages, grant or deny access, and support other custom logic.
longDescription: At a base level, a policy is a yes/no gate. It evaluates to True or False depending on the policy kind and settings. For example, a Group Membership policy evaluates to True if the user is a member of the specified group and False if not. Policies can conditionally apply stages, grant or deny access, and support other custom logic.
---

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