Compare commits

..

71 Commits

Author SHA1 Message Date
authentik-automation[bot]
680feaefa1 release: 2025.8.3 2025-09-16 15:09:16 +00:00
authentik-automation[bot]
8676cd3a43 website/docs: 2025.8.3 release notes (cherry-pick #16809) (#16810)
website/docs: 2025.8.3 release notes (#16809)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-16 16:40:32 +02:00
authentik-automation[bot]
53e0f6b734 stages/email_authenticator: Fix email mfa loop (cherry-pick #16579) (#16807)
stages/email_authenticator: Fix email mfa loop (#16579)

* Return error message instead of infinite loop

* Remove unused code

* check for existing email device

* revert to initial behaviour

* Add test for failing when the user already has an email device

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-09-16 16:20:34 +02:00
authentik-automation[bot]
eaee475662 website/docs: update create oauth provider page (cherry-pick #16617) (#16806)
website/docs: update create oauth provider page (#16617)

* Updated the page to be more consistent with upcoming changes to the saml page

* Add note

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-16 13:33:53 +00:00
authentik-automation[bot]
19b672b3bc lifecycle: fix permission error when running worker as root (cherry-pick #16735) (#16784)
lifecycle: fix permission error when running worker as root (#16735)

* lifecycle: fix permission error when running worker as root



* fix maybe?



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-16 14:49:31 +02:00
authentik-automation[bot]
bf0a31ce86 website/docs: fix docker tabs not rendering properly (cherry-pick #16799) (#16802)
website/docs: fix docker tabs not rendering properly (#16799)

docs: fix docker tabs not rendering properly

Co-authored-by: Connor Peshek <connor@connorpeshek.me>
2025-09-16 11:00:27 +01:00
authentik-automation[bot]
28ff561400 release: 2025.8.2 2025-09-15 15:47:14 +00:00
authentik-automation[bot]
ff2472a551 website/docs: 2025.8.2 release notes (cherry-pick #16773) (#16778)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-15 17:15:44 +02:00
authentik-automation[bot]
dac302e8be sources/oauth/entra_id: do not assume group_id comes from entra (cherry-pick #16456) (#16777)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-15 16:43:34 +02:00
authentik-automation[bot]
930a6f7c6f lib/logging: only show locals when in debug mode (cherry-pick #16772) (#16774)
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-15 15:34:52 +02:00
authentik-automation[bot]
b661b0bb39 website/docs: update ssh rac doc (cherry-pick #16695) (#16720)
website/docs: update ssh rac doc (#16695)

* Added linebreak preservation and changed blocks to yaml syntax

* Update website/docs/add-secure-apps/providers/rac/rac-public-key.md




* Update website/docs/add-secure-apps/providers/rac/rac-public-key.md




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-12 16:05:57 +00:00
authentik-automation[bot]
5203713ca0 website/docs: re-fix sentence about Go (backport of #16736) (#16737)
* Cherry-pick #16736 to version-2025.8 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #16736
Original commit: 515a065831

* fixed conflict

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-09-12 03:33:56 -05:00
authentik-automation[bot]
94794b106e web: Docker versioning compatibility (cherry-pick #16139) (#16732)
web: Docker versioning compatibility (#16139)

* website/docs: update frontend dev docs to always use latest dev images



* website: Clarify instructions.

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
2025-09-11 14:39:12 -05:00
authentik-automation[bot]
eb8c21cf04 website/docs: dev-docs: Minimal landing + landing for dev env (cherry-pick #15246) (#16734)
website/docs: dev-docs: Minimal landing + landing for dev env (#15246)

* wip

* Update index.mdx



* Update website/docs/developer-docs/contributing.md



* Update website/docs/developer-docs/contributing.md



---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-11 14:06:48 -05:00
authentik-automation[bot]
8a501377f2 website/docs: fix typos (cherry-pick #16716) (#16719)
website/docs: fix typos (#16716)

Fix typos

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-11 14:17:33 +00:00
authentik-automation[bot]
5c67a0fecd website/docs: moves display source notes (cherry-pick #16704) (#16718)
website/docs: moves display source notes (#16704)

Moves display source note location to better location

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-11 13:37:43 +00:00
authentik-automation[bot]
145e6a3a4f website/docs: clarify docker compose install (cherry-pick #16696) (#16699)
website/docs: clarify docker compose install (#16696)

* Change order

* WIP

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-10 21:28:56 +01:00
authentik-automation[bot]
0219ed73f5 website/docs: add missing notification rule example doc to sidebar (cherry-pick #16478) (#16479)
website/docs: add missing notification rule example doc to sidebar (#16478)

Adds missing doc to sidebar

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-10 15:54:29 +00:00
authentik-automation[bot]
88ccb7857b website: Redirect Azure to Entra. Add tags for search indexing. (cherry-pick #16474) (#16483)
website: Redirect Azure to Entra. Add tags for search indexing. (#16474)

* website: Add Azure redirects, tags for  search indexing.

* website: Fix redirects tab alignment.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-09-10 14:38:57 +00:00
authentik-automation[bot]
e33d6becba website/docs: add rate limiting info to Email stage docs (cherry-pick #16668) (#16691)
website/docs: add rate limiting info to Email stage docs (#16668)

* add rate limiting info

* added Jens' edits

* Update website/docs/add-secure-apps/flows-stages/stages/email/index.mdx




* Update website/docs/add-secure-apps/flows-stages/stages/email/index.mdx




* Update website/docs/add-secure-apps/flows-stages/stages/email/index.mdx




---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-10 10:35:15 +00:00
authentik-automation[bot]
41417affc0 website/docs: fix typo (cherry-pick #16681) (#16682)
website/docs: fix typo (#16681)

Fix typo

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-09 13:28:51 +00:00
authentik-automation[bot]
1b4a6c3f6d tasks: fix status and healthcheck breaking with connection issues (cherry-pick #16504) (#16673)
tasks: fix status and healthcheck breaking with connection issues (#16504)

* add high priority for inline tasks in API



* also catch psycopg errors directly



* retry locking worker state after failure



* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-08 17:04:17 -05:00
authentik-automation[bot]
aa98edd661 providers/scim: improve error message when object fails to sync (cherry-pick #16625) (#16643)
providers/scim: improve error message when object fails to sync (#16625)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-05 15:14:20 +02:00
authentik-automation[bot]
cb70331c82 providers/oauth2: add missing exp claim for logout token (cherry-pick #16593) (#16598)
providers/oauth2: add missing exp claim for logout token (#16593)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-04 00:23:20 +02:00
authentik-automation[bot]
31e105b190 web/flows: only disable login button when interactive captcha is configured and not loaded (cherry-pick #16586) (#16590)
web/flows: only disable login button when interactive captcha is configured and not loaded (#16586)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-09-03 18:26:33 +02:00
authentik-automation[bot]
08d2615f71 website: Update license text to use "directory" (cherry-pick #16589) (#16592)
website: Update license text to use "directory" (#16589)

To remove any potential confusion

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-03 16:08:50 +00:00
authentik-automation[bot]
bb9e8b1c42 core: bump django from 5.1.11 to v5.1.12 (cherry-pick #16584) (#16591)
core: bump django from 5.1.11 to v5.1.12 (#16584)

bump django from 5.1.11 to 5.1.12

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-09-03 17:39:33 +02:00
authentik-automation[bot]
6b8d7376a6 sources/ldap: fix malformed filter error with special characters in group DN (cherry-pick #16243) (#16585)
sources/ldap: fix malformed filter error with special characters in group DN (#16243)

* sources/ldap: fix malformed filter error with special characters in group DN

Escape special characters in LDAP group DN when constructing membership filters using ldap3's escape_filter_chars() function to prevent LDAPInvalidFilterError exceptions.

* sources/ldap: add tests for special characters in group DN filter escaping

Add comprehensive tests to verify that LDAP group DN values with special characters (parentheses, backslashes, asterisks) are properly escaped when constructing LDAP filters. Tests cover both the membership synchronization process and the escape_filter_chars function directly to ensure malformed filter errors are prevented.

* sources/ldap: fix line length for ruff linting compliance

Split long line in group filter construction to comply with 100 character limit.

* sources/ldap: move escape_filter_chars import to top-level in tests; audit other filter usages

---------

Co-authored-by: Dongwoo Jeong <dongwoo@duck.com>
Co-authored-by: Dongwoo Jeong <dongwoo.jeong@lge.com>
2025-09-03 17:20:34 +02:00
authentik-automation[bot]
4f680d8c06 root: clean up README (cherry-pick #16286) (#16573)
root: clean up README (#16286)

* wip





* Delete website/LICENSE



---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-09-03 15:49:26 +02:00
authentik-automation[bot]
3eeb741975 website: Add text copy of license (cherry-pick #16559) (#16574)
website: Add text copy of license (#16559)

* website: Add License

According to the root LICENSE, the website is licensed under Creative Commons: CC BY-SA 4.0 license. To match the root of the project and the EE dir, a text copy of the license is now included.

Updates AUTH-1246



* Update LICENSE



* Create LICENSE for proprietary logo assets

Added license information for proprietary assets.



---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-03 09:56:28 +00:00
authentik-automation[bot]
39cb638132 website/docs: remove base providers redirect. (cherry-pick #16576) (#16580)
website/docs: remove base providers redirect. (#16576)

Co-authored-by: Connor Peshek <connor@connorpeshek.me>
2025-09-03 10:26:19 +01:00
authentik-automation[bot]
be8f5d21cb website/docs: changes all -> arrows to > (cherry-pick #16569) (#16571)
website/docs: changes all -> arrows to > (#16569)

Changes all -> arrows to > as per style guide. Also fixes some bolding and capitalization

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-02 22:27:26 +00:00
authentik-automation[bot]
acf18836e8 website/docs: update external user information (cherry-pick #16493) (#16568)
website/docs: update external user information (#16493)

Updated information

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-02 14:09:52 +00:00
authentik-automation[bot]
d688621de4 website/docs: improve customize your instance page layout (cherry-pick #16367) (#16566)
website/docs: improve customize your instance page layout (#16367)

* Changes bullet points to a table. WIP

* Finished sentence and added punctuation

* Updated to match other overview/landing pages

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-02 14:27:14 +01:00
authentik-automation[bot]
5173d09191 website/docs: update how to rac and outpost landing page (cherry-pick #16442) (#16498)
website/docs: update how to rac and outpost landing page (#16442)

* Adds outpost deployment to how to rac guide and updates the outpost landing page

* Applied suggestions

* Apply suggestion

* Update website/docs/add-secure-apps/providers/rac/how-to-rac.md




* Update website/docs/add-secure-apps/outposts/index.mdx



* Removed cards

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-01 08:00:48 -05:00
authentik-automation[bot]
45bab4d32f website/integrations: bitwarden: fix ent notice (cherry-pick #16502) (#16511)
website/integrations: bitwarden: fix ent notice (#16502)

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-09-01 08:27:39 +00:00
authentik-automation[bot]
f383e54c72 policies: remove object pk from authentik_policies_execution_time to reduce cardinality (cherry-pick #16500) (#16501)
policies: remove object pk from authentik_policies_execution_time to reduce cardinality (#16500)

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

#16386

Co-authored-by: Jens L. <jens@goauthentik.io>
2025-08-31 14:16:24 +01:00
authentik-automation[bot]
ab6b9b27cc website: Page redirect guide, documentation (backport of #16466) (#16475)
* website/docs: merge docs development environment and writing docs pages (#16402)

* Merges the docs development environment doc into the writing documentation guide to avoid duplication. Also updates the formatting of the writing docs page to make it easier to copy commands.

* Updates sidebar

* Update website/docs/developer-docs/docs/writing-documentation.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/developer-docs/docs/writing-documentation.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/developer-docs/docs/writing-documentation.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update language on -watch commands

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>

* website: Unify Netlify redirects with Docusaurus's client-side router. (backport of #16430) (#16472)

Cherry-pick #16430 to version-2025.8 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #16430
Original commit: 6d81aea5aa

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

* Cherry-pick #16466 to version-2025.8 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #16466
Original commit: 66e17fe280

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-08-29 15:13:32 +00:00
authentik-automation[bot]
db3fb0bf2e website: Unify Netlify redirects with Docusaurus's client-side router. (backport of #16430) (#16472)
Cherry-pick #16430 to version-2025.8 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #16430
Original commit: 6d81aea5aa

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-08-29 14:08:36 +00:00
authentik-automation[bot]
5859e6a5e5 core: fix client-side only validation allowing admin to set blank user password (cherry-pick #16467) (#16469)
Co-authored-by: Jens L. <jens@goauthentik.io>
fix client-side only validation allowing admin to set blank user password (#16467)
2025-08-29 15:20:46 +02:00
authentik-automation[bot]
1d3271fec7 lib/sync/outgoing: fix single object sync timeout (cherry-pick #16447) (#16453)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix single object sync timeout (#16447)
2025-08-28 19:35:33 +02:00
authentik-automation[bot]
673c8ef62c core: bump h2 from 4.2.0 to 4.3.0 (cherry-pick #16446) (#16449)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-08-28 17:14:51 +02:00
authentik-automation[bot]
4614ae320f website/docs: capitalized proper name of stages, removed old version references. (cherry-pick #16414) (#16450)
website/docs: capitalized proper name of stages, removed old version references. (#16414)

* capitalized proper name of stages, removed old version references.

* ken and dominics edits

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-28 09:52:59 -05:00
authentik-automation[bot]
10b103c0bf lib/sync: fix missing f for string (cherry-pick #16423) (#16427)
Co-authored-by: Jens L. <jens@goauthentik.io>
fix missing f for string (#16423)
2025-08-28 15:38:24 +02:00
authentik-automation[bot]
361e64a8a1 website/docs: update internal vs external user information (cherry-pick #16359) (#16421)
website/docs: update internal vs external user information (#16359)

* Update external vs internal user information

* Language updates

* Spelling

* Applied suggestion

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-27 19:45:48 +01:00
authentik-automation[bot]
e56081b863 providers/rac: fix AuthenticatedSession migration (cherry-pick #16400) (#16419)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
fix `AuthenticatedSession` migration (#16400)
2025-08-27 18:43:31 +02:00
authentik-automation[bot]
01a44b281b root: fix security.md (cherry-pick #16345) (#16415)
Co-authored-by: Dominic R <dominic@sdko.org>
fix security.md (#16345)
2025-08-27 18:15:05 +02:00
authentik-automation[bot]
7a6631c6e8 website/docs: adds details to certificates doc (cherry-pick #16335) (#16394)
website/docs: adds details to certificates doc (#16335)

* Clarifies certs directory mounting and adds instruction for manually re-triggering discovery.

* Fixed mounting info

* Update website/docs/sys-mgmt/certificates.md




* Update website/docs/sys-mgmt/certificates.md




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-08-27 16:14:45 +00:00
authentik-automation[bot]
ada973dd44 tasks/schedules: fix api search fields (cherry-pick #16405) (#16410)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix api search fields (#16405)
2025-08-27 17:04:32 +02:00
authentik-automation[bot]
3a7e962bde lifecycle: fix PROMETHEUS_MULTIPROC_DIR missing suffix (cherry-pick #16401) (#16407)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix PROMETHEUS_MULTIPROC_DIR missing suffix (#16401)
2025-08-27 15:32:23 +02:00
authentik-automation[bot]
ae297e2f60 root: update security.md with github reporting link (cherry-pick #16332) (#16395)
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-08-27 14:31:06 +02:00
Teffen Ellis
2bc2b6bd41 website: cherry-pick 2025.8/client side redirects (#16372)
website: Fix stale links that depend on initial route not triggering redirects. (#16369)

website: Fix issue where stale links that depend on initial route are
not redirected.
2025-08-26 14:36:58 -05:00
Marc 'risson' Schmitt
8199371172 providers/oauth2: avoid deadlock during session migration (cherry-pick #16361) (#16364) 2025-08-25 18:29:53 +02:00
Marc 'risson' Schmitt
dac1879de5 website/docs: 2025.8.1 release notes (cherry-pick #16343) (#16344) 2025-08-22 16:53:58 +02:00
authentik-automation[bot]
dd7c6b29d9 release: 2025.8.1 2025-08-22 14:40:58 +00:00
Marc 'risson' Schmitt
41b7e05f59 packages/django-dramatiq-postgres: broker: fix various timing issues (cherry-pick #16340) (#16342)
fix various timing issues (#16340)
2025-08-22 16:08:53 +02:00
Teffen Ellis
c5f5714e02 website: Hotfix version 2025.8/website versioning (#16336)
Co-authored-by: Dominic R <dominic@sdko.org>
Fix version origin detection, build-time URLs  (#15774)
2025-08-22 16:08:34 +02:00
Marc 'risson' Schmitt
d20d8322af outposts: allow ingress path type configuration (cherry-pick #16339) (#16341) 2025-08-22 15:49:33 +02:00
Marc 'risson' Schmitt
288f5d5015 outposts: fix service connection update task arguments (cherry-pick #16312) (#16313) 2025-08-22 14:31:56 +02:00
Marc 'risson' Schmitt
a640eb9180 packages/django-dramatiq-postgres: middleware: fix listening on hosts where ipv6 is not supported (cherry-pick #16308) (#16305) 2025-08-22 14:26:49 +02:00
Marc 'risson' Schmitt
7d48baab3e core: use email backend for test_email management command (cherry-pick #16311) (#16337)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-08-22 14:25:06 +02:00
Tana M Berry
b1cd6d34fc website/docs: add link in 2025.8 rel notes to back-channel logout doc (cherry pick #16306) (#16318)
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-08-21 22:35:36 +02:00
Teffen Ellis
f14d033cef web: Username truncation, field alignment. (cherry-pick #16283) (#16284) 2025-08-21 18:03:55 +02:00
Teffen Ellis
ec75e161e2 web: Fix form group slots (cherry-pick #16276) (#16277) 2025-08-21 17:56:36 +02:00
Teffen Ellis
2e8fb8f2c6 web: Improvements to ReCaptcha resizing (cherry-pick #16171) (#16281) 2025-08-21 17:56:11 +02:00
Teffen Ellis
362bf22139 web: Fix issue where clicking a list item scrolls container (cherry-pick #16174) (#16278) 2025-08-21 17:49:43 +02:00
Marc 'risson' Schmitt
890da9b287 lifecycle: set PROMETHEUS_MULTIPROC_DIR as early as possible (cherry-pick #16298) (#16302) 2025-08-21 16:48:50 +02:00
Marc 'risson' Schmitt
7b8dadf945 providers/oauth2: fix logout token missing sid, fix wrong sub mode used (cherry-pick #16295) (#16299)
fix logout token missing sid, fix wrong sub mode used (#16295)
2025-08-21 16:39:18 +02:00
Teffen Ellis
8f70dbb963 web: Fix hidden textarea required attribute. (cherry-pick #16168) (#16275)
Fix hidden textarea `required` attribute. (#16168)
2025-08-21 14:50:36 +02:00
Teffen Ellis
15505f5caf web: fix "Explore integrations" link in Quick actions (cherry-pick #16274) (#16285)
Co-authored-by: Max <mail@heavygale.de>
fix "Explore integrations" link in Quick actions (#16274)
2025-08-21 14:20:07 +02:00
Marc 'risson' Schmitt
b2d770c0a4 *: Fix dead doc link (cherry-pick #16288) (#16297)
Co-authored-by: Dominic R <dominic@sdko.org>
Fix dead doc link (#16288)
2025-08-21 14:10:33 +02:00
168 changed files with 2112 additions and 1715 deletions

View File

@@ -1 +0,0 @@
website/docs/developer-docs/index.md

4
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,4 @@
# Contributing to authentik
Thanks for your interest in contributing! Please see our [contributing guide](https://docs.goauthentik.io/docs/developer-docs/?utm_source=github) for more information.

View File

@@ -15,15 +15,16 @@
## What is authentik?
authentik is an open-source Identity Provider that emphasizes flexibility and versatility, with support for a wide set of protocols.
authentik is an open-source Identity Provider (IdP) for modern SSO. It supports SAML, OAuth2/OIDC, LDAP, RADIUS, and more, designed for self-hosting from small labs to large production clusters.
Our [enterprise offer](https://goauthentik.io/pricing) can also be used as a self-hosted replacement for large-scale deployments of Okta/Auth0, Entra ID, Ping Identity, or other legacy IdPs for employees and B2B2C use.
Our [enterprise offering](https://goauthentik.io/pricing) is available for organizations to securely replace existing IdPs such as Okta, Auth0, Entra ID, and Ping Identity for robust, large-scale identity management.
## Installation
For small/test setups it is recommended to use Docker Compose; refer to the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github).
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github).
- Docker Compose: recommended for small/test setups. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/docker-compose/).
- Kubernetes (Helm Chart): recommended for larger setups. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/kubernetes/) and the Helm chart [repository](https://github.com/goauthentik/helm).
- AWS CloudFormation: deploy on AWS using our official templates. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/aws/).
- DigitalOcean Marketplace: one-click deployment via the official Marketplace app. See the [app listing](https://marketplace.digitalocean.com/apps/authentik).
## Screenshots
@@ -32,14 +33,20 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
| ![](https://docs.goauthentik.io/img/screen_apps_light.jpg) | ![](https://docs.goauthentik.io/img/screen_apps_dark.jpg) |
| ![](https://docs.goauthentik.io/img/screen_admin_light.jpg) | ![](https://docs.goauthentik.io/img/screen_admin_dark.jpg) |
## Development
## Development and contributions
See [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/?utm_source=github)
See the [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/) for information about setting up local build environments, testing your contributions, and our contribution process.
## Security
See [SECURITY.md](SECURITY.md)
Please see [SECURITY.md](SECURITY.md).
## Adoption and Contributions
## Adoption
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [contribution guide](https://docs.goauthentik.io/docs/developer-docs?utm_source=github).
Using authentik? We'd love to hear your story and feature your logo. Email us at [hello@goauthentik.io](mailto:hello@goauthentik.io) or open a GitHub Issue/PR!
## License
[![MIT License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)](LICENSE)
[![CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey?style=for-the-badge)](website/LICENSE)
[![authentik EE License](https://img.shields.io/badge/License-EE-orange?style=for-the-badge)](authentik/enterprise/LICENSE)

View File

@@ -25,7 +25,28 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
## Reporting a Vulnerability
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io). Be sure to include relevant information like which version you've found the issue in, instructions on how to reproduce the issue, and anything else that might make it easier for us to find the issue.
If you discover a potential vulnerability, please report it responsibly through one of the following channels:
- **Email**: [security@goauthentik.io](mailto:security@goauthentik.io)
- **GitHub**: Submit a private security advisory via our [repositorys advisory portal](https://github.com/goauthentik/authentik/security/advisories/new)
When submitting a report, please include as much detail as possible, such as:
- **Affected version(s)**: The version of authentik where the issue was identified.
- **Steps to reproduce**: A clear description or proof of concept to help us verify the issue.
- **Impact assessment**: How the vulnerability could be exploited and its potential effect.
- **Additional information**: Logs, configuration details (if relevant), or any suggested mitigations.
We kindly ask that you do not disclose the vulnerability publicly until we have confirmed and addressed the issue.
Our team will:
- Acknowledge receipt of your report as quickly as possible.
- Keep you updated on the investigation and resolution progress.
## Researcher Recognition
We value contributions from the security community. For each valid report, we will publish a dedicated entry on our Security Advisory page that optionally includes the reporters name (or preferred alias). Please note that while we do not currently offer monetary bounties, we are committed to giving researchers appropriate credit for their efforts in keeping authentik secure.
## Severity levels

View File

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2025.8.0"
VERSION = "2025.8.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -38,6 +38,7 @@ from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.events.logs import capture_logs
from authentik.events.utils import sanitize_dict
from authentik.lib.config import CONFIG
from authentik.tasks.apps import PRIORITY_HIGH
from authentik.tasks.models import Task
from authentik.tasks.schedules.models import Schedule
from authentik.tenants.models import Tenant
@@ -111,6 +112,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
@actor(
description=_("Find blueprints as `blueprints_find` does, but return a safe dict."),
throws=(DatabaseError, ProgrammingError, InternalError),
priority=PRIORITY_HIGH,
)
def blueprints_find_dict():
blueprints = []

View File

@@ -328,6 +328,12 @@ class SessionUserSerializer(PassiveSerializer):
original = UserSelfSerializer(required=False)
class UserPasswordSetSerializer(PassiveSerializer):
"""Payload to set a users' password directly"""
password = CharField(required=True)
class UsersFilter(FilterSet):
"""Filter for users"""
@@ -585,12 +591,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@permission_required("authentik_core.reset_user_password")
@extend_schema(
request=inline_serializer(
"UserPasswordSetSerializer",
{
"password": CharField(required=True),
},
),
request=UserPasswordSetSerializer,
responses={
204: OpenApiResponse(description="Successfully changed password"),
400: OpenApiResponse(description="Bad request"),
@@ -599,9 +600,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@action(detail=True, methods=["POST"], permission_classes=[])
def set_password(self, request: Request, pk: int) -> Response:
"""Set password for user"""
data = UserPasswordSetSerializer(data=request.data)
data.is_valid(raise_exception=True)
user: User = self.get_object()
try:
user.set_password(request.data.get("password"), request=request)
user.set_password(data.validated_data["password"], request=request)
user.save()
except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password", exc=exc)

View File

@@ -102,6 +102,16 @@ class TestUsersAPI(APITestCase):
self.admin.refresh_from_db()
self.assertTrue(self.admin.check_password(new_pw))
def test_set_password_blank(self):
"""Test Direct password set"""
self.client.force_login(self.admin)
response = self.client.post(
reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}),
data={"password": ""},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]})
def test_recovery(self):
"""Test user recovery link"""
flow = create_test_flow(

View File

@@ -154,6 +154,7 @@ worker:
consumer_listen_timeout: "seconds=30"
task_max_retries: 20
task_default_time_limit: "minutes=10"
lock_purge_interval: "minutes=1"
task_purge_interval: "days=1"
task_expiration: "days=30"
scheduler_interval: "seconds=60"

View File

@@ -43,7 +43,9 @@ def structlog_configure():
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso", utc=False),
structlog.processors.StackInfoRenderer(),
structlog.processors.dict_tracebacks,
structlog.processors.ExceptionRenderer(
structlog.processors.ExceptionDictTransformer(show_locals=CONFIG.get_bool("debug"))
),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
@@ -65,7 +67,14 @@ def get_logger_config():
"json": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.JSONRenderer(sort_keys=True),
"foreign_pre_chain": LOG_PRE_CHAIN + [structlog.processors.dict_tracebacks],
"foreign_pre_chain": LOG_PRE_CHAIN
+ [
structlog.processors.ExceptionRenderer(
structlog.processors.ExceptionDictTransformer(
show_locals=CONFIG.get_bool("debug")
)
),
],
},
"console": {
"()": structlog.stdlib.ProcessorFormatter,

View File

@@ -1,4 +1,5 @@
from dramatiq.actor import Actor
from dramatiq.results.errors import ResultFailure
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, ChoiceField
@@ -110,9 +111,13 @@ class OutgoingSyncProviderStatusMixin:
"override_dry_run": params.validated_data["override_dry_run"],
"pk": params.validated_data["sync_object_id"],
},
retries=0,
rel_obj=provider,
)
msg.get_result(block=True)
try:
msg.get_result(block=True)
except ResultFailure:
pass
task: Task = msg.options["task"]
task.refresh_from_db()
return Response(SyncObjectResultSerializer(instance={"messages": task._messages}).data)

View File

@@ -20,6 +20,7 @@ from authentik.lib.sync.outgoing.exceptions import (
TransientSyncException,
)
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.errors import exception_to_dict
from authentik.lib.utils.reflection import class_to_path, path_to_class
from authentik.tasks.models import Task
@@ -164,16 +165,17 @@ class SyncTasks:
except BadRequestSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, obj=obj)
task.warning(
f"Failed to sync {obj._meta.verbose_name} {str(obj)} due to error: {str(exc)}",
f"Failed to sync {str(obj)} due to error: {str(exc)}",
arguments=exc.args[1:],
obj=sanitize_item(obj),
exception=exception_to_dict(exc),
)
except TransientSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, user=obj)
task.warning(
f"Failed to sync {obj._meta.verbose_name} {str(obj)} due to "
"transient error: {str(exc)}",
f"Failed to sync {str(obj)} due to " f"transient error: {str(exc)}",
obj=sanitize_item(obj),
exception=exception_to_dict(exc),
)
except StopSync as exc:
self.logger.warning("Stopping sync", exc=exc)

View File

@@ -76,6 +76,7 @@ class OutpostConfig:
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
kubernetes_ingress_class_name: str | None = field(default=None)
kubernetes_ingress_path_type: str | None = field(default=None)
kubernetes_httproute_annotations: dict[str, str] = field(default_factory=dict)
kubernetes_httproute_parent_refs: list[dict[str, str]] = field(default_factory=list)
kubernetes_service_type: str = field(default="ClusterIP")
@@ -151,7 +152,7 @@ class OutpostServiceConnection(ScheduledModel, models.Model):
state = cache.get(self.state_key, None)
if not state:
outpost_service_connection_monitor.send_with_options(args=(self.pk), rel_obj=self)
outpost_service_connection_monitor.send_with_options(args=(self.pk,), rel_obj=self)
return OutpostServiceConnectionState("", False)
return state

View File

@@ -26,7 +26,6 @@ HIST_POLICIES_EXECUTION_TIME = Histogram(
"binding_order",
"binding_target_type",
"binding_target_name",
"object_pk",
"object_type",
"mode",
],

View File

@@ -86,7 +86,6 @@ class PolicyEngine:
binding_order=binding.order,
binding_target_type=binding.target_type,
binding_target_name=binding.target_name,
object_pk=str(self.request.obj.pk),
object_type=class_to_path(self.request.obj.__class__),
mode="cache_retrieve",
).time():

View File

@@ -131,7 +131,6 @@ class PolicyProcess(PROCESS_CLASS):
binding_order=self.binding.order,
binding_target_type=self.binding.target_type,
binding_target_name=self.binding.target_name,
object_pk=str(self.request.obj.pk) if self.request.obj else "",
object_type=class_to_path(self.request.obj.__class__) if self.request.obj else "",
mode="execute_process",
).time(),

View File

@@ -11,7 +11,8 @@ def migrate_sessions(apps, schema_editor, model):
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
db_alias = schema_editor.connection.alias
for obj in Model.objects.using(db_alias).all():
objs = list(Model.objects.using(db_alias).select_related("old_session").all())
for obj in objs:
if not obj.old_session:
continue
obj.session = (

View File

@@ -23,7 +23,12 @@ def user_session_deleted_oauth_backchannel_logout_and_tokens_removal(
backchannel_logout_notification_dispatch.send(
revocations=[
(token.provider_id, token.id_token.iss, token.session.user.uid)
(
token.provider_id,
token.id_token.iss,
token.id_token.sub,
instance.session.session_key,
)
for token in access_tokens
],
)

View File

@@ -14,13 +14,19 @@ LOGGER = get_logger()
@actor(description=_("Send a back-channel logout request to the registered client"))
def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None) -> bool:
def send_backchannel_logout_request(
provider_pk: int,
iss: str,
sub: str | None = None,
session_key: str | None = None,
) -> bool:
"""Send a back-channel logout request to the registered client
Args:
provider_pk: The OAuth2 provider's primary key
iss: The issuer URL for the logout token
sub: The subject identifier to include in the logout token
session_key: The authentik session key to hash and include in the logout token
Returns:
bool: True if the request was sent successfully, False otherwise
@@ -33,11 +39,10 @@ def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None)
return
# Generate the logout token
logout_token = create_logout_token(iss, provider, None, sub)
logout_token = create_logout_token(provider, iss, sub, session_key)
# Get the back-channel logout URI from the provider's dedicated backchannel_logout_uri field
# Back-channel logout requires explicit configuration - no fallback to redirect URIs
backchannel_logout_uri = provider.backchannel_logout_uri
if not backchannel_logout_uri:
self.info("No back-channel logout URI found for provider")
@@ -60,9 +65,9 @@ def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None)
def backchannel_logout_notification_dispatch(revocations: list, **kwargs):
"""Handle backchannel logout notifications dispatched via signal"""
for revocation in revocations:
provider_pk, iss, sub = revocation
provider_pk, iss, sub, session_key = revocation
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
send_backchannel_logout_request.send_with_options(
args=(provider_pk, iss, sub),
args=(provider_pk, iss, sub, session_key),
rel_obj=provider,
)

View File

@@ -4,17 +4,18 @@ import re
import uuid
from base64 import b64decode
from binascii import Error
from time import time
from typing import Any
from urllib.parse import urlparse
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.http.response import HttpResponseRedirect
from django.utils.cache import patch_vary_headers
from django.utils.timezone import now
from structlog.stdlib import get_logger
from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER
from authentik.events.models import Event, EventAction
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.errors import BearerTokenError
from authentik.providers.oauth2.id_token import hash_session_key
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
@@ -217,23 +218,25 @@ class HttpResponseRedirectScheme(HttpResponseRedirect):
def create_logout_token(
iss: str,
provider: OAuth2Provider,
session_key: str | None = None,
iss: str,
sub: str | None = None,
session_key: str | None = None,
) -> str:
"""Create a logout token for Back-Channel Logout
As per https://openid.net/specs/openid-connect-backchannel-1_0.html
"""
LOGGER.debug("Creating logout token", provider=provider, session_key=session_key, sub=sub)
LOGGER.debug("Creating logout token", provider=provider, sub=sub)
_now = now()
# Create the logout token payload
payload = {
"iss": str(iss),
"aud": provider.client_id,
"iat": int(time()),
"iat": int(_now.timestamp()),
"exp": int((_now + timedelta_from_string(provider.access_token_validity)).timestamp()),
"jti": str(uuid.uuid4()),
"events": {
"http://schemas.openid.net/event/backchannel-logout": {},

View File

@@ -127,6 +127,9 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
and self.controller.outpost.config.kubernetes_ingress_secret_name
):
tls_hosts.append(external_host_name.hostname)
path_type = "Prefix"
if self.controller.outpost.config.kubernetes_ingress_path_type:
path_type = self.controller.outpost.config.kubernetes_ingress_path_type
if proxy_provider.mode in [
ProxyMode.FORWARD_SINGLE,
ProxyMode.FORWARD_DOMAIN,
@@ -143,7 +146,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
),
),
path="/outpost.goauthentik.io",
path_type="Prefix",
path_type=path_type,
)
]
),
@@ -161,7 +164,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
),
),
path="/",
path_type="Prefix",
path_type=path_type,
)
]
),

View File

@@ -13,7 +13,7 @@ def migrate_sessions(apps, schema_editor):
for token in ConnectionToken.objects.using(db_alias).all():
token.session = (
AuthenticatedSession.objects.using(db_alias)
.filter(session_key=token.old_session.session_key)
.filter(session__session_key=token.old_session.session_key)
.first()
)
if token.session:

View File

@@ -27,3 +27,8 @@ class SCIMRequestException(TransientSyncException):
except ValidationError:
pass
return self._message
def __str__(self):
if self._response:
return self._response.text
return super().__str__()

View File

@@ -4,7 +4,6 @@ import importlib
from collections import OrderedDict
from hashlib import sha512
from pathlib import Path
from tempfile import gettempdir
import orjson
from sentry_sdk import set_tag
@@ -368,6 +367,9 @@ DRAMATIQ = {
"broker_class": "authentik.tasks.broker.Broker",
"channel_prefix": "authentik",
"task_model": "authentik.tasks.models.Task",
"lock_purge_interval": timedelta_from_string(
CONFIG.get("worker.lock_purge_interval")
).total_seconds(),
"task_purge_interval": timedelta_from_string(
CONFIG.get("worker.task_purge_interval")
).total_seconds(),
@@ -424,7 +426,6 @@ DRAMATIQ = {
(
"authentik.tasks.middleware.MetricsMiddleware",
{
"multiproc_dir": str(Path(gettempdir()) / "authentik_prometheus_tmp"),
"prefix": "authentik",
},
),

View File

@@ -5,6 +5,7 @@ from typing import Any
from django.db.models import Q
from ldap3 import SUBTREE
from ldap3.utils.conv import escape_filter_chars
from authentik.core.models import Group, User
from authentik.sources.ldap.models import LDAP_DISTINGUISHED_NAME, LDAP_UNIQUENESS, LDAPSource
@@ -52,7 +53,8 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
for group in page_data:
if self._source.lookup_groups_from_user:
group_dn = group.get("dn", {})
group_filter = f"({self._source.group_membership_field}={group_dn})"
escaped_dn = escape_filter_chars(group_dn)
group_filter = f"({self._source.group_membership_field}={escaped_dn})"
group_members = self._source.connection().extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=group_filter,

View File

@@ -4,6 +4,8 @@ from unittest.mock import MagicMock, patch
from django.db.models import Q
from django.test import TestCase
from ldap3.core.exceptions import LDAPInvalidFilterError
from ldap3.utils.conv import escape_filter_chars
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Group, User
@@ -519,3 +521,89 @@ class LDAPSyncTests(TestCase):
self.assertFalse(User.objects.filter(username__startswith="not-in-the-source").exists())
self.assertFalse(Group.objects.filter(name__startswith="not-in-the-source").exists())
def test_membership_sync_special_chars_in_group_dn(self):
"""Test membership synchronization with special characters in group DN"""
self.source.object_uniqueness_field = "uid"
self.source.group_object_filter = "(objectClass=groupOfNames)"
self.source.lookup_groups_from_user = True
self.source.group_membership_field = "memberOf"
# Mock connection with group DN containing special characters
mock_conn = MagicMock()
# Simulate group with special characters in DN: parentheses, backslashes, asterisks
special_group_dn = "cn=test(group),ou=groups,dc=example,dc=com"
backslash_group_dn = "cn=test\\group,ou=groups,dc=example,dc=com"
asterisk_group_dn = "cn=test*group,ou=groups,dc=example,dc=com"
# Mock the paged_search method that would be called with the filter
mock_standard = MagicMock()
mock_conn.extend.standard = mock_standard
# Test case 1: Group DN with parentheses
with patch("authentik.sources.ldap.models.LDAPSource.connection", return_value=mock_conn):
membership_sync = MembershipLDAPSynchronizer(self.source, Task())
# Simulate group data with special characters in DN
page_data = [{"dn": special_group_dn}]
# This should not raise LDAPInvalidFilterError anymore
try:
membership_sync.sync(page_data)
# Verify that the filter was properly escaped
# The call should have been made with escaped characters
mock_standard.paged_search.assert_called()
call_args = mock_standard.paged_search.call_args
search_filter = call_args[1]["search_filter"]
# The parentheses should be escaped as \28 and \29
self.assertIn("\\28", search_filter) # Escaped (
self.assertIn("\\29", search_filter) # Escaped )
except LDAPInvalidFilterError:
self.fail("LDAPInvalidFilterError should not be raised with escaped filter")
# Test case 2: Group DN with backslashes
with patch("authentik.sources.ldap.models.LDAPSource.connection", return_value=mock_conn):
membership_sync = MembershipLDAPSynchronizer(self.source, Task())
page_data = [{"dn": backslash_group_dn}]
try:
membership_sync.sync(page_data)
call_args = mock_standard.paged_search.call_args
search_filter = call_args[1]["search_filter"]
# The backslash should be escaped as \5c
self.assertIn("\\5c", search_filter) # Escaped \
except LDAPInvalidFilterError:
self.fail("LDAPInvalidFilterError should not be raised with escaped filter")
# Test case 3: Group DN with asterisks
with patch("authentik.sources.ldap.models.LDAPSource.connection", return_value=mock_conn):
membership_sync = MembershipLDAPSynchronizer(self.source, Task())
page_data = [{"dn": asterisk_group_dn}]
try:
membership_sync.sync(page_data)
call_args = mock_standard.paged_search.call_args
search_filter = call_args[1]["search_filter"]
# The asterisk should be escaped as \2a
self.assertIn("\\2a", search_filter) # Escaped *
except LDAPInvalidFilterError:
self.fail("LDAPInvalidFilterError should not be raised with escaped filter")
def test_escape_filter_chars_function(self):
"""Test the escape_filter_chars function directly"""
# Test various special characters that need escaping
test_cases = [
("test(group)", "test\\28group\\29"), # parentheses
("test\\group", "test\\5cgroup"), # backslash
("test*group", "test\\2agroup"), # asterisk
("test(*)group", "test\\28\\2a\\29group"), # multiple special chars
("normalgroup", "normalgroup"), # no special chars
("", ""), # empty string
]
for input_str, expected in test_cases:
with self.subTest(input_str=input_str):
result = escape_filter_chars(input_str)
self.assertEqual(result, expected)

View File

@@ -96,7 +96,11 @@ class EntraIDType(SourceType):
}
def get_base_group_properties(self, source, group_id, **kwargs):
raw_group = kwargs["info"]["raw_groups"][group_id]
raw_groups = kwargs["info"]["raw_groups"]
if group_id in raw_groups:
name = raw_groups[group_id]["displayName"]
else:
name = group_id
return {
"name": raw_group["displayName"],
"name": name,
}

View File

@@ -142,6 +142,11 @@ class AuthenticatorEmailStageView(ChallengeStageView):
user = self.get_pending_user()
stage: AuthenticatorEmailStage = self.executor.current_stage
# For the moment we only allow one email device per user
if EmailDevice.objects.filter(Q(user=user), stage=stage.pk).exists():
return self.executor.stage_invalid(
_("The user already has an email address registered for MFA.")
)
if SESSION_KEY_EMAIL_DEVICE not in self.request.session:
device = EmailDevice(user=user, confirmed=False, stage=stage, name="Email Device")
valid_secs: int = timedelta_from_string(stage.token_expiry).total_seconds()

View File

@@ -108,6 +108,17 @@ class TestAuthenticatorEmailStage(FlowTestCase):
)
def test_stage_submit(self):
"""Test stage email submission"""
# test fail because of existing device
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
self.assertStageResponse(
response,
self.flow,
self.user,
component="ak-stage-access-denied",
)
self.device.delete()
# Initialize the flow
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
@@ -232,6 +243,7 @@ class TestAuthenticatorEmailStage(FlowTestCase):
def test_challenge_generation(self):
"""Test challenge generation"""
# Test with masked email
self.device.delete()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)

View File

@@ -35,7 +35,12 @@ class Command(TenantCommand):
template_context={},
)
try:
send_mail(message.__dict__, stage.pk)
if not stage.use_global_settings:
message.from_email = stage.from_address
send_mail.send(message.__dict__, stage.pk).get_result(block=True)
self.stdout.write(self.style.SUCCESS(f"Test email sent to {options['to']}"))
finally:
if delete_stage:
stage.delete()

View File

@@ -0,0 +1,66 @@
"""Test email management commands"""
from unittest.mock import patch
from django.core import mail
from django.core.mail.backends.locmem import EmailBackend
from django.core.management import call_command
from django.test import TestCase
from authentik.core.tests.utils import create_test_admin_user
from authentik.stages.email.models import EmailStage
class TestEmailManagementCommands(TestCase):
"""Test email management commands"""
def setUp(self):
self.user = create_test_admin_user()
def test_test_email_command_with_stage(self):
"""Test test_email command with specified stage"""
EmailStage.objects.create(
name="test-stage",
from_address="test@authentik.local",
host="localhost",
port=25,
)
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
call_command("test_email", "test@example.com", stage="test-stage")
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik Test-Email")
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
def test_test_email_command_with_global_settings(self):
"""Test test_email command with global settings"""
# Mock the backend to use Django's locmem backend
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
call_command("test_email", "test@example.com")
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "authentik Test-Email")
self.assertEqual(mail.outbox[0].to, ["test@example.com"])
def test_test_email_command_invalid_stage(self):
"""Test test_email command with invalid stage"""
call_command("test_email", "test@example.com", stage="nonexistent")
self.assertEqual(len(mail.outbox), 0)
def test_test_email_command_with_custom_from(self):
"""Test test_email command respects custom from address"""
EmailStage.objects.create(
name="test-stage",
from_address="custom@authentik.local",
host="localhost",
port=25,
)
with patch("authentik.stages.email.models.EmailStage.backend_class", EmailBackend):
call_command("test_email", "test@example.com", stage="test-stage")
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].from_email, "custom@authentik.local")
self.assertEqual(mail.outbox[0].to, ["test@example.com"])

View File

@@ -2,6 +2,8 @@ from authentik.blueprints.apps import ManagedAppConfig
from authentik.lib.utils.time import fqdn_rand
from authentik.tasks.schedules.common import ScheduleSpec
PRIORITY_HIGH = 1000
class AuthentikTasksConfig(ManagedAppConfig):
name = "authentik.tasks"

View File

@@ -14,6 +14,7 @@ from django_redis import get_redis_connection
from dramatiq.broker import Broker
from dramatiq.message import Message
from dramatiq.middleware import Middleware
from psycopg.errors import Error
from redis.exceptions import RedisError
from structlog.stdlib import get_logger
@@ -26,6 +27,7 @@ from authentik.tenants.utils import get_current_tenant
LOGGER = get_logger()
HEALTHCHECK_LOGGER = get_logger("authentik.worker").bind()
DB_ERRORS = (OperationalError, Error, RedisError)
class TenantMiddleware(Middleware):
@@ -175,7 +177,7 @@ class _healthcheck_handler(BaseHTTPRequestHandler):
redis_conn = get_redis_connection()
redis_conn.ping()
self.send_response(200)
except (OperationalError, RedisError): # pragma: no cover
except DB_ERRORS: # pragma: no cover
self.send_response(503)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", "0")
@@ -216,6 +218,14 @@ class WorkerStatusMiddleware(Middleware):
hostname=socket.gethostname(),
version=authentik_full_version(),
)
while True:
try:
WorkerStatusMiddleware.keep(status)
except DB_ERRORS: # pragma: no cover
sleep(10)
pass
def keep(status: WorkerStatus):
lock_id = f"goauthentik.io/worker/status/{status.pk}"
with pglock.advisory(lock_id, side_effect=pglock.Raise):
while True:

View File

@@ -107,7 +107,6 @@ class ScheduleViewSet(
"rel_obj_content_type__app_label",
"rel_obj_content_type__model",
"rel_obj_id",
"description",
)
filterset_class = ScheduleFilter
ordering = (

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 2025.8.0 Blueprint schema",
"title": "authentik 2025.8.3 Blueprint schema",
"required": [
"version",
"entries"

View File

@@ -5,7 +5,7 @@ metadata:
blueprints.goauthentik.io/system-bootstrap: "true"
blueprints.goauthentik.io/system: "true"
blueprints.goauthentik.io/description: |
This blueprint configures the default admin user and group, and configures them for the [Automated install](https://goauthentik.io/docs/installation/automated-install).
This blueprint configures the default admin user and group, and configures them for the [Automated install](https://docs.goauthentik.io/docs/install-config/automated-install?utm_source=bootstrap_blueprint).
context:
username: akadmin
group_name: authentik Admins

View File

@@ -48,7 +48,7 @@ services:
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.3}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
@@ -72,7 +72,7 @@ services:
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.3}
restart: unless-stopped
user: root
volumes:

View File

@@ -1 +1 @@
2025.8.0
2025.8.3

View File

@@ -2,6 +2,10 @@
set -e -o pipefail
MODE_FILE="${TMPDIR}/authentik-mode"
if [[ -z "${PROMETHEUS_MULTIPROC_DIR}" ]]; then
export PROMETHEUS_MULTIPROC_DIR="${TMPDIR:-/tmp}/authentik_prometheus_tmp"
fi
function log {
printf '{"event": "%s", "level": "info", "logger": "bootstrap"}\n' "$@" >/dev/stderr
}
@@ -31,7 +35,7 @@ function check_if_root {
GROUP="authentik:${GROUP_NAME}"
fi
# Fix permissions of certs and media
chown -R authentik:authentik /media /certs
chown -R authentik:authentik /media /certs "${PROMETHEUS_MULTIPROC_DIR}"
chmod ug+rwx /media
chmod ug+rx /certs
exec chpst -u authentik:$GROUP env HOME=/authentik $1
@@ -68,6 +72,8 @@ function prepare_debug {
chown authentik:authentik /unittest.xml
}
mkdir -p "${PROMETHEUS_MULTIPROC_DIR}"
if [[ "$(python -m authentik.lib.config debugger 2>/dev/null)" == "True" ]]; then
prepare_debug
fi

View File

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

View File

@@ -33,15 +33,12 @@ wait_for_db()
_tmp = Path(gettempdir())
worker_class = "lifecycle.worker.DjangoUvicornWorker"
worker_tmp_dir = str(_tmp.joinpath("authentik_gunicorn_tmp"))
prometheus_tmp_dir = str(_tmp.joinpath("authentik_prometheus_tmp"))
os.makedirs(worker_tmp_dir, exist_ok=True)
os.makedirs(prometheus_tmp_dir, exist_ok=True)
bind = f"unix://{str(_tmp.joinpath('authentik-core.sock'))}"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", prometheus_tmp_dir)
preload_app = True

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/authentik",
"version": "2025.8.0",
"version": "2025.8.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/authentik",
"version": "2025.8.0",
"version": "2025.8.3",
"dependencies": {
"@eslint/js": "^9.31.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/authentik",
"version": "2025.8.0",
"version": "2025.8.3",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -237,6 +237,9 @@ class _PostgresConsumer(Consumer):
# Override because dramatiq doesn't allow us setting this manually
self.timeout = Conf().worker["consumer_listen_timeout"]
self.lock_purge_interval = timezone.timedelta(seconds=Conf().lock_purge_interval)
self.lock_purge_last_run = timezone.now()
self.task_purge_interval = timezone.timedelta(seconds=Conf().task_purge_interval)
self.task_purge_last_run = timezone.now() - self.task_purge_interval
@@ -378,6 +381,8 @@ class _PostgresConsumer(Consumer):
# Force creation of listen connection
_ = self.listen_connection
self._purge_locks()
processing = len(self.in_processing)
if processing >= self.prefetch:
# Wait and don't consume the message, other worker will be faster
@@ -415,24 +420,26 @@ class _PostgresConsumer(Consumer):
)
# No message to process
self._purge_locks()
self._auto_purge()
self._scheduler()
return None
def _purge_locks(self):
if timezone.now() - self.lock_purge_last_run < self.lock_purge_interval:
return
while True:
try:
message_id = self.unlock_queue.get(block=False)
except Empty:
return
break
self.logger.debug("Unlocking message", message_id=message_id)
with self.connection.cursor() as cursor:
cursor.execute(
"SELECT pg_advisory_unlock(%s)", (self._get_message_lock_id(message_id),)
)
self.unlock_queue.task_done()
self.lock_purge_last_run = timezone.now()
def _auto_purge(self):
if timezone.now() - self.task_purge_last_run < self.task_purge_interval:
@@ -444,6 +451,7 @@ class _PostgresConsumer(Consumer):
result_expiry__lte=timezone.now(),
).delete()
self.logger.info("Purged messages in all queues", count=count)
self.task_purge_last_run = timezone.now()
def _scheduler(self):
if not self.scheduler:
@@ -451,6 +459,7 @@ class _PostgresConsumer(Consumer):
if timezone.now() - self.scheduler_last_run < self.scheduler_interval:
return
self.scheduler.run()
self.schedule_last_run = timezone.now()
@raise_connection_error
def close(self):
@@ -465,4 +474,7 @@ class _PostgresConsumer(Consumer):
if self._listen_connection is not None:
conn = self._listen_connection
self._listen_connection = None
conn.close()
try:
conn.close()
except DatabaseError:
pass

View File

@@ -56,6 +56,10 @@ class Conf:
def task_model(self) -> str:
return self.conf["task_model"]
@property
def lock_purge_interval(self) -> int:
return self.conf.get("lock_purge_interval", 60)
@property
def task_purge_interval(self) -> int:
# 24 hours

View File

@@ -26,7 +26,7 @@ class HTTPServer(BaseHTTPServer):
self.socket.close()
host, port = self.server_address[:2]
if host == "0.0.0.0": # nosec
if host == "0.0.0.0" and socket.has_dualstack_ipv6(): # nosec
host = "::" # nosec
# Strip IPv6 brackets
@@ -36,7 +36,9 @@ class HTTPServer(BaseHTTPServer):
self.server_address = (host, port)
self.address_family = (
socket.AF_INET6 if isinstance(ip_address(host), IPv6Address) else socket.AF_INET
socket.AF_INET6
if socket.has_dualstack_ipv6() and isinstance(ip_address(host), IPv6Address)
else socket.AF_INET
)
self.socket = socket.create_server(
@@ -141,7 +143,6 @@ class MetricsMiddleware(Middleware):
def __init__(
self,
prefix: str,
multiproc_dir: str,
labels: list[str] | None = None,
):
super().__init__()
@@ -151,9 +152,6 @@ class MetricsMiddleware(Middleware):
self.delayed_messages = set()
self.message_start_times = {}
os.makedirs(multiproc_dir, exist_ok=True)
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", multiproc_dir)
@property
def forks(self):
from django_dramatiq_postgres.forks import worker_metrics

View File

@@ -1,6 +1,6 @@
[project]
name = "authentik"
version = "2025.8.0"
version = "2025.8.3"
description = ""
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
requires-python = "==3.13.*"
@@ -12,7 +12,7 @@ dependencies = [
"dacite==1.9.2",
"deepmerge==2.0",
"defusedxml==0.7.1",
"django==5.1.11",
"django==5.1.12",
"django-countries==7.6.1",
"django-cte==2.0.0",
"django-dramatiq-postgres",

View File

@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2025.8.0
version: 2025.8.3
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@@ -61879,6 +61879,7 @@ components:
- object_pk
UserPasswordSetRequest:
type: object
description: Payload to set a users' password directly
properties:
password:
type: string

16
uv.lock generated
View File

@@ -159,7 +159,7 @@ wheels = [
[[package]]
name = "authentik"
version = "2025.8.0"
version = "2025.8.3"
source = { editable = "." }
dependencies = [
{ name = "argon2-cffi" },
@@ -266,7 +266,7 @@ requires-dist = [
{ name = "dacite", specifier = "==1.9.2" },
{ name = "deepmerge", specifier = "==2.0" },
{ name = "defusedxml", specifier = "==0.7.1" },
{ name = "django", specifier = "==5.1.11" },
{ name = "django", specifier = "==5.1.12" },
{ name = "django-countries", specifier = "==7.6.1" },
{ name = "django-cte", specifier = "==2.0.0" },
{ name = "django-dramatiq-postgres", editable = "packages/django-dramatiq-postgres" },
@@ -899,16 +899,16 @@ wheels = [
[[package]]
name = "django"
version = "5.1.11"
version = "5.1.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/80/bf0f9b0aa434fca2b46fc6a31c39b08ea714b87a0a72a16566f053fb05a8/django-5.1.11.tar.gz", hash = "sha256:3bcdbd40e4d4623b5e04f59c28834323f3086df583058e65ebce99f9982385ce", size = 10734926, upload-time = "2025-06-10T10:12:48.229Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/99/a951d93a27a5bc59fb96edbcdbc03fb9bfac51177f1bc0110888de85af3f/django-5.1.12.tar.gz", hash = "sha256:8a8991b1ec052ef6a44fefd1ef336ab8daa221287bcb91a4a17d5e1abec5bbcc", size = 10737777, upload-time = "2025-09-03T13:09:45.855Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/91/2972ce330c6c0bd5b3200d4c2ad5cbf47eecff5243220c5a56444d3267a0/django-5.1.11-py3-none-any.whl", hash = "sha256:e48091f364007068728aca938e7450fbfe3f2217079bfd2b8af45122585acf64", size = 8277453, upload-time = "2025-06-10T10:12:42.236Z" },
{ url = "https://files.pythonhosted.org/packages/1e/1c/a9520c8263e980b0b9933c9b5ce8f22c9ddf007b062e4eb428b557ff0932/django-5.1.12-py3-none-any.whl", hash = "sha256:9eb695636cea3601b65690f1596993c042206729afb320ca0960b55f8ed4477b", size = 8277454, upload-time = "2025-09-03T13:09:30.997Z" },
]
[[package]]
@@ -1499,15 +1499,15 @@ wheels = [
[[package]]
name = "h2"
version = "4.2.0"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682, upload-time = "2025-02-02T07:43:51.815Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957, upload-time = "2025-02-01T11:02:26.481Z" },
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
]
[[package]]

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@goauthentik/web",
"version": "2025.8.0",
"version": "2025.8.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@goauthentik/web",
"version": "2025.8.0",
"version": "2025.8.3",
"license": "MIT",
"workspaces": [
"./packages/*"

View File

@@ -1,6 +1,6 @@
{
"name": "@goauthentik/web",
"version": "2025.8.0",
"version": "2025.8.3",
"license": "MIT",
"private": true,
"scripts": {

View File

@@ -66,7 +66,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
quickActions: QuickAction[] = [
[msg("Create a new application"), paramURL("/core/applications", { createWizard: true })],
[msg("Check the logs"), paramURL("/events/log")],
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
[msg("Explore integrations"), "https://integrations.goauthentik.io/", true],
[msg("Manage users"), paramURL("/identity/users")],
[
msg("Check the release notes"),

View File

@@ -33,23 +33,7 @@ const DEFAULT_REPUTATION_UPPER_LIMIT = 5;
@customElement("ak-admin-settings-form")
export class AdminSettingsForm extends Form<SettingsRequest> {
//
// Custom property accessors in Lit 2 require a manual call to requestUpdate(). See:
// https://lit.dev/docs/v2/components/properties/#accessors-custom
//
set settings(value: Settings | undefined) {
this._settings = value;
this.requestUpdate();
}
@property({ type: Object })
get settings() {
return this._settings;
}
private _settings?: Settings;
static styles: CSSResult[] = [
public static styles: CSSResult[] = [
...super.styles,
PFList,
css`
@@ -59,25 +43,35 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
`,
];
@property({ attribute: false })
public settings!: Settings;
getSuccessMessage(): string {
return msg("Successfully updated settings.");
}
async send(data: SettingsRequest): Promise<Settings> {
async send(settingsRequest: SettingsRequest): Promise<Settings> {
settingsRequest.flags ??= this.settings.flags;
const result = await new AdminApi(DEFAULT_CONFIG).adminSettingsUpdate({
settingsRequest: data,
settingsRequest,
});
this.dispatchEvent(new CustomEvent("ak-admin-setting-changed"));
return result;
}
renderForm(): TemplateResult {
const { settings } = this;
return html`
<ak-text-input
name="avatars"
label=${msg("Avatars")}
value="${ifDefined(this._settings?.avatars)}"
value="${ifDefined(settings.avatars)}"
input-hint="code"
required
.bighelp=${html`
<p class="pf-c-form__helper-text">
${msg(
@@ -137,27 +131,26 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
)}
</p>
`}
required
>
</ak-text-input>
<ak-switch-input
name="defaultUserChangeName"
label=${msg("Allow users to change name")}
?checked="${this._settings?.defaultUserChangeName}"
?checked=${settings.defaultUserChangeName}
help=${msg("Enable the ability for users to change their name.")}
>
</ak-switch-input>
<ak-switch-input
name="defaultUserChangeEmail"
label=${msg("Allow users to change email")}
?checked="${this._settings?.defaultUserChangeEmail}"
?checked=${settings.defaultUserChangeEmail}
help=${msg("Enable the ability for users to change their email.")}
>
</ak-switch-input>
<ak-switch-input
name="defaultUserChangeUsername"
label=${msg("Allow users to change username")}
?checked="${this._settings?.defaultUserChangeUsername}"
?checked=${settings.defaultUserChangeUsername}
help=${msg("Enable the ability for users to change their username.")}
>
</ak-switch-input>
@@ -166,7 +159,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
label=${msg("Event retention")}
input-hint="code"
required
value="${ifDefined(this._settings?.eventRetention)}"
value="${ifDefined(settings.eventRetention)}"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Duration after which events will be deleted from the database.")}
</p>
@@ -188,19 +181,19 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
label=${msg("Reputation: lower limit")}
required
name="reputationLowerLimit"
value="${this._settings?.reputationLowerLimit ?? DEFAULT_REPUTATION_LOWER_LIMIT}"
value="${settings.reputationLowerLimit ?? DEFAULT_REPUTATION_LOWER_LIMIT}"
help=${msg("Reputation cannot decrease lower than this value. Zero or negative.")}
></ak-number-input>
<ak-number-input
label=${msg("Reputation: upper limit")}
required
name="reputationUpperLimit"
value="${this._settings?.reputationUpperLimit ?? DEFAULT_REPUTATION_UPPER_LIMIT}"
value="${settings.reputationUpperLimit ?? DEFAULT_REPUTATION_UPPER_LIMIT}"
help=${msg("Reputation cannot increase higher than this value. Zero or positive.")}
></ak-number-input>
<ak-form-element-horizontal label=${msg("Footer links")} name="footerLinks">
<ak-array-input
.items=${this._settings?.footerLinks ?? []}
.items=${settings.footerLinks ?? []}
.newItem=${() => ({ name: "", href: "" })}
.row=${(f?: FooterLink) =>
akFooterLinkInput({
@@ -219,7 +212,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
<ak-switch-input
name="gdprCompliance"
label=${msg("GDPR compliance")}
?checked="${this._settings?.gdprCompliance}"
?checked=${settings.gdprCompliance}
help=${msg(
"When enabled, all the events caused by a user will be deleted upon the user's deletion.",
)}
@@ -228,14 +221,14 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
<ak-switch-input
name="impersonation"
label=${msg("Impersonation")}
?checked="${this._settings?.impersonation}"
?checked=${settings.impersonation}
help=${msg("Globally enable/disable impersonation.")}
>
</ak-switch-input>
<ak-switch-input
name="impersonationRequireReason"
label=${msg("Require reason for impersonation")}
?checked="${this._settings?.impersonationRequireReason}"
?checked=${settings.impersonationRequireReason}
help=${msg("Require administrators to provide a reason for impersonating a user.")}
>
</ak-switch-input>
@@ -244,7 +237,7 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
label=${msg("Default token duration")}
input-hint="code"
required
value="${ifDefined(this._settings?.defaultTokenDuration)}"
value="${ifDefined(settings.defaultTokenDuration)}"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Default duration for generated tokens")}
</p>
@@ -255,13 +248,13 @@ export class AdminSettingsForm extends Form<SettingsRequest> {
label=${msg("Default token length")}
required
name="defaultTokenLength"
value="${this._settings?.defaultTokenLength ?? 60}"
value="${settings.defaultTokenLength ?? 60}"
help=${msg("Default length of generated tokens")}
></ak-number-input>
<ak-form-element-horizontal label=${msg("Flags")} name="flags" required>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(this._settings?.flags ?? {})}"
value="${YAML.stringify(settings.flags ?? {})}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">

View File

@@ -53,15 +53,15 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
name="certificateData"
input-hint="code"
placeholder="-----BEGIN CERTIFICATE-----"
required
?revealed=${this.instance === undefined}
?required=${!this.instance}
?revealed=${!this.instance}
help=${msg("PEM-encoded Certificate data.")}
></ak-secret-textarea-input>
<ak-secret-textarea-input
label=${msg("Private Key")}
name="keyData"
input-hint="code"
?revealed=${this.instance === undefined}
?revealed=${!this.instance}
help=${msg(
"Optional Private Key. If this is set, you can use this keypair for encryption.",
)}

View File

@@ -66,7 +66,7 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
</ak-form-element-horizontal>
<ak-secret-textarea-input
name="key"
?revealed=${this.instance === undefined}
?revealed=${!this.instance}
label=${msg("License key")}
input-hint="code"
>

View File

@@ -259,7 +259,7 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
<ak-secret-textarea-input
name="syncKeytab"
label=${msg("Sync keytab")}
?revealed=${this.instance === undefined}
?revealed=${!this.instance}
help=${msg(
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
)}
@@ -287,7 +287,7 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
<ak-secret-textarea-input
name="spnegoKeytab"
label=${msg("SPNEGO keytab")}
?revealed=${this.instance === undefined}
?revealed=${!this.instance}
help=${msg(
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
)}

View File

@@ -442,8 +442,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
name="consumerSecret"
input-hint="code"
help=${msg("Also known as Client Secret.")}
required
?revealed=${this.instance === undefined}
?required=${!this.instance}
?revealed=${!this.instance}
></ak-secret-textarea-input>
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
<input
@@ -530,9 +530,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("Advanced settings")}>
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Policy engine mode")}
required

View File

@@ -414,9 +414,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("Advanced settings")}>
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Policy engine mode")}
required

View File

@@ -574,9 +574,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("Advanced settings")}>
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Policy engine mode")}
required

View File

@@ -121,7 +121,9 @@ export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string {
render(untrustedHTML, container);
const result = container.innerHTML;
const result = container.innerHTML
// Remove all comments as they can interfere with the styles.
.replaceAll("<!---->", "")
.replaceAll(/<!--\?lit\$\d+\$-->/g, "");
return result;
}

View File

@@ -154,11 +154,17 @@ export class ListSelect extends AKElement implements IListSelect {
return elementCount === 0 ? -1 : checkIndex();
}
/**
* Highlight the currently focused item.
*
* @todo
* This doesn't quite work as intended, but this component will likely
* be refined after the PatternFly upgrade.
*/
private highlightFocusedItem() {
this.displayedElements.forEach((item) => {
item.classList.remove("ak-highlight-item");
item.removeAttribute("aria-selected");
item.tabIndex = -1;
});
const currentElement = this.currentElement;
if (!currentElement) {
@@ -168,7 +174,6 @@ export class ListSelect extends AKElement implements IListSelect {
// This is currently a radio emulation; "selected" is true here.
// If this were a checkbox emulation (i.e. multi), "checked" would be appropriate.
currentElement.setAttribute("aria-selected", "true");
currentElement.scrollIntoView({ block: "center", behavior: "smooth" });
}
@bound

View File

@@ -9,7 +9,7 @@ import { html } from "lit";
const ACTIONS: QuickAction[] = [
["Create a new application", "/core/applications"],
["Check the logs", "/events/log"],
["Explore integrations", "https://goauthentik.io/integrations/", true],
["Explore integrations", "https://integrations.goauthentik.io/", true],
["Manage users", "/identity/users"],
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
];

View File

@@ -11,7 +11,7 @@ import { html } from "lit";
const ACTIONS: QuickAction[] = [
["Create a new application", "/core/applications"],
["Check the logs", "/events/log"],
["Explore integrations", "https://goauthentik.io/integrations/", true],
["Explore integrations", "https://integrations.goauthentik.io/", true],
["Manage users", "/identity/users"],
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
];

View File

@@ -18,7 +18,7 @@ import { instanceOfValidationError } from "@goauthentik/api";
import { snakeCase } from "change-case";
import { msg } from "@lit/localize";
import { msg, str } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -246,6 +246,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
return createFileMap<T>(this.shadowRoot?.querySelectorAll("ak-form-element-horizontal"));
}
//#region Validation
public checkValidity(): boolean {
return !!this.form?.checkValidity?.();
}
@@ -261,6 +263,10 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
return reportValidityDeep(form);
}
//#endregion
//#region Submission
/**
* Convert the elements of the form to JSON.[4]
*/
@@ -273,6 +279,7 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
return serializeForm<T>(elements);
}
/**
* Serialize and send the form to the destination. The `send()` method must be overridden for
* this to work. If processing the data results in an error, we catch the error, distribute
@@ -310,6 +317,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
let errorMessage = pluckErrorDetail(error);
let focused = false;
//#region Validation errors
if (instanceOfValidationError(parsedError)) {
// assign all input-related errors to their elements
const elements =
@@ -346,6 +355,23 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
if (parsedError.nonFieldErrors) {
this.nonFieldErrors = parsedError.nonFieldErrors;
} else if (!focused) {
// It's possible that the API has returned a field error that we're
// not aware of. We can still show the error message, to at least
// give the user some feedback.
for (const [fieldName, fieldErrors] of Object.entries(parsedError)) {
if (Array.isArray(fieldErrors)) {
this.nonFieldErrors = [
msg(str`${fieldName}: ${fieldErrors.join(", ")}`),
];
break;
}
}
console.error(
"authentik/forms: API rejected the form submission due to an invalid field that doesn't appear to be in the form. This is likely a bug in authentik.",
parsedError,
);
}
errorMessage = msg("Invalid update request.");
@@ -357,6 +383,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
}
}
//#endregion
showMessage({
message: errorMessage,
level: MessageLevel.error,
@@ -369,6 +397,8 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
//#endregion
//#endregion
//#region Render
public renderFormWrapper(): TemplateResult {

View File

@@ -3,7 +3,6 @@ import { AKElement } from "#elements/Base";
import { msg } from "@lit/localize";
import { css, CSSResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
@@ -18,24 +17,44 @@ export class FormStatic extends AKElement {
static styles: CSSResult[] = [
PFAvatar,
css`
/* Form with user */
.form-control-static {
margin-top: var(--pf-global--spacer--sm);
margin-block-start: var(--pf-global--spacer--sm);
display: flex;
align-items: center;
justify-content: space-between;
}
.form-control-static .avatar {
display: flex;
align-items: center;
}
.form-control-static img {
margin-right: var(--pf-global--spacer--xs);
}
.form-control-static a {
padding-top: var(--pf-global--spacer--xs);
padding-bottom: var(--pf-global--spacer--xs);
line-height: var(--pf-global--spacer--xl);
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: 1rem;
}
.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;
}
}
`,
];
@@ -44,17 +63,22 @@ export class FormStatic extends AKElement {
if (!this.user) {
return nothing;
}
return html`
<div class="form-control-static">
<div class="avatar">
<img
class="pf-c-avatar"
src="${ifDefined(this.userAvatar)}"
alt="${msg("User's avatar")}"
/>
${this.user}
<div class="primary-content">
${this.userAvatar
? html`<img
class="pf-c-avatar"
src=${this.userAvatar}
alt=${msg("User's avatar")}
/>`
: nothing}
<div class="username" aria-label=${msg("Username")}>${this.user}</div>
</div>
<div class="links">
<slot name="link"></slot>
</div>
<slot name="link"></slot>
</div>
`;
}

View File

@@ -24,6 +24,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
css`
.icon-description {
display: flex;
align-items: center;
}
.icon-description i {
font-size: 2em;

View File

@@ -34,7 +34,7 @@ export class BaseDeviceStage<
css`
.pf-c-form__group.pf-m-action {
display: flex;
gap: 16px;
gap: 1rem;
margin-top: 0;
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
flex-direction: column;

View File

@@ -9,7 +9,7 @@ import { ListenerController } from "#elements/utils/listenerController";
import { randomId } from "#elements/utils/randomId";
import { BaseStage } from "#flow/stages/base";
import { CaptchaHandler, iframeTemplate } from "#flow/stages/captcha/shared";
import { CaptchaHandler, CaptchaProvider, iframeTemplate } from "#flow/stages/captcha/shared";
import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/api";
@@ -109,7 +109,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
//#region State
@state()
protected activeHandler: CaptchaHandler | null = null;
protected activeHandler: CaptchaProvider | null = null;
@state()
protected error: string | null = null;
@@ -265,7 +265,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
//#endregion
#handlers = new Map<string, CaptchaHandler>([
/**
* Mapping of captcha provider names to their respective JS API global.
*
* Note that this is a `Map` to ensure the preferred order of discovering provider globals.
*/
#handlers = new Map<CaptchaProvider, CaptchaHandler>([
[
"grecaptcha",
{
@@ -415,7 +420,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
//#endregion
//#region Listeners
//#region Resizing
#loadListener = () => {
const iframe = this.#iframeRef.value;
@@ -423,17 +428,73 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
if (!iframe || !contentDocument) return;
const resizeListener: ResizeObserverCallback = () => {
if (!this.#iframeRef) return;
let synchronizeHeight: () => void;
const target = contentDocument.getElementById("ak-container");
if (this.activeHandler === CaptchaProvider.reCAPTCHA) {
// reCAPTCHA's use of nested iframes prevents their internal resize observer from
// reporting the correct height back to our iframe, so we have to do it ourselves.
if (!target) return;
synchronizeHeight = () => {
if (!this.#iframeRef) return;
this.iframeHeight = Math.round(target.clientHeight);
};
const target = contentDocument.getElementById("ak-container");
const resizeObserver = new ResizeObserver(resizeListener);
if (!target) return;
const innerIFrame = contentDocument.querySelector<HTMLIFrameElement>(
'iframe[style~="height:"]',
);
const innerBottom = innerIFrame?.getBoundingClientRect().bottom ?? 0;
const actualHeight = Math.max(innerBottom, target.clientHeight);
this.iframeHeight = Math.round(actualHeight * 1.1);
if (innerIFrame?.parentElement) {
innerIFrame.parentElement.style.height = `${actualHeight}px`;
}
};
// We watch for any newly inserted iframes, as they may alter the height
// of the parent iframe...
const mutationObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
for (const node of mutation.addedNodes as NodeListOf<HTMLElement>) {
if (node.tagName !== "IFRAME") continue;
// And then resize the iframe to match the new size.
//
// This doesn't fix the issue entirely since the challenge frame
// doesn't yet know the correct height, but at least the user can
// try to load the challenge again with the correct height.
resizeObserver.observe(node as HTMLIFrameElement);
requestAnimationFrame(synchronizeHeight);
}
}
});
mutationObserver.observe(contentDocument.body, {
childList: true,
subtree: true,
});
} else {
synchronizeHeight = () => {
if (!this.#iframeRef) return;
const target = contentDocument.getElementById("ak-container");
if (!target) return;
this.iframeHeight = Math.round(target.clientHeight);
};
}
const resizeObserver = new ResizeObserver(synchronizeHeight);
requestAnimationFrame(() => {
resizeObserver.observe(contentDocument.body);
@@ -442,22 +503,26 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
});
};
//#endregion
//#region Loading
#scriptLoadListener = async (): Promise<void> => {
console.debug("authentik/stages/captcha: script loaded");
this.error = null;
this.#iframeLoaded = false;
for (const [name, handler] of this.#handlers) {
for (const name of this.#handlers.keys()) {
if (!Object.hasOwn(window, name)) {
continue;
}
try {
await this.#run(handler);
await this.#run(name);
console.debug(`authentik/stages/captcha[${name}]: handler succeeded`);
this.activeHandler = handler;
this.activeHandler = name;
return;
} catch (error) {
@@ -469,7 +534,9 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
}
};
async #run(handler: CaptchaHandler) {
async #run(captchaProvider: CaptchaProvider) {
const handler = this.#handlers.get(captchaProvider)!;
if (this.challenge.interactive) {
const iframe = this.#iframeRef.value;
@@ -478,18 +545,44 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
return;
}
const { contentDocument } = iframe;
if (!contentDocument) {
console.debug(
`authentik/stages/captcha: No iframe content window found, skipping.`,
);
return;
}
console.debug(`authentik/stages/captcha: Rendering interactive.`);
const captchaElement = handler.interactive();
const template = iframeTemplate(captchaElement, this.challenge.jsUrl);
const template = iframeTemplate(captchaElement, {
challengeURL: this.challenge.jsUrl,
theme: this.activeTheme,
});
URL.revokeObjectURL(this.#iframeSource);
if (captchaProvider === CaptchaProvider.reCAPTCHA) {
// reCAPTCHA's domain verification can't seem to penetrate the true origin
// of the page when loaded from a blob URL, likely due to their double-nested
// iframe structure.
// We fallback to the deprecated `document.write` to get around this.
this.#iframeSource = "about:blank";
contentDocument.open();
contentDocument.write(template);
contentDocument.close();
const url = URL.createObjectURL(new Blob([template], { type: "text/html" }));
// this.#loadListener();
} else {
URL.revokeObjectURL(this.#iframeSource);
this.#iframeSource = url;
const url = URL.createObjectURL(new Blob([template], { type: "text/html" }));
iframe.src = url;
this.#iframeSource = url;
iframe.src = url;
}
return;
}

View File

@@ -1,7 +1,20 @@
import type { ResolvedUITheme } from "#common/theme";
import { createDocumentTemplate } from "#elements/utils/iframe";
import { html, TemplateResult } from "lit";
/**
* Mapping of captcha provider names to their respective JS API global.
*/
export const CaptchaProvider = {
reCAPTCHA: "grecaptcha",
hCaptcha: "hcaptcha",
Turnstile: "turnstile",
} as const satisfies Record<string, string>;
export type CaptchaProvider = (typeof CaptchaProvider)[keyof typeof CaptchaProvider];
export interface CaptchaHandler {
interactive(): TemplateResult;
execute(): Promise<void>;
@@ -9,6 +22,29 @@ export interface CaptchaHandler {
refresh(): Promise<void>;
}
const ThemeColor = {
dark: "#18191a",
light: "#ffffff",
} as const satisfies Record<ResolvedUITheme, string>;
export function themeMeta(theme: ResolvedUITheme) {
switch (theme) {
case "dark":
return html`
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content=${ThemeColor.dark} />
`;
case "light":
return html` <meta name="color-scheme" content="light" />
<meta name="theme-color" content=${ThemeColor.light} />`;
}
}
export interface IFrameTemplateInit {
challengeURL: string;
theme: ResolvedUITheme;
}
/**
* A container iframe for a hosted Captcha, with an event emitter to monitor
* when the Captcha forces a resize.
@@ -17,10 +53,17 @@ export interface CaptchaHandler {
* margin, adding 2rem of height to our container adds padding and prevents scrollbars
* or hidden rendering.
*/
export function iframeTemplate(children: TemplateResult, challengeURL: string): string {
export function iframeTemplate(
children: TemplateResult,
{ challengeURL, theme }: IFrameTemplateInit,
) {
return createDocumentTemplate({
head: html`<meta charset="UTF-8" />
head: html`
<meta charset="UTF-8" />
${themeMeta(theme)}
`,
body: html`
<script>
"use strict";
@@ -43,6 +86,11 @@ export function iframeTemplate(children: TemplateResult, challengeURL: string):
</script>
<style>
html,
body {
background: ${ThemeColor[theme]};
}
body {
margin: 0;
padding: 0;
@@ -58,8 +106,9 @@ export function iframeTemplate(children: TemplateResult, challengeURL: string):
align-items: center;
justify-content: center;
}
</style>`,
body: html`${children}
<script onload="loadListener()" src="${challengeURL}"></script> `,
</style>
${children}
<script onload="loadListener()" src="${challengeURL}"></script>
`,
});
}

View File

@@ -58,6 +58,12 @@ export class IdentificationStage extends BaseStage<
PFButton,
...AkRememberMeController.styles,
css`
.pf-c-form__group.pf-m-action {
display: flex;
gap: 1rem;
flex-direction: column;
}
/* login page's icons */
.pf-c-login__main-footer-links-item button {
background-color: transparent;
@@ -368,7 +374,9 @@ export class IdentificationStage extends BaseStage<
<div class="pf-c-form__group ${this.challenge.captchaStage ? "" : "pf-m-action"}">
<button
?disabled=${this.challenge.captchaStage && !this.captchaLoaded}
?disabled=${this.challenge.captchaStage &&
this.challenge.captchaStage.interactive &&
!this.captchaLoaded}
type="submit"
class="pf-c-button pf-m-primary pf-m-block"
>

View File

@@ -23,4 +23,4 @@ RUN npm run build
FROM docker.io/library/nginx:1.29.0
COPY --from=docs-builder /work/website/build /usr/share/nginx/html
COPY --from=docs-builder /work/website/docs/build /usr/share/nginx/html

6
website/LICENSE Normal file
View File

@@ -0,0 +1,6 @@
Except where otherwise noted, the contents of this directory are licensed under
the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0).
EXCEPTION: The contents of the `static/img/` directory (including logo and wordmark
images) are copyrighted and not licensed under CC BY-SA. All rights reserved
for those files.

View File

@@ -2,8 +2,11 @@
* @file Docusaurus config.
*
* @import { UserThemeConfig, UserThemeConfigExtra } from "@goauthentik/docusaurus-config";
* @import { AKReleasesPluginOptions } from "@goauthentik/docusaurus-theme/releases/plugin"
* @import * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
* @import {Options as PresetOptions} from '@docusaurus/preset-classic';
* @import { Options as RedirectsPluginOptions } from "@docusaurus/plugin-client-redirects";
* @import { AKRedirectsPluginOptions } from "@goauthentik/docusaurus-theme/redirects/plugin"
*/
import { cp } from "node:fs/promises";
@@ -12,13 +15,22 @@ import { basename, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createDocusaurusConfig } from "@goauthentik/docusaurus-config";
import { RewriteIndex } from "@goauthentik/docusaurus-theme/redirects";
import { parse } from "@goauthentik/docusaurus-theme/redirects/node";
import { prepareReleaseEnvironment } from "@goauthentik/docusaurus-theme/releases/node";
import { remarkLinkRewrite } from "@goauthentik/docusaurus-theme/remark";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const require = createRequire(import.meta.url);
const releaseEnvironment = prepareReleaseEnvironment();
const rootStaticDirectory = resolve(__dirname, "..", "static");
const authentikModulePath = resolve(__dirname, "..", "..");
const packageStaticDirectory = resolve(__dirname, "static");
const redirectsFile = resolve(packageStaticDirectory, "_redirects");
const redirects = await parse(redirectsFile);
const redirectsIndex = new RewriteIndex(redirects);
//#region Copy static files
@@ -67,6 +79,7 @@ export default createDocusaurusConfig({
theme: {
customCss: [require.resolve("@goauthentik/docusaurus-config/css/index.css")],
},
pages: false,
docs: {
routeBasePath: "/",
path: ".",
@@ -103,6 +116,13 @@ export default createDocusaurusConfig({
//#region Plugins
plugins: [
[
"@goauthentik/docusaurus-theme/releases/plugin",
/** @type {AKReleasesPluginOptions} */ ({
docsDirectory: __dirname,
environment: releaseEnvironment,
}),
],
[
"docusaurus-plugin-openapi-docs",
{
@@ -121,6 +141,34 @@ export default createDocusaurusConfig({
},
},
],
// Inject redirects for later use during runtime,
// such as navigating to non-existent page with the client-side router.
[
"@goauthentik/docusaurus-theme/redirects/plugin",
/** @type {AKRedirectsPluginOptions} */ ({
redirects,
}),
],
// Create build-time redirects for later use in HTTP responses,
// such as when navigating to a page for the first time.
//
// The existence of the _redirects file is also picked up by
// Netlify's deployment, which will redirect to the correct URL, even
// if the source is no longer present within the build output,
// such as when a page is removed, renamed, or moved.
[
"@docusaurus/plugin-client-redirects",
/** @type {RedirectsPluginOptions} */ ({
createRedirects(existingPath) {
const redirects = redirectsIndex.findAliases(existingPath);
return redirects;
},
}),
],
],
//#endregion

View File

@@ -11,7 +11,9 @@ import "./ensure-reference-sidebar.mjs";
// @ts-ignore - Allows for project-wide type checking when partially building docs.
import apiReference from "./reference/sidebar";
const DOCS_URL = process.env.DOCS_URL || "https://docs.goauthentik.io";
import { prepareReleaseEnvironment } from "@goauthentik/docusaurus-theme/releases/node";
const releaseEnvironment = prepareReleaseEnvironment();
/**
* @type {SidebarItemConfig}
@@ -21,7 +23,7 @@ const sidebar = {
{
type: "link",
label: "← Back to Developer Docs",
href: new URL("/developer-docs", DOCS_URL).href,
href: new URL("/developer-docs", releaseEnvironment.preReleaseOrigin).href,
className: "navbar-sidebar__upwards",
},
{

View File

@@ -1,5 +1,7 @@
import "./styles.css";
import { useCachedVersionPluginData } from "@goauthentik/docusaurus-theme/components/VersionPicker/utils.ts";
import isInternalUrl from "@docusaurus/isInternalUrl";
import Link from "@docusaurus/Link";
import { isActiveSidebarItem } from "@docusaurus/plugin-content-docs/client";
@@ -7,16 +9,7 @@ import { ThemeClassNames } from "@docusaurus/theme-common";
import type { Props } from "@theme/DocSidebarItem/Link";
import IconExternalLink from "@theme/Icon/ExternalLink";
import clsx from "clsx";
import React from "react";
const docsURL = new URL(process.env.DOCS_URL || "https://docs.goauthentik.io");
function isInternalUrlOrDocsUrl(url: string) {
if (isInternalUrl(url)) return true;
const inputURL = new URL(url);
return inputURL.origin === docsURL.origin;
}
import React, { useMemo } from "react";
const DocSidebarItemLink: React.FC<Props> = ({
item,
@@ -29,7 +22,18 @@ const DocSidebarItemLink: React.FC<Props> = ({
}) => {
const { href, label, className, autoAddBaseUrl } = item;
const isActive = isActiveSidebarItem(item, activePath);
const internalLink = isInternalUrlOrDocsUrl(href);
const versionPluginData = useCachedVersionPluginData();
const apiReferenceOrigin = versionPluginData?.env.apiReferenceOrigin;
const internalLink = useMemo(() => {
if (isInternalUrl(href)) return true;
if (!apiReferenceOrigin) return false;
const inputURL = new URL(href);
return inputURL.origin === apiReferenceOrigin;
}, [href, apiReferenceOrigin]);
return (
<li

View File

@@ -0,0 +1,3 @@
# Headers for static files
/*
X-Frame-Options: DENY

View File

@@ -0,0 +1,11 @@
# @file Redirect rules for the Docusaurus site.
#
# See https://docs.netlify.com/manage/routing/redirects/overview/
#
# Note: The order of the rules defines the priority of the redirect.
# i.e. The first rule that matches the URL will take precedence.
#region api prefix
/api/* /:splat 301!
#endregion

View File

@@ -4,7 +4,7 @@ title: Applications
Applications, as defined in authentik, are used to configure and separate the authorization/access control and the appearance of a specific software application in the **My applications** page.
When a user logs into authentik, they see a list of the applications for which authentik is configured to provide authentication and authorization (the applications that that they are authorized to use).
When a user logs into authentik, they see a list of the applications for which authentik is configured to provide authentication and authorization (the applications that they are authorized to use).
Applications are the "other half" of providers. They typically exist in a 1-to-1 relationship; each application needs a provider and every provider can be used with one application. Applications can, however, use specific, additional providers to augment the functionality of the main provider. For more information, see [Backchannel providers](./manage_apps.mdx#backchannel-providers).

View File

@@ -10,7 +10,7 @@ To add an application to authentik and have it display on users' **My applicatio
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. (Alternatively you can create only an application, without a provider, by clicking **Create.)**
2. Navigate to **Applications > Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can create only an application, without a provider, by clicking **Create.)**
3. In the **New application** box, define the application details, the provider type and configuration settings, and bindings for the application.
- **Application**: provide a name, an optional group for the type of application, the policy engine mode, and optional UI settings.
@@ -73,7 +73,7 @@ return {
### Create an application entitlement
1. Open the Admin interface and navigate to **Applications -> Applications**.
1. Open the Admin interface and navigate to **Applications > Applications**.
2. Click the name of the application for which you want to create an entitlement.
3. Click the **Application entitlements** tab at the top of the page, and then click **Create entitlement**. Provide a name for the entitlement, enter any optional **Attributes**, and then click **Create**.
4. In the list locate the entitlement to which you want to bind a user or group, and then **click the caret (>) to expand the entitlement details.**

View File

@@ -41,7 +41,7 @@ Starting with authentik 2022.8, flows will be exported as YAML, but JSON-based f
To create a flow, follow these steps:
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Flows and Stages -> Flows**.
2. In the Admin interface, navigate to **Flows and Stages > Flows**.
3. Click **Create**, define the flow using the [configuration settings](#flow-configuration-options) described below, and then click **Finish**.
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.

View File

@@ -22,7 +22,7 @@ Starting with authentik 2025.2, for users with appropriate permissions to access
### Manually running a flow with the inspector
1. To access the inspector, open the Admin interface and navigate to **Flows and Stages -> Flows**.
1. To access the inspector, open the Admin interface and navigate to **Flows and Stages > Flows**.
2. Select the specific flow that you want to inspect by clicking its name in the list.

View File

@@ -52,12 +52,12 @@ For detailed instructions, refer to Google documentation.
### Set credentials for the service account
1. On the **Service accounts** page, click the account that you just created.
2. Click the **Keys** tab at top of the page, the click **Add Key -> Create new key**.
2. Click the **Keys** tab at top of the page, the click **Add Key > Create new key**.
3. In the Create box, select JSON as the key type, and then click **Create**.
A pop-up displays with the private key, and the key is saved to your computer as a JSON file.
Later, when you create the stage in authentik, you will add this key in the **Credentials** field.
4. On the service account page, click the **Details** tab, and expand the **Advanced settings** area.
5. Log in to the Admin Console, and then navigate to **Chrome browser -> Connectors**.
5. Log in to the Admin Console, and then navigate to **Chrome browser > Connectors**.
6. Click on **New Provider Configuration**.
7. Under Universal Device Trust, click "Set up".
8. Enter a name.
@@ -68,7 +68,7 @@ For detailed instructions, refer to Google documentation.
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Flows -> Stages**.
2. In the Admin interface, navigate to **Flows > Stages**.
3. Click **Create**, and select **Endpoint Authenticator Google Device Trust Connector Stage**, and in the **New stage** box, define the following fields:
- **Name**: define a descriptive name, such as "chrome-device-trust".

View File

@@ -2,15 +2,19 @@
title: Email stage
---
This stage can be used for email verification. authentik's background worker will send an email using the specified connection details. When an email can't be delivered, delivery is automatically retried periodically.
This stage can be used for email verification. authentik's background worker will send an email using the specified connection details.
![](./email_recovery.png)
When an email can't be delivered, authentik automatically retries periodically.
You can also configure rate-limiting for emails requested by users. See the [configure rate limiting](#configure-rate-limiting-for-emails) section for more information.
For information about creating a stage, refer to our [documentation](../#create-a-stage).
## Behaviour
By default, the email is sent to the currently pending user. To override this, you can set `email` in the plan's context to another email address, which will override the user's email address (the user won't be changed).
For example, create this expression policy and bind it to the email stage:
For example, create this [expression policy](../../../../customize/policies/expression.mdx) and bind it to the email stage:
```python
request.context["flow_plan"].context["email"] = "foo@bar.baz"
@@ -21,6 +25,15 @@ request.context["flow_plan"].context["email"] = "foo@bar.baz"
return True
```
## Configure rate limiting for emails
You can configure the Email stage with _a maximum number of emails_ that can be sent within _a specified time period_.
To configure the rate limiting for recovery emails use these two fields when you create or edit an Email stage:
- **Account Recovery Max Attempts**: set the maximum number of emails to send.
- **Account Recovery Cache Timeout**: specify the time window used to count recent recovery emails sent to the user (account recovery attempts).
## Custom Templates
You can also use custom email templates, to use your own design or layout.
@@ -72,7 +85,7 @@ volumeMounts:
</Tabs>
:::info
If you've add the line and created a file, and can't see if, check the worker logs using `docker compose logs -f worker` or `kubectl logs -f deployment/authentik-worker`.
If you have added the line and created a file, and can't see it, check the worker logs using `docker compose logs -f worker` or `kubectl logs -f deployment/authentik-worker`.
:::
![](./custom_template.png)
@@ -98,7 +111,7 @@ Templates are rendered using Django's templating engine. The following variables
{% block content %}
<tr>
<td class="alert alert-success">
{% blocktrans with username=user.username %} Hi {{ username }},
{% blocktrans with username=user.username %} Hi {{ username }},
{% endblocktrans %}
</td>
</tr>
@@ -107,7 +120,7 @@ Templates are rendered using Django's templating engine. The following variables
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block">
{% trans 'You recently requested to change your password for you authentik account. Use the button below to set a new password.' %}
{% trans 'You recently requested to change your password for you authentik account. Use the button below to set a new password.' %}
</td>
</tr>
<tr>

View File

@@ -40,7 +40,7 @@ stage_3 --> done[["End of the flow"]]
To create a stage, follow these steps:
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Flows and Stages -> Stages**.
2. In the Admin interface, navigate to **Flows and Stages > Stages**.
3. Click **Create**, define the flow using the configuration settings, and then click **Finish**.
After creating the stage, you can then [bind the stage to a flow](#bind-a-stage-to-a-flow) or [bind a policy to the stage](../../../customize/policies/working_with_policies.md) (the policy determines whether or not the stage will be implemented in the flow).
@@ -50,7 +50,7 @@ After creating the stage, you can then [bind the stage to a flow](#bind-a-stage-
To bind a stage to a flow, follow these steps:
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Flows and Stages -> Flows**.
2. In the Admin interface, navigate to **Flows and Stages > Flows**.
3. In the list of flows, click the name of the flow to which you want to bind one or more stages.
4. On the Flow page, click the **Stage Bindings** tab at the top.
5. Here, you can decide if you want to create a new stage and bind it to the flow (**Create and bind Stage**), or if you want to select an existing stage and bind it to the flow (**Bind existing stage**).
@@ -62,7 +62,7 @@ You can use bindings to determine whether or not a stage is presented to a singl
To bind a user or a group to a stage binding for a specific flow, follow these steps:
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Flows and Stages -> Flows**.
2. In the Admin interface, navigate to **Flows and Stages > Flows**.
3. In the list of flows, click the name of the flow to which you want to bind one or more stages.
4. On the Flow page, click the **Stage Bindings** tab at the top.
5. Locate the stage binding to which you want to bind a user or group, and then **click the caret (>) to expand the stage binding details.**

View File

@@ -1,11 +1,11 @@
---
title: User delete stage
title: User Delete stage
---
:::danger
This stage deletes the `pending_user` without any confirmation. You have to make sure the user is aware of this.
:::
This stage is intended for an unenrollment flow. It deletes the currently pending user.
The User Delete stage is intended for an unenrollment flow. It deletes the currently pending user.
The pending user is also removed from the current session.

View File

@@ -1,8 +1,8 @@
---
title: User login stage
title: User Login stage
---
This stage attaches a currently pending user to the current session.
The User Login stage attaches a currently pending user to the current session.
It can be used after `user_write` during an enrollment flow, or after a `password` stage during an authentication flow.

View File

@@ -1,5 +1,5 @@
---
title: User logout stage
title: User Logout stage
---
Opposite stage of [User Login Stages](./user_login/index.md). It removes the user from the current session.

View File

@@ -1,14 +1,14 @@
---
title: User write stage
title: User Write stage
---
This stages writes data from the current flow context to a user.
The User Write stage writes data from the current flow context to a user.
Newly created users can be created as inactive and can be assigned to a selected group.
### Dynamic groups
Starting with authentik 2022.5, users can be added to dynamic groups. To do so, simply set `groups` in the flow plan context before this stage is run, for example
To add users to dynamic groups, set `groups` in the flow plan context before this stage is run. For example:
```python
from authentik.core.models import Group
@@ -22,6 +22,6 @@ return True
By default, this stage will create a new user when none is present in the flow context.
Starting with authentik 2022.12, the stage can by default not create new users to prevent users from creating new accounts without authorization.
To prevent users from creating new accounts without authorization, you can configure the User Write stage to not automatically create new users.
Starting with authentik 2023.1, this option has been expanded to allow user creation, forbid it or force user creation.
Alternatively, you can configure the stage to explicitly allow user creation, forbid it, or force user creation.

View File

@@ -50,6 +50,7 @@ kubernetes_namespace: authentik
kubernetes_ingress_annotations: {}
# Name of the secret that is used for TLS connections, leave empty to disable TLS
kubernetes_ingress_secret_name: authentik-outpost-tls
# pathType to use on routes. Defaults to `Prefix`. Some ingress-nginx deployments need this to be set to `ImplementationSpecific`.
# Service kind created, can be set to LoadBalancer for LDAP outposts for example
kubernetes_service_type: ClusterIP
# Disable any components of the kubernetes integration, can be any of

View File

@@ -21,39 +21,37 @@ Any change made to the outpost's associated app or provider immediately triggers
## Create and configure an outpost
1. To create a new outpost, log in to authentik as an administrator, and open to the Admin interface.
2. Navigate to **Applications --> Outposts** and then click **Create**.
1. Log in to authentik as an administrator and open the authentik Admin interface.
2. Navigate to **Applications** > **Outposts** and then click **Create**.
![](./outpost-create.png)
3. Define the following values:
3. Set the following values:
- **Name**: define a name for the outpost.
- **Type**: select the outpost type (Proxy, LDAP, Radius, RAC).
- **Integration**: select either Docker or Kubernetes, or optionally select `----` and [manually deploy the outpost](#outpost-integrations).
- **Applications**: select the applications that you want the outpost to serve.
- **Advanced settings (optional)**: for further optional configuration settings, refer to [Configuration](#configuration) below.
- **Name**: a name for the new outpost
- **Type**: select the provider type (Proxy, LDAP, Radius, RAC)
- **Integration** (_optional_): select either your [Docker or Kubernetes connection](#more-about-outpost-integrations)
- **Applications**: select the applications that you want the outpost to serve
- **Advanced settings** (*optional*): For further optional configuration settings, refer to [Configuration](#configuration) below.
4. Click **Create** to save your new outpost settings and close the box.
4. Click **Create**.
Upon creation, a service account and a token is generated. The service account only has permissions to read the outpost and provider configuration. This token is used by the outpost to connect to authentik.
### More about outpost integrations
## Outpost integrations
authentik can manage the deployment, updating, and general lifecycle of an outpost. To communicate with the underlying platforms on which the outpost is deployed, authentik has several built-in integrations.
- If you've deployed authentik on Docker Compose, authentik automatically creates an integration for the local docker socket (See [Docker](./integrations/docker.md)).
- If you've deployed authentik on Kubernetes, with `kubernetesIntegration` set to true (default), authentik automatically creates an integrations for the local Kubernetes Cluster (see [Kubernetes](./integrations/kubernetes.md)).
To deploy an outpost with these integrations, select them during the creation of an outpost. A background task is started, which creates the container/deployment. The outpost deployment can be monitored from the **Dashboards -> System Tasks** page in the Admin interface.
To deploy an outpost with these integrations, select them during the creation of an outpost. A background task is started, which creates the container/deployment. The outpost deployment can be monitored from the **Dashboards > System Tasks** page in the Admin interface.
To deploy an outpost manually, see:
- [Kubernetes](./manual-deploy-kubernetes.md)
- [Docker Compose](./manual-deploy-docker-compose.md)
### Configuration
## Configuration
Outposts fetch their configuration from authentik. Below are all the options you can set, and how they influence the outpost.

View File

@@ -16,7 +16,7 @@ As detailed in the steps below, when you add an Entra ID provider in authentik y
### Create the Entra ID provider in authentik
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Applications -> Providers**.
2. In the Admin interface, navigate to **Applications > Providers**.
3. Click **Create**, and in the **New provider** box select **Microsoft Entra Provider** as the type and click **Next**.
4. Define the following fields:
- **Name**: define a descriptive name, such as "Entra provider".
@@ -43,7 +43,7 @@ As detailed in the steps below, when you add an Entra ID provider in authentik y
### Create an Entra ID application in authentik
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Applications -> Applications**.
2. In the Admin interface, navigate to **Applications > Applications**.
3. Click **Create**, and define the following fields:
- **Name**: provide a descriptive name.
- **Slug**: enter the name of the app as you want it to appear in the URL.

View File

@@ -17,7 +17,7 @@ When adding the Google Workspace provider in authentik, you must define the **Ba
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Applications -> Providers**.
2. In the Admin interface, navigate to **Applications > Providers**.
3. Click **Create**, and select **Google Workspace Provider**, and in the **New provider** box, define the following fields:
- **Name**: define a descriptive name, such as "GWS provider".
@@ -42,7 +42,7 @@ When adding the Google Workspace provider in authentik, you must define the **Ba
### Create a Google Workspace application in authentik
1. Log in as an admin to authentik, and go to the Admin interface.
2. In the Admin interface, navigate to **Applications -> Applications**.
2. In the Admin interface, navigate to **Applications > Applications**.
:::info
If you have also configured Google Workspace to log in using authentik following this [ integration guide](/integrations/cloud-providers/google), then this configuration can be done on the same app by adding this new provider as a backchannel provider on the existing app instead of creating a new app.
:::

View File

@@ -40,13 +40,13 @@ For detailed instructions, refer to Google documentation.
### Set credentials for the service account
1. On the **Service accounts** page, click the account that you just created.
2. Click the **Keys** tab at top of the page, the click **Add Key -> Create new key**.
2. Click the **Keys** tab at top of the page, the click **Add Key > Create new key**.
3. In the Create box, select JSON as the key type, and then click **Create**.
A pop-up displays with the private key, and the key is saved to your computer as a JSON file.
Later, when you create your authentik provider for Google Workspace, you will add this key in the **Credentials** field.
4. On the service account page, click the **Details** tab, and expand the **Advanced settings** area.
5. Copy the **Client ID** (under **Domain-wide delegation**), and then click **View Google Workspace Admin Console**.
6. Log in to the Admin Console, and then navigate to **Security -> Access and data control -> API controls**.
6. Log in to the Admin Console, and then navigate to **Security > Access and data control > API controls**.
7. On the **API controls** page, click **Manage Domain Wide Delegation**.
8. On the **Domain Wide Delegation** page, click **Add new**.
9. In the **Add a new client ID** box, paste in the Client ID that you copied from the Admin console earlier (the value from the downloaded JSON file) and paste in the following scope documents:
@@ -59,7 +59,7 @@ For detailed instructions, refer to Google documentation.
The Delegated Subject email address is a required field when creating the provider in authentik.
1. Open to the main Admin console page, and navigate to **Directory -> Users**.
1. Open to the main Admin console page, and navigate to **Directory > Users**.
2. You can either select an existing user's email address or **Add new user** and define the user and email address to use as the Delegated Subject.
3. Save this email address to enter into authentik when you are creating the Google Workspace provider.

View File

@@ -4,7 +4,7 @@ title: Create an LDAP provider
### Create Service account
1. Create a new user account to bind with under _Directory_ -> _Users_ -> _Create_, in this example called `ldapservice`.
1. Create a new user account to bind with under **Directory > Users > Create**, in this example called `ldapservice`.
Note the DN of this user will be `cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io`
@@ -16,22 +16,22 @@ Note: The `default-authentication-flow` validates MFA by default, and currently
#### Create Custom Stages
1. Create a new identification stage. _Flows & Stage_ -> _Stages_ -> _Create_
1. Create a new identification stage. **Flows & Stages > Stages > Create**
![](./general_setup1.png)
2. Name it `ldap-identification-stage`. Select User fields Username and Email (and UPN if it is relevant to your setup).
![](./general_setup2.png)
3. Create a new password stage. _Flows & Stage_ -> _Stages_ -> _Create_
3. Create a new password stage. **Flows & Stages > Stages > Create**
![](./general_setup3.png)
4. Name it `ldap-authentication-password`. Leave the defaults for Backends.
![](./general_setup4.png)
5. Create a new user login stage. _Flows & Stage_ -> _Stages_ -> _Create_
5. Create a new user login stage. **Flows & Stages > Stages > Create**
![](./general_setup5.png)
6. Name it `ldap-authentication-login`.
![](./general_setup6.png)
#### Create Custom Flow
1. Create a new authentication flow under _Flows & Stage_ -> _Flows_ -> _Create_, and name it `ldap-authentication-flow`
1. Create a new authentication flow under **Flows & Stages > Flows > Create**, and name it `ldap-authentication-flow`
![](./general_setup7.png)
2. Click the newly created flow and choose _Stage Bindings_.
![](./general_setup8.png)
@@ -46,20 +46,20 @@ Note: The `default-authentication-flow` validates MFA by default, and currently
### Create LDAP Application & Provider
1. Create the LDAP Application under _Applications_ -> _Applications_ -> _Create With provider_ and name it `LDAP`.
1. Create the LDAP Application under **Applications > Applications > Create With provider** and name it `LDAP`.
![](./general_setup14.png)
![](./general_setup15.png)
### Assign LDAP permissions
1. Navigate to the LDAP Provider under _Applications_ -> _Providers_ -> `Provider for LDAP`.
1. Navigate to the LDAP Provider under **Applications > Providers** > `Provider for LDAP`.
2. Switch to the _Permissions_ tab.
3. Click the _Assign to new user_ button to select a user to assign the full directory search permission to.
4. Select the `ldapservice` user typing in its username. Select the _Search full LDAP directory_ permission and click _Assign_
### Create LDAP Outpost
1. Create (or update) the LDAP Outpost under _Applications_ -> _Outposts_ -> _Create_. Set the Type to `LDAP` and choose the `LDAP` application created in the previous step.
1. Create (or update) the LDAP Outpost under **Applications > Outposts > Create**. Set the Type to `LDAP` and choose the `LDAP` application created in the previous step.
![](./general_setup16.png)
:::info
@@ -91,5 +91,5 @@ ldapsearch \
```
:::info
This query will log the first successful attempt in an event in the _Events_ -> _Logs_ area, further successful logins from the same user are not logged as they are cached in the outpost.
This query will log the first successful attempt in an event in the **Events > Logs** area, further successful logins from the same user are not logged as they are cached in the outpost.
:::

View File

@@ -2,18 +2,14 @@
title: Create an OAuth2 provider
---
To add a provider (and the application that uses the provider for authentication) use the ** Create with provider** option, which creates both the new application and the required provider at the same time. For typical scenarios, authentik recommends that you create both the application and the provider together. (Alternatively, use our legacy process: navigate to **Applications --> Providers**, and then click **Create**.)
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. (Alternatively you can create only an application, without a provider, by clicking **Create**.)
3. In the **New application** box, define the application details, and then click **Next**.
4. Select the **Provider Type** of **OAuth2/OIDC**, and then click **Next**.
5. On the **Configure OAuth2/OpenId Provider** page, provide the configuration settings and then click **Submit** to create and save both the application and the provider.
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 settings, and then click **Next**.
4. Select **OAuth2/OIDC** as the **Provider Type**, and then click **Next**.
5. On the **Configure OAuth2/OpenId Provider** page, provide the configuration settings and then click **Submit** to create both the application and the provider.
:::info
Optionally, configure the provider to have the `offline_access` scope mapping. Starting with authentik 2024.2, by default applications only receive an access token. To receive a refresh token, both applications and authentik must be configured to request the `offline_access` scope. Do this in the Scope mapping area on the **Configure OAuth2/OpenId Provider** page.
Optionally, configure the provider with the `offline_access` scope mapping. By default, applications only receive an access token. To receive a refresh token, applications and authentik must be configured to request the `offline_access` scope. Do this in the Scope mapping area on the **Configure OAuth2/OpenId Provider** page.
:::

View File

@@ -6,7 +6,7 @@ title: Header authentication
### Send HTTP Basic authentication
Proxy providers have the option to _Send HTTP-Basic Authentication_ to the upstream authentication. When the option in the provider is enabled, two attributes must be specified. These attributes are the keys of values which can be saved on a user or group level that contain the credentials.
Proxy providers have the option to _Send HTTP-Basic Authentication_ to the upstream application. When the option in the provider is enabled, two attributes must be specified. These attributes are the keys of values which can be saved on a user or group level that contain the credentials.
For example, with _HTTP-Basic Username Key_ set to `app_username` and _HTTP-Basic Password Key_ set to `app_password`, these attributes would have to be set either on a user or a group the user is member of:

View File

@@ -8,10 +8,6 @@ For overview information, see the [RAC provider](./index.md) documentation. You
<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>
## Prerequisites
The RAC provider requires the deployment of the [RAC Outpost](../../outposts/index.mdx).
## Overview workflow to create an RAC provider
The typical workflow to create and configure a RAC provider is:
@@ -19,6 +15,7 @@ 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.
@@ -35,8 +32,8 @@ The first step is to create the RAC application and provider pair.
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 Property Mappings
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**:
@@ -54,10 +51,10 @@ Next, you need to add property mappings for each remote machine you want to acce
### Create endpoints for the provider
Finally, 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.
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**.
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).
@@ -69,6 +66,21 @@ Finally, you need to create an endpoint for each remote machine. Endpoints are d
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.

View File

@@ -14,8 +14,8 @@ SSH private keys can be configured via several methods:
2. Navigate to **Applications** > **Providers**.
3. Click the **Edit** icon on the RAC provider that requires public key authentication.
4. In the **Settings** codebox enter the private key of the endpoint, for example:
```python
private-key:
```yaml
private-key: |
-----BEGIN SSH PRIVATE KEY-----
SAMPLEgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
@@ -28,6 +28,10 @@ SSH private keys can be configured via several methods:
```
5. Click **Update**.
:::note
The pipe character (`|`) is required to preserve linebreaks in the YAML text. See the [YAML spec](https://yaml.org/spec/1.2.2/#literal-style) for more information.
:::
## Apply a private key to an RAC endpoint
1. Log in to authentik as an administrator, and open the authentik Admin interface.
@@ -35,8 +39,8 @@ SSH private keys can be configured via several methods:
3. Click the name of the RAC provider that the endpoint belongs to.
4. Under **Endpoints**- click on the **Edit** icon next to the endpoint that requires public key authentication.
5. Under **Advanced settings**, in the **Settings** codebox enter the private key of the endpoint:
```python
private-key:
```yaml
private-key: |
-----BEGIN SSH PRIVATE KEY-----
SAMPLEgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
@@ -49,6 +53,10 @@ SSH private keys can be configured via several methods:
```
6. Click **Update**.
:::note
The pipe character (`|`) is required to preserve linebreaks in the YAML text. See the [YAML spec](https://yaml.org/spec/1.2.2/#literal-style) for more information.
:::
## Apply a private key to an RAC property mapping
1. Log in to authentik as an administrator, and open the authentik Admin interface.
@@ -97,5 +105,5 @@ SSH private keys can be configured via several methods:
7. Click **Update**.
:::note
For group attributes, the following expression can be used `request.user.group_attributes(request.http_request)`
For group attributes, the following expression can be used `request.user.group_attributes(request.http_request)`.
:::

View File

@@ -16,7 +16,7 @@ The workflow to implement an SSF provider as a [backchannel provider](../../appl
## Create the SSF provider
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications -> Providers**.
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
2. Click **Create**.
@@ -28,7 +28,7 @@ The workflow to implement an SSF provider as a [backchannel provider](../../appl
## Create the OIDC provider
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications -> Providers**.
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Providers**.
2. Click **Create**.
@@ -38,7 +38,7 @@ The workflow to implement an SSF provider as a [backchannel provider](../../appl
## Create the application
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications -> Applications**.
1. Log in to authentik as an administrator and in the Admin interface navigate to **Applications > Applications**.
2. Click **Create**.

View File

@@ -4,10 +4,6 @@ title: Customize your instance
You can customize the behaviour, look, and available resources for your authentik instance. For more information refer to each of the topics below:
- [Policies](./policies/working_with_policies.md)
- Interfaces:
- [Flow interface](./interfaces/flow)
- [User interface](./interfaces/user)
- [Admin interface](./interfaces/admin)
- [Blueprints](./blueprints/index.mdx)
- [Branding](./branding.md)
import DocCardList from "@theme/DocCardList";
<DocCardList />

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