Compare commits

..

62 Commits

Author SHA1 Message Date
a8134e26c8 website/docs: re-add gtag (cherry-pick #15334) (#15335)
website/docs: re-add gtag (#15334)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-07-01 19:33:23 +02:00
1c637bc0d1 web/elements: fix table search not resetting page when query changes (cherry-pick #15324) (#15326)
web/elements: fix table search not resetting page when query changes (#15324)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-07-01 17:18:43 +02:00
8af668a9d2 website: changelog for security releases (#15291)
* website: changelog for security releases

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/releases/2025/v2025.4.md
2025-06-27 15:43:14 +02:00
96266e2e2b release: 2025.6.3 2025-06-27 15:34:53 +02:00
65373ab217 security: fix CVE-2025-52553 (cherry-pick #15289) (#15290)
security: fix CVE-2025-52553 (#15289)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-27 15:30:42 +02:00
88590e1134 core: bump goauthentik/fips-python from 3.13.3-slim-bookworm-fips to 3.13.5-slim-bookworm-fips in 2025.6 (#15274)
core: bump goauthentik/fips-python from 3.13.3-slim-bookworm-fips to 3.13.4-slim-bookworm-fips in 2025.6

Signed-off-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-27 00:43:45 +02:00
f6ff31e3de web/elements: typing error when variables are not converted to string (cherry-pick #15169) (#15222)
web/elements: typing error when variables are not converted to string (#15169)

fix: typing error when variables are not converted to string

Co-authored-by: Leandro4002 <leandro262009@gmail.com>
Co-authored-by: leandro.saraiva <leandro.saraiva@adonite.com>
2025-06-24 20:24:49 +02:00
cbc14524b3 website/docs: Add steps to troubleshoot /initial-setup/ (cherry-pick #15011) (#15210)
website/docs: Add steps to troubleshoot /initial-setup/ (#15011)

* Add steps to troubleshoot /initial-setup/

* fix linting

* Apply suggestions from code review




* Apply suggestions from code review




* add email part

* Apply suggestions from code review




* Apply suggestions from code review




* change wording

---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
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>
2025-06-24 00:43:56 +02:00
d807038d05 website/docs: fix note at end of rac credentials prompt (cherry-pick #14909) (#15208)
website/docs: fix note at end of rac credentials prompt (#14909)

* Fixes note section at end of document

* tweak to bump build

---------

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-23 22:17:59 +02:00
ba131a50ba website/docs: add credentials prompt for rac doc (cherry-pick #14840) (#15207)
website/docs: add credentials prompt for rac doc (#14840)

* Adds document

* Typo

* Clarified RAC endpoint sentence based on Tana's suggestion.

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




* Small wording improvements

* Added connection security type information

* A word

* Added to sidebar

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




* Applied suggestions from Tana

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




---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-23 21:15:58 +02:00
0e37a66751 ci: fix CodeQL failing on cherry-pick PRs (cherry-pick #15205) (#15206)
* ci: fix CodeQL failing on cherry-pick PRs (#15205)

* fix build

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-06-23 20:41:33 +02:00
57c220dfc2 website: split integrations partially (#15076)
* config for split

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

* update alllll the links

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

* add redirect

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

* add separate job for integrations build

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

* Update website/netlify.toml

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Jens L. <jens@beryju.org>

* Revert "update alllll the links"

This reverts commit 872c5870a8.

* absolute relative URLs only

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

* but use a plugin to rewrite them

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

* fix external URL regex

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

* make rewrite plugin more re-usable

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

* fix the reverse links

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

* lint

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

* fix root redirect

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

* fix rediret

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

* fix root redirect

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

* fix redirect

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	.github/workflows/ci-website.yml
#	website/docusaurus.config.esm.mjs
2025-06-23 15:53:08 +02:00
7339a22080 web/user: fix infinite loop when no user settings flow is set (cherry-pick #15188) (#15192)
web/user: fix infinite loop when no user settings flow is set (#15188)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-23 01:50:38 +02:00
8c2d72affe website/docs: fix egregious maintenance fail (cherry-pick #15176) (#15180)
website/docs: fix egregious maintenance fail (#15176)

fix egregious maintenance fail

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-22 00:58:35 +02:00
9c31e08bd9 website/docs: fixes misplaced sentence (cherry-pick #14998) (#15182)
website/docs: fixes misplaced sentence (#14998)

fixes misplaced sentence

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-22 00:58:16 +02:00
805b9afa80 stages/user_login: fix session binding logging (#15175)
* add tests

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

* fix logging

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

* update test db?

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

* ah there we go; fix mmdb not being reloaded with test settings

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	authentik/root/test_runner.py
2025-06-21 00:23:10 +02:00
bc809bae1e core: bump protobuf from 6.30.2 to v6.31.1 (cherry-pick #14894) (#15173)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-20 14:50:10 +02:00
66773b69ab core: bump urllib3 from 2.4.0 to v2.5.0 (cherry-pick #15131) (#15174)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-20 14:49:59 +02:00
c149701501 sources/ldap: fix sync on empty groups (cherry-pick #15158) (#15171)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
fix sync on empty groups (#15158)
2025-06-20 14:25:55 +02:00
b13b51b73a core: bump requests from 2.32.3 to v2.32.4 (cherry-pick #15129) (#15135)
core: bump requests from 2.32.3 to v2.32.4 (#15129)

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-19 00:23:05 +02:00
ee30cc1ede core: bump tornado from 6.4.2 to v6.5.1 (cherry-pick #15100) (#15116)
core: bump tornado from 6.4.2 to v6.5.1 (#15100)

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-18 18:48:02 +02:00
de095f4a10 web/elements: Add light mode custom css handling (cherry-pick #14944) (#15096)
web/elements: Add light mode custom css handling (#14944)

* fix(ui): Add light mode custom css handling



* Update Base.ts



---------

Signed-off-by: CodeMax IT Solutions Pvt. Ltd. <137166088+cdmx1@users.noreply.github.com>
Co-authored-by: CodeMax IT Solutions Pvt. Ltd. <137166088+cdmx1@users.noreply.github.com>
2025-06-17 20:31:08 +02:00
abf3aa3a7f ci: fix post-release e2e builds failing (cherry-pick #15082) (#15092)
ci: fix post-release e2e builds failing (#15082)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-17 09:37:26 +02:00
3ebae72a76 website/docs: add more to style guide (cherry-pick #14797) (#15077)
website/docs: add more to style guide (#14797)

* lists and variables

* lists and variables

* tweaks

* kens edit

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-16 22:15:37 +02:00
b6b47b669e release: 2025.6.2 2025-06-16 18:20:22 +02:00
8422568c42 website/docs: release notes for 2025.6.2 (cherry-pick #15065) (#15069)
website/docs: release notes for `2025.6.2` (#15065)

* website/docs: release notes for `2025.6.2`

* fixup! website/docs: release notes for `2025.6.2`

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-06-16 18:19:24 +02:00
9973064f50 core: fix transaction test case (cherry-pick #15021) (#15070)
* core: fix transaction test case (#15021)

* move patched ct to root

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

* use our transaction test case as base

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

* fix...?

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

* well apparently that works

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

---------

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

* fixup! core: fix transaction test case (#15021)

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-16 17:55:12 +02:00
4fea65f5cc website/docs: remove commented out config options (cherry-pick #15064) (#15067)
website/docs: remove commented out config options (#15064)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-16 16:56:45 +02:00
784446a47d website/docs: correct minor version in release notes (cherry-pick #15012) (#15068)
website/docs: correct minor version in release notes (#15012)

Signed-off-by: Zeik0s <35345686+Zeik0s@users.noreply.github.com>
Co-authored-by: Zeik0s <35345686+Zeik0s@users.noreply.github.com>
2025-06-16 16:47:44 +02:00
516bc65fc4 web/elements: fix typo in localeComparator (cherry-pick #15054) (#15055)
web/elements: fix typo in localeComparator (#15054)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-16 01:57:26 +02:00
efb59adeff providers/rac: fixes prompt data not being merged with connection_settings (cherry-pick #15037) (#15038)
providers/rac: fixes prompt data not being merged with connection_settings (#15037)

* Fixes line that pulls in prompt data

* fallback to old settings



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-06-13 23:28:44 +02:00
43a2ad66f0 website/docs: also hide the postgres pool_options setting (cherry-pick #15023) (#15032)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-06-13 15:40:49 +02:00
ec0c59f1fc core: bump django from 5.1.10 to 5.1.11 (cherry-pick #14997) (#15010)
core: bump django from 5.1.10 to 5.1.11 (#14997)

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-11 18:25:26 +02:00
8f80072321 core: bump django from 5.1.9 to 5.1.10 (cherry-pick #14951) (#15008)
core: bump django from 5.1.9 to 5.1.10 (#14951)

bump django to 5.1.10

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-06-11 18:09:04 +02:00
33f95c837b brands: fix custom_css being escaped (cherry-pick #14994) (#14996)
brands: fix custom_css being escaped (#14994)

* brands: fix custom_css being escaped



* escape adequately



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-10 17:21:22 +02:00
43637b8a75 internal/outpost: fix incorrect usage of golang SHA API (cherry-pick #14981) (#14982)
internal/outpost: fix incorrect usage of golang SHA API (#14981)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-10 00:28:20 +02:00
7a4518be26 web/elements: fix dual select without sortBy (cherry-pick #14977) (#14979)
web/elements: fix dual select without sortBy (#14977)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-09 19:48:19 +02:00
b94fb53821 stages/email: Only attach logo to email if used (cherry-pick #14835) (#14969)
stages/email: Only attach logo to email if used (#14835)

* Only attach logo to email if used

* Fix MIME logo attachment to adhere to standard

* format, fix web



* not tuple



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Christian Elsen <chriselsen@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-06-09 03:53:51 +02:00
2be5c9633b website/docs: add 2025.6.1 release notes (cherry-pick #14948) (#14949)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-06-06 17:28:58 +02:00
e729e42595 release: 2025.6.1 2025-06-06 16:55:05 +02:00
01d591b84e root: fix bumpversion
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-06-06 16:53:26 +02:00
dd08e1bf66 providers/proxy: add option to override host header with property mappings (cherry-pick #14927) (#14945)
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-06 15:27:10 +02:00
150705f221 web/user: fix user settings flow not loading (cherry-pick #14911) (#14930)
web/user: fix user settings flow not loading (#14911)

* web/user: fix user settings flow not loading



* fix



* unrelated fix: fix select caret color in dark theme



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-06-05 23:36:40 +02:00
6b39f6495e tenants: fix tenant aware celery scheduler (cherry-pick #14921) 2025-06-05 15:15:34 +02:00
639c57245b website/docs: rotate supported versions: 2025.6 (cherry-pick #14856) (#14906)
website/docs: rotate supported versions: `2025.6` (#14856)

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-06-04 18:46:58 +02:00
730600aea4 website/integrations: tailscale (cherry-pick #14499) (#14908)
website/integrations: tailscale (#14499)

* init

* wording

* lint

* Update website/integrations/services/tailscale/index.md



* Dewi's suggestions

* still mention that its a placeholder

* fix



* Update website/integrations/services/tailscale/index.md




* mv to end



* indent

* Update website/integrations/services/tailscale/index.md




* Update website/integrations/services/tailscale/index.md



* tweak to bump build

* another tweak to bump build

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-04 18:37:53 +02:00
e15ce5a3f0 website/release notes: add tailscale to new integrations (cherry-pick #14859) (#14877)
website/release notes: add tailscale to new integrations (#14859)

* website/release notes: add tailscale to new integrations

### What

Adds Tailscale to the list of new integrations this release as it was merged like 5 minutes ago and technically 2025.6 isn't released just yet



* tweaks to bump build

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-04 17:06:01 +02:00
1fc91b004b website/releases: order new integrations alphabetically (cherry-pick #14850) (#14876)
website/releases: order new integrations alphabetically (#14850)

### What

Orders the 2025.6 release note's new integrations alphabetically. It just bothers me.

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-06-04 17:05:33 +02:00
644705e6fe release: 2025.6.0 2025-06-03 22:04:09 +02:00
ff8ef523db website/docs: finalize release notes for 2025.6 (cherry-pick #14854) (#14855)
website/docs: finalize release notes for `2025.6` (#14854)

* remove internal changes from release notes

* add late additions to release notes

* remove release candidate notice from `2025.6`

* rotate supported versions

* rotate releases in sidebar

* Revert "rotate supported versions"

This reverts commit eea9d03e1d.

I'd like to do the release tonight, but I can't merge this because it
needs a review from @teams/security. I'll open a separate PR for it.

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-06-03 22:01:40 +02:00
1051dd19ea providers/rac: apply ConnectionToken scoped-settings last (cherry-pick #14838) (#14853)
providers/rac: apply ConnectionToken scoped-settings last (#14838)

* providers/rac: apply ConnectionToken scoped-settings last



* fix tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-03 21:26:13 +02:00
04cb4fd267 lib/sync: fix static incorrect label of pages (cherry-pick #14851) (#14852)
lib/sync: fix static incorrect label of pages (#14851)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-03 21:26:09 +02:00
da9508f839 website/docs: add LDAP docs for forward deletion and memberUid (cherry-pick #14814) (#14848)
website/docs: add LDAP docs for forward deletion and `memberUid` (#14814)

* website/docs: add LDAP docs for forward deletion and `memberUid`

* reword LDAP docs



---------

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-06-03 19:31:04 +02:00
841a286a25 stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (cherry-pick #14801) (#14847)
stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#14801)

* stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs



* replace removed device type in tests

Android Authenticator with SafetyNet Attestation was removed from
blob.jwt in the previous commit

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Simonyi Gergő <gergo@goauthentik.io>
2025-06-03 19:30:56 +02:00
63c48d7b99 core: bump goauthentik.io/api/v3 from 3.2025041.2 to 3.2025041.4 (cherry-pick #14809) (#14846)
core: bump goauthentik.io/api/v3 from 3.2025041.2 to 3.2025041.4 (#14809)

Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025041.2 to 3.2025041.4.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2025041.2...v3.2025041.4)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-version: 3.2025041.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 19:30:14 +02:00
5994fd2c61 core, web: update translations (cherry-pick #14800) (#14845)
core, web: update translations (#14800)

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-06-03 19:30:06 +02:00
5f745e682e website/integrations: Update Zammad SAML Instructions (cherry-pick #14774) (#14844)
website/integrations: Update Zammad SAML Instructions (#14774)

* Update Zammad SAML Instructions

I just configured Zammad 6.4.1 to work with Authentik 2025.4.1. There seem to have been some changes since these instructions were written. The Name ID Format cannot be left blank. The SSO URL and the logout URL were incorrect. I was getting an Error 422 from Zammad until I turned on signing assertions, so I conclude that is required and I wrote instructions for that. I saw some discussion online elsewhere that the `----BEGIN` and `---END` lines should be removed. I tested it both ways and it worked both ways. I wrote the instructions to keep those lines in because it seemed simplest and most intuitive.



* Incorporate separate instructions for certificate file




* Incorporate simplified copy/paste instructions




* Incoporate formatting change




* Incorporate formatting changes




* Removed reference to custom properties

* Capitalisation




* Formatting




* Formatting




* Updated language




* Update website/integrations/services/zammad/index.md




* Update website/integrations/services/zammad/index.md




* tweak to bump build

* bump build

* use bold font for UI labels

* my typo

* capitalization fix

---------

Signed-off-by: Paco Hope <pacohope@users.noreply.github.com>
Co-authored-by: Paco Hope <pacohope@users.noreply.github.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
2025-06-03 19:29:59 +02:00
6f1b16e7f9 website/integrations: remove trailing slash from budibase redirect (cherry-pick #14823) (#14843)
website/integrations: remove trailing slash from budibase redirect (#14823)

Removes trailing slash from redirect

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-06-03 19:29:51 +02:00
57bce19e7a website/integrations: update cloudflare access callback url (cherry-pick #14807) (#14842)
website/integrations: update cloudflare access callback url (#14807)

Update CLoudflare Access index.md

The callback URL had a trailing / that breaks the callback URL being matched by a strict policy.

Signed-off-by: terafirmanz <53923271+terafirmanz@users.noreply.github.com>
Co-authored-by: terafirmanz <53923271+terafirmanz@users.noreply.github.com>
2025-06-03 19:29:40 +02:00
850c5d5a45 website/docs: remove fluff from release notes 2025.6 (cherry-pick #14819) (#14820)
remove fluff from release notes 2025.6 (#14819)

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-06-03 15:21:51 +02:00
8b7d11f94c web: minor design tweaks (cherry-pick #14803) (#14804)
web: minor design tweaks (#14803)

* fix spacing between header and page desc



* fix icon alignment



* fallback text when we dont have a user yet



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-06-01 23:15:08 +02:00
45737909f6 release: 2025.6.0-rc1 2025-05-31 00:44:04 +02:00
443 changed files with 6041 additions and 5650 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.6.1
current_version = 2025.6.3
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@ -21,6 +21,8 @@ optional_value = final
[bumpversion:file:package.json]
[bumpversion:file:package-lock.json]
[bumpversion:file:docker-compose.yml]
[bumpversion:file:schema.yml]
@ -31,6 +33,4 @@ optional_value = final
[bumpversion:file:internal/constants/constants.go]
[bumpversion:file:web/src/common/constants.ts]
[bumpversion:file:lifecycle/aws/template.yaml]

View File

@ -5,10 +5,8 @@ dist/**
build/**
build_docs/**
*Dockerfile
**/*Dockerfile
blueprints/local
.git
!gen-ts-api/node_modules
!gen-ts-api/dist/**
!gen-go-api/
.venv

View File

@ -100,13 +100,6 @@ updates:
goauthentik:
patterns:
- "@goauthentik/*"
eslint:
patterns:
- "@eslint/*"
- "@typescript-eslint/*"
- "eslint-*"
- "eslint"
- "typescript-eslint"
- package-ecosystem: npm
directory: "/lifecycle/aws"
schedule:

View File

@ -202,7 +202,7 @@ jobs:
uses: actions/cache@v4
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
- name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web

View File

@ -41,60 +41,33 @@ jobs:
- name: test
working-directory: website/
run: npm test
build-container:
build:
runs-on: ubuntu-latest
permissions:
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
name: ${{ matrix.job }}
strategy:
fail-fast: false
matrix:
job:
- build
- build:integrations
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/dev-docs
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
platforms: linux/amd64,linux/arm64
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache,mode=max' || '' }}
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
- name: build
working-directory: website/
run: npm run ${{ matrix.job }}
ci-website-mark:
if: always()
needs:
- lint
- test
- build-container
- build
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1

View File

@ -2,7 +2,7 @@ name: "CodeQL"
on:
push:
branches: [main, "*", next, version*]
branches: [main, next, version*]
pull_request:
branches: [main]
schedule:

View File

@ -20,49 +20,6 @@ jobs:
release: true
registry_dockerhub: true
registry_ghcr: true
build-docs:
runs-on: ubuntu-latest
permissions:
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/docs
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@v2
id: attest
if: true
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-outpost:
runs-on: ubuntu-latest
permissions:
@ -236,6 +193,6 @@ jobs:
SENTRY_ORG: authentik-security-inc
SENTRY_PROJECT: authentik
with:
release: authentik@${{ steps.ev.outputs.version }}
version: authentik@${{ steps.ev.outputs.version }}
sourcemaps: "./web/dist"
url_prefix: "~/static/dist"

View File

@ -1,7 +1,26 @@
# syntax=docker/dockerfile:1
# Stage 1: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24-slim AS node-builder
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24 AS website-builder
ENV NODE_ENV=production
WORKDIR /work/website
RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
--mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
--mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./website /work/website/
COPY ./blueprints /work/blueprints/
COPY ./schema.yml /work/
COPY ./SECURITY.md /work/
RUN npm run build-bundled
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24 AS web-builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
@ -13,7 +32,7 @@ RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
--mount=type=cache,id=npm-ak,sharing=shared,target=/root/.npm \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./package.json /work
@ -24,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \
npm run build:sfe
# Stage 2: Build go proxy
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
ARG TARGETOS
@ -49,8 +68,8 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
COPY ./cmd /go/src/goauthentik.io/cmd
COPY ./authentik/lib /go/src/goauthentik.io/authentik/lib
COPY ./web/static.go /go/src/goauthentik.io/web/static.go
COPY --from=node-builder /work/web/robots.txt /go/src/goauthentik.io/web/robots.txt
COPY --from=node-builder /work/web/security.txt /go/src/goauthentik.io/web/security.txt
COPY --from=web-builder /work/web/robots.txt /go/src/goauthentik.io/web/robots.txt
COPY --from=web-builder /work/web/security.txt /go/src/goauthentik.io/web/security.txt
COPY ./internal /go/src/goauthentik.io/internal
COPY ./go.mod /go/src/goauthentik.io/go.mod
COPY ./go.sum /go/src/goauthentik.io/go.sum
@ -61,7 +80,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
go build -o /go/authentik ./cmd/server
# Stage 3: MaxMind GeoIP
# Stage 4: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
@ -74,10 +93,10 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
mkdir -p /usr/share/GeoIP && \
/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.7.12 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.4-slim-bookworm-fips AS python-base
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.7.8 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
@ -90,7 +109,7 @@ WORKDIR /ak-root/
COPY --from=uv /uv /uvx /bin/
# Stage 6: Python dependencies
# Stage 7: Python dependencies
FROM python-base AS python-deps
ARG TARGETARCH
@ -125,7 +144,7 @@ RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
# Stage 7: Run
# Stage 8: Run
FROM python-base AS final-image
ARG VERSION
@ -168,8 +187,9 @@ COPY ./lifecycle/ /lifecycle
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
COPY --from=go-builder /go/authentik /bin/authentik
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
COPY --from=node-builder /work/web/dist/ /web/dist/
COPY --from=node-builder /work/web/authentik/ /web/authentik/
COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/build/ /website/help/
COPY --from=geoip /usr/share/GeoIP /geoip
USER 1000

View File

@ -86,6 +86,10 @@ dev-create-db:
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
update-test-mmdb: ## Update test GeoIP and ASN Databases
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
#########################
## API Schema
#########################

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2025.6.1"
__version__ = "2025.6.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -0,0 +1,79 @@
"""authentik administration metrics"""
from datetime import timedelta
from django.db.models.functions import ExtractHour
from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.shortcuts import get_objects_for_user
from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import EventAction
class CoordinateSerializer(PassiveSerializer):
"""Coordinates for diagrams"""
x_cord = IntegerField(read_only=True)
y_cord = IntegerField(read_only=True)
class LoginMetricsSerializer(PassiveSerializer):
"""Login Metrics per 1h"""
logins = SerializerMethodField()
logins_failed = SerializerMethodField()
authorizations = SerializerMethodField()
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins(self, _):
"""Get successful logins per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.LOGIN
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.LOGIN_FAILED
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_authorizations(self, _):
"""Get successful authorizations per 8 hours for the last 7 days"""
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event").filter(
action=EventAction.AUTHORIZE_APPLICATION
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
class AdministrationMetricsViewSet(APIView):
"""Login Metrics per 1h"""
permission_classes = [IsAuthenticated]
@extend_schema(responses={200: LoginMetricsSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Login Metrics per 1h"""
serializer = LoginMetricsSerializer(True)
serializer.context["user"] = request.user
return Response(serializer.data)

View File

@ -1,7 +1,6 @@
"""authentik administration overview"""
from django.core.cache import cache
from django_tenants.utils import get_public_schema_name
from drf_spectacular.utils import extend_schema
from packaging.version import parse
from rest_framework.fields import SerializerMethodField
@ -14,7 +13,6 @@ from authentik import __version__, get_build_hash
from authentik.admin.tasks import VERSION_CACHE_KEY, VERSION_NULL, update_latest_version
from authentik.core.api.utils import PassiveSerializer
from authentik.outposts.models import Outpost
from authentik.tenants.utils import get_current_tenant
class VersionSerializer(PassiveSerializer):
@ -37,8 +35,6 @@ class VersionSerializer(PassiveSerializer):
def get_version_latest(self, _) -> str:
"""Get latest version from cache"""
if get_current_tenant().schema_name == get_public_schema_name():
return __version__
version_in_cache = cache.get(VERSION_CACHE_KEY)
if not version_in_cache: # pragma: no cover
update_latest_version.delay()

View File

@ -14,19 +14,3 @@ class AuthentikAdminConfig(ManagedAppConfig):
label = "authentik_admin"
verbose_name = "authentik Admin"
default = True
@ManagedAppConfig.reconcile_global
def clear_update_notifications(self):
"""Clear update notifications on startup if the notification was for the version
we're running now."""
from packaging.version import parse
from authentik.admin.tasks import LOCAL_VERSION
from authentik.events.models import EventAction, Notification
for notification in Notification.objects.filter(event__action=EventAction.UPDATE_AVAILABLE):
if "new_version" not in notification.event.context:
continue
notification_version = notification.event.context["new_version"]
if LOCAL_VERSION >= parse(notification_version):
notification.delete()

View File

@ -1,7 +1,6 @@
"""authentik admin settings"""
from celery.schedules import crontab
from django_tenants.utils import get_public_schema_name
from authentik.lib.utils.time import fqdn_rand
@ -9,7 +8,6 @@ CELERY_BEAT_SCHEDULE = {
"admin_latest_version": {
"task": "authentik.admin.tasks.update_latest_version",
"schedule": crontab(minute=fqdn_rand("admin_latest_version"), hour="*"),
"tenant_schemas": [get_public_schema_name()],
"options": {"queue": "authentik_scheduled"},
}
}

View File

@ -1,6 +1,7 @@
"""authentik admin tasks"""
from django.core.cache import cache
from django.db import DatabaseError, InternalError, ProgrammingError
from django.utils.translation import gettext_lazy as _
from packaging.version import parse
from requests import RequestException
@ -8,7 +9,7 @@ from structlog.stdlib import get_logger
from authentik import __version__, get_build_hash
from authentik.admin.apps import PROM_INFO
from authentik.events.models import Event, EventAction
from authentik.events.models import Event, EventAction, Notification
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_http_session
@ -32,6 +33,20 @@ def _set_prom_info():
)
@CELERY_APP.task(
throws=(DatabaseError, ProgrammingError, InternalError),
)
def clear_update_notifications():
"""Clear update notifications on startup if the notification was for the version
we're running now."""
for notification in Notification.objects.filter(event__action=EventAction.UPDATE_AVAILABLE):
if "new_version" not in notification.event.context:
continue
notification_version = notification.event.context["new_version"]
if LOCAL_VERSION >= parse(notification_version):
notification.delete()
@CELERY_APP.task(bind=True, base=SystemTask)
@prefill_task
def update_latest_version(self: SystemTask):

View File

@ -36,6 +36,11 @@ class TestAdminAPI(TestCase):
body = loads(response.content)
self.assertEqual(len(body), 0)
def test_metrics(self):
"""Test metrics API"""
response = self.client.get(reverse("authentik_api:admin_metrics"))
self.assertEqual(response.status_code, 200)
def test_apps(self):
"""Test apps API"""
response = self.client.get(reverse("authentik_api:apps-list"))

View File

@ -1,12 +1,12 @@
"""test admin tasks"""
from django.apps import apps
from django.core.cache import cache
from django.test import TestCase
from requests_mock import Mocker
from authentik.admin.tasks import (
VERSION_CACHE_KEY,
clear_update_notifications,
update_latest_version,
)
from authentik.events.models import Event, EventAction
@ -72,13 +72,12 @@ class TestAdminTasks(TestCase):
def test_clear_update_notifications(self):
"""Test clear of previous notification"""
admin_config = apps.get_app_config("authentik_admin")
Event.objects.create(
action=EventAction.UPDATE_AVAILABLE, context={"new_version": "99999999.9999999.9999999"}
)
Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={"new_version": "1.1.1"})
Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={})
admin_config.clear_update_notifications()
clear_update_notifications()
self.assertFalse(
Event.objects.filter(
action=EventAction.UPDATE_AVAILABLE, context__new_version="1.1"

View File

@ -3,6 +3,7 @@
from django.urls import path
from authentik.admin.api.meta import AppsViewSet, ModelViewSet
from authentik.admin.api.metrics import AdministrationMetricsViewSet
from authentik.admin.api.system import SystemView
from authentik.admin.api.version import VersionView
from authentik.admin.api.version_history import VersionHistoryViewSet
@ -11,6 +12,11 @@ from authentik.admin.api.workers import WorkerView
api_urlpatterns = [
("admin/apps", AppsViewSet, "apps"),
("admin/models", ModelViewSet, "models"),
path(
"admin/metrics/",
AdministrationMetricsViewSet.as_view(),
name="admin_metrics",
),
path("admin/version/", VersionView.as_view(), name="admin_version"),
("admin/version/history", VersionHistoryViewSet, "version_history"),
path("admin/workers/", WorkerView.as_view(), name="admin_workers"),

View File

@ -1,13 +1,12 @@
"""authentik API AppConfig"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikAPIConfig(ManagedAppConfig):
class AuthentikAPIConfig(AppConfig):
"""authentik API Config"""
name = "authentik.api"
label = "authentik_api"
mountpoint = "api/"
verbose_name = "authentik API"
default = True

View File

@ -1,14 +0,0 @@
from django.test import TestCase
from authentik.blueprints.apps import ManagedAppConfig
from authentik.enterprise.apps import EnterpriseConfig
from authentik.lib.utils.reflection import get_apps
class TestManagedAppConfig(TestCase):
def test_apps_use_managed_app_config(self):
for app in get_apps():
if app.name.startswith("authentik.enterprise"):
self.assertIn(EnterpriseConfig, app.__class__.__bases__)
else:
self.assertIn(ManagedAppConfig, app.__class__.__bases__)

View File

@ -1,9 +1,9 @@
"""authentik brands app"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikBrandsConfig(ManagedAppConfig):
class AuthentikBrandsConfig(AppConfig):
"""authentik Brand app"""
name = "authentik.brands"
@ -12,4 +12,3 @@ class AuthentikBrandsConfig(ManagedAppConfig):
mountpoints = {
"authentik.brands.urls_root": "",
}
default = True

View File

@ -2,9 +2,11 @@
from collections.abc import Iterator
from copy import copy
from datetime import timedelta
from django.core.cache import cache
from django.db.models import QuerySet
from django.db.models.functions import ExtractHour
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
@ -18,6 +20,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.pagination import Pagination
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.providers import ProviderSerializer
@ -25,6 +28,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import Application, User
from authentik.events.logs import LogEventSerializer, capture_logs
from authentik.events.models import EventAction
from authentik.lib.utils.file import (
FilePathSerializer,
FileUploadSerializer,
@ -317,3 +321,18 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Set application icon (as URL)"""
app: Application = self.get_object()
return set_file_url(request, app, "meta_icon")
@permission_required("authentik_core.view_application", ["authentik_events.view_event"])
@extend_schema(responses={200: CoordinateSerializer(many=True)})
@action(detail=True, pagination_class=None, filter_backends=[])
def metrics(self, request: Request, slug: str):
"""Metrics for application logins"""
app = self.get_object()
return Response(
get_objects_for_user(request.user, "authentik_events.view_event").filter(
action=EventAction.AUTHORIZE_APPLICATION,
context__authorized_application__pk=app.pk.hex,
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)

View File

@ -6,6 +6,7 @@ from typing import Any
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.db.models.functions import ExtractHour
from django.db.transaction import atomic
from django.db.utils import IntegrityError
from django.urls import reverse_lazy
@ -51,6 +52,7 @@ from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin
@ -315,6 +317,53 @@ class SessionUserSerializer(PassiveSerializer):
original = UserSelfSerializer(required=False)
class UserMetricsSerializer(PassiveSerializer):
"""User Metrics"""
logins = SerializerMethodField()
logins_failed = SerializerMethodField()
authorizations = SerializerMethodField()
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins(self, _):
"""Get successful logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
return (
get_objects_for_user(request.user, "authentik_events.view_event").filter(
action=EventAction.LOGIN, user__pk=user.pk
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
return (
get_objects_for_user(request.user, "authentik_events.view_event").filter(
action=EventAction.LOGIN_FAILED, context__username=user.username
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_authorizations(self, _):
"""Get failed logins per 8 hours for the last 7 days"""
user = self.context["user"]
request = self.context["request"]
return (
get_objects_for_user(request.user, "authentik_events.view_event").filter(
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
)
# 3 data points per day, so 8 hour spans
.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)
)
class UsersFilter(FilterSet):
"""Filter for users"""
@ -558,6 +607,17 @@ class UserViewSet(UsedByMixin, ModelViewSet):
update_session_auth_hash(self.request, user)
return Response(status=204)
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])
def metrics(self, request: Request, pk: int) -> Response:
"""User metrics per 1h"""
user: User = self.get_object()
serializer = UserMetricsSerializer(instance={})
serializer.context["user"] = user
serializer.context["request"] = request
return Response(serializer.data)
@permission_required("authentik_core.reset_user_password")
@extend_schema(
responses={

View File

@ -10,7 +10,7 @@
{% endblock %}
{% block body %}
<ak-message-container alignment="bottom"></ak-message-container>
<ak-message-container></ak-message-container>
<ak-interface-admin>
<ak-loading></ak-loading>
</ak-interface-admin>

View File

@ -81,6 +81,22 @@ class TestUsersAPI(APITestCase):
response = self.client.get(reverse("authentik_api:user-list"), {"include_groups": "true"})
self.assertEqual(response.status_code, 200)
def test_metrics(self):
"""Test user's metrics"""
self.client.force_login(self.admin)
response = self.client.get(
reverse("authentik_api:user-metrics", kwargs={"pk": self.user.pk})
)
self.assertEqual(response.status_code, 200)
def test_metrics_denied(self):
"""Test user's metrics (non-superuser)"""
self.client.force_login(self.user)
response = self.client.get(
reverse("authentik_api:user-metrics", kwargs={"pk": self.user.pk})
)
self.assertEqual(response.status_code, 403)
def test_recovery_no_flow(self):
"""Test user recovery link (no recovery flow set)"""
self.client.force_login(self.admin)

View File

@ -1,36 +1,28 @@
"""Events API Views"""
from datetime import timedelta
from json import loads
import django_filters
from django.db.models import Count, ExpressionWrapper, F, QuerySet
from django.db.models import DateTimeField as DjangoDateTimeField
from django.db.models.aggregates import Count
from django.db.models.fields.json import KeyTextTransform, KeyTransform
from django.db.models.functions import TruncHour
from django.db.models.functions import ExtractDay, ExtractHour
from django.db.models.query_utils import Q
from django.utils.timezone import now
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import ChoiceField, DateTimeField, DictField, IntegerField
from rest_framework.fields import DictField, IntegerField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.core.api.object_types import TypeCreateSerializer
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.events.models import Event, EventAction
class EventVolumeSerializer(PassiveSerializer):
"""Count of events of action created on day"""
action = ChoiceField(choices=EventAction.choices)
time = DateTimeField()
count = IntegerField()
class EventSerializer(ModelSerializer):
"""Event Serializer"""
@ -61,7 +53,7 @@ class EventsFilter(django_filters.FilterSet):
"""Filter for events"""
username = django_filters.CharFilter(
field_name="user", label="Username", method="filter_username"
field_name="user", lookup_expr="username", label="Username"
)
context_model_pk = django_filters.CharFilter(
field_name="context",
@ -86,19 +78,12 @@ class EventsFilter(django_filters.FilterSet):
field_name="action",
lookup_expr="icontains",
)
actions = django_filters.MultipleChoiceFilter(
field_name="action",
choices=EventAction.choices,
)
brand_name = django_filters.CharFilter(
field_name="brand",
lookup_expr="name",
label="Brand name",
)
def filter_username(self, queryset, name, value):
return queryset.filter(Q(user__username=value) | Q(context__username=value))
def filter_context_model_pk(self, queryset, name, value):
"""Because we store the PK as UUID.hex,
we need to remove the dashes that a client may send. We can't use a
@ -171,37 +156,45 @@ class EventViewSet(ModelViewSet):
return Response(EventTopPerUserSerializer(instance=events, many=True).data)
@extend_schema(
responses={200: EventVolumeSerializer(many=True)},
parameters=[
OpenApiParameter(
"history_days",
type=OpenApiTypes.NUMBER,
location=OpenApiParameter.QUERY,
required=False,
default=7,
),
],
responses={200: CoordinateSerializer(many=True)},
)
@action(detail=False, methods=["GET"], pagination_class=None)
def volume(self, request: Request) -> Response:
"""Get event volume for specified filters and timeframe"""
queryset: QuerySet[Event] = self.filter_queryset(self.get_queryset())
delta = timedelta(days=7)
time_delta = request.query_params.get("history_days", 7)
if time_delta:
delta = timedelta(days=min(int(time_delta), 60))
queryset = self.filter_queryset(self.get_queryset())
return Response(queryset.get_events_per(timedelta(days=7), ExtractHour, 7 * 3))
@extend_schema(
responses={200: CoordinateSerializer(many=True)},
filters=[],
parameters=[
OpenApiParameter(
"action",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
OpenApiParameter(
"query",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
],
)
@action(detail=False, methods=["GET"], pagination_class=None)
def per_month(self, request: Request):
"""Get the count of events per month"""
filtered_action = request.query_params.get("action", EventAction.LOGIN)
try:
query = loads(request.query_params.get("query", "{}"))
except ValueError:
return Response(status=400)
return Response(
queryset.filter(created__gte=now() - delta)
.annotate(hour=TruncHour("created"))
.annotate(
time=ExpressionWrapper(
F("hour") - (F("hour__hour") % 6) * timedelta(hours=1),
output_field=DjangoDateTimeField(),
)
)
.values("time", "action")
.annotate(count=Count("pk"))
.order_by("time", "action")
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(action=filtered_action)
.filter(**query)
.get_events_per(timedelta(weeks=4), ExtractDay, 30)
)
@extend_schema(responses={200: TypeCreateSerializer(many=True)})

View File

@ -15,13 +15,13 @@ class MMDBContextProcessor(EventContextProcessor):
self.reader: Reader | None = None
self._last_mtime: float = 0.0
self.logger = get_logger()
self.open()
self.load()
def path(self) -> str | None:
"""Get the path to the MMDB file to load"""
raise NotImplementedError
def open(self):
def load(self):
"""Get GeoIP Reader, if configured, otherwise none"""
path = self.path()
if path == "" or not path:
@ -44,7 +44,7 @@ class MMDBContextProcessor(EventContextProcessor):
diff = self._last_mtime < mtime
if diff > 0:
self.logger.info("Found new MMDB Database, reopening", diff=diff, path=path)
self.open()
self.load()
except OSError as exc:
self.logger.warning("Failed to check MMDB age", exc=exc)

View File

@ -1,5 +1,7 @@
"""authentik events models"""
import time
from collections import Counter
from datetime import timedelta
from difflib import get_close_matches
from functools import lru_cache
@ -9,6 +11,11 @@ from uuid import uuid4
from django.apps import apps
from django.db import connection, models
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import Extract
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.http.request import QueryDict
from django.utils.timezone import now
@ -117,6 +124,60 @@ class EventAction(models.TextChoices):
CUSTOM_PREFIX = "custom_"
class EventQuerySet(QuerySet):
"""Custom events query set with helper functions"""
def get_events_per(
self,
time_since: timedelta,
extract: Extract,
data_points: int,
) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
_now = now()
max_since = timedelta(days=60)
# Allow maximum of 60 days to limit load
if time_since.total_seconds() > max_since.total_seconds():
time_since = max_since
date_from = _now - time_since
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(_now - F("created"), output_field=DurationField()))
.annotate(age_interval=extract("age"))
.values("age_interval")
.annotate(count=Count("pk"))
.order_by("age_interval")
)
data = Counter({int(d["age_interval"]): d["count"] for d in result})
results = []
interval_delta = time_since / data_points
for interval in range(1, -data_points, -1):
results.append(
{
"x_cord": time.mktime((_now + (interval_delta * interval)).timetuple()) * 1000,
"y_cord": data[interval * -1],
}
)
return results
class EventManager(Manager):
"""Custom helper methods for Events"""
def get_queryset(self) -> QuerySet:
"""use custom queryset"""
return EventQuerySet(self.model, using=self._db)
def get_events_per(
self,
time_since: timedelta,
extract: Extract,
data_points: int,
) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per(time_since, extract, data_points)
class Event(SerializerModel, ExpiringModel):
"""An individual Audit/Metrics/Notification/Error Event"""
@ -132,6 +193,8 @@ class Event(SerializerModel, ExpiringModel):
# Shadow the expires attribute from ExpiringModel to override the default duration
expires = models.DateTimeField(default=default_event_duration)
objects = EventManager()
@staticmethod
def _get_app_from_request(request: HttpRequest) -> str:
if not isinstance(request, HttpRequest):

View File

@ -37,9 +37,6 @@ class WebsocketMessageInstruction(IntEnum):
# Provider specific message
PROVIDER_SPECIFIC = 3
# Session ended
SESSION_END = 4
@dataclass(slots=True)
class WebsocketMessage:
@ -148,14 +145,6 @@ class OutpostConsumer(JsonWebsocketConsumer):
asdict(WebsocketMessage(instruction=WebsocketMessageInstruction.TRIGGER_UPDATE))
)
def event_session_end(self, event):
"""Event handler which is called when a session is ended"""
self.send_json(
asdict(
WebsocketMessage(instruction=WebsocketMessageInstruction.SESSION_END, args=event)
)
)
def event_provider_specific(self, event):
"""Event handler which can be called by provider-specific
implementations to send specific messages to the outpost"""

View File

@ -1,24 +1,17 @@
"""authentik outpost signals"""
from django.contrib.auth.signals import user_logged_out
from django.core.cache import cache
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
from django.dispatch import receiver
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.brands.models import Brand
from authentik.core.models import AuthenticatedSession, Provider, User
from authentik.core.models import Provider
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.utils.reflection import class_to_path
from authentik.outposts.models import Outpost, OutpostServiceConnection
from authentik.outposts.tasks import (
CACHE_KEY_OUTPOST_DOWN,
outpost_controller,
outpost_post_save,
outpost_session_end,
)
from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save
LOGGER = get_logger()
UPDATE_TRIGGERING_MODELS = (
@ -80,17 +73,3 @@ def pre_delete_cleanup(sender, instance: Outpost, **_):
instance.user.delete()
cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, instance)
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
@receiver(user_logged_out)
def logout_revoke_direct(sender: type[User], request: HttpRequest, **_):
"""Catch logout by direct logout and forward to providers"""
if not request.session or not request.session.session_key:
return
outpost_session_end.delay(request.session.session_key)
@receiver(pre_delete, sender=AuthenticatedSession)
def logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
"""Catch logout by expiring sessions being deleted"""
outpost_session_end.delay(instance.session.session_key)

View File

@ -1,6 +1,5 @@
"""outpost tasks"""
from hashlib import sha256
from os import R_OK, access
from pathlib import Path
from socket import gethostname
@ -50,11 +49,6 @@ LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "goauthentik.io/outposts/teardown/%s"
def hash_session_key(session_key: str) -> str:
"""Hash the session key for sending session end signals"""
return sha256(session_key.encode("ascii")).hexdigest()
def controller_for_outpost(outpost: Outpost) -> type[BaseController] | None:
"""Get a controller for the outpost, when a service connection is defined"""
if not outpost.service_connection:
@ -295,20 +289,3 @@ def outpost_connection_discovery(self: SystemTask):
url=unix_socket_path,
)
self.set_status(TaskStatus.SUCCESSFUL, *messages)
@CELERY_APP.task()
def outpost_session_end(session_id: str):
"""Update outpost instances connected to a single outpost"""
layer = get_channel_layer()
hashed_session_id = hash_session_key(session_id)
for outpost in Outpost.objects.all():
LOGGER.info("Sending session end signal to outpost", outpost=outpost)
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
async_to_sync(layer.group_send)(
group,
{
"type": "event.session.end",
"session_id": hashed_session_id,
},
)

View File

@ -1,11 +1,9 @@
"""Websocket tests"""
from dataclasses import asdict
from unittest.mock import patch
from channels.routing import URLRouter
from channels.testing import WebsocketCommunicator
from django.contrib.contenttypes.models import ContentType
from django.test import TransactionTestCase
from authentik import __version__
@ -16,12 +14,6 @@ from authentik.providers.proxy.models import ProxyProvider
from authentik.root import websocket
def patched__get_ct_cached(app_label, codename):
"""Caches `ContentType` instances like its `QuerySet` does."""
return ContentType.objects.get(app_label=app_label, permission__codename=codename)
@patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached)
class TestOutpostWS(TransactionTestCase):
"""Websocket tests"""

View File

@ -1,12 +1,11 @@
"""Authentik policy dummy app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikPolicyDummyConfig(ManagedAppConfig):
class AuthentikPolicyDummyConfig(AppConfig):
"""Authentik policy_dummy app config"""
name = "authentik.policies.dummy"
label = "authentik_policies_dummy"
verbose_name = "authentik Policies.Dummy"
default = True

View File

@ -1,12 +1,11 @@
"""authentik Event Matcher policy app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikPoliciesEventMatcherConfig(ManagedAppConfig):
class AuthentikPoliciesEventMatcherConfig(AppConfig):
"""authentik Event Matcher policy app config"""
name = "authentik.policies.event_matcher"
label = "authentik_policies_event_matcher"
verbose_name = "authentik Policies.Event Matcher"
default = True

View File

@ -1,12 +1,11 @@
"""Authentik policy_expiry app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikPolicyExpiryConfig(ManagedAppConfig):
class AuthentikPolicyExpiryConfig(AppConfig):
"""Authentik policy_expiry app config"""
name = "authentik.policies.expiry"
label = "authentik_policies_expiry"
verbose_name = "authentik Policies.Expiry"
default = True

View File

@ -1,12 +1,11 @@
"""Authentik policy_expression app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikPolicyExpressionConfig(ManagedAppConfig):
class AuthentikPolicyExpressionConfig(AppConfig):
"""Authentik policy_expression app config"""
name = "authentik.policies.expression"
label = "authentik_policies_expression"
verbose_name = "authentik Policies.Expression"
default = True

View File

@ -1,12 +1,11 @@
"""Authentik policy geoip app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikPolicyGeoIPConfig(ManagedAppConfig):
class AuthentikPolicyGeoIPConfig(AppConfig):
"""Authentik policy_geoip app config"""
name = "authentik.policies.geoip"
label = "authentik_policies_geoip"
verbose_name = "authentik Policies.GeoIP"
default = True

View File

@ -1,12 +1,11 @@
"""authentik Password policy app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikPoliciesPasswordConfig(ManagedAppConfig):
class AuthentikPoliciesPasswordConfig(AppConfig):
"""authentik Password policy app config"""
name = "authentik.policies.password"
label = "authentik_policies_password"
verbose_name = "authentik Policies.Password"
default = True

View File

@ -1,12 +1,11 @@
"""authentik ldap provider app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikProviderLDAPConfig(ManagedAppConfig):
class AuthentikProviderLDAPConfig(AppConfig):
"""authentik ldap provider app config"""
name = "authentik.providers.ldap"
label = "authentik_providers_ldap"
verbose_name = "authentik Providers.LDAP"
default = True

View File

@ -10,11 +10,3 @@ class AuthentikProviderProxyConfig(ManagedAppConfig):
label = "authentik_providers_proxy"
verbose_name = "authentik Providers.Proxy"
default = True
@ManagedAppConfig.reconcile_tenant
def proxy_set_defaults(self):
from authentik.providers.proxy.models import ProxyProvider
for provider in ProxyProvider.objects.all():
provider.set_oauth_defaults()
provider.save()

View File

@ -0,0 +1,23 @@
"""Proxy provider signals"""
from django.contrib.auth.signals import user_logged_out
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.http import HttpRequest
from authentik.core.models import AuthenticatedSession, User
from authentik.providers.proxy.tasks import proxy_on_logout
@receiver(user_logged_out)
def logout_proxy_revoke_direct(sender: type[User], request: HttpRequest, **_):
"""Catch logout by direct logout and forward to proxy providers"""
if not request.session or not request.session.session_key:
return
proxy_on_logout.delay(request.session.session_key)
@receiver(pre_delete, sender=AuthenticatedSession)
def logout_proxy_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
"""Catch logout by expiring sessions being deleted"""
proxy_on_logout.delay(instance.session.session_key)

View File

@ -0,0 +1,38 @@
"""proxy provider tasks"""
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.db import DatabaseError, InternalError, ProgrammingError
from authentik.outposts.consumer import OUTPOST_GROUP
from authentik.outposts.models import Outpost, OutpostType
from authentik.providers.oauth2.id_token import hash_session_key
from authentik.providers.proxy.models import ProxyProvider
from authentik.root.celery import CELERY_APP
@CELERY_APP.task(
throws=(DatabaseError, ProgrammingError, InternalError),
)
def proxy_set_defaults():
"""Ensure correct defaults are set for all providers"""
for provider in ProxyProvider.objects.all():
provider.set_oauth_defaults()
provider.save()
@CELERY_APP.task()
def proxy_on_logout(session_id: str):
"""Update outpost instances connected to a single outpost"""
layer = get_channel_layer()
hashed_session_id = hash_session_key(session_id)
for outpost in Outpost.objects.filter(type=OutpostType.PROXY):
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
async_to_sync(layer.group_send)(
group,
{
"type": "event.provider.specific",
"sub_type": "logout",
"session_id": hashed_session_id,
},
)

View File

@ -66,7 +66,10 @@ class RACClientConsumer(AsyncWebsocketConsumer):
def init_outpost_connection(self):
"""Initialize guac connection settings"""
self.token = (
ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"])
ConnectionToken.filter_not_expired(
token=self.scope["url_route"]["kwargs"]["token"],
session__session__session_key=self.scope["session"].session_key,
)
.select_related("endpoint", "provider", "session", "session__user")
.first()
)

View File

@ -87,3 +87,22 @@ class TestRACViews(APITestCase):
)
body = loads(flow_response.content)
self.assertEqual(body["component"], "ak-stage-access-denied")
def test_different_session(self):
"""Test request"""
self.client.force_login(self.user)
response = self.client.get(
reverse(
"authentik_providers_rac:start",
kwargs={"app": self.app.slug, "endpoint": str(self.endpoint.pk)},
)
)
self.assertEqual(response.status_code, 302)
flow_response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
body = loads(flow_response.content)
next_url = body["to"]
self.client.logout()
final_response = self.client.get(next_url)
self.assertEqual(final_response.url, reverse("authentik_core:if-user"))

View File

@ -20,6 +20,9 @@ from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
PLAN_CONNECTION_SETTINGS = "connection_settings"
class RACStartView(PolicyAccessView):
@ -65,7 +68,10 @@ class RACInterface(InterfaceView):
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
# Early sanity check to ensure token still exists
token = ConnectionToken.filter_not_expired(token=self.kwargs["token"]).first()
token = ConnectionToken.filter_not_expired(
token=self.kwargs["token"],
session__session__session_key=request.session.session_key,
).first()
if not token:
return redirect("authentik_core:if-user")
self.token = token
@ -109,10 +115,15 @@ class RACFinalStage(RedirectStage):
return super().dispatch(request, *args, **kwargs)
def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
settings = self.executor.plan.context.get(PLAN_CONNECTION_SETTINGS)
if not settings:
settings = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}).get(
PLAN_CONNECTION_SETTINGS
)
token = ConnectionToken.objects.create(
provider=self.provider,
endpoint=self.endpoint,
settings=self.executor.plan.context.get("connection_settings", {}),
settings=settings or {},
session=self.request.session["authenticatedsession"],
expires=now() + timedelta_from_string(self.provider.connection_expiry),
expiring=True,

View File

@ -1,12 +1,11 @@
"""authentik radius provider app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikProviderRadiusConfig(ManagedAppConfig):
class AuthentikProviderRadiusConfig(AppConfig):
"""authentik radius provider app config"""
name = "authentik.providers.radius"
label = "authentik_providers_radius"
verbose_name = "authentik Providers.Radius"
default = True

View File

@ -1,13 +1,12 @@
"""authentik SAML IdP app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikProviderSAMLConfig(ManagedAppConfig):
class AuthentikProviderSAMLConfig(AppConfig):
"""authentik SAML IdP app config"""
name = "authentik.providers.saml"
label = "authentik_providers_saml"
verbose_name = "authentik Providers.SAML"
mountpoint = "application/saml/"
default = True

View File

@ -1,29 +1,12 @@
"""test decorators api"""
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.test import APITestCase
from rest_framework.viewsets import ModelViewSet
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_user
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request
from authentik.rbac.decorators import permission_required
class MVS(ModelViewSet):
queryset = Application.objects.all()
lookup_field = "slug"
@permission_required("authentik_core.view_application", ["authentik_events.view_event"])
@action(detail=True, pagination_class=None, filter_backends=[])
def test(self, request: Request, slug: str):
self.get_object()
return Response(status=200)
class TestAPIDecorators(APITestCase):
@ -35,33 +18,41 @@ class TestAPIDecorators(APITestCase):
def test_obj_perm_denied(self):
"""Test object perm denied"""
request = get_request("", user=self.user)
self.client.force_login(self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
response = MVS.as_view({"get": "test"})(request, slug=app.slug)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 403)
def test_obj_perm_global(self):
"""Test object perm successful (global)"""
assign_perm("authentik_core.view_application", self.user)
assign_perm("authentik_events.view_event", self.user)
self.client.force_login(self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
request = get_request("", user=self.user)
response = MVS.as_view({"get": "test"})(request, slug=app.slug)
self.assertEqual(response.status_code, 200, response.data)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 200)
def test_obj_perm_scoped(self):
"""Test object perm successful (scoped)"""
assign_perm("authentik_events.view_event", self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
assign_perm("authentik_core.view_application", self.user, app)
request = get_request("", user=self.user)
response = MVS.as_view({"get": "test"})(request, slug=app.slug)
self.client.force_login(self.user)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 200)
def test_other_perm_denied(self):
"""Test other perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
assign_perm("authentik_core.view_application", self.user, app)
request = get_request("", user=self.user)
response = MVS.as_view({"get": "test"})(request, slug=app.slug)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 403)

View File

@ -1,13 +1,12 @@
"""authentik Recovery app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikRecoveryConfig(ManagedAppConfig):
class AuthentikRecoveryConfig(AppConfig):
"""authentik Recovery app config"""
name = "authentik.recovery"
label = "authentik_recovery"
verbose_name = "authentik Recovery"
mountpoint = "recovery/"
default = True

View File

@ -98,7 +98,13 @@ def _get_startup_tasks_default_tenant() -> list[Callable]:
def _get_startup_tasks_all_tenants() -> list[Callable]:
"""Get all tasks to be run on startup for all tenants"""
return []
from authentik.admin.tasks import clear_update_notifications
from authentik.providers.proxy.tasks import proxy_set_defaults
return [
clear_update_notifications,
proxy_set_defaults,
]
@worker_ready.connect

View File

@ -3,21 +3,40 @@
import os
from argparse import ArgumentParser
from unittest import TestCase
from unittest.mock import patch
import pytest
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.test.runner import DiscoverRunner
from structlog.stdlib import get_logger
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.lib.config import CONFIG
from authentik.lib.sentry import sentry_init
from authentik.root.signals import post_startup, pre_startup, startup
from tests.e2e.utils import get_docker_tag
# globally set maxDiff to none to show full assert error
TestCase.maxDiff = None
def get_docker_tag() -> str:
"""Get docker-tag based off of CI variables"""
env_pr_branch = "GITHUB_HEAD_REF"
default_branch = "GITHUB_REF"
branch_name = os.environ.get(default_branch, "main")
if os.environ.get(env_pr_branch, "") != "":
branch_name = os.environ[env_pr_branch]
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
return f"gh-{branch_name}"
def patched__get_ct_cached(app_label, codename):
"""Caches `ContentType` instances like its `QuerySet` does."""
return ContentType.objects.get(app_label=app_label, permission__codename=codename)
class PytestTestRunner(DiscoverRunner): # pragma: no cover
"""Runs pytest to discover and run tests."""
@ -36,33 +55,26 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
if kwargs.get("no_capture", False):
self.args.append("--capture=no")
self._setup_test_environment()
def _setup_test_environment(self):
"""Configure test environment settings"""
settings.TEST = True
settings.CELERY["task_always_eager"] = True
CONFIG.set("events.context_processors.geoip", "tests/GeoLite2-City-Test.mmdb")
CONFIG.set("events.context_processors.asn", "tests/GeoLite2-ASN-Test.mmdb")
CONFIG.set("blueprints_dir", "./blueprints")
CONFIG.set(
"outposts.container_image_base",
f"ghcr.io/goauthentik/dev-%(type)s:{get_docker_tag()}",
)
CONFIG.set("tenants.enabled", False)
CONFIG.set("outposts.disable_embedded_outpost", False)
CONFIG.set("error_reporting.sample_rate", 0)
CONFIG.set("error_reporting.environment", "testing")
CONFIG.set("error_reporting.send_pii", True)
# Test-specific configuration
test_config = {
"events.context_processors.geoip": "tests/GeoLite2-City-Test.mmdb",
"events.context_processors.asn": "tests/GeoLite2-ASN-Test.mmdb",
"blueprints_dir": "./blueprints",
"outposts.container_image_base": f"ghcr.io/goauthentik/dev-%(type)s:{get_docker_tag()}",
"tenants.enabled": False,
"outposts.disable_embedded_outpost": False,
"error_reporting.sample_rate": 0,
"error_reporting.environment": "testing",
"error_reporting.send_pii": True,
}
for key, value in test_config.items():
CONFIG.set(key, value)
ASN_CONTEXT_PROCESSOR.load()
GEOIP_CONTEXT_PROCESSOR.load()
sentry_init()
self.logger.debug("Test environment configured")
# Send startup signals
pre_startup.send(sender=self, mode="test")
startup.send(sender=self, mode="test")
post_startup.send(sender=self, mode="test")
@ -85,21 +97,7 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
help="Disable any capturing of stdout/stderr during tests.",
)
def _validate_test_label(self, label: str) -> bool:
"""Validate test label format"""
if not label:
return False
# Check for invalid characters, but allow forward slashes and colons
# for paths and pytest markers
invalid_chars = set('\\*?"<>|')
if any(c in label for c in invalid_chars):
self.logger.error("Invalid characters in test label", label=label)
return False
return True
def run_tests(self, test_labels: list[str], extra_tests=None, **kwargs):
def run_tests(self, test_labels, extra_tests=None, **kwargs):
"""Run pytest and return the exitcode.
It translates some of Django's test command option to pytest's.
@ -109,17 +107,10 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
The extra_tests argument has been deprecated since Django 5.x
It is kept for compatibility with PyCharm's Django test runner.
"""
if not test_labels:
self.logger.error("No test files specified")
return 1
for label in test_labels:
if not self._validate_test_label(label):
return 1
valid_label_found = False
label_as_path = os.path.abspath(label)
# File path has been specified
if os.path.exists(label_as_path):
self.args.append(label_as_path)
@ -127,30 +118,30 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
elif "::" in label:
self.args.append(label)
valid_label_found = True
# Convert dotted module path to file_path::class::method
else:
# Check if the label is a dotted module path
path_pieces = label.split(".")
# Check whether only class or class and method are specified
for i in range(-1, -3, -1):
try:
path = os.path.join(*path_pieces[:i]) + ".py"
if os.path.exists(path):
if i < -1:
path_method = path + "::" + "::".join(path_pieces[i:])
self.args.append(path_method)
else:
self.args.append(path)
valid_label_found = True
break
except (TypeError, IndexError):
continue
path = os.path.join(*path_pieces[:i]) + ".py"
label_as_path = os.path.abspath(path)
if os.path.exists(label_as_path):
path_method = label_as_path + "::" + "::".join(path_pieces[i:])
self.args.append(path_method)
valid_label_found = True
break
if not valid_label_found:
self.logger.error("Test file not found", label=label)
return 1
raise RuntimeError(
f"One of the test labels: {label!r}, "
f"is not supported. Use a dotted module name or "
f"path instead."
)
self.logger.info("Running tests", test_files=self.args)
try:
return pytest.main(self.args)
except Exception as e:
self.logger.error("Error running tests", error=str(e), test_files=self.args)
return 1
with patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached):
try:
return pytest.main(self.args)
except Exception as e:
self.logger.error("Error running tests", error=str(e), test_files=self.args)
return 1

View File

@ -71,37 +71,31 @@ def ldap_sync_single(source_pk: str):
return
# Delete all sync tasks from the cache
DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete()
task = chain(
# User and group sync can happen at once, they have no dependencies on each other
group(
ldap_sync_paginator(source, UserLDAPSynchronizer)
+ ldap_sync_paginator(source, GroupLDAPSynchronizer),
),
# Membership sync needs to run afterwards
group(
ldap_sync_paginator(source, MembershipLDAPSynchronizer),
),
# Finally, deletions. What we'd really like to do here is something like
# ```
# user_identifiers = <ldap query>
# User.objects.exclude(
# usersourceconnection__identifier__in=user_uniqueness_identifiers,
# ).delete()
# ```
# This runs into performance issues in large installations. So instead we spread the
# work out into three steps:
# 1. Get every object from the LDAP source.
# 2. Mark every object as "safe" in the database. This is quick, but any error could
# mean deleting users which should not be deleted, so we do it immediately, in
# large chunks, and only queue the deletion step afterwards.
# 3. Delete every unmarked item. This is slow, so we spread it over many tasks in
# small chunks.
group(
ldap_sync_paginator(source, UserLDAPForwardDeletion)
+ ldap_sync_paginator(source, GroupLDAPForwardDeletion),
),
# The order of these operations needs to be preserved as each depends on the previous one(s)
# 1. User and group sync can happen simultaneously
# 2. Membership sync needs to run afterwards
# 3. Finally, user and group deletions can happen simultaneously
user_group_sync = ldap_sync_paginator(source, UserLDAPSynchronizer) + ldap_sync_paginator(
source, GroupLDAPSynchronizer
)
task()
membership_sync = ldap_sync_paginator(source, MembershipLDAPSynchronizer)
user_group_deletion = ldap_sync_paginator(
source, UserLDAPForwardDeletion
) + ldap_sync_paginator(source, GroupLDAPForwardDeletion)
# Celery is buggy with empty groups, so we are careful only to add non-empty groups.
# See https://github.com/celery/celery/issues/9772
task_groups = []
if user_group_sync:
task_groups.append(group(user_group_sync))
if membership_sync:
task_groups.append(group(membership_sync))
if user_group_deletion:
task_groups.append(group(user_group_deletion))
all_tasks = chain(task_groups)
all_tasks()
def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> list:

View File

@ -1,12 +1,11 @@
"""authentik plex config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikSourcePlexConfig(ManagedAppConfig):
class AuthentikSourcePlexConfig(AppConfig):
"""authentik source plex config"""
name = "authentik.sources.plex"
label = "authentik_sources_plex"
verbose_name = "authentik Sources.Plex"
default = True

View File

@ -1,12 +1,11 @@
"""Authenticator"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageAuthenticatorConfig(ManagedAppConfig):
class AuthentikStageAuthenticatorConfig(AppConfig):
"""Authenticator App config"""
name = "authentik.stages.authenticator"
label = "authentik_stages_authenticator"
verbose_name = "authentik Stages.Authenticator"
default = True

View File

@ -1,12 +1,11 @@
"""SMS"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageAuthenticatorSMSConfig(ManagedAppConfig):
class AuthentikStageAuthenticatorSMSConfig(AppConfig):
"""SMS App config"""
name = "authentik.stages.authenticator_sms"
label = "authentik_stages_authenticator_sms"
verbose_name = "authentik Stages.Authenticator.SMS"
default = True

View File

@ -1,12 +1,11 @@
"""TOTP"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageAuthenticatorTOTPConfig(ManagedAppConfig):
class AuthentikStageAuthenticatorTOTPConfig(AppConfig):
"""TOTP App config"""
name = "authentik.stages.authenticator_totp"
label = "authentik_stages_authenticator_totp"
verbose_name = "authentik Stages.Authenticator.TOTP"
default = True

View File

@ -1,12 +1,11 @@
"""Authenticator Validation Stage"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageAuthenticatorValidateConfig(ManagedAppConfig):
class AuthentikStageAuthenticatorValidateConfig(AppConfig):
"""Authenticator Validation Stage"""
name = "authentik.stages.authenticator_validate"
label = "authentik_stages_authenticator_validate"
verbose_name = "authentik Stages.Authenticator.Validate"
default = True

View File

@ -1,12 +1,11 @@
"""authentik captcha app"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageCaptchaConfig(ManagedAppConfig):
class AuthentikStageCaptchaConfig(AppConfig):
"""authentik captcha app"""
name = "authentik.stages.captcha"
label = "authentik_stages_captcha"
verbose_name = "authentik Stages.Captcha"
default = True

View File

@ -1,12 +1,11 @@
"""authentik consent app"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageConsentConfig(ManagedAppConfig):
class AuthentikStageConsentConfig(AppConfig):
"""authentik consent app"""
name = "authentik.stages.consent"
label = "authentik_stages_consent"
verbose_name = "authentik Stages.Consent"
default = True

View File

@ -1,12 +1,11 @@
"""authentik deny stage app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageDenyConfig(ManagedAppConfig):
class AuthentikStageDenyConfig(AppConfig):
"""authentik deny stage config"""
name = "authentik.stages.deny"
label = "authentik_stages_deny"
verbose_name = "authentik Stages.Deny"
default = True

View File

@ -1,12 +1,11 @@
"""authentik dummy stage config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageDummyConfig(ManagedAppConfig):
class AuthentikStageDummyConfig(AppConfig):
"""authentik dummy stage config"""
name = "authentik.stages.dummy"
label = "authentik_stages_dummy"
verbose_name = "authentik Stages.Dummy"
default = True

View File

@ -1,12 +1,11 @@
"""authentik identification stage app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageIdentificationConfig(ManagedAppConfig):
class AuthentikStageIdentificationConfig(AppConfig):
"""authentik identification stage config"""
name = "authentik.stages.identification"
label = "authentik_stages_identification"
verbose_name = "authentik Stages.Identification"
default = True

View File

@ -1,12 +1,11 @@
"""authentik invitation stage app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageInvitationConfig(ManagedAppConfig):
class AuthentikStageInvitationConfig(AppConfig):
"""authentik invitation stage config"""
name = "authentik.stages.invitation"
label = "authentik_stages_invitation"
verbose_name = "authentik Stages.Invitation"
default = True

View File

@ -1,12 +1,11 @@
"""authentik core app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStagePasswordConfig(ManagedAppConfig):
class AuthentikStagePasswordConfig(AppConfig):
"""authentik password stage config"""
name = "authentik.stages.password"
label = "authentik_stages_password"
verbose_name = "authentik Stages.Password"
default = True

View File

@ -1,12 +1,11 @@
"""authentik prompt stage app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStagePromptConfig(ManagedAppConfig):
class AuthentikStagePromptConfig(AppConfig):
"""authentik prompt stage config"""
name = "authentik.stages.prompt"
label = "authentik_stages_prompt"
verbose_name = "authentik Stages.Prompt"
default = True

View File

@ -1,12 +1,11 @@
"""authentik redirect app"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageRedirectConfig(ManagedAppConfig):
class AuthentikStageRedirectConfig(AppConfig):
"""authentik redirect app"""
name = "authentik.stages.redirect"
label = "authentik_stages_redirect"
verbose_name = "authentik Stages.Redirect"
default = True

View File

@ -1,12 +1,11 @@
"""authentik delete stage app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageUserDeleteConfig(ManagedAppConfig):
class AuthentikStageUserDeleteConfig(AppConfig):
"""authentik delete stage config"""
name = "authentik.stages.user_delete"
label = "authentik_stages_user_delete"
verbose_name = "authentik Stages.User Delete"
default = True

View File

@ -1,12 +1,11 @@
"""authentik login stage app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageUserLoginConfig(ManagedAppConfig):
class AuthentikStageUserLoginConfig(AppConfig):
"""authentik login stage config"""
name = "authentik.stages.user_login"
label = "authentik_stages_user_login"
verbose_name = "authentik Stages.User Login"
default = True

View File

@ -101,9 +101,9 @@ class BoundSessionMiddleware(SessionMiddleware):
SESSION_KEY_BINDING_GEO, GeoIPBinding.NO_BINDING
)
if configured_binding_net != NetworkBinding.NO_BINDING:
self.recheck_session_net(configured_binding_net, last_ip, new_ip)
BoundSessionMiddleware.recheck_session_net(configured_binding_net, last_ip, new_ip)
if configured_binding_geo != GeoIPBinding.NO_BINDING:
self.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
BoundSessionMiddleware.recheck_session_geo(configured_binding_geo, last_ip, new_ip)
# If we got to this point without any error being raised, we need to
# update the last saved IP to the current one
if SESSION_KEY_BINDING_NET in request.session or SESSION_KEY_BINDING_GEO in request.session:
@ -111,7 +111,8 @@ class BoundSessionMiddleware(SessionMiddleware):
# (== basically requires the user to be logged in)
request.session[request.session.model.Keys.LAST_IP] = new_ip
def recheck_session_net(self, binding: NetworkBinding, last_ip: str, new_ip: str):
@staticmethod
def recheck_session_net(binding: NetworkBinding, last_ip: str, new_ip: str):
"""Check network/ASN binding"""
last_asn = ASN_CONTEXT_PROCESSOR.asn(last_ip)
new_asn = ASN_CONTEXT_PROCESSOR.asn(new_ip)
@ -158,7 +159,8 @@ class BoundSessionMiddleware(SessionMiddleware):
new_ip,
)
def recheck_session_geo(self, binding: GeoIPBinding, last_ip: str, new_ip: str):
@staticmethod
def recheck_session_geo(binding: GeoIPBinding, last_ip: str, new_ip: str):
"""Check GeoIP binding"""
last_geo = GEOIP_CONTEXT_PROCESSOR.city(last_ip)
new_geo = GEOIP_CONTEXT_PROCESSOR.city(new_ip)
@ -179,8 +181,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.continent != new_geo.continent:
raise SessionBindingBroken(
"geoip.continent",
last_geo.continent,
new_geo.continent,
last_geo.continent.to_dict(),
new_geo.continent.to_dict(),
last_ip,
new_ip,
)
@ -192,8 +194,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.country != new_geo.country:
raise SessionBindingBroken(
"geoip.country",
last_geo.country,
new_geo.country,
last_geo.country.to_dict(),
new_geo.country.to_dict(),
last_ip,
new_ip,
)
@ -202,8 +204,8 @@ class BoundSessionMiddleware(SessionMiddleware):
if last_geo.city != new_geo.city:
raise SessionBindingBroken(
"geoip.city",
last_geo.city,
new_geo.city,
last_geo.city.to_dict(),
new_geo.city.to_dict(),
last_ip,
new_ip,
)

View File

@ -3,6 +3,7 @@
from time import sleep
from unittest.mock import patch
from django.http import HttpRequest
from django.urls import reverse
from django.utils.timezone import now
@ -17,7 +18,12 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.utils.time import timedelta_from_string
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.user_login.models import UserLoginStage
from authentik.stages.user_login.middleware import (
BoundSessionMiddleware,
SessionBindingBroken,
logout_extra,
)
from authentik.stages.user_login.models import GeoIPBinding, NetworkBinding, UserLoginStage
class TestUserLoginStage(FlowTestCase):
@ -192,3 +198,52 @@ class TestUserLoginStage(FlowTestCase):
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
response = self.client.get(reverse("authentik_api:application-list"))
self.assertEqual(response.status_code, 403)
def test_binding_net_break_log(self):
"""Test logout_extra with exception"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-ASN-Test.json
for args, expect in [
[[NetworkBinding.BIND_ASN, "8.8.8.8", "8.8.8.8"], ["network.missing"]],
[[NetworkBinding.BIND_ASN, "1.0.0.1", "1.128.0.1"], ["network.asn"]],
[
[NetworkBinding.BIND_ASN_NETWORK, "12.81.96.1", "12.81.128.1"],
["network.asn_network"],
],
[[NetworkBinding.BIND_ASN_NETWORK_IP, "1.0.0.1", "1.0.0.2"], ["network.ip"]],
]:
with self.subTest(args[0]):
with self.assertRaises(SessionBindingBroken) as cm:
BoundSessionMiddleware.recheck_session_net(*args)
self.assertEqual(cm.exception.reason, expect[0])
# Ensure the request can be logged without throwing errors
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
request.user = self.user
logout_extra(request, cm.exception)
def test_binding_geo_break_log(self):
"""Test logout_extra with exception"""
# IPs from https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
for args, expect in [
[[GeoIPBinding.BIND_CONTINENT, "8.8.8.8", "8.8.8.8"], ["geoip.missing"]],
[[GeoIPBinding.BIND_CONTINENT, "2.125.160.216", "67.43.156.1"], ["geoip.continent"]],
[
[GeoIPBinding.BIND_CONTINENT_COUNTRY, "81.2.69.142", "89.160.20.112"],
["geoip.country"],
],
[
[GeoIPBinding.BIND_CONTINENT_COUNTRY_CITY, "2.125.160.216", "81.2.69.142"],
["geoip.city"],
],
]:
with self.subTest(args[0]):
with self.assertRaises(SessionBindingBroken) as cm:
BoundSessionMiddleware.recheck_session_geo(*args)
self.assertEqual(cm.exception.reason, expect[0])
# Ensure the request can be logged without throwing errors
self.client.force_login(self.user)
request = HttpRequest()
request.session = self.client.session
request.user = self.user
logout_extra(request, cm.exception)

View File

@ -1,12 +1,11 @@
"""authentik logout stage app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageUserLogoutConfig(ManagedAppConfig):
class AuthentikStageUserLogoutConfig(AppConfig):
"""authentik logout stage config"""
name = "authentik.stages.user_logout"
label = "authentik_stages_user_logout"
verbose_name = "authentik Stages.User Logout"
default = True

View File

@ -1,12 +1,11 @@
"""authentik write stage app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikStageUserWriteConfig(ManagedAppConfig):
class AuthentikStageUserWriteConfig(AppConfig):
"""authentik write stage config"""
name = "authentik.stages.user_write"
label = "authentik_stages_user_write"
verbose_name = "authentik Stages.User Write"
default = True

View File

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

View File

@ -2,13 +2,17 @@ package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak/entrypoint"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/ldap"
)
@ -21,15 +25,65 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
RunE: func(cmd *cobra.Command, args []string) error {
err := entrypoint.OutpostMain("authentik.outpost.ldap", ldap.NewServer)
if err != nil {
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL := config.Get().AuthentikHost
if akURL == "" {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken := config.Get().AuthentikToken
if akToken == "" {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = ldap.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
return err
},
}

View File

@ -2,13 +2,17 @@ package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak/entrypoint"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/proxyv2"
)
@ -24,15 +28,65 @@ Optionally, you can set these:
- AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST`
var rootCmd = &cobra.Command{
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
RunE: func(cmd *cobra.Command, args []string) error {
err := entrypoint.OutpostMain("authentik.outpost.proxy", proxyv2.NewProxyServer)
if err != nil {
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL := config.Get().AuthentikHost
if akURL == "" {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken := config.Get().AuthentikToken
if akToken == "" {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = proxyv2.NewProxyServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
return err
},
}

View File

@ -2,13 +2,16 @@ package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak/entrypoint"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/rac"
)
@ -21,15 +24,65 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
RunE: func(cmd *cobra.Command, args []string) error {
err := entrypoint.OutpostMain("authentik.outpost.rac", rac.NewServer)
if err != nil {
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
if !found {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
if !found {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = rac.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
return err
},
}

View File

@ -2,13 +2,16 @@ package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak/entrypoint"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/radius"
)
@ -21,15 +24,65 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
RunE: func(cmd *cobra.Command, args []string) error {
err := entrypoint.OutpostMain("authentik.outpost.radius", radius.NewServer)
if err != nil {
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
if !found {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
if !found {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = radius.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
return err
},
}

View File

@ -22,12 +22,21 @@ import (
)
var rootCmd = &cobra.Command{
Use: "authentik",
Short: "Start authentik instance",
Version: constants.FullVersion(),
PersistentPreRun: common.PreRun,
Use: "authentik",
Short: "Start authentik instance",
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer("authentik.core")
debug.EnableDebugServer()
l := log.WithField("logger", "authentik.root")
if config.Get().ErrorReporting.Enabled {
@ -90,7 +99,7 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
})
srv := proxyv2.NewProxyServer(ac)
ws.ProxyServer = srv.(*proxyv2.ProxyServer)
ws.ProxyServer = srv
ac.Server = srv
l.Debug("attempting to start outpost")
err := ac.StartBackgroundTasks()

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3}
restart: unless-stopped
command: server
environment:
@ -55,7 +55,7 @@ services:
redis:
condition: service_healthy
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3}
restart: unless-stopped
command: worker
environment:

10
go.mod
View File

@ -4,7 +4,6 @@ go 1.24.0
require (
beryju.io/ldap v0.1.0
github.com/avast/retry-go/v4 v4.6.1
github.com/coreos/go-oidc/v3 v3.14.1
github.com/getsentry/sentry-go v0.33.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
@ -17,22 +16,21 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.3
github.com/grafana/pyroscope-go v1.2.2
github.com/jellydator/ttlcache/v3 v3.3.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/pires/go-proxyproto v0.8.1
github.com/prometheus/client_golang v1.22.0
github.com/redis/go-redis/v9 v9.10.0
github.com/redis/go-redis/v9 v9.8.0
github.com/sethvargo/go-envconfig v1.3.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025061.2
goauthentik.io/api/v3 v3.2025041.4
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.15.0
golang.org/x/sync v0.14.0
gopkg.in/yaml.v2 v2.4.0
layeh.com/radius v0.0.0-20210819152912-ad72663a72ab
)
@ -60,10 +58,8 @@ require (
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect

20
go.sum
View File

@ -41,8 +41,6 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@ -180,10 +178,6 @@ github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2e
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.2.2 h1:uvKCyZMD724RkaCEMrSTC38Yn7AnFe8S2wiAIYdDPCE=
github.com/grafana/pyroscope-go v1.2.2/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -251,8 +245,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
@ -268,8 +262,6 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -298,8 +290,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025061.2 h1:bKmrl82Gz6J8lz3f+QIH9g+MEkl3MvkMXF34GktesA0=
goauthentik.io/api/v3 v3.2025061.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2025041.4 h1:cGqzWYnUHrWDoaXWDpIL/kWnX9sFrIhkYDye0P0OEAo=
goauthentik.io/api/v3 v3.2025041.4/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -384,8 +376,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -1,17 +0,0 @@
package common
import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func PreRun(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
}

View File

@ -33,4 +33,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2025.6.1"
const VERSION = "2025.6.3"

View File

@ -5,24 +5,19 @@ import (
"fmt"
"net/http"
"net/http/pprof"
"os"
"runtime"
"github.com/gorilla/mux"
"github.com/grafana/pyroscope-go"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/config"
"goauthentik.io/internal/utils/web"
)
var l = log.WithField("logger", "authentik.debugger.go")
func EnableDebugServer(appName string) {
func EnableDebugServer() {
l := log.WithField("logger", "authentik.go_debugger")
if !config.Get().Debug {
return
}
h := mux.NewRouter()
enablePyroscope(appName)
h.HandleFunc("/debug/pprof/", pprof.Index)
h.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
h.HandleFunc("/debug/pprof/profile", pprof.Profile)
@ -59,38 +54,3 @@ func EnableDebugServer(appName string) {
}
}()
}
func enablePyroscope(appName string) {
p, pok := os.LookupEnv("AUTHENTIK_PYROSCOPE_HOST")
if !pok {
return
}
l.Debug("Enabling pyroscope")
runtime.SetMutexProfileFraction(5)
runtime.SetBlockProfileRate(5)
hostname, err := os.Hostname()
if err != nil {
panic(err)
}
_, err = pyroscope.Start(pyroscope.Config{
ApplicationName: appName,
ServerAddress: p,
Logger: pyroscope.StandardLogger,
Tags: map[string]string{"hostname": hostname},
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileInuseSpace,
pyroscope.ProfileGoroutines,
pyroscope.ProfileMutexCount,
pyroscope.ProfileMutexDuration,
pyroscope.ProfileBlockCount,
pyroscope.ProfileBlockDuration,
},
})
if err != nil {
panic(err)
}
}

View File

@ -13,7 +13,6 @@ import (
"syscall"
"time"
"github.com/avast/retry-go/v4"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/gorilla/websocket"
@ -26,6 +25,8 @@ import (
"goauthentik.io/internal/utils/web"
)
type WSHandler func(ctx context.Context, args map[string]interface{})
const ConfigLogLevel = "log_level"
// APIController main controller which connects to the authentik api via http and ws
@ -42,11 +43,12 @@ type APIController struct {
reloadOffset time.Duration
eventConn *websocket.Conn
lastWsReconnect time.Time
wsIsReconnecting bool
eventHandlers []EventHandler
refreshHandlers []func()
wsConn *websocket.Conn
lastWsReconnect time.Time
wsIsReconnecting bool
wsBackoffMultiplier int
wsHandlers []WSHandler
refreshHandlers []func()
instanceUUID uuid.UUID
}
@ -81,19 +83,20 @@ func NewAPIController(akURL url.URL, token string) *APIController {
// Because we don't know the outpost UUID, we simply do a list and pick the first
// The service account this token belongs to should only have access to a single outpost
outposts, _ := retry.DoWithData[*api.PaginatedOutpostList](
func() (*api.PaginatedOutpostList, error) {
outposts, _, err := apiClient.OutpostsApi.OutpostsInstancesList(context.Background()).Execute()
return outposts, err
},
retry.Attempts(0),
retry.Delay(time.Second*3),
retry.OnRetry(func(attempt uint, err error) {
log.WithError(err).Error("Failed to fetch outpost configuration, retrying in 3 seconds")
}),
)
var outposts *api.PaginatedOutpostList
var err error
for {
outposts, _, err = apiClient.OutpostsApi.OutpostsInstancesList(context.Background()).Execute()
if err == nil {
break
}
log.WithError(err).Error("Failed to fetch outpost configuration, retrying in 3 seconds")
time.Sleep(time.Second * 3)
}
if len(outposts.Results) < 1 {
log.Panic("No outposts found with given token, ensure the given token corresponds to an authenitk Outpost")
panic("No outposts found with given token, ensure the given token corresponds to an authenitk Outpost")
}
outpost := outposts.Results[0]
@ -116,25 +119,22 @@ func NewAPIController(akURL url.URL, token string) *APIController {
token: token,
logger: log,
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
instanceUUID: uuid.New(),
Outpost: outpost,
eventHandlers: []EventHandler{},
refreshHandlers: make([]func(), 0),
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
instanceUUID: uuid.New(),
Outpost: outpost,
wsHandlers: []WSHandler{},
wsBackoffMultiplier: 1,
refreshHandlers: make([]func(), 0),
}
ac.logger.WithField("offset", ac.reloadOffset.String()).Debug("HA Reload offset")
err = ac.initEvent(akURL, outpost.Pk)
err = ac.initWS(akURL, outpost.Pk)
if err != nil {
go ac.recentEvents()
go ac.reconnectWS()
}
ac.configureRefreshSignal()
return ac
}
func (a *APIController) Log() *log.Entry {
return a.logger
}
// Start Starts all handlers, non-blocking
func (a *APIController) Start() error {
err := a.Server.Refresh()
@ -196,7 +196,7 @@ func (a *APIController) OnRefresh() error {
return err
}
func (a *APIController) getEventPingArgs() map[string]interface{} {
func (a *APIController) getWebsocketPingArgs() map[string]interface{} {
args := map[string]interface{}{
"version": constants.VERSION,
"buildHash": constants.BUILD(""),
@ -222,12 +222,12 @@ func (a *APIController) StartBackgroundTasks() error {
"build": constants.BUILD(""),
}).Set(1)
go func() {
a.logger.Debug("Starting Event Handler...")
a.startEventHandler()
a.logger.Debug("Starting WS Handler...")
a.startWSHandler()
}()
go func() {
a.logger.Debug("Starting Event health notifier...")
a.startEventHealth()
a.logger.Debug("Starting WS Health notifier...")
a.startWSHealth()
}()
go func() {
a.logger.Debug("Starting Interval updater...")

View File

@ -1,37 +0,0 @@
package ak
import (
"context"
"github.com/mitchellh/mapstructure"
)
type EventKind int
const (
// Code used to acknowledge a previous message
EventKindAck EventKind = 0
// Code used to send a healthcheck keepalive
EventKindHello EventKind = 1
// Code received to trigger a config update
EventKindTriggerUpdate EventKind = 2
// Code received to trigger some provider specific function
EventKindProviderSpecific EventKind = 3
// Code received to identify the end of a session
EventKindSessionEnd EventKind = 4
)
type EventHandler func(ctx context.Context, msg Event) error
type Event struct {
Instruction EventKind `json:"instruction"`
Args interface{} `json:"args"`
}
func (wm Event) ArgsAs(out interface{}) error {
return mapstructure.Decode(wm.Args, out)
}
type EventArgsSessionEnd struct {
SessionID string `mapstructure:"session_id"`
}

View File

@ -11,7 +11,6 @@ import (
"strings"
"time"
"github.com/avast/retry-go/v4"
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus"
"goauthentik.io/internal/config"
@ -31,7 +30,7 @@ func (ac *APIController) getWebsocketURL(akURL url.URL, outpostUUID string, quer
return wsUrl
}
func (ac *APIController) initEvent(akURL url.URL, outpostUUID string) error {
func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
query := akURL.Query()
query.Set("instance_uuid", ac.instanceUUID.String())
@ -58,19 +57,19 @@ func (ac *APIController) initEvent(akURL url.URL, outpostUUID string) error {
return err
}
ac.eventConn = ws
ac.wsConn = ws
// Send hello message with our version
msg := Event{
Instruction: EventKindHello,
Args: ac.getEventPingArgs(),
msg := websocketMessage{
Instruction: WebsocketInstructionHello,
Args: ac.getWebsocketPingArgs(),
}
err = ws.WriteJSON(msg)
if err != nil {
ac.logger.WithField("logger", "authentik.outpost.events").WithError(err).Warning("Failed to hello to authentik")
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithError(err).Warning("Failed to hello to authentik")
return err
}
ac.lastWsReconnect = time.Now()
ac.logger.WithField("logger", "authentik.outpost.events").WithField("outpost", outpostUUID).Info("Successfully connected websocket")
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithField("outpost", outpostUUID).Info("Successfully connected websocket")
return nil
}
@ -78,19 +77,19 @@ func (ac *APIController) initEvent(akURL url.URL, outpostUUID string) error {
func (ac *APIController) Shutdown() {
// Cleanly close the connection by sending a close message and then
// waiting (with timeout) for the server to close the connection.
err := ac.eventConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
err := ac.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
ac.logger.WithError(err).Warning("failed to write close message")
return
}
err = ac.eventConn.Close()
err = ac.wsConn.Close()
if err != nil {
ac.logger.WithError(err).Warning("failed to close websocket")
}
ac.logger.Info("finished shutdown")
}
func (ac *APIController) recentEvents() {
func (ac *APIController) reconnectWS() {
if ac.wsIsReconnecting {
return
}
@ -101,47 +100,46 @@ func (ac *APIController) recentEvents() {
Path: strings.ReplaceAll(ac.Client.GetConfig().Servers[0].URL, "api/v3", ""),
}
attempt := 1
_ = retry.Do(
func() error {
q := u.Query()
q.Set("attempt", strconv.Itoa(attempt))
u.RawQuery = q.Encode()
err := ac.initEvent(u, ac.Outpost.Pk)
attempt += 1
if err != nil {
return err
for {
q := u.Query()
q.Set("attempt", strconv.Itoa(attempt))
u.RawQuery = q.Encode()
err := ac.initWS(u, ac.Outpost.Pk)
attempt += 1
if err != nil {
ac.logger.Infof("waiting %d seconds to reconnect", ac.wsBackoffMultiplier)
time.Sleep(time.Duration(ac.wsBackoffMultiplier) * time.Second)
ac.wsBackoffMultiplier = ac.wsBackoffMultiplier * 2
// Limit to 300 seconds (5m)
if ac.wsBackoffMultiplier >= 300 {
ac.wsBackoffMultiplier = 300
}
} else {
ac.wsIsReconnecting = false
return nil
},
retry.Delay(1*time.Second),
retry.MaxDelay(5*time.Minute),
retry.DelayType(retry.BackOffDelay),
retry.Attempts(0),
retry.OnRetry(func(attempt uint, err error) {
ac.logger.Infof("waiting %d seconds to reconnect", attempt)
}),
)
ac.wsBackoffMultiplier = 1
return
}
}
}
func (ac *APIController) startEventHandler() {
logger := ac.logger.WithField("loop", "event-handler")
func (ac *APIController) startWSHandler() {
logger := ac.logger.WithField("loop", "ws-handler")
for {
var wsMsg Event
if ac.eventConn == nil {
go ac.recentEvents()
var wsMsg websocketMessage
if ac.wsConn == nil {
go ac.reconnectWS()
time.Sleep(time.Second * 5)
continue
}
err := ac.eventConn.ReadJSON(&wsMsg)
err := ac.wsConn.ReadJSON(&wsMsg)
if err != nil {
ConnectionStatus.With(prometheus.Labels{
"outpost_name": ac.Outpost.Name,
"outpost_type": ac.Server.Type(),
"uuid": ac.instanceUUID.String(),
}).Set(0)
logger.WithError(err).Warning("event read error")
go ac.recentEvents()
logger.WithError(err).Warning("ws read error")
go ac.reconnectWS()
time.Sleep(time.Second * 5)
continue
}
@ -151,8 +149,7 @@ func (ac *APIController) startEventHandler() {
"uuid": ac.instanceUUID.String(),
}).Set(1)
switch wsMsg.Instruction {
case EventKindAck:
case EventKindTriggerUpdate:
case WebsocketInstructionTriggerUpdate:
time.Sleep(ac.reloadOffset)
logger.Debug("Got update trigger...")
err := ac.OnRefresh()
@ -167,33 +164,30 @@ func (ac *APIController) startEventHandler() {
"build": constants.BUILD(""),
}).SetToCurrentTime()
}
default:
for _, h := range ac.eventHandlers {
err := h(context.Background(), wsMsg)
if err != nil {
ac.logger.WithError(err).Warning("failed to run event handler")
}
case WebsocketInstructionProviderSpecific:
for _, h := range ac.wsHandlers {
h(context.Background(), wsMsg.Args)
}
}
}
}
func (ac *APIController) startEventHealth() {
func (ac *APIController) startWSHealth() {
ticker := time.NewTicker(time.Second * 10)
for ; true; <-ticker.C {
if ac.eventConn == nil {
go ac.recentEvents()
if ac.wsConn == nil {
go ac.reconnectWS()
time.Sleep(time.Second * 5)
continue
}
err := ac.SendEventHello(map[string]interface{}{})
err := ac.SendWSHello(map[string]interface{}{})
if err != nil {
ac.logger.WithField("loop", "event-health").WithError(err).Warning("event write error")
go ac.recentEvents()
ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error")
go ac.reconnectWS()
time.Sleep(time.Second * 5)
continue
} else {
ac.logger.WithField("loop", "event-health").Trace("hello'd")
ac.logger.WithField("loop", "ws-health").Trace("hello'd")
ConnectionStatus.With(prometheus.Labels{
"outpost_name": ac.Outpost.Name,
"outpost_type": ac.Server.Type(),
@ -236,19 +230,19 @@ func (ac *APIController) startIntervalUpdater() {
}
}
func (a *APIController) AddEventHandler(handler EventHandler) {
a.eventHandlers = append(a.eventHandlers, handler)
func (a *APIController) AddWSHandler(handler WSHandler) {
a.wsHandlers = append(a.wsHandlers, handler)
}
func (a *APIController) SendEventHello(args map[string]interface{}) error {
allArgs := a.getEventPingArgs()
func (a *APIController) SendWSHello(args map[string]interface{}) error {
allArgs := a.getWebsocketPingArgs()
for key, value := range args {
allArgs[key] = value
}
aliveMsg := Event{
Instruction: EventKindHello,
aliveMsg := websocketMessage{
Instruction: WebsocketInstructionHello,
Args: allArgs,
}
err := a.eventConn.WriteJSON(aliveMsg)
err := a.wsConn.WriteJSON(aliveMsg)
return err
}

View File

@ -0,0 +1,19 @@
package ak
type websocketInstruction int
const (
// WebsocketInstructionAck Code used to acknowledge a previous message
WebsocketInstructionAck websocketInstruction = 0
// WebsocketInstructionHello Code used to send a healthcheck keepalive
WebsocketInstructionHello websocketInstruction = 1
// WebsocketInstructionTriggerUpdate Code received to trigger a config update
WebsocketInstructionTriggerUpdate websocketInstruction = 2
// WebsocketInstructionProviderSpecific Code received to trigger some provider specific function
WebsocketInstructionProviderSpecific websocketInstruction = 3
)
type websocketMessage struct {
Instruction websocketInstruction `json:"instruction"`
Args map[string]interface{} `json:"args"`
}

View File

@ -15,7 +15,7 @@ func URLMustParse(u string) *url.URL {
return ur
}
func TestEventWebsocketURL(t *testing.T) {
func TestWebsocketURL(t *testing.T) {
u := URLMustParse("http://localhost:9000?foo=bar")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}
@ -23,7 +23,7 @@ func TestEventWebsocketURL(t *testing.T) {
assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?foo=bar", nu.String())
}
func TestEventWebsocketURL_Query(t *testing.T) {
func TestWebsocketURL_Query(t *testing.T) {
u := URLMustParse("http://localhost:9000?foo=bar")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}
@ -33,7 +33,7 @@ func TestEventWebsocketURL_Query(t *testing.T) {
assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?bar=baz&foo=bar", nu.String())
}
func TestEventWebsocketURL_Subpath(t *testing.T) {
func TestWebsocketURL_Subpath(t *testing.T) {
u := URLMustParse("http://localhost:9000/foo/bar/")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}

View File

@ -1,51 +0,0 @@
package entrypoint
import (
"errors"
"net/url"
"os"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
)
func OutpostMain(appName string, server func(ac *ak.APIController) ak.Outpost) error {
debug.EnableDebugServer(appName)
akURL := config.Get().AuthentikHost
if akURL == "" {
return errors.New("environment variable `AUTHENTIK_HOST` not set")
}
akToken := config.Get().AuthentikToken
if akToken == "" {
return errors.New("environment variable `AUTHENTIK_TOKEN` not set")
}
akURLActual, err := url.Parse(akURL)
if err != nil {
return err
}
ex := common.Init()
defer common.Defer()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = server(ac)
err = ac.Start()
if err != nil {
ac.Log().WithError(err).Panic("Failed to run server")
return err
}
for {
<-ex
return nil
}
}

View File

@ -48,20 +48,20 @@ func doGlobalSetup(outpost api.Outpost, globalConfig *api.Config) {
if globalConfig.ErrorReporting.Enabled {
if !initialSetup {
l.WithField("env", globalConfig.ErrorReporting.Environment).Debug("Error reporting enabled")
err := sentry.Init(sentry.ClientOptions{
Dsn: globalConfig.ErrorReporting.SentryDsn,
Environment: globalConfig.ErrorReporting.Environment,
EnableTracing: true,
TracesSampler: sentryutils.SamplerFunc(float64(globalConfig.ErrorReporting.TracesSampleRate)),
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgentOutpost(), http.DefaultTransport),
IgnoreErrors: []string{
http.ErrAbortHandler.Error(),
},
})
if err != nil {
l.WithField("env", globalConfig.ErrorReporting.Environment).WithError(err).Warning("Failed to initialise sentry")
}
}
err := sentry.Init(sentry.ClientOptions{
Dsn: globalConfig.ErrorReporting.SentryDsn,
Environment: globalConfig.ErrorReporting.Environment,
EnableTracing: true,
TracesSampler: sentryutils.SamplerFunc(float64(globalConfig.ErrorReporting.TracesSampleRate)),
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgentOutpost(), http.DefaultTransport),
IgnoreErrors: []string{
http.ErrAbortHandler.Error(),
},
})
if err != nil {
l.WithField("env", globalConfig.ErrorReporting.Environment).WithError(err).Warning("Failed to initialise sentry")
}
}

View File

@ -55,10 +55,11 @@ func MockAK(outpost api.Outpost, globalConfig api.Config) *APIController {
token: token,
logger: log,
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
instanceUUID: uuid.New(),
Outpost: outpost,
refreshHandlers: make([]func(), 0),
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
instanceUUID: uuid.New(),
Outpost: outpost,
wsBackoffMultiplier: 1,
refreshHandlers: make([]func(), 0),
}
ac.logger.WithField("offset", ac.reloadOffset.String()).Debug("HA Reload offset")
return ac

View File

@ -127,7 +127,7 @@ func (fe *FlowExecutor) getAnswer(stage StageComponent) string {
return ""
}
func (fe *FlowExecutor) SessionCookie() *http.Cookie {
func (fe *FlowExecutor) GetSession() *http.Cookie {
return fe.session
}

View File

@ -1,19 +0,0 @@
package flow
import "github.com/golang-jwt/jwt/v5"
type SessionCookieClaims struct {
jwt.Claims
SessionID string `json:"sid"`
Authenticated bool `json:"authenticated"`
}
func (fe *FlowExecutor) Session() *jwt.Token {
sc := fe.SessionCookie()
if sc == nil {
return nil
}
t, _, _ := jwt.NewParser().ParseUnverified(sc.Value, &SessionCookieClaims{})
return t
}

View File

@ -38,14 +38,7 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
username, err := instance.binder.GetUsername(bindDN)
if err == nil {
selectedApp = instance.GetAppSlug()
c, err := instance.binder.Bind(username, req)
if c == ldap.LDAPResultSuccess {
f := instance.GetFlags(req.BindDN)
ls.connectionsSync.Lock()
ls.connections[f.SessionID()] = conn
ls.connectionsSync.Unlock()
}
return c, err
return instance.binder.Bind(username, req)
} else {
req.Log().WithError(err).Debug("Username not for instance")
}

View File

@ -27,9 +27,8 @@ func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResul
passed, err := fe.Execute()
flags := flags.UserFlags{
Session: fe.SessionCookie(),
SessionJWT: fe.Session(),
UserPk: flags.InvalidUserPK,
Session: fe.GetSession(),
UserPk: flags.InvalidUserPK,
}
// only set flags if we don't have flags for this DN yet
// as flags are only checked during the bind, we can remember whether a certain DN

View File

@ -1,20 +0,0 @@
package ldap
import "net"
func (ls *LDAPServer) Close(dn string, conn net.Conn) error {
ls.connectionsSync.Lock()
defer ls.connectionsSync.Unlock()
key := ""
for k, c := range ls.connections {
if c == conn {
key = k
break
}
}
if key == "" {
return nil
}
delete(ls.connections, key)
return nil
}

View File

@ -1,30 +1,16 @@
package flags
import (
"crypto/sha256"
"encoding/hex"
"net/http"
"github.com/golang-jwt/jwt/v5"
"goauthentik.io/api/v3"
"goauthentik.io/internal/outpost/flow"
)
const InvalidUserPK = -1
type UserFlags struct {
UserInfo *api.User
UserPk int32
CanSearch bool
Session *http.Cookie
SessionJWT *jwt.Token
}
func (uf UserFlags) SessionID() string {
if uf.SessionJWT == nil {
return ""
}
h := sha256.New()
h.Write([]byte(uf.SessionJWT.Claims.(*flow.SessionCookieClaims).SessionID))
return hex.EncodeToString(h.Sum(nil))
UserInfo *api.User
UserPk int32
CanSearch bool
Session *http.Cookie
}

View File

@ -18,26 +18,21 @@ import (
)
type LDAPServer struct {
s *ldap.Server
log *log.Entry
ac *ak.APIController
cs *ak.CryptoStore
defaultCert *tls.Certificate
providers []*ProviderInstance
connections map[string]net.Conn
connectionsSync sync.Mutex
s *ldap.Server
log *log.Entry
ac *ak.APIController
cs *ak.CryptoStore
defaultCert *tls.Certificate
providers []*ProviderInstance
}
func NewServer(ac *ak.APIController) ak.Outpost {
func NewServer(ac *ak.APIController) *LDAPServer {
ls := &LDAPServer{
log: log.WithField("logger", "authentik.outpost.ldap"),
ac: ac,
cs: ak.NewCryptoStore(ac.Client.CryptoApi),
providers: []*ProviderInstance{},
connections: map[string]net.Conn{},
connectionsSync: sync.Mutex{},
log: log.WithField("logger", "authentik.outpost.ldap"),
ac: ac,
cs: ak.NewCryptoStore(ac.Client.CryptoApi),
providers: []*ProviderInstance{},
}
ac.AddEventHandler(ls.handleWSSessionEnd)
s := ldap.NewServer()
s.EnforceLDAP = true
@ -55,7 +50,6 @@ func NewServer(ac *ak.APIController) ak.Outpost {
s.BindFunc("", ls)
s.UnbindFunc("", ls)
s.SearchFunc("", ls)
s.CloseFunc("", ls)
return ls
}
@ -123,23 +117,3 @@ func (ls *LDAPServer) TimerFlowCacheExpiry(ctx context.Context) {
p.binder.TimerFlowCacheExpiry(ctx)
}
}
func (ls *LDAPServer) handleWSSessionEnd(ctx context.Context, msg ak.Event) error {
if msg.Instruction != ak.EventKindSessionEnd {
return nil
}
mmsg := ak.EventArgsSessionEnd{}
err := msg.ArgsAs(&mmsg)
if err != nil {
return err
}
ls.connectionsSync.Lock()
defer ls.connectionsSync.Unlock()
ls.log.Info("Disconnecting session due to session end event")
conn, ok := ls.connections[mmsg.SessionID]
if !ok {
return nil
}
delete(ls.connections, mmsg.SessionID)
return conn.Close()
}

View File

@ -44,40 +44,38 @@ func (ds *DirectSearcher) SearchSubschema(req *search.Request) (ldap.ServerSearc
{
Name: "attributeTypes",
Values: []string{
"( 0.9.2342.19200300.100.1.1 NAME 'uid' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 0.9.2342.19200300.100.1.3 NAME 'mail' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 0.9.2342.19200300.100.1.41 NAME 'mobile' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.102 NAME 'memberOf' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' NO-USER-MODIFICATION )",
"( 1.2.840.113556.1.2.13 NAME 'displayName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.131 NAME 'co' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.141 NAME 'department' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.146 NAME 'company' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.1 NAME 'name' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE NO-USER-MODIFICATION )",
"( 1.2.840.113556.1.4.221 NAME 'sAMAccountName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.261 NAME 'division' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.44 NAME 'homeDirectory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.750 NAME 'groupType' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.782 NAME 'objectCategory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.0 NAME 'uidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.1 NAME 'gidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.12 NAME 'memberUid' SYNTAX '1.3.6.1.4.1.1466.115.121.1.26' )",
"( 2.5.18.1 NAME 'createTimestamp' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION )",
"( 2.5.18.2 NAME 'modifyTimestamp' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION )",
"( 2.5.21.2 NAME 'dITContentRules' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.21.5 NAME 'attributeTypes' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.21.6 NAME 'objectClasses' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.4.0 NAME 'objectClass' SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' NO-USER-MODIFICATION )",
"( 2.5.4.4 NAME 'sn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.3 NAME 'cn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.6 NAME 'c' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.7 NAME 'l' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.10 NAME 'o' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.11 NAME 'ou' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.12 NAME 'title' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.13 NAME 'description' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
"( 2.5.4.20 NAME 'telephoneNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.3 NAME 'cn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.31 NAME 'member' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' )",
"( 2.5.4.4 NAME 'sn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.42 NAME 'givenName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.6 NAME 'c' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.4.7 NAME 'l' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 2.5.21.2 NAME 'dITContentRules' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.21.5 NAME 'attributeTypes' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 2.5.21.6 NAME 'objectClasses' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' NO-USER-MODIFICATION )",
"( 0.9.2342.19200300.100.1.1 NAME 'uid' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 0.9.2342.19200300.100.1.3 NAME 'mail' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 0.9.2342.19200300.100.1.41 NAME 'mobile' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.13 NAME 'displayName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.146 NAME 'company' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.102 NAME 'memberOf' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' NO-USER-MODIFICATION )",
"( 1.2.840.113556.1.2.131 NAME 'co' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.2.141 NAME 'department' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.1 NAME 'name' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE NO-USER-MODIFICATION )",
"( 1.2.840.113556.1.4.44 NAME 'homeDirectory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.221 NAME 'sAMAccountName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.261 NAME 'division' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.750 NAME 'groupType' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.2.840.113556.1.4.782 NAME 'objectCategory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.0 NAME 'uidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.1 NAME 'gidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
"( 1.3.6.1.1.1.1.12 NAME 'memberUid' SYNTAX '1.3.6.1.4.1.1466.115.121.1.26' )",
// Custom attributes
// Temporarily use 1.3.6.1.4.1.26027.1.1 as a base

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