Compare commits

..

76 Commits

Author SHA1 Message Date
authentik-automation[bot]
2fedc3d0a0 release: 2025.10.2 2025-11-19 15:07:06 +00:00
authentik-automation[bot]
7f0b45f921 website/docs: add 2025.8.5 and 2025.10.2 release notes (cherry-pick #18268 to version-2025.10) (#18270)
website/docs: add 2025.8.5 and 2025.10.2 release notes (#18268)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-19 15:29:42 +01:00
authentik-automation[bot]
3905c281ad internal: Automated internal backport: 5000-sidebar.sec.patch to authentik-2025.10 (#18260)
Automated internal backport of patch 5000-sidebar.sec.patch to authentik-2025.10

Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-19 15:10:38 +01:00
authentik-automation[bot]
e6099d43f5 internal: Automated internal backport: 1498-oauth2-cc-user-active.sec.patch to authentik-2025.10 (#18259)
Automated internal backport of patch 1498-oauth2-cc-user-active.sec.patch to authentik-2025.10

Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-11-19 14:51:31 +01:00
authentik-automation[bot]
a91145bc7b internal: Automated internal backport: 1487-invitation-expiry.sec.patch to authentik-2025.10 (#18258)
Automated internal backport of patch 1487-invitation-expiry.sec.patch to authentik-2025.10

Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-11-19 14:51:03 +01:00
authentik-automation[bot]
3f38d5c7d9 stages/prompt: fix choices with labels causing error on submit (cherry-pick #18183 to version-2025.10) (#18236)
stages/prompt: fix choices with labels causing error on submit (#18183)

* stages/prompt: fix choices with labels causing error on submit



* fix tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-18 18:33:10 +01:00
authentik-automation[bot]
c00df0573c website/docs: update application description (cherry-pick #18125 to version-2025.10) (#18127)
website/docs: update application description (#18125)

Update due to 2025.10 changes

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-11-18 16:34:55 +00:00
authentik-automation[bot]
c3a0edee00 website/docs: Add instructions for installing RC versions (cherry-pick #18099 to version-2025.10) (#18193)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-11-17 23:52:39 +00:00
authentik-automation[bot]
8b81ca36ea web/sfe: downgrade bootstrap that was accidentally upgraded (cherry-pick #18157 to version-2025.10) (#18171)
* Cherry-pick #18157 to version-2025.10 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #18157
Original commit: 4caece7fef

* fix conflict

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-16 20:03:40 +01:00
authentik-automation[bot]
698de68a36 web: Disable library <datalist> on Firefox. (cherry-pick #18103 to version-2025.10) (#18135)
Cherry-pick #18103 to version-2025.10 (with conflicts)
This cherry-pick has conflicts that need manual resolution.

Original PR: #18103
Original commit: 1115e6f

Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-11-14 19:41:50 +01:00
authentik-automation[bot]
db35593b24 packages/django-channels-postgres/layer: fix query when subscribed to multiple channels (cherry-pick #18152 to version-2025.10) (#18153)
packages/django-channels-postgres/layer: fix query when subscribed to multiple channels (#18152)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-11-14 19:41:12 +01:00
authentik-automation[bot]
445fa31b57 web/admin: link to user on invitation list page (cherry-pick #18132 to version-2025.10) (#18134)
web/admin: link to user on invitation list page (#18132)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-13 22:38:06 +01:00
authentik-automation[bot]
a9aa1bf2c2 web/flows: improvements for hCaptcha (cherry-pick #16882 to version-2025.10) (#18128)
web/flows: improvements for hCaptcha (#16882)

* improvements for hCaptcha
Issue #16755

* web: Format.

---------

Co-authored-by: Tealk <12276250+Tealk@users.noreply.github.com>
Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
2025-11-13 21:02:26 +01:00
authentik-automation[bot]
d018f0381c packages/django-dramatiq-postgres: broker: ensure locking happens with the same connection (cherry-pick #18095 to version-2025.10) (#18119)
packages/django-dramatiq-postgres: broker: ensure locking happens with the same connection (#18095)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-11-13 17:18:13 +00:00
authentik-automation[bot]
7dd1cd5c59 website/docs: fix wording in stages overview (cherry-pick #18061 to version-2025.10) (#18120)
website/docs: fix wording in stages overview (#18061)

Change flow to stage

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-11-13 16:03:31 +00:00
authentik-automation[bot]
c219a6804a web: Fix tab activation, blank provider URLs (cherry-pick #18031 to version-2025.10) (#18101)
web: Fix tab activation, blank provider URLs (#18031)

web: Fix tab activation.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-11-13 12:47:38 +01:00
authentik-automation[bot]
d9310d04b0 web: Fix RAC modal visibility. (cherry-pick #17941 to version-2025.10) (#18097)
web: Fix RAC modal visibility. (#17941)

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-11-12 23:22:02 +01:00
authentik-automation[bot]
f471ef0e2e cmd/server/healthcheck: remove worker HTTP healthcheck (cherry-pick #18090 to version-2025.10) (#18091)
cmd/server/healthcheck: remove worker HTTP healthcheck (#18090)

* cmd/server/healthcheck: remove worker HTTP healthcheck



* lint



---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-11-12 16:12:17 +01:00
authentik-automation[bot]
31a010c108 core: improve app launch URL formatting (cherry-pick #18076 to version-2025.10) (#18087)
core: improve app launch URL formatting (#18076)

* core: improve app launch URL formatting



* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-12 13:06:46 +01:00
authentik-automation[bot]
96e6ab291e providers/scim: allow custom schema data (cherry-pick #18073 to version-2025.10) (#18075)
providers/scim: allow custom schema data (#18073)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-12 00:54:08 +01:00
authentik-automation[bot]
ebf68311c2 events: fix timezone not set for log events (cherry-pick #18067 to version-2025.10) (#18071)
events: fix timezone not set for log events (#18067)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-11 21:20:06 +01:00
Jens L.
fd365b2a09 ci: revert to upstream GHA for release (#18058) (#18065)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-11-11 18:50:24 +01:00
authentik-automation[bot]
41104da41f ci: attempt to fix integration tests using dind (cherry-pick #18066 to version-2025.10) (#18069)
ci: attempt to fix integration tests using dind (#18066)

* ci: attempt to fix integration tests using dind



* bump dind version



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-11 18:18:53 +01:00
authentik-automation[bot]
7edebdec03 website/docs: update discord social login script example (cherry-pick #18026 to version-2025.10) (#18057)
website/docs: update discord social login script example (#18026)

update the guild membership example to no longer cause an exception from a missing import.

Closes #18025

Signed-off-by: TMUniversal <10200399+TMUniversal@users.noreply.github.com>
Co-authored-by: TMUniversal <10200399+TMUniversal@users.noreply.github.com>
2025-11-11 13:02:48 +01:00
authentik-automation[bot]
fb56a54eb1 website/release notes: fix broken urls (cherry-pick #18041 to version-2025.10) (#18044)
website/release notes: fix broken urls (#18041)

* website: fix bad escaping of URLs in release notes

## What

Fixes bad escaping of URLs in the release notes that resulted in mangled output.

v2024.6.4 had entries that looked like this:

```
##### `GET` /providers/google_workspace/{#123;id}#125;/
```

v2025.4.md had entries that looked like this:

```
##### `GET` /policies/unique_password/{#125;#123;policy_uuid}/
```

A couple of straightforward search-and-replaces has fixed the issue.

## Notes

Two of the release notes had bad escaping of URLs. I'm not sure how the error was made or got past,
but it was obvious when visiting the page.

@Beryju suggested that the bug is due to our using `{...}` to symbolize parameters in a URL while
Docusaurus wants to interpret `{...}` as an internal template instruction, resulting in odd
behavior. In either case, docusarus interpreted the hashtagged entries as links to unrelated issues
in Github (the same two issues, which were "bump version of pylint" and "bump version of sentry"),
which could be very confusing.

The inconsistencies between the two releases, and the working releases, suggests that the error was
introduced manually.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2025-11-10 15:50:29 -05:00
Jens L.
31cd6eb8ce ci: fix migrate-from-stable for old versions (#18019) (#18024)
ci: better logic for picking previous stable version

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-11-10 15:56:45 +01:00
authentik-automation[bot]
092c5eb33c website/docs: updates img-src csp (cherry-pick #18010 to version-2025.10) (#18012) 2025-11-06 21:11:37 +00:00
authentik-automation[bot]
3e41bba54d core: bump django from 5.2.7 to 5.2.8 (cherry-pick #17967 to version-2025.10) (#18003)
core: bump django from 5.2.7 to 5.2.8 (#17967)

* bump django from 5.2.7 to 5.2.8

* longer urls



* add debug statements

* Remove debug statements

* import MAX_URL_LENGTH constant from django.http.response

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-11-06 15:50:16 +01:00
authentik-automation[bot]
9f8fd6eabe website/docs: remove broken info box and fix sentence (cherry-pick #17963 to version-2025.10) (#17965)
webiste/docs: remove broken info box and fix sentence (#17963)

Remove broken info box and fix sentence.

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-11-05 15:29:14 +00:00
authentik-automation[bot]
35fb55da15 website/docs: added Note about email_verified scope mapping is set to false by default (cherry-pick #17942 to version-2025.10) (#17961)
website/docs: added Note about email_verified scope mapping is set to false by default (#17942)

* added Note about email_verified set to false

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




* edits

* more edits

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




---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-11-05 06:58:29 -06:00
authentik-automation[bot]
b1d571a5af tasks/schedules: fix rel obj not being associated or updated (cherry-pick #17934 to version-2025.10) (#17936)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix rel obj not being associated or updated (#17934)
2025-11-04 15:45:01 +01:00
authentik-automation[bot]
fb589592b5 brands: sort matched brand by match length (cherry-pick #17920 to version-2025.10) (#17935)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-11-04 14:41:42 +01:00
authentik-automation[bot]
6468bb5707 brands: add more matching tests (cherry-pick #16185 to version-2025.10) (#17924)
brands: add more matching tests (#16185)

* brands: reproduce matching error



* try some things



* fix tests



* fix tests



* Update authentik/brands/tests.py




* fix tests again?



* wip



---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-03 21:29:33 +00:00
authentik-automation[bot]
70406664dc release: 2025.10.1 2025-11-03 16:42:08 +00:00
authentik-automation[bot]
c58c194180 website/docs: 2025.10.1 release notes (cherry-pick #17918 to version-2025.10) (#17919)
website/docs: 2025.10.1 release notes (#17918)

* website/docs: 2025.10.1 release notes



* Apply suggestions from code review




* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-11-03 17:05:18 +01:00
authentik-automation[bot]
fad87741e7 providers/oauth2: fix kid always required for federation (cherry-pick #17914 to version-2025.10) (#17917)
providers/oauth2: fix kid always required for federation (#17914)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-03 16:26:31 +01:00
authentik-automation[bot]
f6679895e5 providers/radius: revert fix inverted message authenticator validation (#17855) (cherry-pick #17915 to version-2025.10) (#17916)
providers/radius: revert fix inverted message authenticator validation (#17855) (#17915)

Revert "providers/radius: fix inverted message authenticator validation (#17855)"

This reverts commit 09e3301c8f.

Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-03 16:26:17 +01:00
authentik-automation[bot]
a573a72ecb providers/radius: fix inverted message authenticator validation (cherry-pick #17855 to version-2025.10) (#17888)
providers/radius: fix inverted message authenticator validation (#17855)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-11-01 18:28:06 +01:00
authentik-automation[bot]
b72709ebbc web/a11y: User library -- fix issues surrounding element focus, ARIA labeling. (cherry-pick #17522 to version-2025.10) (#17828)
web/a11y: User library -- fix issues surrounding element focus, ARIA labeling. (#17522)

* web/a11y: Fix issues surrounding element focus, aria labeling.

* web: Fix focus

* web: Fix nested focus

* web: Fix menu visibility when anchor positioning is not supported.

* web: Fix icon fallback behavior, labels.

* web: Fix flickering, descriptions.

* web: Fix excess width on mobile.

* web: Fix rendering artifacts on mobile.

* web: Remove aria-controls behavior.

- This is buggy, similar to aria-owns, and may cause crashes.

* web: Fix tabpanel focus attempting to scroll page.

* web: Fix issues surrounding consistent tab panel parameter testing.

* web: add shared helpers.

* web: Tidy comments.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-11-01 17:05:19 +01:00
authentik-automation[bot]
449742fbc0 web: Consistent Tab Panel URL Parameters (cherry-pick #17804 to version-2025.10) (#17859)
web: Consistent Tab Panel URL Parameters (#17804)

* web: Fix tabpanel focus attempting to scroll page.

* web: Fix issues surrounding consistent tab panel parameter testing.

* web: add shared helpers.

* web: Tidy comments.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-11-01 17:04:43 +01:00
authentik-automation[bot]
1b02cc0dae internal: full openssl path (cherry-pick #17856 to version-2025.10) (#17860) 2025-10-31 15:40:51 +01:00
authentik-automation[bot]
b0945ee7e9 outpost: revert breaking signals change (cherry-pick #17847 to version-2025.10) (#17848)
outpost: revert breaking signals change (#17847)

I have no idea why this breaks tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-31 02:20:17 +01:00
authentik-automation[bot]
6682136af1 outposts: update permissions more eagerly (cherry-pick #17783 to version-2025.10) (#17841)
outposts: update permissions more eagerly (#17783)

* wip

* wip

* a

* a



* rm

* this

* rm test files

* cover one more case



---------

Signed-off-by: Dominic R <dominic@sdko.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-10-31 00:33:54 +01:00
authentik-automation[bot]
24cb5ae4c1 tasks: sanitize log attributes (cherry-pick #17833 to version-2025.10) (#17842)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-10-30 19:03:13 +01:00
authentik-automation[bot]
9e272c7121 core: bump astral-sh/uv from 0.9.5 to 0.9.6 (cherry-pick #17820 to version-2025.10) (#17835)
core: bump astral-sh/uv from 0.9.5 to 0.9.6 (#17820)

Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.9.5 to 0.9.6.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.9.5...0.9.6)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-30 18:05:56 +01:00
authentik-automation[bot]
5dc7b7cdae web/admin: fix scim provider form (cherry-pick #17831 to version-2025.10) (#17834)
web/admin: fix scim provider form (#17831)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-30 17:52:38 +01:00
authentik-automation[bot]
2e2c52e49c internal/web/proxy: fix return status code during startup (cherry-pick #17827 to version-2025.10) (#17832)
internal/web/proxy: fix return status code during startup (#17827)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-10-30 17:37:03 +01:00
Jens L.
38f1ef0506 ci: rework internal repo (#17797) (#17829)
* ci: rework internal repo



* also fix retention workflow



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-10-30 17:32:03 +01:00
authentik-automation[bot]
3517562549 internal: fix go deprecation for +build (cherry-pick #17806 to version-2025.10) (#17824)
internal: fix go deprecation for +build (#17806)

Co-authored-by: Dominic R <dominic@sdko.org>
2025-10-30 15:48:50 +01:00
authentik-automation[bot]
cdbe40143d root: use hashes for dockerfile FROM (cherry-pick #17795 to version-2025.10) (#17798)
* Cherry-pick #17795 to version-2025.10 (with conflicts)

This cherry-pick has conflicts that need manual resolution.

Original PR: #17795
Original commit: 6f35c32190

* fix conflict

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

---------

Signed-off-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-29 14:01:28 +01:00
authentik-automation[bot]
5816f0d17c tasks: delay startup signals (cherry-pick #17769 to version-2025.10) (#17775)
tasks: delay startup signals (#17769)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-10-28 18:15:23 +00:00
authentik-automation[bot]
907ea8b2e9 packages/django-postgres-cache: use upsert instead of select/update in a transaction (cherry-pick #17760 to version-2025.10) (#17767)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-10-28 16:26:14 +01:00
authentik-automation[bot]
b38af89960 providers/oauth2: move encryption key field (cherry-pick #17722 to version-2025.10) (#17729)
providers/oauth2: move encryption key field (#17722)

it is often mis configured

closes #17678

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-28 16:14:11 +01:00
authentik-automation[bot]
d52db187bf providers/radius: fix panic when no cert is configured (cherry-pick #17762 to version-2025.10) (#17766)
providers/radius: fix panic when no cert is configured (#17762)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-28 16:12:21 +01:00
authentik-automation[bot]
2093e0e63f sources/oauth: Make PKCE verifier 128 characters (cherry-pick #17763 to version-2025.10) (#17765)
Co-authored-by: Alex Whitehead-Smith <alex.me.smith@gmail.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-10-28 16:07:13 +01:00
authentik-automation[bot]
2791d87ceb providers/proxy: fix missing JWT/claims header (cherry-pick #17759 to version-2025.10) (#17764)
providers/proxy: fix missing JWT/claims header (#17759)

* replace interface{} with any



* fix raw token not saved to map or json



* also fix proxy claims



* fix test



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-28 15:48:25 +01:00
authentik-automation[bot]
fdc3d95b59 root: Add Dockerfile label org.opencontainers.image.source (cherry-pick #17756 to version-2025.10) (#17757)
root: Add Dockerfile label org.opencontainers.image.source (#17756)

Add label source in dockerfiles

Co-authored-by: Erwan Hervé <62173453+Erwan-loot@users.noreply.github.com>
2025-10-28 13:48:44 +01:00
authentik-automation[bot]
de7a61cee0 website/docs: fix placeholder leftover (cherry-pick #17737 to version-2025.10) (#17738)
website/docs: fix placeholder leftover (#17737)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-27 21:31:46 +01:00
authentik-automation[bot]
f2805b9b8a release: 2025.10.0 2025-10-27 19:35:16 +00:00
authentik-automation[bot]
f48a91fbf4 website/docs: finalise 2025.10 release notes (cherry-pick #17728 to version-2025.10) (#17733)
website/docs: finalise 2025.10 release notes (#17728)

* website/docs: finalise 2025.10 release notes



* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-27 19:01:01 +00:00
authentik-automation[bot]
f056c0808d website/docs: update flow context ref (cherry-pick #17723 to version-2025.10) (#17732)
website/docs: update flow context ref (#17723)

* website/docs: update flow context ref



* format



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




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




---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-10-27 19:39:09 +01:00
authentik-automation[bot]
06a6d45139 enterprise: handle cached naive timezone (cherry-pick #17695 to version-2025.10) (#17730)
enterprise: handle cached naive timezone (#17695)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-27 19:13:52 +01:00
authentik-automation[bot]
0e12642f12 website/docs: blueprints: add a bit more info (cherry-pick #17704 to version-2025.10) (#17708)
website/docs: blueprints: add a bit more info (#17704)

* website/docs: blueprints: add a bit more info

* this might be worth mentioning

* fix

* a bit more info

Co-authored-by: Dominic R <dominic@sdko.org>
2025-10-26 14:18:03 +00:00
authentik-automation[bot]
01406d364e website/docs: add short-lived certificate recommendation (cherry-pick #17628 to version-2025.10) (#17633)
website/docs: add short-lived certificate recommendation (#17628)

Add certificate recommendation

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-10-25 02:32:38 +00:00
authentik-automation[bot]
b9b16dba59 website/docs: release notes: Add Zot integration (cherry-pick #17700 to version-2025.10) (#17701)
Co-authored-by: Dominic R <dominic@sdko.org>
2025-10-25 01:03:48 +00:00
authentik-automation[bot]
1ef83f3295 website/docs: eap add info about custom validation (cherry-pick #17642 to version-2025.10) (#17699)
website/docs: eap add info about custom validation (#17642)

* add info about custom validation

* tweaked table

* remove bullet

* remove other bullet

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-10-24 21:07:58 +00:00
authentik-automation[bot]
343506d104 website/docs: add note about invite link not bound (cherry-pick #17657 to version-2025.10) (#17672)
website/docs: add note about invite link not bound (#17657)

* invite link not bound

* marcelo's truth

* jens tweak

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-10-24 11:43:32 -05:00
authentik-automation[bot]
aeb4e1057e providers/proxy: drop headers with underscores (cherry-pick #17650 to version-2025.10) (#17651)
providers/proxy: drop headers with underscores (#17650)

drop any headers with underscores that we set in the remote system

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-22 16:28:52 +02:00
authentik-automation[bot]
0bcd1c268c website/docs: rel notes 2025.10: add 3 more integration guides (cherry-pick #17641 to version-2025.10) (#17652)
website/docs: rel notes 2025.10: add 3 more integration guides (#17641)

* add 3 more int guides

* Apply suggestion from @dominic-r



* is github's suggestion thingy usually this buggy

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-10-22 13:48:02 +00:00
authentik-automation[bot]
ecba1ffe94 enterprise: add prometheus metrics for license usage and expiry (cherry-pick #17606 to version-2025.10) (#17637)
enterprise: add prometheus metrics for license usage and expiry (#17606)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-21 18:55:14 +02:00
authentik-automation[bot]
b7d303936c release: 2025.10.0-rc3 2025-10-21 13:21:18 +00:00
authentik-automation[bot]
c1bc2a4565 ci: use forked release action to deal with large release notes (cherry-pick #17625 to version-2025.10) (#17626)
ci: use forked release action to deal with large release notes (#17625)

* ci: use forked release action to deal with large release notes



* bump build



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-10-21 14:39:57 +02:00
authentik-automation[bot]
1422c3aff3 core, web: update translations (cherry-pick #17605 to version-2025.10) (#17627)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-10-21 14:26:37 +02:00
authentik-automation[bot]
d4a77583ea website: fix active menu link background overlap (cherry-pick #17607 to version-2025.10) (#17620)
website: fix active menu link background overlap (#17607)

Co-authored-by: Dominic R <dominic@sdko.org>
2025-10-21 07:12:41 -04:00
authentik-automation[bot]
78d270bf25 release: 2025.10.0-rc2 2025-10-21 00:19:36 +00:00
authentik-automation[bot]
6d1c7f90e2 release: 2025.10.0-rc1 2025-10-20 23:43:29 +00:00
767 changed files with 227304 additions and 199667 deletions

View File

@@ -1,81 +0,0 @@
name: Bug report
description: Create a report to help us improve
labels: ["bug", "triage"]
type: bug
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this bug report!
- type: textarea
id: describe-the-bug
attributes:
label: Describe the bug
description: "A clear and concise description of what the bug is."
placeholder: "Describe the issue"
validations:
required: true
- type: textarea
id: how-to-reproduce
attributes:
label: How to reproduce
description: "Steps to reproduce the behavior."
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: "A clear and concise description of what you expected to happen."
placeholder: "The behavior that I expect to see is [...]"
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: "If applicable, add screenshots to help explain your problem."
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: "Add any other context about the problem here."
placeholder: "Also note that [...]"
validations:
required: false
- type: dropdown
id: deployment-method
attributes:
label: Deployment Method
description: "What deployment method are you using for authentik? Only Docker, Kubernetes and AWS CloudFormation are supported."
options:
- Docker
- Kubernetes
- AWS CloudFormation
- Other (please specify)
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: "What version of authentik are you using?"
placeholder: "[e.g. 2025.10.1]"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: "Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks."
render: shell
validations:
required: false

View File

@@ -1,49 +0,0 @@
name: Documentation suggestion/problem
description: Suggest an improvement or report a problem in our docs
labels: ["area: docs", "triage"]
type: task
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this documentation issue!
- type: markdown
attributes:
value: |
**Consider opening a PR!**
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR.
For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).
- type: textarea
id: issue
attributes:
label: Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link?
description: "A clear and concise description of what the problem is, or where the document can be improved."
placeholder: "I believe we need more details about [...]"
validations:
required: true
- type: input
id: link
attributes:
label: Link
description: "Provide the URL or link to the exact page in the documentation to which you are referring."
placeholder: "If there are multiple pages, list them all"
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
description: "A clear and concise description of what you suggest as a solution"
placeholder: "This issue could be resolved by [...]"
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: "Add any other context or screenshots about the documentation issue here."
placeholder: "Also note that [...]"
validations:
required: false

View File

@@ -1,41 +0,0 @@
name: Feature request
description: Suggest an idea for a feature
labels: ["enhancement", "triage"]
type: feature
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this feature request!
- type: textarea
id: related-to-problem
attributes:
label: Is your feature request related to a problem?
description: "A clear and concise description of what the problem is."
placeholder: "I'm always frustrated when [...]"
validations:
required: true
- type: textarea
id: feature
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
placeholder: "I'd like authentik to have [...]"
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Describe alternatives that you've considered
description: "A clear and concise description of any alternative solutions or features you've considered."
placeholder: "I've tried this but [...]"
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: "Add any other context or screenshots about the feature request here."
placeholder: "Also note that [...]"
validations:
required: false

39
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,39 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Output of docker-compose logs or kubectl logs respectively
**Version and Deployment (please complete the following information):**
<!--
Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/.
-->
- authentik version: [e.g. 2025.2.0]
- Deployment: [e.g. docker-compose, helm]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/goauthentik/authentik/discussions
about: Please ask questions via GitHub Discussions rather than creating issues.
- name: authentik Discord
url: https://discord.com/invite/jg33eMhnj6
about: For community support, visit our Discord server.

22
.github/ISSUE_TEMPLATE/docs_issue.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Documentation issue
about: Suggest an improvement or report a problem
title: ""
labels: documentation
assignees: ""
---
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
**Provide the URL or link to the exact page in the documentation to which you are referring.**
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the documentation issue here.
**Consider opening a PR!**
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).

View File

@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: enhancement
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,17 @@
---
name: Hackathon Idea
about: Propose an idea for the hackathon
title: ""
labels: hackathon
assignees: ""
---
**Describe the idea**
A clear concise description of the idea you want to implement
You're also free to work on existing GitHub issues, whether they be feature requests or bugs, just link the existing GitHub issue here.
<!-- Don't modify below here -->
If you want to help working on this idea or want to contribute in any other way, react to this issue with a :rocket:

View File

@@ -1,7 +0,0 @@
---
name: Blank issue
about: This issue type is only for internal use
title:
labels:
assignees:
---

32
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
name: Question
about: Ask a question about a feature or specific configuration
title: ""
labels: question
assignees: ""
---
**Describe your question/**
A clear and concise description of what you're trying to do.
**Relevant info**
i.e. Version of other software you're using, specifics of your setup
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Output of docker-compose logs or kubectl logs respectively
**Version and Deployment (please complete the following information):**
<!--
Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/.
-->
- authentik version: [e.g. 2025.2.0]
- Deployment: [e.g. docker-compose, helm]
**Additional context**
Add any other context about the problem here.

View File

@@ -21,12 +21,12 @@ runs:
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
- name: Install uv
if: ${{ contains(inputs.dependencies, 'python') }}
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v5
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v5
with:
enable-cache: true
- name: Setup python
if: ${{ contains(inputs.dependencies, 'python') }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v5
with:
python-version-file: "pyproject.toml"
- name: Install Python deps
@@ -43,7 +43,7 @@ runs:
registry-url: 'https://registry.npmjs.org'
- name: Setup go
if: ${{ contains(inputs.dependencies, 'go') }}
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v5
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v5
with:
go-version-file: "go.mod"
- name: Setup docker cache

View File

@@ -1,4 +1,3 @@
---
git:
filters:
- filter_type: file

View File

@@ -42,8 +42,8 @@ jobs:
# Needed for checkout
contents: read
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables

View File

@@ -49,7 +49,7 @@ jobs:
tags: ${{ steps.ev.outputs.imageTagsJSON }}
shouldPush: ${{ steps.ev.outputs.shouldPush }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -69,7 +69,7 @@ jobs:
matrix:
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev

View File

@@ -18,11 +18,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
@@ -46,7 +46,7 @@ jobs:
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
id: cpr
with:
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -21,7 +21,7 @@ jobs:
command:
- prettier-check
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Install Dependencies
working-directory: website/
run: npm ci
@@ -32,7 +32,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
with:
node-version-file: website/package.json
@@ -55,7 +55,7 @@ jobs:
env:
NODE_ENV: production
run: npm run build -w api
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: api-docs
path: website/api/build
@@ -66,8 +66,8 @@ jobs:
- lint
- build
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: api-docs
path: website/api/build

View File

@@ -21,7 +21,7 @@ jobs:
check-changes-applied:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
@@ -42,6 +42,6 @@ jobs:
- check-changes-applied
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: generate docs

View File

@@ -21,7 +21,7 @@ jobs:
command:
- prettier-check
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Install dependencies
working-directory: website/
run: npm ci
@@ -32,7 +32,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
with:
node-version-file: website/package.json
@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
with:
node-version-file: website/package.json
@@ -69,11 +69,11 @@ jobs:
id-token: write
attestations: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
@@ -117,6 +117,6 @@ jobs:
- build-container
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@@ -18,7 +18,7 @@ jobs:
- version-2025-4
- version-2025-2
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- run: |
current="$(pwd)"
dir="/tmp/authentik/${{ matrix.version }}"

View File

@@ -37,7 +37,7 @@ jobs:
- mypy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run job
@@ -45,7 +45,7 @@ jobs:
test-migrations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run migrations
@@ -71,23 +71,24 @@ jobs:
- 18-alpine
run_id: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: checkout stable
run: |
set -e -o pipefail
# Copy current, latest config to local
cp authentik/lib/default.yml local.env.yml
cp -R .github ..
cp -R scripts ..
# Previous stable tag
prev_stable=$(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
# Current version family based on
current_version_family=$(cat internal/constants/VERSION | grep -vE -- 'rc[0-9]+$' || true)
current_version_family=$(python -c "from authentik import VERSION; print(VERSION)" | grep -vE -- 'rc[0-9]+$')
if [[ -n $current_version_family ]]; then
prev_stable=$current_version_family
fi
echo "::notice::Checking out ${prev_stable} as stable version..."
git checkout ${prev_stable}
git checkout $(prev_stable)
rm -rf .github/ scripts/
mv ../.github ../scripts .
- name: Setup authentik env (stable)
@@ -136,7 +137,7 @@ jobs:
- 18-alpine
run_id: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
with:
@@ -156,11 +157,11 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
- name: run integration
run: |
uv run coverage run manage.py test tests/integration
@@ -194,7 +195,7 @@ jobs:
- name: flows
glob: tests/e2e/test_flows*
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Setup e2e env (chrome, etc)
@@ -232,7 +233,7 @@ jobs:
- test-e2e
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
build:
@@ -260,7 +261,7 @@ jobs:
pull-requests: write
timeout-minutes: 120
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: prepare variables

View File

@@ -21,8 +21,8 @@ jobs:
lint-golint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
with:
go-version-file: "go.mod"
- name: Prepare and generate API
@@ -34,7 +34,7 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: golangci-lint
uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v8
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
with:
version: latest
args: --timeout 5000s --verbose
@@ -42,8 +42,8 @@ jobs:
test-unittest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
with:
go-version-file: "go.mod"
- name: Setup authentik env
@@ -63,7 +63,7 @@ jobs:
- test-unittest
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
build-container:
@@ -86,11 +86,11 @@ jobs:
id-token: write
attestations: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
@@ -145,10 +145,10 @@ jobs:
goos: [linux]
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
with:
go-version-file: "go.mod"
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5

View File

@@ -31,7 +31,7 @@ jobs:
- command: lit-analyse
project: web
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
with:
node-version-file: ${{ matrix.project }}/package.json
@@ -48,7 +48,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
with:
node-version-file: web/package.json
@@ -68,7 +68,7 @@ jobs:
- lint
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
test:
@@ -76,7 +76,7 @@ jobs:
- ci-web-mark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
with:
node-version-file: web/package.json

View File

@@ -29,20 +29,20 @@ jobs:
github.event.pull_request.head.repo.full_name == github.repository)
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Compress images
id: compress
uses: calibreapp/image-actions@420075c115b26f8785e293c5bd5bef0911c506e5 # main
uses: calibreapp/image-actions@05b1cf44e88c3b041b841452482df9497f046ef7 # main
with:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
compressOnly: ${{ github.event_name != 'pull_request' }}
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}"
id: cpr
with:

View File

@@ -16,17 +16,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Setup authentik env
uses: ./.github/actions/setup
- run: uv run ak update_webauthn_mds
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
id: cpr
with:
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -10,14 +10,14 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
if: ${{ env.GH_APP_ID != '' }}
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
env:
GH_APP_ID: ${{ secrets.GH_APP_ID }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
if: ${{ steps.app-token.outcome != 'skipped' }}
with:
fetch-depth: 0

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Cleanup
run: |

View File

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

View File

@@ -5,10 +5,10 @@ on:
push:
branches: [main]
paths:
- packages/tsconfig/**
- packages/docusaurus-config/**
- packages/eslint-config/**
- packages/prettier-config/**
- packages/docusaurus-config/**
- packages/tsconfig/**
- packages/esbuild-plugin-live-reload/**
workflow_dispatch:
@@ -24,14 +24,13 @@ jobs:
fail-fast: false
matrix:
package:
# The order of the `*config` packages should not be changed, as they depend on each other.
- packages/tsconfig
- packages/docusaurus-config
- packages/eslint-config
- packages/prettier-config
- packages/docusaurus-config
- packages/tsconfig
- packages/esbuild-plugin-live-reload
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 2
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
@@ -44,8 +43,6 @@ jobs:
with:
files: |
${{ matrix.package }}/package.json
- name: Install Dependencies
run: npm ci
- name: Publish package
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ${{ matrix.package }}

View File

@@ -24,7 +24,7 @@ jobs:
language: ["go", "javascript", "python"]
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Initialize CodeQL

View File

@@ -26,5 +26,5 @@ jobs:
image: semgrep/semgrep
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- run: semgrep ci

View File

@@ -29,12 +29,12 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout main
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: main
token: "${{ steps.app-token.outputs.token }}"
@@ -57,12 +57,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout main
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: main
token: ${{ steps.generate_token.outputs.token }}
@@ -73,7 +73,7 @@ jobs:
- name: Bump version
run: "make bump version=${{ inputs.next_version }}.0-rc1"
- name: Create pull request
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
with:
token: ${{ steps.generate_token.outputs.token }}
branch: release-bump-${{ inputs.next_version }}

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
environment: internal-production
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: main
- run: |

View File

@@ -31,9 +31,9 @@ jobs:
id-token: write
attestations: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
@@ -83,12 +83,12 @@ jobs:
- radius
- rac
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
with:
go-version-file: "go.mod"
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: prepare variables
@@ -146,8 +146,8 @@ jobs:
goos: [linux, darwin]
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
with:
go-version-file: "go.mod"
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
@@ -168,7 +168,7 @@ jobs:
export CGO_ENABLED=0
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
- name: Upload binaries to release
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # v2
uses: svenstaro/upload-release-action@81c65b7cd4de9b2570615ce3aad67a41de5b1a13 # v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
@@ -186,8 +186,8 @@ jobs:
AWS_REGION: eu-central-1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 # v5
with:
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
aws-region: ${{ env.AWS_REGION }}
@@ -202,7 +202,7 @@ jobs:
- build-outpost-binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Run test suite in final docker images
run: |
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
@@ -218,7 +218,7 @@ jobs:
- build-outpost-binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -232,7 +232,7 @@ jobs:
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
docker cp ${container}:web/ .
- name: Create a Sentry.io release
uses: getsentry/action-release@128c5058bbbe93c8e02147fe0a9c713f166259a6 # v3
uses: getsentry/action-release@4f502acc1df792390abe36f2dcb03612ef144818 # v3
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

View File

@@ -35,10 +35,8 @@ jobs:
echo "major_version=${{ inputs.version }}" | grep -oE "^major_version=[0-9]{4}\.[0-9]{1,2}" >> "$GITHUB_OUTPUT"
- id: changelog-url
run: |
if [ "${{ inputs.release_reason }}" = "feature" ]; then
if [ "${{ inputs.release_reason }}" = "feature" ] || [ "${{ inputs.release_reason }}" = "prerelease" ]; then
changelog_url="https://docs.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}"
elif [ "${{ inputs.release_reason }}" = "prerelease" ]; then
changelog_url="https://next.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}"
else
changelog_url="https://docs.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}#fixed-in-$(echo -n ${{ inputs.version }} | sed 's/\.//g')"
fi
@@ -50,7 +48,7 @@ jobs:
name: Pre-release test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- run: make test-docker
bump-authentik:
name: Bump authentik version
@@ -61,7 +59,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@@ -70,7 +68,7 @@ jobs:
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
token: "${{ steps.app-token.outputs.token }}"
@@ -89,7 +87,7 @@ jobs:
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
git push --follow-tags
- name: Create Release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:
token: "${{ steps.app-token.outputs.token }}"
tag_name: "version/${{ inputs.version }}"
@@ -108,7 +106,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@@ -118,7 +116,7 @@ jobs:
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: "${{ github.repository_owner }}/helm"
token: "${{ steps.app-token.outputs.token }}"
@@ -130,7 +128,7 @@ jobs:
sed -E -i 's/[0-9]{4}\.[0-9]{1,2}\.[0-9]+$/${{ inputs.version }}/' charts/authentik/Chart.yaml
./scripts/helm-docs.sh
- name: Create pull request
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
with:
token: "${{ steps.app-token.outputs.token }}"
branch: bump-${{ inputs.version }}
@@ -150,7 +148,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@@ -160,7 +158,7 @@ jobs:
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
repository: "${{ github.repository_owner }}/version"
token: "${{ steps.app-token.outputs.token }}"
@@ -185,7 +183,7 @@ jobs:
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url' version.json > version.new.json
mv version.new.json version.json
- name: Create pull request
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
with:
token: "${{ steps.app-token.outputs.token }}"
branch: bump-${{ inputs.version }}

View File

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

View File

@@ -21,15 +21,15 @@ jobs:
steps:
- id: generate_token
if: ${{ github.event_name != 'pull_request' }}
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
if: ${{ github.event_name != 'pull_request' }}
with:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
if: ${{ github.event_name == 'pull_request' }}
- name: Setup authentik env
uses: ./.github/actions/setup
@@ -44,7 +44,7 @@ jobs:
make web-check-compile
- name: Create Pull Request
if: ${{ github.event_name != 'pull_request' }}
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
with:
token: ${{ steps.generate_token.outputs.token }}
branch: extract-compile-backend-translation

View File

@@ -0,0 +1,41 @@
---
# Rename transifex pull requests to have a correct naming
# Also enables auto squash-merge
name: Translation - Auto-rename Transifex PRs
on:
pull_request:
types: [opened, reopened]
permissions:
# Permission to rename PR
pull-requests: write
jobs:
rename_pr:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- id: generate_token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Get current title
id: title
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
title=$(gh pr view ${{ github.event.pull_request.number }} --json "title" -q ".title")
echo "title=${title}" >> "$GITHUB_OUTPUT"
- name: Rename
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
gh pr edit ${{ github.event.pull_request.number }} -t "translate: ${{ steps.title.outputs.title }}" --add-label dependencies
- uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3
with:
token: ${{ steps.generate_token.outputs.token }}
pull-request-number: ${{ github.event.pull_request.number }}
merge-method: squash

View File

@@ -26,10 +26,6 @@ website/api/reference
node_modules
coverage
## Vendored files
vendored
*.min.js
## Configs
*.log
*.yaml

View File

@@ -26,7 +26,7 @@ RUN npm run build && \
npm run build:sfe
# Stage 2: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:a02d35efc036053fdf0da8c15919276bf777a80cbfda6a35c5e9f087e652adfc AS go-builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.3-trixie@sha256:7534a6264850325fcce93e47b87a0e3fddd96b308440245e6ab1325fa8a44c91 AS go-builder
ARG TARGETOS
ARG TARGETARCH
@@ -76,7 +76,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 4: Download uv
FROM ghcr.io/astral-sh/uv:0.9.14@sha256:fef8e5fb8809f4b57069e919ffcd1529c92b432a2c8d8ad1768087b0b018d840 AS uv
FROM ghcr.io/astral-sh/uv:0.9.6@sha256:4b96ee9429583983fd172c33a02ecac5242d63fb46bc27804748e38c1cc9ad0d AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff AS python-base

View File

@@ -17,20 +17,21 @@ pg_user := $(shell uv run python -m authentik.lib.config postgresql.user 2>/dev/
pg_host := $(shell uv run python -m authentik.lib.config postgresql.host 2>/dev/null)
pg_name := $(shell uv run python -m authentik.lib.config postgresql.name 2>/dev/null)
UNAME := $(shell uname)
# For macOS users, add the libxml2 installed from brew libxmlsec1 to the build path
# to prevent SAML-related tests from failing and ensure correct pip dependency compilation
# These functions are only evaluated when called in specific targets
LIBXML2_EXISTS = $(shell brew list libxml2 2> /dev/null)
KRB5_EXISTS = $(shell brew list krb5 2> /dev/null)
LIBXML2_LDFLAGS = -L$(shell brew --prefix libxml2)/lib $(LDFLAGS)
LIBXML2_CPPFLAGS = -I$(shell brew --prefix libxml2)/include $(CPPFLAGS)
LIBXML2_PKG_CONFIG = $(shell brew --prefix libxml2)/lib/pkgconfig:$(PKG_CONFIG_PATH)
KRB_PATH =
ifneq ($(KRB5_EXISTS),)
KRB_PATH = PATH="$(shell brew --prefix krb5)/sbin:$(shell brew --prefix krb5)/bin:$$PATH"
ifeq ($(UNAME), Darwin)
# Only add for brew users who installed libxmlsec1
BREW_EXISTS := $(shell command -v brew 2> /dev/null)
ifdef BREW_EXISTS
LIBXML2_EXISTS := $(shell brew list libxml2 2> /dev/null)
ifdef LIBXML2_EXISTS
BREW_LDFLAGS := -L$(shell brew --prefix libxml2)/lib $(LDFLAGS)
BREW_CPPFLAGS := -I$(shell brew --prefix libxml2)/include $(CPPFLAGS)
BREW_PKG_CONFIG_PATH := $(shell brew --prefix libxml2)/lib/pkgconfig:$(PKG_CONFIG_PATH)
endif
endif
endif
all: lint-fix lint gen web test ## Lint, build, and test everything
@@ -49,7 +50,7 @@ go-test:
go test -timeout 0 -v -race -cover ./...
test: ## Run the server tests and produce a coverage report (locally)
$(KRB_PATH) uv run coverage run manage.py test --keepdb $(or $(filter-out $@,$(MAKECMDGOALS)),authentik)
uv run coverage run manage.py test --keepdb authentik
uv run coverage html
uv run coverage report
@@ -65,11 +66,11 @@ lint: ## Lint the python and golang sources
golangci-lint run -v
core-install:
ifneq ($(LIBXML2_EXISTS),)
ifdef LIBXML2_EXISTS
# Clear cache to ensure fresh compilation
uv cache clean
# Force compilation from source for lxml and xmlsec with correct environment
LDFLAGS="$(LIBXML2_LDFLAGS)" CPPFLAGS="$(LIBXML2_CPPFLAGS)" PKG_CONFIG_PATH="$(LIBXML2_PKG_CONFIG)" uv sync --frozen --reinstall-package lxml --reinstall-package xmlsec --no-binary-package lxml --no-binary-package xmlsec
LDFLAGS="$(BREW_LDFLAGS)" CPPFLAGS="$(BREW_CPPFLAGS)" PKG_CONFIG_PATH="$(BREW_PKG_CONFIG_PATH)" uv sync --frozen --reinstall-package lxml --reinstall-package xmlsec --no-binary-package lxml --no-binary-package xmlsec
else
uv sync --frozen
endif
@@ -196,12 +197,11 @@ endif
cp ${PWD}/schema.yml ${PWD}/${GEN_API_PY}
make -C ${PWD}/${GEN_API_PY} build version=${NPM_VERSION}
gen-client-go: ## Build and install the authentik API for Golang
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
mkdir -p ${PWD}/${GEN_API_GO}
ifeq ($(wildcard ${PWD}/${GEN_API_GO}/.*),)
git clone --depth 1 https://github.com/goauthentik/client-go.git ${PWD}/${GEN_API_GO}
else
cd ${PWD}/${GEN_API_GO} && git reset --hard
cd ${PWD}/${GEN_API_GO} && git pull
endif
cp ${PWD}/schema.yml ${PWD}/${GEN_API_GO}

View File

@@ -10,7 +10,7 @@
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/goauthentik/authentik/ci-web.yml?branch=main&label=web%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
[![Code Coverage](https://img.shields.io/codecov/c/gh/goauthentik/authentik?style=for-the-badge)](https://codecov.io/gh/goauthentik/authentik)
![Latest version](https://img.shields.io/docker/v/authentik/server?sort=semver&style=for-the-badge)
[![](https://img.shields.io/badge/Help%20translate-transifex-blue?style=for-the-badge)](https://explore.transifex.com/authentik/authentik/)
[![](https://img.shields.io/badge/Help%20translate-transifex-blue?style=for-the-badge)](https://www.transifex.com/authentik/authentik/)
## What is authentik?

View File

@@ -18,10 +18,10 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
(.x being the latest patch release for each version)
| Version | Supported |
| ---------- | ---------- |
| 2025.8.x | ✅ |
| 2025.10.x | ✅ |
| Version | Supported |
| --------- | --------- |
| 2025.6.x | ✅ |
| 2025.8.x | ✅ |
## Reporting a Vulnerability

View File

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

View File

@@ -27,16 +27,16 @@ except OSError:
ipc_key = None
def validate_auth(header: bytes, format="bearer") -> str | None:
def validate_auth(header: bytes) -> str | None:
"""Validate that the header is in a correct format,
returns type and credentials"""
auth_credentials = header.decode().strip()
if auth_credentials == "" or " " not in auth_credentials:
return None
auth_type, _, auth_credentials = auth_credentials.partition(" ")
if not compare_digest(auth_type.lower(), format):
if auth_type.lower() != "bearer":
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
return None
raise AuthenticationFailed("Unsupported authentication type")
if auth_credentials == "": # nosec # noqa
raise AuthenticationFailed("Malformed header")
return auth_credentials

View File

@@ -24,7 +24,8 @@ class TestAPIAuth(TestCase):
def test_invalid_type(self):
"""Test invalid type"""
self.assertIsNone(bearer_auth(b"foo bar"))
with self.assertRaises(AuthenticationFailed):
bearer_auth(b"foo bar")
def test_invalid_empty(self):
"""Test invalid type"""
@@ -33,8 +34,9 @@ class TestAPIAuth(TestCase):
def test_invalid_no_token(self):
"""Test invalid with no token"""
auth = b64encode(b":abc").decode()
self.assertIsNone(bearer_auth(f"Basic :{auth}".encode()))
with self.assertRaises(AuthenticationFailed):
auth = b64encode(b":abc").decode()
self.assertIsNone(bearer_auth(f"Basic :{auth}".encode()))
def test_bearer_valid(self):
"""Test valid token"""

View File

@@ -5,7 +5,6 @@ from pathlib import Path
from django.conf import settings
from django.db import models
from django.dispatch import Signal
from django.http import HttpRequest
from drf_spectacular.utils import extend_schema
from rest_framework.fields import (
BooleanField,
@@ -64,8 +63,7 @@ class ConfigView(APIView):
permission_classes = [AllowAny]
@staticmethod
def get_capabilities(request: HttpRequest) -> list[Capabilities]:
def get_capabilities(self) -> list[Capabilities]:
"""Get all capabilities this server instance supports"""
caps = []
deb_test = settings.DEBUG or settings.TEST
@@ -78,19 +76,18 @@ class ConfigView(APIView):
for processor in get_context_processors():
if cap := processor.capability():
caps.append(cap)
if request.tenant.impersonation:
if self.request.tenant.impersonation:
caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG: # pragma: no cover
caps.append(Capabilities.CAN_DEBUG)
if "authentik.enterprise" in settings.INSTALLED_APPS:
caps.append(Capabilities.IS_ENTERPRISE)
for _, result in capabilities.send(sender=ConfigView):
for _, result in capabilities.send(sender=self):
if result:
caps.append(result)
return caps
@staticmethod
def get_config(request: HttpRequest) -> ConfigSerializer:
def get_config(self) -> ConfigSerializer:
"""Get Config"""
return ConfigSerializer(
{
@@ -101,7 +98,7 @@ class ConfigView(APIView):
"send_pii": CONFIG.get("error_reporting.send_pii"),
"traces_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.4)),
},
"capabilities": ConfigView.get_capabilities(request),
"capabilities": self.get_capabilities(),
"cache_timeout": CONFIG.get_int("cache.timeout"),
"cache_timeout_flows": CONFIG.get_int("cache.timeout_flows"),
"cache_timeout_policies": CONFIG.get_int("cache.timeout_policies"),
@@ -111,4 +108,4 @@ class ConfigView(APIView):
@extend_schema(responses={200: ConfigSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Retrieve public configuration options"""
return Response(ConfigView.get_config(request).data)
return Response(self.get_config().data)

View File

@@ -1,50 +0,0 @@
from collections.abc import Callable
from functools import wraps
from typing import Literal
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
def validate(serializer_type: type[Serializer], location: Literal["body", "query"] = "body"):
"""Validate incoming data with the specified serializer. Raw data can either be taken
from request body or query string, defaulting to body.
Validated data is added to the function this decorator is used on with a named parameter
based on the location of the data.
Example:
@validate(MySerializer)
@validate(MyQuerySerializer, location="query")
def my_action(self, request, *, body: MySerializer, query: MyQuerySerializer):
...
"""
def wrapper_outer(func: Callable):
@wraps(func)
def wrapper(self: ViewSet, request: Request, *args, **kwargs) -> Response:
data = {}
if location == "body":
data = request.data
elif location == "query":
data = request.query_params
else:
raise ValueError(f"Invalid data location '{location}'")
instance = serializer_type(
data=data,
context={
"request": request,
},
)
instance.is_valid(raise_exception=True)
kwargs[location] = instance
return func(self, request, *args, **kwargs)
return wrapper
return wrapper_outer

View File

@@ -118,10 +118,7 @@ class Command(BaseCommand):
model_instance: Model = model()
if not isinstance(model_instance, SerializerModel):
continue
try:
serializer_class = model_instance.serializer
except NotImplementedError as exc:
raise NotImplementedError(model_instance) from exc
serializer_class = model_instance.serializer
serializer = serializer_class(
context={
SERIALIZER_CONTEXT_BLUEPRINT: False,

View File

@@ -42,15 +42,6 @@ from authentik.core.models import (
User,
UserSourceConnection,
)
from authentik.endpoints.connectors.agent.models import (
AgentDeviceConnection,
AppleNonce,
DeviceAuthenticationToken,
)
from authentik.endpoints.connectors.agent.models import (
DeviceToken as EndpointDeviceToken,
)
from authentik.endpoints.models import Connector, Device, DeviceConnection, DeviceFactSnapshot
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import LicenseUsage
from authentik.enterprise.providers.google_workspace.models import (
@@ -121,7 +112,6 @@ def excluded_models() -> list[type[Model]]:
OutpostServiceConnection,
Policy,
PolicyBindingModel,
Connector,
# Classes that have other dependencies
Session,
AuthenticatedSession,
@@ -149,13 +139,6 @@ def excluded_models() -> list[type[Model]]:
MicrosoftEntraProviderGroup,
EndpointDevice,
EndpointDeviceConnection,
EndpointDeviceToken,
Device,
DeviceConnection,
DeviceAuthenticationToken,
AppleNonce,
AgentDeviceConnection,
DeviceFactSnapshot,
DeviceToken,
StreamEvent,
UserConsent,
@@ -330,7 +313,6 @@ class Importer:
serializer_kwargs = {}
model_instance = existing_models.first()
override_serializer_instance = False
if (
not isinstance(model(), BaseMetaModel)
and model_instance
@@ -359,7 +341,11 @@ class Importer:
model=model,
**cleanse_dict(updated_identifiers),
)
override_serializer_instance = True
model_instance = model()
# pk needs to be set on the model instance otherwise a new one will be generated
if "pk" in updated_identifiers:
model_instance.pk = updated_identifiers["pk"]
serializer_kwargs["instance"] = model_instance
try:
full_data = self.__update_pks_for_attrs(entry.get_attrs(self._import))
except ValueError as exc:
@@ -382,12 +368,6 @@ class Importer:
entry=entry,
serializer=serializer,
) from exc
if override_serializer_instance:
model_instance = model()
# pk needs to be set on the model instance otherwise a new one will be generated
if "pk" in updated_identifiers:
model_instance.pk = updated_identifiers["pk"]
serializer.instance = model_instance
return serializer
def _apply_permissions(self, instance: Model, entry: BlueprintEntry):
@@ -466,7 +446,7 @@ class Importer:
self._apply_permissions(instance, entry)
elif state == BlueprintEntryDesiredState.ABSENT:
instance: Model | None = serializer.instance
if instance and instance.pk:
if instance.pk:
instance.delete()
self.logger.debug("Deleted model", mode=instance)
continue

View File

@@ -13,7 +13,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from authentik.api.validation import validate
from authentik.core.api.users import ParamUserSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
@@ -86,7 +85,8 @@ class AdminDeviceViewSet(ViewSet):
parameters=[ParamUserSerializer],
responses={200: DeviceSerializer(many=True)},
)
@validate(ParamUserSerializer, "query")
def list(self, request: Request, query: ParamUserSerializer) -> Response:
def list(self, request: Request) -> Response:
"""Get all devices for current user"""
return Response(DeviceSerializer(self.get_devices(**query.validated_data), many=True).data)
args = ParamUserSerializer(data=request.query_params)
args.is_valid(raise_exception=True)
return Response(DeviceSerializer(self.get_devices(**args.validated_data), many=True).data)

View File

@@ -14,7 +14,6 @@ from drf_spectacular.utils import (
extend_schema_field,
)
from guardian.shortcuts import get_objects_for_user
from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.request import Request
@@ -23,12 +22,9 @@ from rest_framework.serializers import ListSerializer, ValidationError
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from authentik.api.authentication import TokenAuthentication
from authentik.api.validation import validate
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.core.models import Group, User
from authentik.endpoints.connectors.agent.auth import AgentAuth
from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
@@ -231,11 +227,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
search_fields = ["name", "is_superuser"]
filterset_class = GroupFilter
ordering = ["name"]
authentication_classes = [
TokenAuthentication,
SessionAuthentication,
AgentAuth,
]
def get_ql_fields(self):
from djangoql.schema import BoolField, StrField
@@ -298,14 +289,13 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
filter_backends=[],
permission_classes=[],
)
@validate(UserAccountSerializer)
def add_user(self, request: Request, body: UserAccountSerializer, pk: str) -> Response:
def add_user(self, request: Request, pk: str) -> Response:
"""Add user to group"""
group: Group = self.get_object()
user: User = (
get_objects_for_user(request.user, "authentik_core.view_user")
.filter(
pk=body.validated_data.get("pk"),
pk=request.data.get("pk"),
)
.first()
)
@@ -329,14 +319,13 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
filter_backends=[],
permission_classes=[],
)
@validate(UserAccountSerializer)
def remove_user(self, request: Request, body: UserAccountSerializer, pk: str) -> Response:
def remove_user(self, request: Request, pk: str) -> Response:
"""Remove user from group"""
group: Group = self.get_object()
user: User = (
get_objects_for_user(request.user, "authentik_core.view_user")
.filter(
pk=body.validated_data.get("pk"),
pk=request.data.get("pk"),
)
.first()
)

View File

@@ -21,7 +21,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from authentik.api.validation import validate
from authentik.blueprints.api import ManagedSerializer
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
@@ -129,20 +128,23 @@ class PropertyMappingViewSet(
],
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
@validate(PropertyMappingTestSerializer)
def test(self, request: Request, pk: str, body: PropertyMappingTestSerializer) -> Response:
def test(self, request: Request, pk: str) -> Response:
"""Test Property Mapping"""
_mapping: PropertyMapping = self.get_object()
# Use `get_subclass` to get correct class and correct `.evaluate` implementation
mapping: PropertyMapping = PropertyMapping.objects.get_subclass(pk=_mapping.pk)
# FIXME: when we separate policy mappings between ones for sources
# and ones for providers, we need to make the user field optional for the source mapping
test_params = self.PropertyMappingTestSerializer(data=request.data)
if not test_params.is_valid():
return Response(test_params.errors, status=400)
format_result = str(request.GET.get("format_result", "false")).lower() == "true"
context: dict = body.validated_data.get("context", {})
context: dict = test_params.validated_data.get("context", {})
context.setdefault("user", None)
if user := body.validated_data.get("user"):
if user := test_params.validated_data.get("user"):
# User permission check, only allow mapping testing for users that are readable
users = get_objects_for_user(request.user, "authentik_core.view_user").filter(
pk=user.pk
@@ -150,7 +152,7 @@ class PropertyMappingViewSet(
if not users.exists():
raise PermissionDenied()
context["user"] = user
if group := body.validated_data.get("group"):
if group := test_params.validated_data.get("group"):
# Group permission check, only allow mapping testing for groups that are readable
groups = get_objects_for_user(request.user, "authentik_core.view_group").filter(
pk=group.pk

View File

@@ -56,7 +56,6 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"name",
"slug",
"enabled",
"promoted",
"authentication_flow",
"enrollment_flow",
"user_property_mappings",

View File

@@ -3,7 +3,7 @@
from typing import Any
from django.utils.timezone import now
from drf_spectacular.utils import OpenApiResponse, extend_schema
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
@@ -12,7 +12,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.api.validation import validate
from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
@@ -108,12 +107,6 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
}
class TokenSetKeySerializer(PassiveSerializer):
"""Set token's key"""
key = CharField()
class TokenViewSerializer(PassiveSerializer):
"""Show token's current key"""
@@ -177,7 +170,12 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
@permission_required("authentik_core.set_token_key")
@extend_schema(
request=TokenSetKeySerializer(),
request=inline_serializer(
"TokenSetKey",
{
"key": CharField(),
},
),
responses={
204: OpenApiResponse(description="Successfully changed key"),
400: OpenApiResponse(description="Missing key"),
@@ -185,12 +183,11 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
},
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
@validate(TokenSetKeySerializer)
def set_key(self, request: Request, identifier: str, body: TokenSetKeySerializer) -> Response:
def set_key(self, request: Request, identifier: str) -> Response:
"""Set token key. Action is logged as event. `authentik_core.set_token_key` permission
is required."""
token: Token = self.get_object()
key = body.validated_data.get("key")
key = request.data.get("key")
if not key:
return Response(status=400)
token.key = key

View File

@@ -12,7 +12,6 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from yaml import ScalarNode
from authentik.api.validation import validate
from authentik.blueprints.v1.common import (
Blueprint,
BlueprintEntry,
@@ -161,10 +160,11 @@ class TransactionalApplicationView(APIView):
200: TransactionApplicationResponseSerializer(),
},
)
@validate(TransactionApplicationSerializer)
def put(self, request: Request, body: TransactionApplicationSerializer) -> Response:
def put(self, request: Request) -> Response:
"""Convert data into a blueprint, validate it and apply it"""
blueprint: Blueprint = body.validated_data
data = TransactionApplicationSerializer(data=request.data)
data.is_valid(raise_exception=True)
blueprint: Blueprint = data.validated_data
for entry in blueprint.entries:
full_model = entry.get_model(blueprint)
app, __, model = full_model.partition(".")

View File

@@ -31,7 +31,6 @@ from drf_spectacular.utils import (
inline_serializer,
)
from guardian.shortcuts import get_objects_for_user
from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
@@ -53,8 +52,6 @@ from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.authentication import TokenAuthentication
from authentik.api.validation import validate
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin
@@ -78,7 +75,6 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.endpoints.connectors.agent.auth import AgentAuth
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
@@ -436,11 +432,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
serializer_class = UserSerializer
filterset_class = UsersFilter
search_fields = ["email", "name", "uuid", "username"]
authentication_classes = [
TokenAuthentication,
SessionAuthentication,
AgentAuth,
]
def get_ql_fields(self):
from djangoql.schema import BoolField, StrField
@@ -538,13 +529,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
pagination_class=None,
filter_backends=[],
)
@validate(UserServiceAccountSerializer)
def service_account(self, request: Request, body: UserServiceAccountSerializer) -> Response:
def service_account(self, request: Request) -> Response:
"""Create a new user account that is marked as a service account"""
expires = body.validated_data.get("expires", now() + timedelta(days=360))
data = UserServiceAccountSerializer(data=request.data)
data.is_valid(raise_exception=True)
expires = data.validated_data.get("expires", now() + timedelta(days=360))
username = body.validated_data["name"]
expiring = body.validated_data["expiring"]
username = data.validated_data["name"]
expiring = data.validated_data["expiring"]
with atomic():
try:
user: User = User.objects.create(
@@ -562,7 +554,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"user_uid": user.uid,
"user_pk": user.pk,
}
if body.validated_data["create_group"] and self.request.user.has_perm(
if data.validated_data["create_group"] and self.request.user.has_perm(
"authentik_core.add_group"
):
group = Group.objects.create(name=username)
@@ -633,12 +625,13 @@ class UserViewSet(UsedByMixin, ModelViewSet):
},
)
@action(detail=True, methods=["POST"], permission_classes=[])
@validate(UserPasswordSetSerializer)
def set_password(self, request: Request, pk: int, body: UserPasswordSetSerializer) -> Response:
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(body.validated_data["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

@@ -1,21 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-23 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0051_group_authentik_c_is_supe_1e5a97_idx"),
]
operations = [
migrations.AddField(
model_name="source",
name="promoted",
field=models.BooleanField(
default=False,
help_text="When enabled, this source will be displayed as a prominent button on the login page, instead of a small icon.",
),
),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-25 16:36
import django.core.validators
import re
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0052_source_promoted"),
]
operations = [
migrations.AlterField(
model_name="application",
name="slug",
field=models.TextField(
help_text="Internal application name, used in URLs.",
unique=True,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-a-zA-Z0-9_]+\\Z"),
"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.",
"invalid",
)
],
),
),
migrations.AlterField(
model_name="source",
name="slug",
field=models.TextField(
help_text="Internal source name, used in URLs.",
unique=True,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-a-zA-Z0-9_]+\\Z"),
"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.",
"invalid",
)
],
),
),
]

View File

@@ -11,7 +11,6 @@ from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.contrib.sessions.base_session import AbstractBaseSession
from django.core.validators import validate_slug
from django.db import models
from django.db.models import Q, QuerySet, options
from django.db.models.constants import LOOKUP_SEP
@@ -45,19 +44,18 @@ from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGT
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME = "goauthentik.io/user/token-maximum-lifetime" # nosec
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
_USER_ATTR_PREFIX = f"{USER_PATH_SYSTEM_PREFIX}/user"
USER_ATTRIBUTE_DEBUG = f"{_USER_ATTR_PREFIX}/debug"
USER_ATTRIBUTE_GENERATED = f"{_USER_ATTR_PREFIX}/generated"
USER_ATTRIBUTE_EXPIRES = f"{_USER_ATTR_PREFIX}/expires"
USER_ATTRIBUTE_DELETE_ON_LOGOUT = f"{_USER_ATTR_PREFIX}/delete-on-logout"
USER_ATTRIBUTE_SOURCES = f"{_USER_ATTR_PREFIX}/sources"
USER_ATTRIBUTE_TOKEN_EXPIRING = f"{_USER_ATTR_PREFIX}/token-expires" # nosec
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME = f"{_USER_ATTR_PREFIX}/token-maximum-lifetime" # nosec
USER_ATTRIBUTE_CHANGE_USERNAME = f"{_USER_ATTR_PREFIX}/can-change-username"
USER_ATTRIBUTE_CHANGE_NAME = f"{_USER_ATTR_PREFIX}/can-change-name"
USER_ATTRIBUTE_CHANGE_EMAIL = f"{_USER_ATTR_PREFIX}/can-change-email"
USER_PATH_SERVICE_ACCOUNT = f"{USER_PATH_SYSTEM_PREFIX}/service-accounts"
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
# used_by API that allows models to specify if they shadow an object
@@ -535,11 +533,7 @@ class Application(SerializerModel, PolicyBindingModel):
add custom fields and other properties"""
name = models.TextField(help_text=_("Application's display Name."))
slug = models.TextField(
validators=[validate_slug],
help_text=_("Internal application name, used in URLs."),
unique=True,
)
slug = models.SlugField(help_text=_("Internal application name, used in URLs."), unique=True)
group = models.TextField(blank=True, default="")
provider = models.OneToOneField(
@@ -725,22 +719,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
MANAGED_INBUILT = "goauthentik.io/sources/inbuilt"
name = models.TextField(help_text=_("Source's display Name."))
slug = models.TextField(
validators=[validate_slug],
help_text=_("Internal source name, used in URLs."),
unique=True,
)
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
user_path_template = models.TextField(default="goauthentik.io/sources/%(slug)s")
enabled = models.BooleanField(default=True)
promoted = models.BooleanField(
default=False,
help_text=_(
"When enabled, this source will be displayed as a prominent button on the "
"login page, instead of a small icon."
),
)
user_property_mappings = models.ManyToManyField(
"PropertyMapping", default=None, blank=True, related_name="source_userpropertymappings_set"
)
@@ -945,7 +928,7 @@ class ExpiringModel(models.Model):
return self.delete(*args, **kwargs)
@classmethod
def filter_not_expired(cls, **kwargs) -> QuerySet["Self"]:
def filter_not_expired(cls, **kwargs) -> QuerySet["Token"]:
"""Filer for tokens which are not expired yet or are not expiring,
and match filters in `kwargs`"""
for obj in cls.objects.filter(**kwargs).filter(Q(expires__lt=now(), expiring=True)):

View File

@@ -1,13 +1,11 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<script data-id="authentik-config">
"use strict";
<script>
window.authentik = {
locale: "{{ LANGUAGE_CODE }}",
config: JSON.parse('{{ config_json|escapejs }}' || "{}"),
brand: JSON.parse('{{ brand_json|escapejs }}' || "{}"),
config: JSON.parse('{{ config_json|escapejs }}'),
brand: JSON.parse('{{ brand_json|escapejs }}'),
versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}",

View File

@@ -1,13 +0,0 @@
{% load i18n %}
<div class="ak-c-placeholder" id="ak-placeholder" slot="placeholder">
<span
class="pf-c-spinner"
role="progressbar"
aria-valuetext="Loading..."
>
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>

View File

@@ -3,10 +3,8 @@
{% load authentik_core %}
<!DOCTYPE html>
<html
data-theme="{% if ui_theme == "dark" %}dark{% else %}light{% endif %}"
data-theme-choice="{% if ui_theme == "dark" %}dark{% elif ui_theme == "light" %}light{% else %}auto{% endif %}"
>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
@@ -20,8 +18,11 @@
{% include "base/theme.html" %}
<style data-id="brand-css">{{ brand_css }}</style>
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<style>{{ brand_css }}</style>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %}
{% endblock %}
{% for key, value in html_meta.items %}

View File

@@ -1,45 +1,10 @@
{% load static %}
{% load authentik_core %}
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/interface-%v.css' %}" />
{% if ui_theme == "dark" %}
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#18191a">
{% elif ui_theme == "light" %}
{% elif ui_theme == "light" %}
<meta name="color-scheme" content="light" />
<meta name="theme-color" content="#ffffff">
{% else %}
<script data-id="theme-script">
"use strict";
(function () {
try {
const initialThemeChoice =
new URLSearchParams(window.location.search).get("theme") ||
window.localStorage?.getItem("theme");
const themeChoice =
initialThemeChoice || document.documentElement.dataset.themeChoice || "auto";
document.documentElement.dataset.themeChoice = themeChoice;
if (themeChoice === "auto") {
document.documentElement.dataset.theme = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches
? "dark"
: "light";
} else {
document.documentElement.dataset.theme = themeChoice;
}
} catch (e) {
console.error("Failed to apply theme", e);
}
})();
</script>
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">

View File

@@ -11,6 +11,6 @@
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container alignment="bottom"></ak-message-container>
<ak-interface-admin>
{% include "base/placeholder.html" %}
<ak-loading></ak-loading>
</ak-interface-admin>
{% endblock %}

View File

@@ -12,13 +12,10 @@
{% endblock %}
{% block card %}
<div class="pf-c-form">
<form method="POST" class="pf-c-form">
<p>{% trans message %}</p>
<div class="pf-c-form__group">
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary pf-m-block">
{% trans 'Go home' %}
</a>
</div>
</div>
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">
{% trans 'Go home' %}
</a>
</form>
{% endblock %}

View File

@@ -11,6 +11,6 @@
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container></ak-message-container>
<ak-interface-user>
{% include "base/placeholder.html" %}
<ak-loading></ak-loading>
</ak-interface-user>
{% endblock %}

View File

@@ -2,66 +2,82 @@
{% load static %}
{% load i18n %}
{% load authentik_core %}
{% block head_before %}
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}
{% endblock %}
{% block head %}
<style data-id="static-styles">
:root {
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url }}");
}
<style>
:root {
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--lg: var(--ak-flow-background);
}
/* Form with user */
.form-control-static {
margin-top: 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);
}
</style>
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/static-%v.css' %}" />
{% endblock %}
{% block body %}
<div class="pf-c-background-image">
</div>
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container></ak-message-container>
<div class="pf-c-page__drawer">
<div class="pf-c-drawer pf-m-collapsed">
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<div class="pf-c-login" data-layout="stacked">
<main class="pf-c-login__main" aria-label="Authentication form">
<div class="pf-c-login__main-header pf-c-brand">
<img class="branding-logo" src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
</div>
<div class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl" data-test-id="card-title">
{% block card_title %}
{% endblock %}
</h1>
</div>
<div class="pf-c-login__main-body">
{% block card %}
{% endblock %}
</div>
</main>
<footer aria-label="Site footer" class="pf-c-login__footer pf-m-dark">
<ul class="pf-c-list pf-m-inline">
{% for link in footer_links %}
<li>
<a href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
<li>
<span>
{% trans 'Powered by authentik' %}
</span>
</li>
</ul>
</footer>
</div>
</div>
<div class="pf-c-login stacked">
<div class="ak-login-container">
<main class="pf-c-login__main">
<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
</div>
</div>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% block card_title %}
{% endblock %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
{% endblock %}
</div>
</main>
<footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline">
{% for link in footer_links %}
<li>
<a href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
<li>
<span>
{% trans 'Powered by authentik' %}
</span>
</li>
</ul>
</footer>
</div>
</div>
{% endblock %}

View File

@@ -8,8 +8,6 @@ from authentik.brands.models import Brand
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
class TestApplicationsViews(FlowTestCase):
@@ -17,7 +15,7 @@ class TestApplicationsViews(FlowTestCase):
def setUp(self) -> None:
self.user = create_test_admin_user()
self.app = Application.objects.create(
self.allowed = Application.objects.create(
name="allowed", slug="allowed", meta_launch_url="https://goauthentik.io/%(username)s"
)
@@ -30,7 +28,7 @@ class TestApplicationsViews(FlowTestCase):
response = self.client.get(
reverse(
"authentik_core:application-launch",
kwargs={"application_slug": self.app.slug},
kwargs={"application_slug": self.allowed.slug},
),
follow=True,
)
@@ -54,63 +52,8 @@ class TestApplicationsViews(FlowTestCase):
response = self.client.get(
reverse(
"authentik_core:application-launch",
kwargs={"application_slug": self.app.slug},
kwargs={"application_slug": self.allowed.slug},
),
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f"https://goauthentik.io/{self.user.username}")
def test_redirect_application_auth_flow(self):
"""Test launching an application with a provider and an authentication flow set"""
self.client.logout()
auth_flow = create_test_flow()
prov = OAuth2Provider.objects.create(
name=generate_id(),
authentication_flow=auth_flow,
)
self.app.provider = prov
self.app.save()
with self.assertFlowFinishes() as plan:
response = self.client.get(
reverse(
"authentik_core:application-launch",
kwargs={"application_slug": self.app.slug},
),
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url,
reverse("authentik_core:if-flow", kwargs={"flow_slug": auth_flow.slug}),
)
plan = plan()
self.assertEqual(len(plan.bindings), 1)
self.assertTrue(plan.bindings[0].stage.is_in_memory)
def test_redirect_application_no_auth(self):
"""Test launching an application with a provider and an authentication flow set"""
self.client.logout()
empty_flow = create_test_flow()
brand: Brand = create_test_brand()
brand.flow_authentication = empty_flow
brand.save()
prov = OAuth2Provider.objects.create(
name=generate_id(),
)
self.app.provider = prov
self.app.save()
with self.assertFlowFinishes() as plan:
response = self.client.get(
reverse(
"authentik_core:application-launch",
kwargs={"application_slug": self.app.slug},
),
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url,
reverse("authentik_core:if-flow", kwargs={"flow_slug": empty_flow.slug}),
)
plan = plan()
self.assertEqual(len(plan.bindings), 1)
self.assertTrue(plan.bindings[0].stage.is_in_memory)

View File

@@ -8,10 +8,11 @@ from guardian.utils import get_anonymous_user
from authentik.core.models import SourceUserMatchingModes, User
from authentik.core.sources.flow_manager import Action
from authentik.core.sources.stage import PostSourceStage
from authentik.core.tests.utils import RequestFactory, create_test_flow
from authentik.core.tests.utils import create_test_flow
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
@@ -33,11 +34,10 @@ class TestSourceFlowManager(TestCase):
enrollment_flow=self.enrollment_flow,
)
self.identifier = generate_id()
self.request_factory = RequestFactory()
def test_unauthenticated_enroll(self):
"""Test un-authenticated user enrolling"""
request = self.request_factory.get("/", user=AnonymousUser())
request = get_request("/", user=AnonymousUser())
flow_manager = OAuthSourceFlowManager(
self.source, request, self.identifier, {"info": {}}, {}
)
@@ -53,7 +53,7 @@ class TestSourceFlowManager(TestCase):
UserOAuthSourceConnection.objects.create(
user=get_anonymous_user(), source=self.source, identifier=self.identifier
)
request = self.request_factory.get("/", user=AnonymousUser())
request = get_request("/", user=AnonymousUser())
flow_manager = OAuthSourceFlowManager(
self.source, request, self.identifier, {"info": {}}, {}
)
@@ -67,7 +67,7 @@ class TestSourceFlowManager(TestCase):
def test_authenticated_link(self):
"""Test authenticated user linking"""
user = User.objects.create(username="foo", email="foo@bar.baz")
request = self.request_factory.get("/", user=user)
request = get_request("/", user=user)
flow_manager = OAuthSourceFlowManager(
self.source, request, self.identifier, {"info": {}}, {}
)
@@ -87,7 +87,7 @@ class TestSourceFlowManager(TestCase):
UserOAuthSourceConnection.objects.create(
user=user, source=self.source, identifier=self.identifier
)
request = self.request_factory.get("/", user=user)
request = get_request("/", user=user)
flow_manager = OAuthSourceFlowManager(
self.source, request, self.identifier, {"info": {}}, {}
)
@@ -100,11 +100,7 @@ class TestSourceFlowManager(TestCase):
def test_unauthenticated_link(self):
"""Test un-authenticated user linking"""
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=get_anonymous_user()),
self.identifier,
{"info": {}},
{},
self.source, get_request("/"), self.identifier, {"info": {}}, {}
)
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
@@ -118,11 +114,7 @@ class TestSourceFlowManager(TestCase):
# Without email, deny
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=AnonymousUser()),
self.identifier,
{"info": {}},
{},
self.source, get_request("/", user=AnonymousUser()), self.identifier, {"info": {}}, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.DENY)
@@ -130,7 +122,7 @@ class TestSourceFlowManager(TestCase):
# With email
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {
@@ -150,11 +142,7 @@ class TestSourceFlowManager(TestCase):
# Without username, deny
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=AnonymousUser()),
self.identifier,
{"info": {}},
{},
self.source, get_request("/", user=AnonymousUser()), self.identifier, {"info": {}}, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.DENY)
@@ -162,7 +150,7 @@ class TestSourceFlowManager(TestCase):
# With username
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {"username": "foo"},
@@ -181,7 +169,7 @@ class TestSourceFlowManager(TestCase):
# With non-existent username, enroll
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {
@@ -196,7 +184,7 @@ class TestSourceFlowManager(TestCase):
# With username
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {"username": "foo"},
@@ -213,7 +201,7 @@ class TestSourceFlowManager(TestCase):
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {"username": "foo"},
@@ -242,7 +230,7 @@ class TestSourceFlowManager(TestCase):
flow_manager = OAuthSourceFlowManager(
self.source,
self.request_factory.get("/", user=AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{
"info": {"username": "foo"},

View File

@@ -4,9 +4,9 @@ from django.test import TestCase
from authentik.core.auth import TokenBackend
from authentik.core.models import Token, TokenIntents, User
from authentik.core.tests.utils import RequestFactory
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.tests.utils import get_request
class TestTokenAuth(TestCase):
@@ -17,9 +17,8 @@ class TestTokenAuth(TestCase):
self.token = Token.objects.create(
expiring=False, user=self.user, intent=TokenIntents.INTENT_APP_PASSWORD
)
self.request_factory = RequestFactory()
# To test with session we need to create a request and pass it through all middlewares
self.request = self.request_factory.get("/")
self.request = get_request("/")
self.request.session[SESSION_KEY_PLAN] = FlowPlan("test")
def test_token_auth(self):

View File

@@ -1,12 +1,5 @@
"""Test Utils"""
from typing import Any
from django.contrib.auth.models import AnonymousUser
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpRequest
from django.test import RequestFactory as BaseRequestFactory
from django.utils.text import slugify
from authentik.brands.models import Brand
@@ -67,45 +60,3 @@ def create_test_cert(alg=PrivateKeyAlg.RSA) -> CertificateKeyPair:
)
builder.common_name = generate_id()
return builder.save()
def dummy_get_response(request: HttpRequest): # pragma: no cover
"""Dummy get_response for SessionMiddleware"""
return None
class RequestFactory(BaseRequestFactory):
def generic(
self,
method: str,
path: str,
data: Any = "",
content_type="application/octet-stream",
secure=False,
*,
headers=None,
query_params=None,
**extra,
):
user = extra.pop("user", None)
request = super().generic(
method,
path,
data,
content_type,
secure,
headers=headers,
query_params=query_params,
**extra,
)
request.user = user if user else AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
middleware = MessageMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
return request

View File

@@ -21,9 +21,6 @@ class UILoginButton:
# Icon URL, used as-is
icon_url: str | None = None
# Whether this source should be displayed as a prominent button
promoted: bool = False
class UserSettingSerializer(PassiveSerializer):
"""Serializer for User settings for stages and sources"""

View File

@@ -16,6 +16,7 @@ from authentik.flows.models import FlowDesignation, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import (
SESSION_KEY_APPLICATION_PRE,
ToDefaultFlow,
)
from authentik.stages.consent.stage import (
@@ -36,14 +37,10 @@ class RedirectToAppLaunch(View):
# Check if we're authenticated already, saves us the flow run
if request.user.is_authenticated:
return HttpResponseRedirect(app.get_launch_url(request.user))
self.request.session[SESSION_KEY_APPLICATION_PRE] = app
# otherwise, do a custom flow plan that includes the application that's
# being accessed, to improve usability
if app and app.provider and app.provider.authentication_flow:
flow = app.provider.authentication_flow
else:
flow = ToDefaultFlow.get_flow(
request=request, designation=FlowDesignation.AUTHENTICATION
)
flow = ToDefaultFlow(request=request, designation=FlowDesignation.AUTHENTICATION).get_flow()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
try:
@@ -58,8 +55,6 @@ class RedirectToAppLaunch(View):
)
except FlowNonApplicableException:
raise Http404 from None
# We redirect with an in_memory stage instead of `?next=...` as the launch URL
# might be formatted with the user, which hasn't logged in yet
plan.append_stage(in_memory_stage(RedirectToAppStage))
return plan.to_redirect(request, flow)

View File

@@ -8,6 +8,7 @@ from django.http.response import HttpResponse
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from django.views.generic.base import RedirectView, TemplateView
from rest_framework.request import Request
from authentik import authentik_build_hash
from authentik.admin.tasks import LOCAL_VERSION
@@ -46,7 +47,7 @@ class InterfaceView(TemplateView):
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
brand = CurrentBrandSerializer(self.request.brand)
kwargs["config_json"] = dumps(ConfigView.get_config(self.request).data)
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
kwargs["ui_theme"] = brand.data["ui_theme"]
kwargs["brand_json"] = dumps(brand.data)
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"

View File

@@ -9,14 +9,9 @@ from django.http.response import HttpResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_filters import FilterSet
from django_filters.filters import BooleanFilter, MultipleChoiceFilter
from django_filters.filters import BooleanFilter
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
extend_schema,
extend_schema_field,
)
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
@@ -33,13 +28,12 @@ from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.validation import validate
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import UserTypes
from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair, KeyType
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter, SecretKeyFilter
@@ -56,7 +50,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
cert_expiry = SerializerMethodField()
cert_subject = SerializerMethodField()
private_key_available = SerializerMethodField()
key_type = SerializerMethodField()
private_key_type = SerializerMethodField()
certificate_download_url = SerializerMethodField()
private_key_download_url = SerializerMethodField()
@@ -96,12 +90,14 @@ class CertificateKeyPairSerializer(ModelSerializer):
"""Show if this keypair has a private key configured or not"""
return instance.key_data != "" and instance.key_data is not None
@extend_schema_field(ChoiceField(choices=KeyType.choices, allow_null=True))
def get_key_type(self, instance: CertificateKeyPair) -> str | None:
"""Get the key algorithm type from the certificate's public key"""
def get_private_key_type(self, instance: CertificateKeyPair) -> str | None:
"""Get the private key's type, if set"""
if not self._should_include_details:
return None
return instance.key_type
key = instance.private_key
if key:
return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "")
return None
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
"""Get URL to download certificate"""
@@ -165,7 +161,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
"cert_expiry",
"cert_subject",
"private_key_available",
"key_type",
"private_key_type",
"certificate_download_url",
"private_key_download_url",
"managed",
@@ -202,31 +198,12 @@ class CertificateKeyPairFilter(FilterSet):
label="Only return certificate-key pairs with keys", method="filter_has_key"
)
key_type = MultipleChoiceFilter(
choices=KeyType.choices,
label="Filter by key algorithm type",
method="filter_key_type",
)
def filter_has_key(self, queryset, name, value): # pragma: no cover
"""Only return certificate-key pairs with keys"""
if not value:
return queryset
return queryset.exclude(key_data__exact="")
def filter_key_type(self, queryset, name, value): # pragma: no cover
"""Filter certificates by key type using the public key from the certificate"""
if not value:
return queryset
# value is a list of KeyType enum values from MultipleChoiceFilter
filtered_pks = []
for cert in queryset:
if cert.key_type in value:
filtered_pks.append(cert.pk)
return queryset.filter(pk__in=filtered_pks)
class Meta:
model = CertificateKeyPair
fields = ["name", "managed"]
@@ -251,17 +228,6 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
required=False,
description="Only return certificate-key pairs with keys",
),
OpenApiParameter(
"key_type",
OpenApiTypes.STR,
required=False,
many=True,
enum=[choice[0] for choice in KeyType.choices],
description=(
"Filter by key algorithm type (RSA, EC, DSA, etc). "
"Can be specified multiple times (e.g. '?key_type=rsa&key_type=ec')"
),
),
OpenApiParameter("include_details", bool, default=True),
]
)
@@ -277,16 +243,17 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
},
)
@action(detail=False, methods=["POST"])
@validate(CertificateGenerationSerializer)
def generate(self, request: Request, body: CertificateGenerationSerializer) -> Response:
def generate(self, request: Request) -> Response:
"""Generate a new, self-signed certificate-key pair"""
raw_san = body.validated_data.get("subject_alt_name", "")
data = CertificateGenerationSerializer(data=request.data)
data.is_valid(raise_exception=True)
raw_san = data.validated_data.get("subject_alt_name", "")
sans = raw_san.split(",") if raw_san != "" else []
builder = CertificateBuilder(body.validated_data["name"])
builder.alg = body.validated_data["alg"]
builder = CertificateBuilder(data.validated_data["name"])
builder.alg = data.validated_data["alg"]
builder.build(
subject_alt_names=sans,
validity_days=int(body.validated_data["validity_days"]),
validity_days=int(data.validated_data["validity_days"]),
)
instance = builder.save()
serializer = self.get_serializer(instance)

View File

@@ -1,6 +1,6 @@
"""authentik crypto app config"""
from dramatiq.broker import get_broker
from datetime import UTC, datetime
from authentik.blueprints.apps import ManagedAppConfig
from authentik.lib.generators import generate_id
@@ -45,7 +45,10 @@ class AuthentikCryptoConfig(ManagedAppConfig):
cert: CertificateKeyPair | None = CertificateKeyPair.objects.filter(
managed=MANAGED_KEY
).first()
if not cert:
now = datetime.now(tz=UTC)
if not cert or (
now < cert.certificate.not_valid_after_utc or now > cert.certificate.not_valid_after_utc
):
self._create_update_cert()
@ManagedAppConfig.reconcile_tenant
@@ -67,12 +70,6 @@ class AuthentikCryptoConfig(ManagedAppConfig):
},
)
@ManagedAppConfig.reconcile_global
def tasks_middlewares(self):
from authentik.crypto.tasks import CertificateWatcherMiddleware
get_broker().add_middleware(CertificateWatcherMiddleware())
@property
def tenant_schedule_specs(self) -> list[ScheduleSpec]:
from authentik.crypto.tasks import certificate_discovery

View File

@@ -6,11 +6,6 @@ from uuid import uuid4
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes, PublicKeyTypes
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import Certificate, load_pem_x509_certificate
@@ -25,16 +20,6 @@ from authentik.lib.models import CreatedUpdatedModel, SerializerModel
LOGGER = get_logger()
class KeyType(models.TextChoices):
"""Cryptographic key algorithm types"""
RSA = "rsa", _("RSA")
EC = "ec", _("Elliptic Curve")
DSA = "dsa", _("DSA")
ED25519 = "ed25519", _("Ed25519")
ED448 = "ed448", _("Ed448")
def fingerprint_sha256(cert: Certificate) -> str:
"""Get SHA256 Fingerprint of certificate"""
return hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8")
@@ -118,22 +103,6 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
else ""
) # nosec
@property
def key_type(self) -> str | None:
"""Get the key algorithm type from the certificate's public key"""
public_key = self.certificate.public_key()
if isinstance(public_key, RSAPublicKey):
return KeyType.RSA
if isinstance(public_key, EllipticCurvePublicKey):
return KeyType.EC
if isinstance(public_key, DSAPublicKey):
return KeyType.DSA
if isinstance(public_key, Ed25519PublicKey):
return KeyType.ED25519
if isinstance(public_key, Ed448PublicKey):
return KeyType.ED448
return None
def __str__(self) -> str:
return f"Certificate-Key Pair {self.name}"

View File

@@ -2,29 +2,17 @@
from glob import glob
from pathlib import Path
from sys import platform
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509.base import load_pem_x509_certificate
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from dramatiq.actor import actor
from dramatiq.middleware import Middleware
from structlog.stdlib import get_logger
from watchdog.events import (
FileCreatedEvent,
FileModifiedEvent,
FileSystemEvent,
FileSystemEventHandler,
)
from watchdog.observers import Observer
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.config import CONFIG
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.schedules.models import Schedule
from authentik.tenants.models import Tenant
LOGGER = get_logger()
@@ -47,65 +35,6 @@ def ensure_certificate_valid(body: str):
return body
class CertificateWatcherMiddleware(Middleware):
"""Middleware to start certificate file watcher"""
def start_certificate_watcher(self):
"""Start certificate file watcher"""
observer = Observer()
kwargs = {}
if platform.startswith("linux"):
kwargs["event_filter"] = (FileCreatedEvent, FileModifiedEvent)
observer.schedule(
CertificateEventHandler(),
CONFIG.get("cert_discovery_dir"),
recursive=True,
**kwargs,
)
observer.start()
def after_worker_boot(self, broker, worker):
if not settings.TEST:
self.start_certificate_watcher()
class CertificateEventHandler(FileSystemEventHandler):
"""Event handler for certificate file events"""
# We only ever get creation and modification events.
# See the creation of the Observer instance above for the event filtering.
# Even though we filter to only get file events, we might still get
# directory events as some implementations such as inotify do not support
# filtering on file/directory.
def dispatch(self, event: FileSystemEvent) -> None:
"""Call specific event handler method. Ignores directory changes."""
if event.is_directory:
return None
return super().dispatch(event)
def on_created(self, event: FileSystemEvent):
"""Process certificate file creation"""
LOGGER.debug(
"Certificate file created, triggering discovery",
file=event.src_path,
)
for tenant in Tenant.objects.filter(ready=True):
with tenant:
Schedule.dispatch_by_actor(certificate_discovery)
def on_modified(self, event: FileSystemEvent):
"""Process certificate file modification"""
LOGGER.debug(
"Certificate file modified, triggering discovery",
file=event.src_path,
)
for tenant in Tenant.objects.filter(ready=True):
with tenant:
Schedule.dispatch_by_actor(certificate_discovery)
@actor(description=_("Discover, import and update certificates from the filesystem."))
def certificate_discovery():
self = CurrentTask.get_task()
@@ -137,35 +66,12 @@ def certificate_discovery():
except (OSError, ValueError) as exc:
LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path)
for name, cert_data in certs.items():
# First, try to find by filename-based managed field
cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()
# If not found by filename and we have a private key, check for existing key match
if not cert and name in private_keys:
existing_with_key = (
CertificateKeyPair.objects.filter(
managed__startswith="goauthentik.io/crypto/discovered/",
key_data=private_keys[name],
)
.exclude(key_data="")
.first()
)
if existing_with_key:
cert = existing_with_key
# Update name and managed field to reflect the new filename
if cert.name != name:
cert.name = name
cert.managed = MANAGED_DISCOVERED % name
cert.save()
# Create new certificate if not found
if not cert:
cert = CertificateKeyPair(
name=name,
managed=MANAGED_DISCOVERED % name,
)
# Update certificate data if changed
dirty = False
if cert.certificate_data != cert_data:
cert.certificate_data = cert_data

View File

@@ -2,7 +2,6 @@
from json import loads
from os import makedirs
from pathlib import Path
from tempfile import TemporaryDirectory
from cryptography.x509.extensions import SubjectAlternativeName
@@ -320,8 +319,6 @@ class TestCrypto(APITestCase):
def test_discovery(self):
"""Test certificate discovery"""
# This test generates 2 separate cert/key combinations
# and verifies they both import properly
name = generate_id()
builder = CertificateBuilder(name)
with self.assertRaises(ValueError):
@@ -330,15 +327,6 @@ class TestCrypto(APITestCase):
subject_alt_names=[],
validity_days=3,
)
name2 = generate_id()
builder2 = CertificateBuilder(name2)
with self.assertRaises(ValueError):
builder2.save()
builder2.build(
subject_alt_names=[],
validity_days=3,
)
with TemporaryDirectory() as temp_dir:
with open(f"{temp_dir}/foo.pem", "w+", encoding="utf-8") as _cert:
_cert.write(builder.certificate)
@@ -346,9 +334,9 @@ class TestCrypto(APITestCase):
_key.write(builder.private_key)
makedirs(f"{temp_dir}/foo.bar", exist_ok=True)
with open(f"{temp_dir}/foo.bar/fullchain.pem", "w+", encoding="utf-8") as _cert:
_cert.write(builder2.certificate)
_cert.write(builder.certificate)
with open(f"{temp_dir}/foo.bar/privkey.pem", "w+", encoding="utf-8") as _key:
_key.write(builder2.private_key)
_key.write(builder.private_key)
with CONFIG.patch("cert_discovery_dir", temp_dir):
certificate_discovery.send()
keypair: CertificateKeyPair = CertificateKeyPair.objects.filter(
@@ -360,58 +348,3 @@ class TestCrypto(APITestCase):
self.assertTrue(
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
)
def test_discovery_updating_same_private_key(self):
"""Test certificate discovery updating certs with matching private keys"""
name = generate_id()
builder = CertificateBuilder(name)
builder.build(
subject_alt_names=[],
validity_days=3,
)
with TemporaryDirectory() as temp_dir:
# First discovery: write cert as "original"
with open(f"{temp_dir}/original.pem", "w+", encoding="utf-8") as _cert:
_cert.write(builder.certificate)
with open(f"{temp_dir}/original.key", "w+", encoding="utf-8") as _key:
_key.write(builder.private_key)
with CONFIG.patch("cert_discovery_dir", temp_dir):
certificate_discovery.send()
# Verify "original" cert was created
original = CertificateKeyPair.objects.filter(
managed=MANAGED_DISCOVERED % "original"
).first()
self.assertIsNotNone(original)
self.assertEqual(original.name, "original")
self.assertIsNotNone(original.private_key)
# Second discovery: write same cert/key as "renamed"
Path(f"{temp_dir}/original.pem").unlink()
Path(f"{temp_dir}/original.key").unlink()
with open(f"{temp_dir}/renamed.pem", "w+", encoding="utf-8") as _cert:
_cert.write(builder.certificate)
with open(f"{temp_dir}/renamed.key", "w+", encoding="utf-8") as _key:
_key.write(builder.private_key)
with CONFIG.patch("cert_discovery_dir", temp_dir):
certificate_discovery.send()
# Verify the cert was updated
renamed = CertificateKeyPair.objects.filter(
managed=MANAGED_DISCOVERED % "renamed"
).first()
self.assertIsNotNone(renamed, "Renamed certificate should exist")
self.assertEqual(renamed.name, "renamed")
self.assertEqual(renamed.pk, original.pk, "Should be same database object")
# Verify no new cert was created
final_count = CertificateKeyPair.objects.filter(
managed__startswith="goauthentik.io/crypto/discovered/"
).count()
self.assertEqual(
1, final_count, "Should not create duplicate cert for same private key"
)

View File

@@ -1,51 +0,0 @@
from rest_framework import mixins
from rest_framework.fields import SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
from authentik.endpoints.models import Connector
class ConnectorSerializer(ModelSerializer, MetaNameSerializer):
component = SerializerMethodField()
def get_component(self, obj: Connector) -> str: # pragma: no cover
"""Get object component so that we know how to edit the object"""
if obj.__class__ == Connector:
return ""
return obj.component
class Meta:
model = Connector
fields = [
"connector_uuid",
"name",
"enabled",
"component",
"verbose_name",
"verbose_name_plural",
"meta_model_name",
]
class ConnectorViewSet(
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""Connector Viewset"""
queryset = Connector.objects.none()
serializer_class = ConnectorSerializer
search_fields = [
"name",
]
def get_queryset(self): # pragma: no cover
return Connector.objects.select_subclasses()

View File

@@ -1,31 +0,0 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.models import DeviceAccessGroup
class DeviceAccessGroupSerializer(ModelSerializer):
class Meta:
model = DeviceAccessGroup
fields = [
"pbm_uuid",
"name",
]
class DeviceAccessGroupViewSet(UsedByMixin, ModelViewSet):
"""DeviceAccessGroup Viewset"""
queryset = DeviceAccessGroup.objects.all()
serializer_class = DeviceAccessGroupSerializer
search_fields = [
"pbm_uuid",
"name",
]
filterset_fields = [
"pbm_uuid",
"name",
]
ordering = ["name"]

View File

@@ -1,27 +0,0 @@
from rest_framework.fields import SerializerMethodField
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.api.connectors import ConnectorSerializer
from authentik.endpoints.api.device_fact_snapshots import DeviceFactSnapshotSerializer
from authentik.endpoints.models import DeviceConnection
class DeviceConnectionSerializer(ModelSerializer):
connector_obj = ConnectorSerializer(source="connector", read_only=True)
latest_snapshot = SerializerMethodField(allow_null=True)
def get_latest_snapshot(self, instance: DeviceConnection) -> DeviceFactSnapshotSerializer:
snapshot = instance.devicefactsnapshot_set.order_by("-created").first()
if not snapshot:
return None
return DeviceFactSnapshotSerializer(snapshot).data
class Meta:
model = DeviceConnection
fields = [
"device",
"connector",
"connector_obj",
"latest_snapshot",
]

View File

@@ -1,21 +0,0 @@
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.facts import DeviceFacts
from authentik.endpoints.models import DeviceFactSnapshot
class DeviceFactSnapshotSerializer(ModelSerializer):
data = DeviceFacts()
class Meta:
model = DeviceFactSnapshot
fields = [
"data",
"connection",
"created",
"expires",
]
extra_kwargs = {
"created": {"read_only": True},
"expires": {"read_only": True},
}

View File

@@ -1,27 +0,0 @@
from authentik.endpoints.api.connectors import ConnectorSerializer
from authentik.endpoints.models import DeviceUserBinding
from authentik.policies.api.bindings import PolicyBindingSerializer, PolicyBindingViewSet
class DeviceUserBindingSerializer(PolicyBindingSerializer):
connector_obj = ConnectorSerializer(source="connector", read_only=True)
class Meta:
model = DeviceUserBinding
fields = PolicyBindingSerializer.Meta.fields + [
"is_primary",
"connector",
"connector_obj",
]
extra_kwargs = {"connector": {"read_only": True}}
class DeviceUserBindingViewSet(PolicyBindingViewSet):
"""PolicyBinding Viewset"""
queryset = (
DeviceUserBinding.objects.all()
.select_related("target", "group", "user")
.prefetch_related("policy")
) # prefetching policy so we resolve the subclass

View File

@@ -1,78 +0,0 @@
from rest_framework import mixins
from rest_framework.fields import SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.api.device_access_group import DeviceAccessGroupSerializer
from authentik.endpoints.api.device_connections import DeviceConnectionSerializer
from authentik.endpoints.api.device_fact_snapshots import DeviceFactSnapshotSerializer
from authentik.endpoints.models import Device
class EndpointDeviceSerializer(ModelSerializer):
access_group_obj = DeviceAccessGroupSerializer(source="access_group", required=False)
facts = SerializerMethodField()
def get_facts(self, instance: Device) -> DeviceFactSnapshotSerializer:
return DeviceFactSnapshotSerializer(instance.cached_facts).data
class Meta:
model = Device
fields = [
"device_uuid",
"pbm_uuid",
"name",
"access_group",
"access_group_obj",
"expiring",
"expires",
"facts",
"attributes",
]
class EndpointDeviceDetailsSerializer(EndpointDeviceSerializer):
connections_obj = DeviceConnectionSerializer(many=True, source="deviceconnection_set")
def get_facts(self, instance: Device) -> DeviceFactSnapshotSerializer:
return DeviceFactSnapshotSerializer(instance.facts).data
class Meta(EndpointDeviceSerializer.Meta):
fields = EndpointDeviceSerializer.Meta.fields + [
"connections_obj",
"policies",
"connections",
]
class DeviceViewSet(
UsedByMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
queryset = Device.objects.all().select_related("access_group")
serializer_class = EndpointDeviceSerializer
search_fields = [
"name",
"identifier",
]
ordering = ["identifier"]
filterset_fields = ["name", "identifier"]
def get_serializer_class(self):
if self.action == "retrieve":
return EndpointDeviceDetailsSerializer
return super().get_serializer_class()
def get_queryset(self):
if self.action == "retrieve":
return super().get_queryset().prefetch_related("connections")
return super().get_queryset()

View File

@@ -1,32 +0,0 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.endpoints.api.connectors import ConnectorSerializer
from authentik.endpoints.models import EndpointStage
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.flows.api.stages import StageSerializer
class EndpointStageSerializer(EnterpriseRequiredMixin, StageSerializer):
"""EndpointStage Serializer"""
connector_obj = ConnectorSerializer(source="connector", read_only=True)
class Meta:
model = EndpointStage
fields = StageSerializer.Meta.fields + [
"connector",
"connector_obj",
]
class EndpointStageViewSet(UsedByMixin, ModelViewSet):
"""EndpointStage Viewset"""
queryset = EndpointStage.objects.all()
serializer_class = EndpointStageSerializer
filterset_fields = [
"name",
]
search_fields = ["name"]
ordering = ["name"]

View File

@@ -1,12 +0,0 @@
"""authentik endpoints app config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikEndpointsConfig(ManagedAppConfig):
"""authentik endpoints app config"""
name = "authentik.endpoints"
label = "authentik_endpoints"
verbose_name = "authentik Endpoints"
default = True

View File

@@ -1,66 +0,0 @@
from rest_framework.fields import (
BooleanField,
CharField,
IntegerField,
SerializerMethodField,
)
from authentik.api.v3.config import ConfigSerializer, ConfigView
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.models import CertificateKeyPair
from authentik.endpoints.connectors.agent.models import AgentConnector
from authentik.endpoints.models import Device
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.views.jwks import JWKSView
class AgentConfigSerializer(PassiveSerializer):
device_id = SerializerMethodField()
refresh_interval = SerializerMethodField()
authorization_flow = SerializerMethodField()
jwks = SerializerMethodField()
nss_uid_offset = IntegerField()
nss_gid_offset = IntegerField()
auth_terminate_session_on_expiry = BooleanField()
system_config = SerializerMethodField()
def get_device_id(self, instance: AgentConnector) -> str:
device: Device = self.context["device"]
return device.pk
def get_refresh_interval(self, instance: AgentConnector) -> int:
return int(timedelta_from_string(instance.refresh_interval).total_seconds())
def get_authorization_flow(self, instance: AgentConnector) -> str | None:
if not instance.authorization_flow:
return None
return instance.authorization_flow.slug
def get_jwks(self, instance: AgentConnector) -> dict:
kp = CertificateKeyPair.objects.filter(managed=MANAGED_KEY).first()
return {"keys": [JWKSView.get_jwk_for_key(kp, "sig")]}
def get_system_config(self, instance: AgentConnector) -> ConfigSerializer:
return ConfigView.get_config(self.context["request"]).data
class EnrollSerializer(PassiveSerializer):
device_serial = CharField(required=True)
device_name = CharField(required=True)
class AgentTokenResponseSerializer(PassiveSerializer):
token = CharField(required=True)
expires_in = IntegerField(required=0)
class AgentAuthenticationResponse(PassiveSerializer):
url = CharField()

View File

@@ -1,169 +0,0 @@
from typing import cast
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.fields import (
CharField,
ChoiceField,
)
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.endpoints.api.connectors import ConnectorSerializer
from authentik.endpoints.connectors.agent.api.agent import (
AgentConfigSerializer,
AgentTokenResponseSerializer,
EnrollSerializer,
)
from authentik.endpoints.connectors.agent.auth import (
AgentAuth,
AgentEnrollmentAuth,
)
from authentik.endpoints.connectors.agent.models import (
AgentConnector,
AgentDeviceConnection,
DeviceToken,
EnrollmentToken,
)
from authentik.endpoints.facts import DeviceFacts, OSFamily
from authentik.endpoints.models import Device
from authentik.lib.utils.reflection import ConditionalInheritance
class AgentConnectorSerializer(ConnectorSerializer):
class Meta(ConnectorSerializer.Meta):
model = AgentConnector
fields = ConnectorSerializer.Meta.fields + [
"snapshot_expiry",
"auth_session_duration",
"auth_terminate_session_on_expiry",
"refresh_interval",
"authorization_flow",
"nss_uid_offset",
"nss_gid_offset",
"challenge_key",
"jwt_federation_providers",
]
class MDMConfigSerializer(PassiveSerializer):
platform = ChoiceField(choices=OSFamily.choices)
enrollment_token = PrimaryKeyRelatedField(queryset=EnrollmentToken.objects.all())
def validate_platform(self, platform: OSFamily) -> OSFamily:
if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]:
raise ValidationError(_("Selected platform not supported"))
return platform
def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken:
if token.is_expired:
raise ValidationError(_("Token is expired"))
if token.connector != self.context["connector"]:
raise ValidationError(_("Invalid token for connector"))
return token
class MDMConfigResponseSerializer(PassiveSerializer):
config = CharField(required=True)
class AgentConnectorViewSet(
ConditionalInheritance(
"authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin"
),
UsedByMixin,
ModelViewSet,
):
queryset = AgentConnector.objects.all()
serializer_class = AgentConnectorSerializer
search_fields = ["name"]
ordering = ["name"]
filterset_fields = ["name", "enabled"]
@extend_schema(
request=MDMConfigSerializer(),
responses=MDMConfigResponseSerializer(),
)
@action(methods=["POST"], detail=True)
def mdm_config(self, request: Request, pk) -> Response:
"""Generate configuration for MDM systems to deploy authentik Agent"""
connector = cast(AgentConnector, self.get_object())
data = MDMConfigSerializer(data=request.data, context={"connector": connector})
data.is_valid(raise_exception=True)
token = data.validated_data["enrollment_token"]
if not request.user.has_perm("view_enrollment_token_key", token):
raise PermissionDenied()
ctrl = connector.controller(connector)
payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token)
return Response({"config": payload})
@extend_schema(
request=EnrollSerializer(),
responses={200: AgentTokenResponseSerializer},
)
@action(
methods=["POST"],
detail=False,
authentication_classes=[AgentEnrollmentAuth],
)
def enroll(self, request: Request):
token: EnrollmentToken = request.auth
data = EnrollSerializer(data=request.data)
data.is_valid(raise_exception=True)
device, _ = Device.objects.get_or_create(
identifier=data.validated_data["device_serial"],
defaults={
"name": data.validated_data["device_name"],
"expiring": False,
"access_group": token.device_group,
},
)
connection, _ = AgentDeviceConnection.objects.update_or_create(
device=device,
connector=token.connector,
)
token = DeviceToken.objects.create(device=connection, expiring=False)
return Response(
{
"token": token.key,
"expires_in": 0,
}
)
@extend_schema(
request=OpenApiTypes.NONE,
responses=AgentConfigSerializer(),
)
@action(methods=["GET"], detail=False, authentication_classes=[AgentAuth])
def agent_config(self, request: Request):
token: DeviceToken = request.auth
connector: AgentConnector = token.device.connector.agentconnector
return Response(
AgentConfigSerializer(
connector, context={"request": request, "device": token.device.device}
).data
)
@extend_schema(
request=DeviceFacts(),
responses={204: OpenApiResponse(description="Successfully checked in")},
)
@action(methods=["POST"], detail=False, authentication_classes=[AgentAuth])
def check_in(self, request: Request):
token: DeviceToken = request.auth
data = DeviceFacts(data=request.data)
data.is_valid(raise_exception=True)
connection: AgentDeviceConnection = token.device
connection.create_snapshot(data.validated_data)
return Response(status=204)

View File

@@ -1,58 +0,0 @@
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.tokens import TokenViewSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.endpoints.api.device_access_group import DeviceAccessGroupSerializer
from authentik.endpoints.connectors.agent.models import EnrollmentToken
from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
class EnrollmentTokenSerializer(ModelSerializer):
device_group_obj = DeviceAccessGroupSerializer(
source="device_group", read_only=True, required=False
)
class Meta:
model = EnrollmentToken
fields = [
"token_uuid",
"device_group",
"device_group_obj",
"connector",
"name",
"expiring",
"expires",
]
class EnrollmentTokenViewSet(UsedByMixin, ModelViewSet):
queryset = EnrollmentToken.objects.all().prefetch_related("device_group")
serializer_class = EnrollmentTokenSerializer
search_fields = [
"name",
"connector__name",
]
ordering = ["token_uuid"]
filterset_fields = ["token_uuid", "connector"]
@permission_required("authentik_endpoints_connectors_agent.view_enrollment_token_key")
@extend_schema(
responses={
200: TokenViewSerializer(many=False),
404: OpenApiResponse(description="Token not found or expired"),
}
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["GET"])
def view_key(self, request: Request, pk: str) -> Response:
"""Return token key and log access"""
token: EnrollmentToken = self.get_object()
Event.new(EventAction.SECRET_VIEW, secret=token).from_http(request) # noqa # nosec
return Response(TokenViewSerializer({"key": token.key}).data)

View File

@@ -1,12 +0,0 @@
"""authentik endpoints app config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikEndpointsConnectorAgentAppConfig(ManagedAppConfig):
"""authentik endpoints app config"""
name = "authentik.endpoints.connectors.agent"
label = "authentik_endpoints_connectors_agent"
verbose_name = "authentik Endpoints.Connectors.Agent"
default = True

View File

@@ -1,42 +0,0 @@
from typing import Any
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request
from authentik.api.authentication import IPCUser, validate_auth
from authentik.core.middleware import CTX_AUTH_VIA
from authentik.core.models import User
from authentik.endpoints.connectors.agent.models import DeviceToken, EnrollmentToken
class DeviceUser(IPCUser):
username = "authentik:endpoints:device"
class AgentEnrollmentAuth(BaseAuthentication):
def authenticate(self, request: Request) -> tuple[User, Any] | None:
auth = get_authorization_header(request)
key = validate_auth(auth)
token = EnrollmentToken.filter_not_expired(key=key).first()
if not token:
raise PermissionDenied()
CTX_AUTH_VIA.set("endpoint_token_enrollment")
return (DeviceUser(), token)
class AgentAuth(BaseAuthentication):
def authenticate(self, request: Request) -> tuple[User, Any] | None:
auth = get_authorization_header(request)
key = validate_auth(auth, format="bearer+agent")
if not key:
return None
device_token = DeviceToken.filter_not_expired(key=key).first()
if not device_token:
raise PermissionDenied()
if device_token.device.device.is_expired:
raise PermissionDenied()
CTX_AUTH_VIA.set("endpoint_token")
return (DeviceUser(), device_token)

View File

@@ -1,131 +0,0 @@
from plistlib import PlistFormat, dumps
from uuid import uuid4
from xml.etree.ElementTree import Element, SubElement, tostring # nosec
from django.http import HttpRequest
from django.urls import reverse
from authentik.endpoints.connectors.agent.models import AgentConnector, EnrollmentToken
from authentik.endpoints.controller import BaseController
from authentik.endpoints.facts import OSFamily
def csp_create_replace_item(loc_uri, data_value) -> Element:
"""Create a Replace/Item element with the specified LocURI and Data"""
replace = Element("Replace")
item = SubElement(replace, "Item")
# Meta section
meta = SubElement(item, "Meta")
format_elem = SubElement(meta, "Format")
format_elem.set("xmlns", "syncml:metinf")
format_elem.text = "chr"
# Target section
target = SubElement(item, "Target")
loc_uri_elem = SubElement(target, "LocURI")
loc_uri_elem.text = loc_uri
# Data section
data = SubElement(item, "Data")
data.text = data_value
return replace
class AgentConnectorController(BaseController[AgentConnector]):
def supported_enrollment_methods(self):
return []
def generate_mdm_config(
self, target_platform: OSFamily, request: HttpRequest, token: EnrollmentToken
) -> str:
if target_platform == OSFamily.windows:
return self._generate_mdm_config_windows(request, token)
if target_platform in [OSFamily.iOS, OSFamily.macOS]:
return self._generate_mdm_config_macos(request, token)
raise ValueError(f"Unsupported platform for MDM Configuration: {target_platform}")
def _generate_mdm_config_windows(self, request: HttpRequest, token: EnrollmentToken) -> str:
base_uri = (
"./Vendor/MSFT/Registry/HKLM/SOFTWARE/authentik Security Inc./Platform/ManagedConfig"
)
token_item = csp_create_replace_item(
base_uri + "/RegistrationToken",
token.key,
)
url_item = csp_create_replace_item(
base_uri + "/URL",
request.build_absolute_uri(reverse("authentik_core:root-redirect")),
)
payload = tostring(token_item, encoding="unicode") + tostring(url_item, encoding="unicode")
return payload
def _generate_mdm_config_macos(self, request: HttpRequest, token: EnrollmentToken) -> str:
token_uuid = str(token.pk).upper()
payload = dumps(
{
"PayloadContent": [
# Config for authentik Platform Agent (sysd)
{
"PayloadDisplayName": "authentik Platform",
"PayloadIdentifier": f"io.goauthentik.platform.{token_uuid}",
"PayloadType": "io.goauthentik.platform",
"PayloadUUID": str(uuid4()),
"PayloadVersion": 1,
"RegistrationToken": token.key,
"URL": request.build_absolute_uri(reverse("authentik_core:root-redirect")),
},
# Config for MDM-associated domains (required for PSSO)
{
"PayloadDisplayName": "Associated Domains",
"PayloadIdentifier": f"com.apple.associated-domains.{token_uuid}",
"PayloadType": "com.apple.associated-domains",
"PayloadUUID": str(uuid4()),
"PayloadVersion": 1,
"Configuration": [
{
"ApplicationIdentifier": "232G855Y8N.io.goauthentik.platform.agent",
"AssociatedDomains": [f"authsrv:{request.get_host()}"],
"EnableDirectDownloads": False,
}
],
},
# Config for Platform SSO
{
"PayloadDisplayName": "Platform Single Sign-On",
"PayloadIdentifier": f"com.apple.extensiblesso.{token_uuid}",
"PayloadType": "com.apple.extensiblesso",
"PayloadUUID": str(uuid4()),
"PayloadVersion": 1,
"ExtensionIdentifier": "io.goauthentik.platform.psso",
"TeamIdentifier": "232G855Y8N",
"Type": "Redirect",
"URLs": [request.build_absolute_uri("")],
"PlatformSSO": {
"AccountDisplayName": "authentik",
"AllowDeviceIdentifiersInAttestation": True,
"AuthenticationMethod": "UserSecureEnclaveKey",
"EnableAuthorization": True,
"EnableCreateUserAtLogin": True,
"FileVaultPolicy": ["RequireAuthentication"],
"LoginPolicy": ["RequireAuthentication"],
"NewUserAuthorizationMode": "Standard",
"UnlockPolicy": ["RequireAuthentication"],
"UseSharedDeviceKeys": True,
"UserAuthorizationMode": "Standard",
},
},
],
"PayloadDisplayName": "authentik Platform",
"PayloadIdentifier": str(self.connector.pk).upper(),
"PayloadScope": "System",
"PayloadType": "Configuration",
"PayloadUUID": str(self.connector.pk).upper(),
"PayloadVersion": 1,
},
fmt=PlistFormat.FMT_XML,
).decode()
return payload

View File

@@ -1,190 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-20 20:14
import authentik.core.models
import authentik.lib.generators
import authentik.lib.utils.time
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_crypto", "0004_alter_certificatekeypair_name"),
("authentik_endpoints", "0001_initial"),
("authentik_flows", "0028_flowtoken_revoke_on_execution"),
]
operations = [
migrations.CreateModel(
name="AgentDeviceConnection",
fields=[
(
"deviceconnection_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_endpoints.deviceconnection",
),
),
("apple_signing_key", models.TextField()),
("apple_encryption_key", models.TextField()),
("apple_key_exchange_key", models.TextField()),
("apple_sign_key_id", models.TextField()),
("apple_enc_key_id", models.TextField()),
],
options={
"abstract": False,
},
bases=("authentik_endpoints.deviceconnection",),
),
migrations.CreateModel(
name="AgentDeviceUserBinding",
fields=[
(
"deviceuserbinding_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_endpoints.deviceuserbinding",
),
),
("apple_secure_enclave_key", models.TextField()),
("apple_enclave_key_id", models.TextField()),
],
options={
"abstract": False,
},
bases=("authentik_endpoints.deviceuserbinding",),
),
migrations.CreateModel(
name="AgentConnector",
fields=[
(
"connector_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_endpoints.connector",
),
),
("nss_uid_offset", models.PositiveIntegerField(default=1000)),
("nss_gid_offset", models.PositiveIntegerField(default=1000)),
("auth_terminate_session_on_expiry", models.BooleanField(default=False)),
(
"refresh_interval",
models.TextField(
default="minutes=30",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
(
"authentication_flow",
models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_flows.flow",
),
),
(
"challenge_key",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="authentik_crypto.certificatekeypair",
),
),
],
options={
"verbose_name": "Agent Connector",
"verbose_name_plural": "Agent Connectors",
},
bases=("authentik_endpoints.connector",),
),
migrations.CreateModel(
name="DeviceToken",
fields=[
("expires", models.DateTimeField(default=None, null=True)),
("expiring", models.BooleanField(default=True)),
(
"token_uuid",
models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
),
("key", models.TextField(default=authentik.lib.generators.generate_key)),
(
"device",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_endpoints_connectors_agent.agentdeviceconnection",
),
),
],
options={
"abstract": False,
"indexes": [
models.Index(fields=["expires"], name="authentik_e_expires_675725_idx"),
models.Index(fields=["expiring"], name="authentik_e_expirin_8eb6d1_idx"),
models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_bd42e6_idx"
),
],
},
),
migrations.CreateModel(
name="EnrollmentToken",
fields=[
("expires", models.DateTimeField(default=None, null=True)),
("expiring", models.BooleanField(default=True)),
(
"token_uuid",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
("name", models.TextField()),
("key", models.TextField(default=authentik.core.models.default_token_key)),
(
"connector",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_endpoints_connectors_agent.agentconnector",
),
),
(
"device_group",
models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_endpoints.devicegroup",
),
),
],
options={
"verbose_name": "Enrollment Token",
"verbose_name_plural": "Enrollment Tokens",
"permissions": [("view_enrollment_token_key", "View token's key")],
"indexes": [
models.Index(fields=["expires"], name="authentik_e_expires_98b99c_idx"),
models.Index(fields=["expiring"], name="authentik_e_expirin_9a17f7_idx"),
models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_7ad82a_idx"
),
models.Index(fields=["key"], name="authentik_e_key_8dafaf_idx"),
],
},
),
]

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