Compare commits

..

150 Commits

Author SHA1 Message Date
Jens Langhammer
43628f308d initial steps for concurrent execution
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-10-03 19:51:57 +02:00
dependabot[bot]
6fe83119f6 web: bump @codemirror/lang-html from 6.4.10 to 6.4.11 in /web (#17229)
Bumps [@codemirror/lang-html](https://github.com/codemirror/lang-html) from 6.4.10 to 6.4.11.
- [Changelog](https://github.com/codemirror/lang-html/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/lang-html/compare/6.4.10...6.4.11)

---
updated-dependencies:
- dependency-name: "@codemirror/lang-html"
  dependency-version: 6.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 17:20:36 +02:00
dependabot[bot]
e8e3b0fbca web: bump pino from 9.12.0 to 9.13.0 in /web (#17231)
Bumps [pino](https://github.com/pinojs/pino) from 9.12.0 to 9.13.0.
- [Release notes](https://github.com/pinojs/pino/releases)
- [Commits](https://github.com/pinojs/pino/compare/v9.12.0...v9.13.0)

---
updated-dependencies:
- dependency-name: pino
  dependency-version: 9.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 17:20:23 +02:00
dependabot[bot]
52c395cc92 web: bump @fortawesome/fontawesome-free from 7.0.1 to 7.1.0 in /web (#17230)
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 7.0.1 to 7.1.0.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/7.x/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/compare/7.0.1...7.1.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-free"
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 17:20:05 +02:00
dependabot[bot]
ac4e39d92a web: bump vite from 7.1.7 to 7.1.9 in /web (#17232)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.7 to 7.1.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 17:04:18 +02:00
dependabot[bot]
3bd7ee7a3d ci: bump peter-evans/create-or-update-comment from 4 to 5 (#17227)
Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 4 to 5.
- [Release notes](https://github.com/peter-evans/create-or-update-comment/releases)
- [Commits](https://github.com/peter-evans/create-or-update-comment/compare/v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/create-or-update-comment
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 17:03:56 +02:00
Jens L.
66fcccdd39 packages/django-dramatiq-postgres: fix error when updating task with no changes (#16728) 2025-10-03 13:34:08 +00:00
Teffen Ellis
b5cf26451a web: Fix avatar image load flash. (#17220) 2025-10-03 01:26:58 +02:00
Teffen Ellis
5cdc5e8d2b web: Fix tab theme consistency, table overflow. (#17222) 2025-10-03 01:25:29 +02:00
Jens L.
c49bab9fc4 providers/rac: remove autobahn import (#17224)
not installed in production, oops

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-10-03 00:52:59 +02:00
dependabot[bot]
93e88686c8 web: bump the react group across 2 directories with 4 updates (#17211)
Bumps the react group with 4 updates in the /packages/docusaurus-config directory: [react](https://github.com/facebook/react/tree/HEAD/packages/react), [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react), [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).
Bumps the react group with 4 updates in the /web directory: [react](https://github.com/facebook/react/tree/HEAD/packages/react), [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react), [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).


Updates `react` from 19.1.1 to 19.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react)

Updates `@types/react` from 19.1.16 to 19.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.1.1 to 19.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react-dom)

Updates `@types/react-dom` from 19.1.9 to 19.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `react` from 19.1.1 to 19.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react)

Updates `@types/react` from 19.1.16 to 19.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.1.1 to 19.2.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react-dom)

Updates `@types/react-dom` from 19.1.9 to 19.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: "@types/react"
  dependency-version: 19.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: react-dom
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: react
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: "@types/react"
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: react-dom
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 22:44:35 +02:00
Teffen Ellis
60dd28825d web/a11y: Admin overview regions. (#17170)
* web/a11y: Admin overview regions.

* web: Fix status overflow on smaller viewports.

* web: Use present check over defined.
2025-10-02 10:32:18 -04:00
Jens L.
986f082b59 packages/django-postgres-cache: Initial implementation of postgres cache (#16653)
* start db cache

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

* update codeowners

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

* handle db error in keys

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

* implement rest of the methods

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

* fix unrelated warning on startup for cache

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

* fix migrations?

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

* add readme

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

* dynamic dependency...?

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

* types

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

* rip out django_redis

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

* format

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

* fix tests?

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

* fix get default

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

* some cleanup

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

* simplify to use ORM

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

* remove old migrations that use cache instead of doing dynamic things

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

* fix migration

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

* Update packages/django-postgres-cache/django_postgres_cache/models.py

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update packages/django-postgres-cache/django_postgres_cache/migrations/0001_initial.py

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix redis imports

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* more redis removal

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-10-02 16:01:28 +02:00
Marc 'risson' Schmitt
8f644c3d3a packages/django-dramatiq-postgres: broker: fix task expiration (#17178) 2025-10-02 12:45:56 +00:00
Marcelo Elizeche Landó
40811eabc9 core: Add ak_send_email function in expression context (#16941)
* Add ak_send_email function in expression context

* Add docs for ak_send_email

* refactor the flow, simplify tests

* fix linting

* Update website/docs/expressions/_functions.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>

* Catch specific exceptions, better imports

* Add the option to send email to multiple recipients

* fix linting

* move imports

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

* simplify

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

* simplify ak_send_email to use send_mails for all cases

* change :::note to :::info, fix linting

---------

Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
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-10-02 14:12:13 +02:00
Marc 'risson' Schmitt
c715a596d8 root: channels: use postgres (#13532)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-10-02 14:08:56 +02:00
dependabot[bot]
dc9007fb6f ci: bump peter-evans/find-comment from 3 to 4 (#17203)
Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 3 to 4.
- [Release notes](https://github.com/peter-evans/find-comment/releases)
- [Commits](https://github.com/peter-evans/find-comment/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peter-evans/find-comment
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:50:42 +02:00
dependabot[bot]
acb02d5df3 core: bump goauthentik.io/api/v3 from 3.2025100.15 to 3.2025100.16 (#17205)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025100.15 to 3.2025100.16.
- [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.2025100.15...v3.2025100.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:50:11 +02:00
dependabot[bot]
72907f1320 website: bump @types/node from 24.6.1 to 24.6.2 in /website (#17208)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.6.1 to 24.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:49:51 +02:00
dependabot[bot]
a8e62e4a97 core: bump github.com/go-ldap/ldap/v3 from 3.4.11 to 3.4.12 (#17204)
Bumps [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap) from 3.4.11 to 3.4.12.
- [Release notes](https://github.com/go-ldap/ldap/releases)
- [Commits](https://github.com/go-ldap/ldap/compare/v3.4.11...v3.4.12)

---
updated-dependencies:
- dependency-name: github.com/go-ldap/ldap/v3
  dependency-version: 3.4.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:48:43 +02:00
dependabot[bot]
1cbec1876d web: bump the eslint group across 2 directories with 2 updates (#17209)
Bumps the eslint group with 2 updates in the /packages/eslint-config directory: [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) and [eslint-plugin-wc](https://github.com/43081j/eslint-plugin-wc).
Bumps the eslint group with 1 update in the /web directory: [eslint-plugin-wc](https://github.com/43081j/eslint-plugin-wc).


Updates `eslint-plugin-react-hooks` from 5.2.0 to 6.1.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

Updates `eslint-plugin-wc` from 3.0.1 to 3.0.2
- [Release notes](https://github.com/43081j/eslint-plugin-wc/releases)
- [Changelog](https://github.com/43081j/eslint-plugin-wc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/43081j/eslint-plugin-wc/compare/3.0.1...3.0.2)

Updates `eslint-plugin-wc` from 3.0.1 to 3.0.2
- [Release notes](https://github.com/43081j/eslint-plugin-wc/releases)
- [Changelog](https://github.com/43081j/eslint-plugin-wc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/43081j/eslint-plugin-wc/compare/3.0.1...3.0.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: eslint
- dependency-name: eslint-plugin-wc
  dependency-version: 3.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: eslint-plugin-wc
  dependency-version: 3.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:48:22 +02:00
dependabot[bot]
f5e5af9415 web: bump the storybook group across 1 directory with 5 updates (#17210)
Bumps the storybook group with 4 updates in the /web directory: [@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs), [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links), [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) and [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite).


Updates `@storybook/addon-docs` from 9.1.9 to 9.1.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.10/code/addons/docs)

Updates `@storybook/addon-links` from 9.1.9 to 9.1.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.10/code/addons/links)

Updates `@storybook/web-components` from 9.1.9 to 9.1.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.10/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 9.1.9 to 9.1.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.10/code/frameworks/web-components-vite)

Updates `storybook` from 9.1.9 to 9.1.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.10/code/core)

---
updated-dependencies:
- dependency-name: "@storybook/addon-docs"
  dependency-version: 9.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-version: 9.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-version: 9.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-version: 9.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 9.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:48:08 +02:00
dependabot[bot]
9cf8150df5 web: bump @types/node from 24.6.0 to 24.6.2 in /packages/esbuild-plugin-live-reload (#17212)
web: bump @types/node in /packages/esbuild-plugin-live-reload

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.6.0 to 24.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:47:00 +02:00
dependabot[bot]
3038122c79 web: bump @types/node from 24.6.0 to 24.6.2 in /packages/prettier-config (#17213)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.6.0 to 24.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:46:48 +02:00
dependabot[bot]
639d6da4f2 lifecycle/aws: bump aws-cdk from 2.1029.3 to 2.1029.4 in /lifecycle/aws (#17214)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1029.3 to 2.1029.4.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1029.4/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1029.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:46:14 +02:00
dependabot[bot]
a89cc8e6d1 web: bump @types/node from 22.15.19 to 24.6.2 in /web (#17215)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.19 to 24.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 11:45:55 +02:00
Teffen Ellis
7e8492aecf web: Fix nested table column span behavior. (#17177) 2025-10-02 03:47:32 +00:00
Teffen Ellis
2e8a1d80a3 web: Fix numeric values in search select inputs, search input fixes (#16928)
* web: Fix numeric values in search select inputs.

* web: Fix ARIA attributes on menu items.

* web: Fix issues surrounding nested modal actions, selectors, labels.

* web: Prepare group forms for testing, ARIA, etc.

* web: Clarify when spinner buttons are busy.

* web: Fix dark theme toggle input visibility.

* web: Fix issue where tests complete before optional search inputs load.

* web: Add user creation tests, group creation. Flesh out fixtures.
2025-10-02 03:04:38 +00:00
Jens L.
9e4b6098fd ci: don't log postgres always (#17201)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-10-02 02:31:56 +02:00
authentik-automation[bot]
686631ca84 core, web: update translations (#17202)
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-10-02 02:30:02 +02:00
dependabot[bot]
e7565944e9 build(deps): bump django from 5.1.12 to 5.1.13 (#17198)
* build(deps): bump django from 5.1.12 to 5.1.13

Bumps [django](https://github.com/django/django) from 5.1.12 to 5.1.13.
- [Commits](https://github.com/django/django/compare/5.1.12...5.1.13)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.1.13
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* lock

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-10-02 01:31:59 +02:00
Marcelo Elizeche Landó
8208a569da core: bump boto3 from 1.40.31 to v1.40.43 (#17182) 2025-10-02 01:01:02 +02:00
Marcelo Elizeche Landó
511a43d8c0 core: bump asgiref from 3.9.1 to v3.9.2 (#17180) 2025-10-02 00:47:23 +02:00
Marcelo Elizeche Landó
1e3cd8677a core: bump zope-interface from 8.0 to v8.0.1 (#17197) 2025-10-02 00:47:08 +02:00
Marcelo Elizeche Landó
f33a576993 core: bump anyio from 4.10.0 to v4.11.0 (#17179) 2025-10-02 00:46:19 +02:00
Marcelo Elizeche Landó
9bb3cb37bf core: bump pyparsing from 3.2.4 to v3.2.5 (#17193) 2025-10-01 21:38:56 +00:00
Marcelo Elizeche Landó
422d6d8267 core: bump typing-inspection from 0.4.1 to v0.4.2 (#17196) 2025-10-01 21:38:30 +00:00
Marcelo Elizeche Landó
db74683803 core: bump std-uritemplate from 2.0.5 to v2.0.6 (#17194) 2025-10-01 21:37:27 +00:00
Marcelo Elizeche Landó
c1ec60fc24 core: bump click from 8.2.1 to v8.3.0 (#17184) 2025-10-01 21:30:17 +00:00
Marcelo Elizeche Landó
913457108d core: bump bcrypt from 4.3.0 to v5.0.0 (#17181) 2025-10-01 21:27:53 +00:00
Marcelo Elizeche Landó
0dd5bf95b8 core: bump gevent from 25.8.2 to v25.9.1 (#17185) 2025-10-01 21:27:26 +00:00
Marcelo Elizeche Landó
2c94da4b63 core: bump microsoft-kiota-authentication-azure from 1.9.6 to v1.9.7 (#17189) 2025-10-01 21:25:07 +00:00
Marcelo Elizeche Landó
0d63bf74d1 core: bump jsii from 1.114.1 to v1.115.0 (#17187) 2025-10-01 21:23:44 +00:00
Marcelo Elizeche Landó
ffadac7450 core: bump txaio from 25.6.1 to v25.9.2 (#17195) 2025-10-01 23:16:06 +02:00
Marcelo Elizeche Landó
3d81e9f056 core: bump cattrs from 25.1.1 to v25.2.0 (#17183) 2025-10-01 23:13:38 +02:00
Marcelo Elizeche Landó
17682075e2 core: bump prometheus-client from 0.22.1 to v0.23.1 (#17192) 2025-10-01 23:12:08 +02:00
Marcelo Elizeche Landó
e538b88fd1 core: bump msal from 1.33.0 to v1.34.0 (#17191) 2025-10-01 23:11:13 +02:00
Marcelo Elizeche Landó
921246166a core: bump microsoft-kiota-http from 1.9.6 to v1.9.7 (#17190) 2025-10-01 23:11:06 +02:00
Marcelo Elizeche Landó
68a5e738e6 core: bump markupsafe from 3.0.2 to v3.0.3 (#17188) 2025-10-01 21:00:59 +00:00
Marcelo Elizeche Landó
ae0823741e core: bump google-auth from 2.40.3 to v2.41.1 (#17186) 2025-10-01 22:56:33 +02:00
Jens L.
2613f335c0 ci: output postgres logs for CI debugging (#17176)
* ci: output postgres logs for CI debugging

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

* group logs

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

* actually log statements

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

* stop container

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-10-01 21:27:39 +02:00
Marc 'risson' Schmitt
3d81a5dbd0 packages/django-dramatiq-postgres: typing (#16978) 2025-10-01 18:32:47 +02:00
Teffen Ellis
8f329f3b3e web: Remove brand column. (#17173) 2025-10-01 12:07:24 -04:00
dependabot[bot]
fbe5f17378 web: bump the sentry group across 1 directory with 2 updates (#17160)
Bumps the sentry group with 2 updates in the /web directory: [@sentry/browser](https://github.com/getsentry/sentry-javascript) and @spotlightjs/spotlight.


Updates `@sentry/browser` from 10.16.0 to 10.17.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.16.0...10.17.0)

Updates `@spotlightjs/spotlight` from 4.1.1 to 4.1.2

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-version: 10.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
- dependency-name: "@spotlightjs/spotlight"
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 17:55:08 +02:00
Teffen Ellis
2f3bac6b1a web: Fix Recent Events toolbar height. (#17172) 2025-10-01 17:54:50 +02:00
authentik-automation[bot]
a76bf31f67 web: bump API Client version (#17174)
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-10-01 17:23:12 +02:00
Jens L.
5c4e6a0d9f stages/user_login: add user to query (#17171)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-10-01 17:04:37 +02:00
Alexander Tereshkin
eeb5cb08cd sources: add Telegram source (#15749)
* sources: add Telegram source (#2232)

* sources/telegram: put telegram user info into policy context (#2232)

* sources/telegram: replace regular input for bot token with a "secret" one (#2232)

* sources/telegram: fix typo on Telegram source form

* sources/telegram: added UserSourceConnection/GroupSourceConnection and SourceFlowManager subclasses for Telegram source

* sources/telegram: improved code layout

* sources/telegram: collapsed migrations

* sources/telegram: fix lint errors

* sources/telegram: fixed lint errors in docs

* sources/telegram: fix app config

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

* sources/telegram: add user source settings UI so that the users can disconnect Telegram source from their account

* sources/telegram: clean up code per @risson's suggestions

* sources/telegram: improve docs based on @tanberry's suggestions

* sources/telegram: fix minor docs formatting issue

* sources/teleram: add tests for views

* sources/telegram: update serielizer field types references to be in line with convention

* sources/telegram: add missing type annotations

* sources/telegram: add check for source.enabled in the redirect view

* sources/telegram: add pre-authentication flow to telegram source

* sources: add Telegram source (#2232)

* sources/telegram: added UserSourceConnection/GroupSourceConnection and SourceFlowManager subclasses for Telegram source

* sources/telegram: collapsed migrations

* sources/telegram: fix lint errors

* sources/telegram: clean up code per @risson's suggestions

* sources/teregram: fix merge errors

* sources/telegram: improve docs wording

* Standardized documentation

* sources/telegram: added telegram source package to the list of ignored modules for mypy

* sources/telegram: fix TS lint errors

* sources/telegram: improve test coverage

* web: bump @types/node from 22.15.19 to 24.5.2 in /web (#16989)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.19 to 24.5.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.5.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: dewi-tik <dewi@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 17:03:38 +02:00
Tana M Berry
1f2d411a7c website/docs: updates to say use info not note (#17141)
* updates to say use info not note

* Update website/docs/developer-docs/docs/style-guide.mdx

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

* Update website/docs/developer-docs/docs/style-guide.mdx

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

* Update website/docs/developer-docs/docs/style-guide.mdx

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

* Update website/docs/developer-docs/docs/templates/reference.tmpl.md

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

* Update website/docs/developer-docs/docs/templates/reference.tmpl.md

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

* Update website/docs/developer-docs/docs/style-guide.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* dewi and dominic edits

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-10-01 09:51:28 -05:00
authentik-automation[bot]
c3dad275d1 core, web: update translations (#17061)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-10-01 15:17:29 +02:00
authentik-automation[bot]
709cf89985 stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#17154)
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-10-01 13:54:21 +02:00
dependabot[bot]
49070a2404 core: bump library/golang from 1.25-bookworm to 1.25.1-bookworm (#17155)
Bumps library/golang from 1.25-bookworm to 1.25.1-bookworm.

---
updated-dependencies:
- dependency-name: library/golang
  dependency-version: 1.25.1-bookworm
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:54:17 +02:00
dependabot[bot]
343ae59ece website: bump @types/react from 19.1.15 to 19.1.16 in /website (#17156)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.15 to 19.1.16.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:54:14 +02:00
dependabot[bot]
aa33384147 website: bump @types/node from 24.6.0 to 24.6.1 in /website (#17157)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.6.0 to 24.6.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:54:11 +02:00
dependabot[bot]
d02e79ab51 website: bump typescript from 5.9.2 to 5.9.3 in /website (#17158)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2 to 5.9.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:54:07 +02:00
dependabot[bot]
c5cf1653fb core: bump goauthentik.io/api/v3 from 3.2025100.14 to 3.2025100.15 (#17159)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025100.14 to 3.2025100.15.
- [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.2025100.14...v3.2025100.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:54:02 +02:00
dependabot[bot]
000fa61648 web: bump the storybook group across 1 directory with 5 updates (#17161)
Bumps the storybook group with 4 updates in the /web directory: [@storybook/addon-docs](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/docs), [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links), [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) and [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite).


Updates `@storybook/addon-docs` from 9.1.8 to 9.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.9/code/addons/docs)

Updates `@storybook/addon-links` from 9.1.8 to 9.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.9/code/addons/links)

Updates `@storybook/web-components` from 9.1.8 to 9.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.9/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 9.1.8 to 9.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.9/code/frameworks/web-components-vite)

Updates `storybook` from 9.1.8 to 9.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v9.1.9/code/core)

---
updated-dependencies:
- dependency-name: "@storybook/addon-docs"
  dependency-version: 9.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-version: 9.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-version: 9.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-version: 9.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 9.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:53:58 +02:00
dependabot[bot]
58516148c8 web: bump @goauthentik/api from 2025.10.0-rc1-1758925111 to 2025.10.0-rc1-1759234079 in /web in the goauthentik group across 1 directory (#17162)
web: bump @goauthentik/api

Bumps the goauthentik group with 1 update in the /web directory: [@goauthentik/api](https://github.com/goauthentik/authentik).


Updates `@goauthentik/api` from 2025.10.0-rc1-1758925111 to 2025.10.0-rc1-1759234079
- [Release notes](https://github.com/goauthentik/authentik/releases)
- [Commits](https://github.com/goauthentik/authentik/commits)

---
updated-dependencies:
- dependency-name: "@goauthentik/api"
  dependency-version: 2025.10.0-rc1-1759234079
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: goauthentik
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:53:55 +02:00
dependabot[bot]
6464d89a16 web: bump the react group across 2 directories with 1 update (#17163)
Bumps the react group with 1 update in the /packages/docusaurus-config directory: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).
Bumps the react group with 1 update in the /web directory: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `@types/react` from 19.1.15 to 19.1.16
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react` from 19.1.15 to 19.1.16
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: "@types/react"
  dependency-version: 19.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:53:52 +02:00
dependabot[bot]
db1afbf206 web: bump typescript from 5.9.2 to 5.9.3 in /packages/docusaurus-config (#17164)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2 to 5.9.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:53:49 +02:00
dependabot[bot]
99e83254e5 web: bump typescript from 5.9.2 to 5.9.3 in /packages/esbuild-plugin-live-reload (#17165)
web: bump typescript in /packages/esbuild-plugin-live-reload

Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2 to 5.9.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:53:45 +02:00
dependabot[bot]
54603310fb web: bump typescript from 5.9.2 to 5.9.3 in /packages/eslint-config (#17166)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2 to 5.9.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:53:42 +02:00
dependabot[bot]
4e84de4dee web: bump typescript from 5.9.2 to 5.9.3 in /packages/prettier-config (#17167)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2 to 5.9.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 13:53:39 +02:00
Teffen Ellis
b0892c4245 web: Fix default RADIUS EAP-TLS cert without license. (#17152)
* web: Fix default RADIUS EAP-TLS cert without license.

* web: Add default label.

* web: Fix enterprise only test path.
2025-10-01 07:16:21 -04:00
Teffen Ellis
1eb78ac9ae web/a11y: Brand form (#16011)
* web: Flesh out form validation.

* web: Remove required attribute.

* web: Add labels.

* web: Use screen reader friendly labels.
2025-09-30 21:43:25 +00:00
Teffen Ellis
9af3ab3215 web/a11y: Notifications drawer (#17031)
* web/a11y: Notifications drawer

* web: Add strict label selection.

* web: Add ARIA roles to API drawer, distiguish from notifications region.

* web: Fix type.
2025-09-30 21:20:57 +00:00
Teffen Ellis
fb72088b80 web: Clean up render interfaces. (#16031)
Co-Author: ken@goauthentik.io
2025-09-30 19:23:55 +00:00
Teffen Ellis
a5b1ac1a56 web/a11y: Status label (#17148)
web/a11y: status label.
2025-09-30 14:35:55 -04:00
Teffen Ellis
25d128d7cc web: Additional text field properties, ARIA fixes (#17115)
* web: Fix label ARIA.

* web: Wrap helper text on larger viewports.

* web: Add placeholder style.

* web: Fix ARIA grouping, label association.

* web: Add missing text properties. Fix ARIA association of help values.
2025-09-30 14:20:20 -04:00
Teffen Ellis
190683611c web/e2e: User creation (#17149)
* web: Match autocomplete properties.

* web: Fix label ARIA.

* web: Fix ARIA grouping, label association.

* web: Add missing text properties. Fix ARIA association of help values.

* web: Flesh out tests, ARIA selection.
2025-09-30 14:18:48 -04:00
Teffen Ellis
3f84d76eba web/a11y: Tree view (#17147)
* web/a11y: Tree view

* web: Add label.
2025-09-30 17:49:11 +00:00
Teffen Ellis
4d986aa4af web/a11y: Fix dark theme color contrast (#17144)
* web: Fix color regressions.

* web: Use Patternfly color.

* web: Remove unused.
2025-09-30 13:29:15 -04:00
Teffen Ellis
5e64335717 web: Table refresh timestamp. (#17145)
* web: Table refresh timestamp.

* web: update label.

* web: Fix contrast, types.
2025-09-30 13:09:37 -04:00
Jens L.
54e1bcb791 providers/oauth2: add ui_locales support for OIDC (#17140)
* providers/oauth2: add ui_locales support for OIDC

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

* sanitise language code

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

* account for mocked requests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-30 17:51:08 +02:00
Dewi Roberts
f83b2920e6 website/integrations: convert all note boxes to info boxes (#17139)
Replace all note boxes with info boxes
2025-09-30 11:35:51 -04:00
Dewi Roberts
da69b6d716 website/docs: replaces all note boxes with info boxes (#17138)
Replaces all note boxes with info boxes
2025-09-30 11:34:08 -04:00
Dominic R
334c6d1c09 website/docs: developer docs: adjust sentence for writing docs (#17137)
As per Tana's request

Signed-off-by: Dominic R <dominic@sdko.org>
2025-09-30 13:38:54 +00:00
Dominic R
0b667c8019 core: Add input validation for service account creation (#16964)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-30 14:07:41 +02:00
dependabot[bot]
4bceac1757 website: bump @types/node from 24.5.2 to 24.6.0 in /website (#17126)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.5.2 to 24.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:04:03 +02:00
dependabot[bot]
e9ca1643ee ci: bump actions/setup-node from 4 to 5 (#17123)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:01:10 +02:00
dependabot[bot]
3b77e243b0 website: bump the build group in /website with 6 updates (#17124)
Bumps the build group in /website with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@rspack/binding-darwin-arm64](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.5.7` | `1.5.8` |
| [@rspack/binding-linux-arm64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.5.7` | `1.5.8` |
| [@rspack/binding-linux-x64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.5.7` | `1.5.8` |
| [lightningcss-darwin-arm64](https://github.com/parcel-bundler/lightningcss) | `1.30.1` | `1.30.2` |
| [lightningcss-linux-arm64-gnu](https://github.com/parcel-bundler/lightningcss) | `1.30.1` | `1.30.2` |
| [lightningcss-linux-x64-gnu](https://github.com/parcel-bundler/lightningcss) | `1.30.1` | `1.30.2` |


Updates `@rspack/binding-darwin-arm64` from 1.5.7 to 1.5.8
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.5.8/packages/rspack)

Updates `@rspack/binding-linux-arm64-gnu` from 1.5.7 to 1.5.8
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.5.8/packages/rspack)

Updates `@rspack/binding-linux-x64-gnu` from 1.5.7 to 1.5.8
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.5.8/packages/rspack)

Updates `lightningcss-darwin-arm64` from 1.30.1 to 1.30.2
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.30.1...v1.30.2)

Updates `lightningcss-linux-arm64-gnu` from 1.30.1 to 1.30.2
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.30.1...v1.30.2)

Updates `lightningcss-linux-x64-gnu` from 1.30.1 to 1.30.2
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.30.1...v1.30.2)

---
updated-dependencies:
- dependency-name: "@rspack/binding-darwin-arm64"
  dependency-version: 1.5.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-arm64-gnu"
  dependency-version: 1.5.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-x64-gnu"
  dependency-version: 1.5.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: lightningcss-darwin-arm64
  dependency-version: 1.30.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: lightningcss-linux-arm64-gnu
  dependency-version: 1.30.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: lightningcss-linux-x64-gnu
  dependency-version: 1.30.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:01:07 +02:00
dependabot[bot]
46cfa471f4 website: bump the eslint group in /website with 3 updates (#17125)
Bumps the eslint group in /website with 3 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@typescript-eslint/eslint-plugin` from 8.44.1 to 8.45.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.44.1 to 8.45.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/parser)

Updates `typescript-eslint` from 8.44.1 to 8.45.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: typescript-eslint
  dependency-version: 8.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:01:03 +02:00
dependabot[bot]
e48cfec1b4 web: bump @sentry/browser from 10.15.0 to 10.16.0 in /web in the sentry group across 1 directory (#17127)
web: bump @sentry/browser in /web in the sentry group across 1 directory

Bumps the sentry group with 1 update in the /web directory: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 10.15.0 to 10.16.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/10.15.0...10.16.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-version: 10.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:00:59 +02:00
dependabot[bot]
47c09c30c6 web: bump the eslint group across 2 directories with 3 updates (#17128)
Bumps the eslint group with 1 update in the /packages/eslint-config directory: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).
Bumps the eslint group with 1 update in the /web directory: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `typescript-eslint` from 8.44.1 to 8.45.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/typescript-eslint)

Updates `typescript-eslint` from 8.44.1 to 8.45.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/typescript-eslint)

Updates `@typescript-eslint/eslint-plugin` from 8.44.1 to 8.45.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.44.1 to 8.45.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/parser)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.45.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: typescript-eslint
  dependency-version: 8.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:00:56 +02:00
dependabot[bot]
639ad8cc94 lifecycle/aws: bump cross-env from 10.0.0 to 10.1.0 in /lifecycle/aws (#17130)
Bumps [cross-env](https://github.com/kentcdodds/cross-env) from 10.0.0 to 10.1.0.
- [Release notes](https://github.com/kentcdodds/cross-env/releases)
- [Changelog](https://github.com/kentcdodds/cross-env/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kentcdodds/cross-env/compare/v10.0.0...v10.1.0)

---
updated-dependencies:
- dependency-name: cross-env
  dependency-version: 10.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:00:52 +02:00
dependabot[bot]
f8a8b70df8 web: bump pino from 9.11.0 to 9.12.0 in /packages/esbuild-plugin-live-reload (#17131)
web: bump pino in /packages/esbuild-plugin-live-reload

Bumps [pino](https://github.com/pinojs/pino) from 9.11.0 to 9.12.0.
- [Release notes](https://github.com/pinojs/pino/releases)
- [Commits](https://github.com/pinojs/pino/compare/v9.11.0...v9.12.0)

---
updated-dependencies:
- dependency-name: pino
  dependency-version: 9.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:00:49 +02:00
dependabot[bot]
973bc3898a web: bump @types/node from 24.5.2 to 24.6.0 in /packages/esbuild-plugin-live-reload (#17132)
web: bump @types/node in /packages/esbuild-plugin-live-reload

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.5.2 to 24.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:00:45 +02:00
dependabot[bot]
b5230fcb2d web: bump @types/node from 24.5.2 to 24.6.0 in /packages/prettier-config (#17133)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.5.2 to 24.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:00:41 +02:00
dependabot[bot]
6f38eaa1cd web: bump @types/node from 22.15.19 to 24.6.0 in /web (#17134)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.19 to 24.6.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 13:00:37 +02:00
Jens L.
48a7a707fd root: fix rustup error during build when buildcache version is outdated (#17121)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-30 13:00:06 +02:00
Teffen Ellis
78b156d149 web/a11y: Table header -- Search input (#17117)
* web: Fix pagination jitter during loading.

* web: Fix issues surrounding search input ARIA, alignment, etc.
2025-09-30 06:39:23 -04:00
Teffen Ellis
99b3daf46a web: Fix table child alignment (#17114)
* web: Reduce cut off.

* web: Fix issue where inherited style causes modal issues.
2025-09-30 06:38:33 -04:00
Teffen Ellis
eb739ad4d7 web/a11y: Table header -- Fix pagination jitter, prepare alignment (#17116)
web: Fix pagination jitter during loading.
2025-09-30 12:36:33 +02:00
Teffen Ellis
eb5045b809 web: Fix native icon colors when using dark theme. (#17118) 2025-09-30 12:34:27 +02:00
Teffen Ellis
dd9ac5f838 web: Apply consistent background color when input is disabled or readonly. (#17105) 2025-09-29 23:25:54 +02:00
Jens L.
8107338742 website/docs: 2025.8.4 release notes (#17119)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-29 23:22:28 +02:00
Marc 'risson' Schmitt
cfb126eaad web: revert bump the swc group across 1 directory with 11 updates (#17113)
Revert "web: bump the swc group across 1 directory with 11 updates (#17079)"

This reverts commit 38020de4f1.
2025-09-29 22:39:48 +02:00
Marc 'risson' Schmitt
c65060b3d0 ci: fix node version in docker image build (#17110) 2025-09-29 18:28:44 +00:00
transifex-integration[bot]
79fc574980 translate: Updates for file web/xliff/en.xlf in pt_BR (#17111)
* Translate web/xliff/en.xlf in pt_BR

100% translated source file: 'web/xliff/en.xlf'
on 'pt_BR'.

* Translate web/xliff/en.xlf in pt_BR

100% translated source file: 'web/xliff/en.xlf'
on 'pt_BR'.

* Translate web/xliff/en.xlf in pt_BR

100% translated source file: 'web/xliff/en.xlf'
on 'pt_BR'.

* Translate web/xliff/en.xlf in pt_BR

100% translated source file: 'web/xliff/en.xlf'
on 'pt_BR'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-09-29 18:22:52 +00:00
Marc 'risson' Schmitt
a87f182503 tasks: reduce default number of retries and max backoff (#17107)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-29 17:17:23 +00:00
Marc 'risson' Schmitt
0dba78a757 packages/django-dramatiq-postgres: broker: fix new messages not being picked up when too many messages are waiting (#17106)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-29 18:54:03 +02:00
boesr
bb8c007e63 website/docs: additional documentation for ak_user_by (#17098)
* adds additional documentation to search for user by attribute

* changes attribute naming in expression example

Co-authored-by: Jens L. <jens@beryju.org>
Signed-off-by: boesr <88541074+boesr@users.noreply.github.com>

* Adjusts ak_user_by to style guide

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: boesr <88541074+boesr@users.noreply.github.com>

* Update website/docs/expressions/_functions.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: boesr <88541074+boesr@users.noreply.github.com>

---------

Signed-off-by: boesr <88541074+boesr@users.noreply.github.com>
Co-authored-by: Jens L. <jens@beryju.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-09-29 10:49:23 -04:00
Jens L.
5cdd4d6d54 stages/identification: fix mismatched error messages (#17090)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-29 16:24:18 +02:00
Jens L.
e0f754c789 providers/oauth2: fix authentication error with identical app passwords (#17100)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-29 16:22:47 +02:00
transifex-integration[bot]
8be7a035d5 translate: Updates for file web/xliff/en.xlf in de (#17099)
Translate web/xliff/en.xlf in de

100% translated source file: 'web/xliff/en.xlf'
on 'de'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-09-29 14:05:29 +00:00
transifex-integration[bot]
ffef94dcc2 translate: Updates for file locale/en/LC_MESSAGES/django.po in de (#17096)
Translate locale/en/LC_MESSAGES/django.po in de

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'de'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-09-29 15:21:05 +02:00
dependabot[bot]
8ba0ccac48 core: bump goauthentik.io/api/v3 from 3.2025100.11 to 3.2025100.14 (#17071)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025100.11 to 3.2025100.14.
- [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.2025100.11...v3.2025100.14)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-version: 3.2025100.14
  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-09-29 15:20:19 +02:00
dependabot[bot]
90b1f483d1 website: bump @types/react from 19.1.13 to 19.1.15 in /website (#17075)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.13 to 19.1.15.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.15
  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-09-29 15:19:07 +02:00
Dominic R
3e587560eb website/integrations: add cloudflare access redirect (#17094)
Signed-off-by: Dominic R <dominic@sdko.org>
2025-09-29 08:51:21 -04:00
Marc 'risson' Schmitt
f92abbf291 cmd/server/healthcheck: info log success instead of debug (#17093) 2025-09-29 12:48:38 +00:00
Dominic R
e0917490e3 website/integrations: cloudflare (#17039)
* wip

* e

* codereview

co-authored-by: dewi <dewi@goauthentik.io>

* Update website/integrations/security/cloudflare-access/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/security/cloudflare-access/index.md

Signed-off-by: Dominic R <dominic@sdko.org>

* lintfix

* Revert "lintfix"

This reverts commit b7643f4e8a.

* lintfix?????????

* Apply suggestion from @tanberry

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Apply suggestion from @tanberry

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Apply suggestion from @tanberry

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

* Apply suggestion from @tanberry

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: dewi <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-09-29 08:18:37 -04:00
Marc 'risson' Schmitt
30698778c3 rbac: optimize rbac assigned by users query (#17015) 2025-09-29 14:12:40 +02:00
Jérôme W.
922f01d7de web: Fix layout class for 'row' in LibraryPage (#16752)
Fix layout class for 'row' in LibraryPage

Signed-off-by: Jérôme W. <jerome@wnetworks.org>
2025-09-29 14:11:24 +02:00
authentik-automation[bot]
8a1b6c8b07 *: Auto compress images (#16733)
[create-pull-request] automated change

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dewi-tik <204862582+dewi-tik@users.noreply.github.com>
2025-09-29 13:39:15 +02:00
dependabot[bot]
e6d9293fea core: bump github.com/go-openapi/runtime from 0.28.0 to 0.29.0 (#17072)
Bumps [github.com/go-openapi/runtime](https://github.com/go-openapi/runtime) from 0.28.0 to 0.29.0.
- [Release notes](https://github.com/go-openapi/runtime/releases)
- [Commits](https://github.com/go-openapi/runtime/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: github.com/go-openapi/runtime
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:38:33 +02:00
dependabot[bot]
abc42d6f6d website: bump the build group in /website with 6 updates (#17076)
Bumps the build group in /website with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@swc/core-darwin-arm64](https://github.com/swc-project/swc) | `1.13.19` | `1.13.20` |
| [@swc/core-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.13.19` | `1.13.20` |
| [@swc/core-linux-x64-gnu](https://github.com/swc-project/swc) | `1.13.19` | `1.13.20` |
| [@swc/html-darwin-arm64](https://github.com/swc-project/swc) | `1.13.19` | `1.13.20` |
| [@swc/html-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.13.19` | `1.13.20` |
| [@swc/html-linux-x64-gnu](https://github.com/swc-project/swc) | `1.13.19` | `1.13.20` |


Updates `@swc/core-darwin-arm64` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-linux-arm64-gnu` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-linux-x64-gnu` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/html-darwin-arm64` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/html-linux-arm64-gnu` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/html-linux-x64-gnu` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

---
updated-dependencies:
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-darwin-arm64"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-arm64-gnu"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-x64-gnu"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:38:28 +02:00
dependabot[bot]
5807d86d20 web: bump @spotlightjs/spotlight from 4.0.0 to 4.1.1 in /web in the sentry group across 1 directory (#17077)
web: bump @spotlightjs/spotlight

Bumps the sentry group with 1 update in the /web directory: @spotlightjs/spotlight.


Updates `@spotlightjs/spotlight` from 4.0.0 to 4.1.1

---
updated-dependencies:
- dependency-name: "@spotlightjs/spotlight"
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:38:25 +02:00
dependabot[bot]
f1ba6f6786 web: bump the rollup group across 1 directory with 4 updates (#17078)
Bumps the rollup group with 4 updates in the /web directory: [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup), [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup), [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup) and [rollup](https://github.com/rollup/rollup).


Updates `@rollup/rollup-darwin-arm64` from 4.52.2 to 4.52.3
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.2...v4.52.3)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.52.2 to 4.52.3
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.2...v4.52.3)

Updates `@rollup/rollup-linux-x64-gnu` from 4.52.2 to 4.52.3
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.2...v4.52.3)

Updates `rollup` from 4.52.2 to 4.52.3
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.2...v4.52.3)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-version: 4.52.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-version: 4.52.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-version: 4.52.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: rollup
  dependency-version: 4.52.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:38:21 +02:00
dependabot[bot]
38020de4f1 web: bump the swc group across 1 directory with 11 updates (#17079)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc).


Updates `@swc/core` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-darwin-arm64` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-darwin-x64` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-linux-arm-gnueabihf` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-linux-arm64-gnu` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-linux-arm64-musl` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-linux-x64-gnu` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-linux-x64-musl` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-win32-arm64-msvc` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-win32-ia32-msvc` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

Updates `@swc/core-win32-x64-msvc` from 1.13.19 to 1.13.20
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.19...v1.13.20)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-version: 1.13.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:38:17 +02:00
dependabot[bot]
ba21c9a417 web: bump the react group across 2 directories with 1 update (#17083)
Bumps the react group with 1 update in the /packages/docusaurus-config directory: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).
Bumps the react group with 1 update in the /web directory: [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `@types/react` from 19.1.13 to 19.1.15
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react` from 19.1.13 to 19.1.15
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: "@types/react"
  dependency-version: 19.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:38:14 +02:00
dependabot[bot]
860598fc61 web: bump knip from 5.64.0 to 5.64.1 in /web (#17084)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 5.64.0 to 5.64.1.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Changelog](https://github.com/webpro-nl/knip/blob/main/packages/knip/.release-it.json)
- [Commits](https://github.com/webpro-nl/knip/commits/5.64.1/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 5.64.1
  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-09-29 13:38:10 +02:00
dependabot[bot]
a2d5c652b8 web: bump pino from 9.11.0 to 9.12.0 in /web (#17085)
Bumps [pino](https://github.com/pinojs/pino) from 9.11.0 to 9.12.0.
- [Release notes](https://github.com/pinojs/pino/releases)
- [Commits](https://github.com/pinojs/pino/compare/v9.11.0...v9.12.0)

---
updated-dependencies:
- dependency-name: pino
  dependency-version: 9.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:38:06 +02:00
dependabot[bot]
fd731c23bf core: bump axllent/mailpit from v1.27.8 to v1.27.9 in /tests/e2e (#17086)
Bumps axllent/mailpit from v1.27.8 to v1.27.9.

---
updated-dependencies:
- dependency-name: axllent/mailpit
  dependency-version: v1.27.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 13:38:03 +02:00
Jens L.
68292fede2 enterprise/stages/mtls: Improve Email address extraction (#17068)
* enterprise/stages/mtls: improve email attribute extraction

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

* return error from outpost flow executor correctly

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-28 19:28:52 +02:00
Jens L.
dce25e3fc1 web/admin: fix federation sources automatically selected (#17069)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-28 19:28:43 +02:00
Jens L.
b2c6ec284c tasks: fix errors found in tests (#17062)
fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-27 03:47:22 +02:00
Jens L.
1790c7efed tasks: fix logger name (#17009)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-27 01:48:42 +02:00
authentik-automation[bot]
44a04705e3 web: bump API Client version (#17058)
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-09-26 22:45:15 +00:00
Jens L.
1028c962c7 providers/oauth2: only issue new refresh token if old one is about to expire (#16905)
* providers/oauth2: only issue new refresh token if old one is about to expire

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

* make configurable

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

* tests and fixes

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

* sigh

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-27 00:18:14 +02:00
Jens L.
1c30c16c35 ci: cherry-pick branch in folder, include target branch in title (#17054)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-26 18:45:01 +02:00
Connor Peshek
87a28d63ed sources/saml: add location selection for Signature node (#15626)
* sources/saml: add location selection for Signature node

---------

Signed-off-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: connor <connor@connors-MacBook-Pro.local>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: connor peshek <connorpeshek@connors-MacBook-Pro.local>
Co-authored-by: Katsushi Kobayashi < ikob@acm.org>
2025-09-26 11:07:51 -05:00
Jens L.
8c635ebb02 */bindings: order by pk (#17027)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-26 17:53:54 +02:00
authentik-automation[bot]
85e9803da8 core, web: update translations (#17036)
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-09-26 16:04:16 +02:00
authentik-automation[bot]
1db0ba1cc1 web: bump API Client version (#17048)
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-09-26 15:04:06 +02:00
Jens L.
b3e9c46cf4 tests/e2e: less hardcoded names (#17047)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-09-26 14:55:16 +02:00
Jens L.
4ec785a598 core/api: Better naming for partial user/group serializer, optimise bindings (#17022)
* core: add index on Group.is_superuser (#17011)

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

* update go code

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

* also optimise bindings

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

* typo

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

* remove unused

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-09-26 14:43:39 +02:00
dependabot[bot]
d4e5ee4bf5 core: bump goauthentik.io/api/v3 from 3.2025100.10 to 3.2025100.11 (#17040)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2025100.10 to 3.2025100.11.
- [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.2025100.10...v3.2025100.11)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-version: 3.2025100.11
  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-09-26 14:23:56 +02:00
dependabot[bot]
3f87279535 website: bump the build group in /website with 6 updates (#17042)
Bumps the build group in /website with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@swc/core-darwin-arm64](https://github.com/swc-project/swc) | `1.13.5` | `1.13.19` |
| [@swc/core-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.13.5` | `1.13.19` |
| [@swc/core-linux-x64-gnu](https://github.com/swc-project/swc) | `1.13.5` | `1.13.19` |
| [@swc/html-darwin-arm64](https://github.com/swc-project/swc) | `1.13.8` | `1.13.19` |
| [@swc/html-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.13.8` | `1.13.19` |
| [@swc/html-linux-x64-gnu](https://github.com/swc-project/swc) | `1.13.8` | `1.13.19` |


Updates `@swc/core-darwin-arm64` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-linux-arm64-gnu` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-linux-x64-gnu` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/html-darwin-arm64` from 1.13.8 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.8...v1.13.19)

Updates `@swc/html-linux-arm64-gnu` from 1.13.8 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.8...v1.13.19)

Updates `@swc/html-linux-x64-gnu` from 1.13.8 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.8...v1.13.19)

---
updated-dependencies:
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-darwin-arm64"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-arm64-gnu"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-x64-gnu"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-26 14:23:47 +02:00
dependabot[bot]
5fe0de5267 web: bump the swc group across 1 directory with 11 updates (#17043)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc).


Updates `@swc/core` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-darwin-arm64` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-darwin-x64` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-linux-arm-gnueabihf` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-linux-arm64-gnu` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-linux-arm64-musl` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-linux-x64-gnu` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-linux-x64-musl` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-win32-arm64-msvc` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-win32-ia32-msvc` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

Updates `@swc/core-win32-x64-msvc` from 1.13.5 to 1.13.19
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.13.5...v1.13.19)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-version: 1.13.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-26 14:23:10 +02:00
Dominic R
8a0e14b3bb website/integrations: Move Cloudflare Access Documentation. (#17038)
No other changes were made except moving this documentation. Cloudflare Access is authentication, not networking related, so it's logical to move it to this category.
2025-09-26 07:09:02 -04:00
660 changed files with 24300 additions and 4804 deletions

View File

@@ -179,7 +179,7 @@ runs:
fi
# Create a unique branch name for the cherry-pick
CHERRY_PICK_BRANCH="cherry-pick-${PR_NUMBER}-to-${TARGET_BRANCH}"
CHERRY_PICK_BRANCH="cherry-pick/${PR_NUMBER}-to-${TARGET_BRANCH}"
# Check if a cherry-pick PR already exists
EXISTING_PR=$(gh pr list --head "$CHERRY_PICK_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
@@ -201,7 +201,7 @@ runs:
git push origin "$CHERRY_PICK_BRANCH"
# Create PR for the cherry-pick
CHERRY_PICK_TITLE="$PR_TITLE (cherry-pick #$PR_NUMBER)"
CHERRY_PICK_TITLE="$PR_TITLE (cherry-pick #$PR_NUMBER to $TARGET_BRANCH)"
CHERRY_PICK_BODY="Cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\` branch.
**Original PR:** #$PR_NUMBER
@@ -236,7 +236,7 @@ runs:
git push origin "$CHERRY_PICK_BRANCH"
# Create PR with conflict notice
CONFLICT_TITLE="$PR_TITLE (backport of #$PR_NUMBER)"
CONFLICT_TITLE="$PR_TITLE (cherry-pick #$PR_NUMBER to $TARGET_BRANCH)"
CONFLICT_BODY="⚠️ **This cherry-pick has conflicts that require manual resolution.**
Cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\` branch.

View File

@@ -3,6 +3,7 @@ services:
image: docker.io/library/postgres:${PSQL_TAG:-16}
volumes:
- db-data:/var/lib/postgresql/data
command: "-c log_statement=all"
environment:
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"

28
.github/actions/test-results/action.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: "Process test results"
description: Convert test results to JUnit, add them to GitHub Actions and codecov
inputs:
flags:
description: Codecov flags
runs:
using: "composite"
steps:
- uses: codecov/codecov-action@v5
with:
flags: ${{ inputs.flags }}
use_oidc: true
- uses: codecov/test-results-action@v1
with:
flags: ${{ inputs.flags }}
file: unittest.xml
use_oidc: true
- name: PostgreSQL Logs
shell: bash
run: |
if [[ $ACTIONS_RUNNER_DEBUG == 'true' || $ACTIONS_STEP_DEBUG == 'true' ]]; then
docker stop setup-postgresql-1
echo "::group::PostgreSQL Logs"
docker logs setup-postgresql-1
echo "::endgroup::"
fi

View File

@@ -72,6 +72,13 @@ jobs:
run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: Setup node
if: ${{ !inputs.release }}
uses: actions/setup-node@v5
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: generate ts client
if: ${{ !inputs.release }}
run: make gen-client-ts

View File

@@ -113,6 +113,10 @@ jobs:
CI_TOTAL_RUNS: "5"
run: |
uv run make ci-test
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
flags: unit-migrate
test-unittest:
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
runs-on: ubuntu-latest
@@ -139,17 +143,10 @@ jobs:
CI_TOTAL_RUNS: "5"
run: |
uv run make ci-test
- if: ${{ always() }}
uses: codecov/codecov-action@v5
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
flags: unit
use_oidc: true
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: unit
file: unittest.xml
use_oidc: true
test-integration:
runs-on: ubuntu-latest
timeout-minutes: 30
@@ -163,17 +160,10 @@ jobs:
run: |
uv run coverage run manage.py test tests/integration
uv run coverage xml
- if: ${{ always() }}
uses: codecov/codecov-action@v5
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
flags: integration
use_oidc: true
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: integration
file: unittest.xml
use_oidc: true
test-e2e:
name: test-e2e (${{ matrix.job.name }})
runs-on: ubuntu-latest
@@ -222,17 +212,10 @@ jobs:
run: |
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage xml
- if: ${{ always() }}
uses: codecov/codecov-action@v5
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
flags: e2e
use_oidc: true
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: e2e
file: unittest.xml
use_oidc: true
ci-core-mark:
if: always()
needs:

View File

@@ -20,14 +20,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Find Comment
uses: peter-evans/find-comment@v3
uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: authentik translations instructions
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v4
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}

View File

@@ -24,6 +24,7 @@ Makefile @goauthentik/infrastructure
.editorconfig @goauthentik/infrastructure
CODEOWNERS @goauthentik/infrastructure
# Backend packages
packages/django-postgres-cache @goauthentik/backend
packages/django-dramatiq-postgres @goauthentik/backend
# Web packages
packages/docusaurus-config @goauthentik/frontend

View File

@@ -26,7 +26,7 @@ RUN npm run build && \
npm run build:sfe
# Stage 2: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25-bookworm AS go-builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.1-bookworm AS go-builder
ARG TARGETOS
ARG TARGETARCH
@@ -119,7 +119,11 @@ RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/v
libltdl-dev && \
curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec" \
# https://github.com/rust-lang/rustup/issues/2949
# Fixes issues where the rust version in the build cache is older than latest
# and rustup tries to update it, which fails
RUSTUP_PERMIT_COPY_RENAME="true"
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=bind,target=uv.lock,src=uv.lock \

View File

@@ -0,0 +1,9 @@
from django.dispatch import receiver
from authentik.admin.tasks import _set_prom_info
from authentik.root.signals import post_startup
@receiver(post_startup)
def post_startup_admin_metrics(sender, **_):
_set_prom_info()

View File

@@ -2,7 +2,6 @@
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq import actor
from packaging.version import parse
from requests import RequestException
@@ -13,7 +12,7 @@ from authentik.admin.apps import PROM_INFO
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_http_session
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
VERSION_NULL = "0.0.0"
@@ -35,7 +34,7 @@ def _set_prom_info():
@actor(description=_("Update latest version info."))
def update_latest_version():
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
if CONFIG.get_bool("disable_update_check"):
cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT)
self.info("Version check disabled.")
@@ -72,6 +71,3 @@ def update_latest_version():
except (RequestException, IndexError) as exc:
cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT)
raise exc
_set_prom_info()

View File

@@ -1,5 +1,8 @@
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
from collections.abc import Callable
from typing import Any
from django.utils.translation import gettext_lazy as _
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.plumbing import (
@@ -8,6 +11,7 @@ from drf_spectacular.plumbing import (
build_basic_type,
build_object_type,
)
from drf_spectacular.renderers import OpenApiJsonRenderer
from drf_spectacular.settings import spectacular_settings
from drf_spectacular.types import OpenApiTypes
from rest_framework.settings import api_settings
@@ -15,34 +19,28 @@ from rest_framework.settings import api_settings
from authentik.api.apps import AuthentikAPIConfig
from authentik.api.pagination import PAGINATION_COMPONENT_NAME, PAGINATION_SCHEMA
def build_standard_type(obj, **kwargs):
"""Build a basic type with optional add owns."""
schema = build_basic_type(obj)
schema.update(kwargs)
return schema
GENERIC_ERROR = build_object_type(
description=_("Generic API Error"),
properties={
"detail": build_standard_type(OpenApiTypes.STR),
"code": build_standard_type(OpenApiTypes.STR),
"detail": build_basic_type(OpenApiTypes.STR),
"code": build_basic_type(OpenApiTypes.STR),
},
required=["detail"],
)
VALIDATION_ERROR = build_object_type(
description=_("Validation Error"),
properties={
api_settings.NON_FIELD_ERRORS_KEY: build_array_type(build_standard_type(OpenApiTypes.STR)),
"code": build_standard_type(OpenApiTypes.STR),
api_settings.NON_FIELD_ERRORS_KEY: build_array_type(build_basic_type(OpenApiTypes.STR)),
"code": build_basic_type(OpenApiTypes.STR),
},
required=[],
additionalProperties={},
)
def create_component(generator: SchemaGenerator, name, schema, type_=ResolvedComponent.SCHEMA):
def create_component(
generator: SchemaGenerator, name: str, schema: Any, type_=ResolvedComponent.SCHEMA
) -> ResolvedComponent:
"""Register a component and return a reference to it."""
component = ResolvedComponent(
name=name,
@@ -54,7 +52,18 @@ def create_component(generator: SchemaGenerator, name, schema, type_=ResolvedCom
return component
def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
"""Filter out all API Views which are not mounted under /api"""
return [
(path, path_regex, method, callback)
for path, path_regex, method, callback in endpoints
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
]
def postprocess_schema_responses(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Workaround to set a default response for endpoints.
Workaround suggested at
<https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357>
@@ -104,7 +113,11 @@ def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
return result
def postprocess_schema_pagination(result, generator: SchemaGenerator, **kwargs):
def postprocess_schema_pagination(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
declare them globally and refer to them"""
to_replace = {
"ordering": create_component(
generator,
@@ -157,19 +170,24 @@ def postprocess_schema_pagination(result, generator: SchemaGenerator, **kwargs):
}
for path in result["paths"].values():
for method in path.values():
# print(method["parameters"])
for idx, param in enumerate(method.get("parameters", [])):
for replace_name, replace_ref in to_replace.items():
if param["name"] == replace_name:
method["parameters"][idx] = replace_ref.ref
# print(method["parameters"])
return result
def preprocess_schema_exclude_non_api(endpoints, **kwargs):
"""Filter out all API Views which are not mounted under /api"""
return [
(path, path_regex, method, callback)
for path, path_regex, method, callback in endpoints
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
]
def postprocess_schema_remove_unused(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Remove unused components"""
# To check if the schema is used, render it to JSON and then substring check that
# less efficient than walking through the tree but a lot simpler and no
# possibility that we miss something
raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
for key in result["components"][ResolvedComponent.SCHEMA].keys():
if raw.count(key) > 1:
continue
del generator.registry._components[(key, ResolvedComponent.SCHEMA)]
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
return result

View File

@@ -12,7 +12,7 @@ from django.db import DatabaseError, InternalError, ProgrammingError
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask, CurrentTaskNotFound
from django_dramatiq_postgres.middleware import CurrentTaskNotFound
from dramatiq.actor import actor
from dramatiq.middleware import Middleware
from structlog.stdlib import get_logger
@@ -39,6 +39,7 @@ from authentik.events.logs import capture_logs
from authentik.events.utils import sanitize_dict
from authentik.lib.config import CONFIG
from authentik.tasks.apps import PRIORITY_HIGH
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task
from authentik.tasks.schedules.models import Schedule
from authentik.tenants.models import Tenant
@@ -155,7 +156,7 @@ def blueprints_find() -> list[BlueprintFile]:
throws=(DatabaseError, ProgrammingError, InternalError),
)
def blueprints_discovery(path: str | None = None):
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
count = 0
for blueprint in blueprints_find():
if path and blueprint.path != path:
@@ -195,7 +196,7 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
@actor(description=_("Apply single blueprint."))
def apply_blueprint(instance_pk: UUID):
try:
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
except CurrentTaskNotFound:
self = Task()
self.set_uid(str(instance_pk))

View File

@@ -29,8 +29,8 @@ from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
class GroupMemberSerializer(ModelSerializer):
"""Stripped down user serializer to show relevant users for groups"""
class PartialUserSerializer(ModelSerializer):
"""Partial User Serializer, does not include child relations."""
attributes = JSONDictField(required=False)
uid = CharField(read_only=True)
@@ -94,11 +94,11 @@ class GroupSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_children", "false")).lower() == "true"
@extend_schema_field(GroupMemberSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
@extend_schema_field(PartialUserSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[PartialUserSerializer] | None:
if not self._should_include_users:
return None
return GroupMemberSerializer(instance.users, many=True).data
return PartialUserSerializer(instance.users, many=True).data
@extend_schema_field(GroupChildSerializer(many=True))
def get_children_obj(self, instance: Group) -> list[GroupChildSerializer] | None:

View File

@@ -97,8 +97,8 @@ class ParamUserSerializer(PassiveSerializer):
user = PrimaryKeyRelatedField(queryset=User.objects.all().exclude_anonymous(), required=False)
class UserGroupSerializer(ModelSerializer):
"""Simplified Group Serializer for user's groups"""
class PartialGroupSerializer(ModelSerializer):
"""Partial Group Serializer, does not include child relations."""
attributes = JSONDictField(required=False)
parent_name = CharField(source="parent.name", read_only=True, allow_null=True)
@@ -143,11 +143,11 @@ class UserSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_groups", "true")).lower() == "true"
@extend_schema_field(UserGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[UserGroupSerializer] | None:
@extend_schema_field(PartialGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[PartialGroupSerializer] | None:
if not self._should_include_groups:
return None
return UserGroupSerializer(instance.ak_groups, many=True).data
return PartialGroupSerializer(instance.ak_groups, many=True).data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -334,6 +334,21 @@ class UserPasswordSetSerializer(PassiveSerializer):
password = CharField(required=True)
class UserServiceAccountSerializer(PassiveSerializer):
"""Payload to create a service account"""
name = CharField(
required=True,
validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))],
)
create_group = BooleanField(default=False)
expiring = BooleanField(default=True)
expires = DateTimeField(
required=False,
help_text="If not provided, valid for 360 days",
)
class UsersFilter(FilterSet):
"""Filter for users"""
@@ -494,18 +509,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@permission_required(None, ["authentik_core.add_user", "authentik_core.add_token"])
@extend_schema(
request=inline_serializer(
"UserServiceAccountSerializer",
{
"name": CharField(required=True),
"create_group": BooleanField(default=False),
"expiring": BooleanField(default=True),
"expires": DateTimeField(
required=False,
help_text="If not provided, valid for 360 days",
),
},
),
request=UserServiceAccountSerializer,
responses={
200: inline_serializer(
"UserServiceAccountResponse",
@@ -527,11 +531,12 @@ class UserViewSet(UsedByMixin, ModelViewSet):
)
def service_account(self, request: Request) -> Response:
"""Create a new user account that is marked as a service account"""
username = request.data.get("name")
create_group = request.data.get("create_group", False)
expiring = request.data.get("expiring", True)
expires = request.data.get("expires", now() + timedelta(days=360))
data = UserServiceAccountSerializer(data=request.data)
data.is_valid(raise_exception=True)
expires = data.validated_data.get("expires", now() + timedelta(days=360))
username = data.validated_data["name"]
expiring = data.validated_data["expiring"]
with atomic():
try:
user: User = User.objects.create(
@@ -549,10 +554,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"user_uid": user.uid,
"user_pk": user.pk,
}
if create_group and self.request.user.has_perm("authentik_core.add_group"):
group = Group.objects.create(
name=username,
)
if data.validated_data["create_group"] and self.request.user.has_perm(
"authentik_core.add_group"
):
group = Group.objects.create(name=username)
group.users.add(user)
response["group_pk"] = str(group.pk)
token = Token.objects.create(
@@ -565,7 +570,29 @@ class UserViewSet(UsedByMixin, ModelViewSet):
response["token"] = token.key
return Response(response)
except IntegrityError as exc:
return Response(data={"non_field_errors": [str(exc)]}, status=400)
error_msg = str(exc).lower()
if "unique" in error_msg:
return Response(
data={
"non_field_errors": [
_("A user/group with these details already exists")
]
},
status=400,
)
else:
LOGGER.warning("Service account creation failed", exc=exc)
return Response(
data={"non_field_errors": [_("Unable to create user")]},
status=400,
)
except (ValueError, TypeError) as exc:
LOGGER.error("Unexpected error during service account creation", exc=exc)
return Response(
data={"non_field_errors": [_("Unknown error occurred")]},
status=500,
)
@extend_schema(responses={200: SessionUserSerializer(many=False)})
@action(
@@ -719,7 +746,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
return Response(status=204)
@extend_schema(
request=OpenApiTypes.NONE,
request=None,
responses={
"204": OpenApiResponse(description="Successfully ended impersonation"),
},

View File

@@ -13,14 +13,6 @@ import authentik.core.models
import authentik.lib.models
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
session_keys = cache.keys(KEY_PREFIX + "*")
cache.delete_many(session_keys)
def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Token = apps.get_model("authentik_core", "token")
@@ -151,9 +143,6 @@ class Migration(migrations.Migration):
"abstract": False,
},
),
migrations.RunPython(
code=migrate_sessions,
),
migrations.AlterField(
model_name="application",
name="meta_launch_url",

View File

@@ -7,15 +7,10 @@ from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_K
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.utils.timezone import now, timedelta
from authentik.lib.migrations import progress_bar
from authentik.root.middleware import ClientIPMiddleware
SESSION_CACHE_ALIAS = "default"
class PickleSerializer:
"""
Simple wrapper around pickle to be used in signing.dumps()/loads() and
@@ -83,27 +78,6 @@ def _migrate_session(
)
def migrate_redis_sessions(apps, schema_editor):
from django.core.cache import caches
db_alias = schema_editor.connection.alias
cache = caches[SESSION_CACHE_ALIAS]
# Not a redis cache, skipping
if not hasattr(cache, "keys"):
return
print("\nMigrating Redis sessions to database, this might take a couple of minutes...")
for key, session_data in progress_bar(cache.get_many(cache.keys(f"{KEY_PREFIX}*")).items()):
_migrate_session(
apps=apps,
db_alias=db_alias,
session_key=key.removeprefix(KEY_PREFIX),
session_data=session_data,
expires=now() + timedelta(seconds=cache.ttl(key)),
)
def migrate_database_sessions(apps, schema_editor):
DjangoSession = apps.get_model("sessions", "Session")
db_alias = schema_editor.connection.alias
@@ -231,10 +205,6 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Authenticated Sessions",
},
),
migrations.RunPython(
code=migrate_redis_sessions,
reverse_code=migrations.RunPython.noop,
),
migrations.RunPython(
code=migrate_database_sessions,
reverse_code=migrations.RunPython.noop,

View File

@@ -406,6 +406,8 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
def locale(self, request: HttpRequest | None = None) -> str:
"""Get the locale the user has configured"""
if request and hasattr(request, "LANGUAGE_CODE"):
return request.LANGUAGE_CODE
try:
return self.attributes.get("settings", {}).get("locale", "")

View File

@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from django_postgres_cache.tasks import clear_expired_cache
from dramatiq.actor import actor
from structlog.stdlib import get_logger
@@ -15,14 +15,14 @@ from authentik.core.models import (
User,
)
from authentik.lib.utils.db import chunked_queryset
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
@actor(description=_("Remove expired objects."))
def clean_expired_models():
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
for cls in ExpiringModel.__subclasses__():
cls: ExpiringModel
objects = (
@@ -33,11 +33,12 @@ def clean_expired_models():
obj.expire_action()
LOGGER.debug("Expired models", model=cls, amount=amount)
self.info(f"Expired {amount} {cls._meta.verbose_name_plural}")
clear_expired_cache()
@actor(description=_("Remove temporary users created by SAML Sources."))
def clean_temporary_users():
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
_now = datetime.now()
deleted_users = 0
for user in User.objects.filter(**{f"attributes__{USER_ATTRIBUTE_GENERATED}": True}):

View File

@@ -469,3 +469,274 @@ class TestUsersAPI(APITestCase):
body = loads(response.content)
self.assertEqual(len(body["results"]), 2)
self.assertEqual(body["results"][0]["pk"], user.pk)
def test_service_account_validation_empty_username(self):
"""Test service account creation with empty/blank username validation"""
self.client.force_login(self.admin)
# Test with empty string
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "",
"create_group": True,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"name": ["This field may not be blank."]},
)
# Test with only whitespace
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": " ",
"create_group": True,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"name": ["This field may not be blank."]},
)
# Test with tab and newline characters
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "\t\n",
"create_group": True,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"name": ["This field may not be blank."]},
)
def test_service_account_validation_valid_username(self):
"""Test service account creation with valid username"""
self.client.force_login(self.admin)
# Test with valid username
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "valid-service-account",
"create_group": True,
},
)
self.assertEqual(response.status_code, 200)
# Verify response structure
body = loads(response.content)
self.assertIn("username", body)
self.assertIn("user_uid", body)
self.assertIn("user_pk", body)
self.assertIn("group_pk", body) # Should exist since create_group=True
self.assertIn("token", body)
# Verify field types
self.assertEqual(body["username"], "valid-service-account")
self.assertIsInstance(body["user_pk"], int)
self.assertIsInstance(body["user_uid"], str)
self.assertIsInstance(body["token"], str)
self.assertIsInstance(body["group_pk"], str)
def test_service_account_validation_without_group(self):
"""Test service account creation without creating a group"""
self.client.force_login(self.admin)
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "no-group-service-account",
"create_group": False,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertIn("username", body)
self.assertIn("user_uid", body)
self.assertIn("user_pk", body)
self.assertIn("token", body)
# Should NOT have group_pk when create_group=False
self.assertNotIn("group_pk", body)
def test_service_account_validation_duplicate_username(self):
"""Test service account creation with duplicate username"""
self.client.force_login(self.admin)
# Create first service account
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "duplicate-test",
"create_group": True,
},
)
self.assertEqual(response.status_code, 200)
# Attempt to create second with same username
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "duplicate-test",
"create_group": True,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"name": ["This field must be unique."]},
)
def test_service_account_validation_invalid_create_group(self):
"""Test service account creation with invalid create_group field"""
self.client.force_login(self.admin)
# Test with string instead of boolean
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"create_group": "invalid",
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"create_group": ["Must be a valid boolean."]},
)
# Test with number instead of boolean
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"create_group": 123,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"create_group": ["Must be a valid boolean."]},
)
def test_service_account_validation_invalid_expiring(self):
"""Test service account creation with invalid expiring field"""
self.client.force_login(self.admin)
# Test with string instead of boolean
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"expiring": "invalid",
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"expiring": ["Must be a valid boolean."]},
)
def test_service_account_validation_invalid_expires(self):
"""Test service account creation with invalid expires field"""
self.client.force_login(self.admin)
# Test with invalid datetime string
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"expires": "invalid-datetime",
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{
"expires": [
"Datetime has wrong format. Use one of these formats instead: "
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]."
]
},
)
# Test with invalid format
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"expires": "2024-13-45", # Invalid month/day
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{
"expires": [
"Datetime has wrong format. Use one of these formats instead: "
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]."
]
},
)
def test_service_account_validation_multiple_errors(self):
"""Test service account creation with multiple validation errors"""
self.client.force_login(self.admin)
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "", # Empty username
"create_group": "invalid", # Invalid boolean
"expiring": 123, # Invalid boolean
"expires": "not-a-date", # Invalid datetime
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{
"name": ["This field may not be blank."],
"create_group": ["Must be a valid boolean."],
"expiring": ["Must be a valid boolean."],
"expires": [
"Datetime has wrong format. Use one of these formats instead: "
"YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]."
],
},
)
def test_service_account_validation_user_friendly_duplicate_error(self):
"""Test that duplicate username returns user-friendly error, not database error"""
self.client.force_login(self.admin)
# Create first service account
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "duplicate-username-test",
"create_group": True,
},
)
self.assertEqual(response.status_code, 200)
# Attempt to create second with same username
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "duplicate-username-test",
"create_group": True,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content,
{"name": ["This field must be unique."]},
)

View File

@@ -30,6 +30,7 @@ from authentik.flows.views.interface import FlowInterfaceView
from authentik.root.asgi_middleware import AuthMiddlewareStack
from authentik.root.messages.consumer import MessageConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware
from authentik.tenants.channels import TenantsAwareMiddleware
urlpatterns = [
path(
@@ -97,7 +98,9 @@ api_urlpatterns = [
websocket_urlpatterns = [
path(
"ws/client/",
ChannelsLoggingMiddleware(AuthMiddlewareStack(MessageConsumer.as_asgi())),
ChannelsLoggingMiddleware(
TenantsAwareMiddleware(AuthMiddlewareStack(MessageConsumer.as_asgi()))
),
),
]

View File

@@ -20,6 +20,11 @@ from authentik.lib.models import CreatedUpdatedModel, SerializerModel
LOGGER = get_logger()
def fingerprint_sha256(cert: Certificate) -> str:
"""Get SHA256 Fingerprint of certificate"""
return hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8")
class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
is set, otherwise it can be used to verify remote data."""
@@ -82,7 +87,7 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
@property
def fingerprint_sha256(self) -> str:
"""Get SHA256 Fingerprint of certificate_data"""
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode("utf-8")
return fingerprint_sha256(self.certificate)
@property
def fingerprint_sha1(self) -> str:

View File

@@ -7,13 +7,12 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509.base import load_pem_x509_certificate
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.config import CONFIG
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
@@ -38,7 +37,7 @@ def ensure_certificate_valid(body: str):
@actor(description=_("Discover, import and update certificates from the filesystem."))
def certificate_discovery():
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
certs = {}
private_keys = {}
discovered = 0

View File

@@ -27,7 +27,7 @@ class TestCrypto(APITestCase):
def test_model_private(self):
"""Test model private key"""
cert = CertificateKeyPair.objects.create(
name="test",
name=generate_id(),
certificate_data="foo",
key_data="foo",
)
@@ -271,7 +271,7 @@ class TestCrypto(APITestCase):
keypair = create_test_cert()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
@@ -303,7 +303,7 @@ class TestCrypto(APITestCase):
keypair = create_test_cert()
OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_id=generate_id(),
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],

View File

@@ -1,6 +1,5 @@
from django.db.models.aggregates import Count
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from structlog import get_logger
@@ -8,7 +7,7 @@ from authentik.enterprise.policies.unique_password.models import (
UniquePasswordPolicy,
UserPasswordHistory,
)
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
@@ -19,7 +18,7 @@ LOGGER = get_logger()
)
)
def check_and_purge_password_history():
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
if not UniquePasswordPolicy.objects.exists():
UserPasswordHistory.objects.all().delete()
@@ -39,7 +38,7 @@ def trim_password_histories():
UniquePasswordPolicy policies.
"""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
# No policy, we'll let the cleanup above do its thing
if not UniquePasswordPolicy.objects.exists():

View File

@@ -4,7 +4,7 @@ from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.users import PartialGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
@@ -13,7 +13,7 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
"""GoogleWorkspaceProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
group_obj = PartialGroupSerializer(source="group", read_only=True)
class Meta:

View File

@@ -3,7 +3,7 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser
@@ -13,7 +13,7 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
"""GoogleWorkspaceProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
user_obj = PartialUserSerializer(source="user", read_only=True)
class Meta:

View File

@@ -4,7 +4,7 @@ from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.users import PartialGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
@@ -13,7 +13,7 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
"""MicrosoftEntraProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
group_obj = PartialGroupSerializer(source="group", read_only=True)
class Meta:

View File

@@ -3,7 +3,7 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser
@@ -13,7 +13,7 @@ from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class MicrosoftEntraProviderUserSerializer(ModelSerializer):
"""MicrosoftEntraProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
user_obj = PartialUserSerializer(source="user", read_only=True)
class Meta:

View File

@@ -4,7 +4,6 @@ from uuid import UUID
from django.http import HttpRequest
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from requests.exceptions import RequestException
from structlog.stdlib import get_logger
@@ -20,7 +19,7 @@ from authentik.enterprise.providers.ssf.models import (
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
session = get_http_session()
LOGGER = get_logger()
@@ -74,7 +73,7 @@ def _check_app_access(stream: Stream, event_data: dict) -> bool:
@actor(description=_("Send an SSF event."))
def send_ssf_event(stream_uuid: UUID, event_data: dict[str, Any]):
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
stream = Stream.objects.filter(pk=stream_uuid).first()
if not stream:

View File

@@ -2,6 +2,7 @@ SPECTACULAR_SETTINGS = {
"POSTPROCESSING_HOOKS": [
"authentik.api.schema.postprocess_schema_responses",
"authentik.api.schema.postprocess_schema_pagination",
"authentik.api.schema.postprocess_schema_remove_unused",
"authentik.enterprise.search.schema.postprocess_schema_search_autocomplete",
"drf_spectacular.hooks.postprocess_schema_enums",
],

View File

@@ -7,6 +7,8 @@ from cryptography.x509 import (
Certificate,
NameOID,
ObjectIdentifier,
RFC822Name,
SubjectAlternativeName,
UnsupportedGeneralNameType,
load_pem_x509_certificate,
)
@@ -15,7 +17,7 @@ from django.utils.translation import gettext_lazy as _
from authentik.brands.models import Brand
from authentik.core.models import User
from authentik.crypto.models import CertificateKeyPair
from authentik.crypto.models import CertificateKeyPair, fingerprint_sha256
from authentik.enterprise.stages.mtls.models import (
CertAttributes,
MutualTLSStage,
@@ -137,7 +139,7 @@ class MTLSStageView(ChallengeStageView):
case CertAttributes.COMMON_NAME:
cert_attr = self.get_cert_attribute(cert, NameOID.COMMON_NAME)
case CertAttributes.EMAIL:
cert_attr = self.get_cert_attribute(cert, NameOID.EMAIL_ADDRESS)
cert_attr = self.get_cert_email(cert)
match stage.user_attribute:
case UserAttributes.USERNAME:
user_attr = "username"
@@ -171,7 +173,7 @@ class MTLSStageView(ChallengeStageView):
self.executor.plan.context.setdefault(PLAN_CONTEXT_PROMPT, {})
self.executor.plan.context[PLAN_CONTEXT_PROMPT].update(
{
"email": self.get_cert_attribute(cert, NameOID.EMAIL_ADDRESS),
"email": self.get_cert_email(cert),
"name": self.get_cert_attribute(cert, NameOID.COMMON_NAME),
}
)
@@ -183,6 +185,13 @@ class MTLSStageView(ChallengeStageView):
return None
return str(attr[0].value)
def get_cert_email(self, cert: Certificate) -> str | None:
ext = cert.extensions.get_extension_for_class(SubjectAlternativeName)
_cert_attr = ext.value.get_values_for_type(RFC822Name)
if len(_cert_attr) < 1:
return None
return str(_cert_attr[0])
def dispatch(self, request, *args, **kwargs):
stage: MutualTLSStage = self.executor.current_stage
certs = [
@@ -210,6 +219,7 @@ class MTLSStageView(ChallengeStageView):
if not cert and stage.mode == TLSMode.OPTIONAL:
self.logger.info("No certificate given, continuing")
return self.executor.stage_ok()
self.logger.debug("Received certificate", cert=fingerprint_sha256(cert))
existing_user = self.check_if_user(cert)
if self.executor.flow.designation == FlowDesignation.ENROLLMENT:
self.enroll_prepare_user(cert)

View File

@@ -4,7 +4,6 @@ from uuid import UUID
from django.db.models.query_utils import Q
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from guardian.shortcuts import get_anonymous_user
from structlog.stdlib import get_logger
@@ -19,7 +18,7 @@ from authentik.events.models import (
from authentik.lib.utils.db import chunked_queryset
from authentik.policies.engine import PolicyEngine
from authentik.policies.models import PolicyBinding, PolicyEngineMode
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
@@ -38,7 +37,7 @@ def event_trigger_dispatch(event_uuid: UUID):
)
def event_trigger_handler(event_uuid: UUID, trigger_name: str):
"""Check if policies attached to NotificationRule match event"""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
event: Event = Event.objects.filter(event_uuid=event_uuid).first()
if not event:
@@ -131,7 +130,7 @@ def gdpr_cleanup(user_pk: int):
@actor(description=_("Cleanup seen notifications and notifications whose event expired."))
def notification_cleanup():
"""Cleanup seen notifications and notifications whose event expired."""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
notifications = Notification.objects.filter(Q(event=None) | Q(seen=True))
amount = notifications.count()
notifications.delete()

View File

@@ -46,5 +46,5 @@ class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
serializer_class = FlowStageBindingSerializer
filterset_fields = "__all__"
search_fields = ["stage__name"]
ordering = ["order"]
ordering_fields = ["order", "stage__name"]
ordering = ["order", "pk"]
ordering_fields = ["order", "stage__name", "target__uuid", "pk"]

View File

@@ -54,6 +54,7 @@ class Challenge(PassiveSerializer):
flow_info = ContextualFlowInfo(required=False)
component = CharField(default="")
xid = CharField(required=False)
response_errors = DictField(
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False

View File

@@ -143,10 +143,12 @@ class FlowPlan:
request: HttpRequest,
flow: Flow,
allowed_silent_types: list["StageView"] | None = None,
**get_params,
) -> HttpResponse:
"""Redirect to the flow executor for this flow plan"""
from authentik.flows.views.executor import (
SESSION_KEY_PLAN,
FlowContainer,
FlowExecutorView,
)
@@ -157,6 +159,7 @@ class FlowPlan:
# No unskippable stages found, so we can directly return the response of the last stage
final_stage: type[StageView] = self.bindings[-1].stage.view
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
temp_exec.container = FlowContainer(request)
temp_exec.current_stage = self.bindings[-1].stage
temp_exec.current_stage_view = final_stage
temp_exec.setup(request, flow.slug)
@@ -174,6 +177,9 @@ class FlowPlan:
):
get_qs["inspector"] = "available"
for key, value in get_params:
get_qs[key] = value
return redirect_with_qs(
"authentik_core:if-flow",
get_qs,

View File

@@ -192,6 +192,7 @@ class ChallengeStageView(StageView):
)
flow_info.is_valid()
challenge.initial_data["flow_info"] = flow_info.data
challenge.initial_data["xid"] = self.executor.container.exec_id
if isinstance(challenge, WithUserInfoChallenge):
# If there's a pending user, update the `username` field
# this field is only used by password managers.

View File

@@ -29,7 +29,7 @@ window.authentik.flow = {
{% block body %}
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container></ak-message-container>
<ak-flow-executor flowSlug="{{ flow.slug }}">
<ak-flow-executor flowSlug="{{ flow.slug }}" xid="{{ xid }}">
<ak-loading></ak-loading>
</ak-flow-executor>
{% endblock %}

View File

@@ -1,6 +1,7 @@
"""authentik multi-stage authentication engine"""
from copy import deepcopy
from uuid import uuid4
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
@@ -63,6 +64,7 @@ from authentik.policies.engine import PolicyEngine
LOGGER = get_logger()
# Argument used to redirect user after login
NEXT_ARG_NAME = "next"
SESSION_KEY_PLAN_CONTAINER = "authentik/flows/plan_container/%s"
SESSION_KEY_PLAN = "authentik/flows/plan"
SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
SESSION_KEY_GET = "authentik/flows/get"
@@ -70,6 +72,7 @@ SESSION_KEY_POST = "authentik/flows/post"
SESSION_KEY_HISTORY = "authentik/flows/history"
QS_KEY_TOKEN = "flow_token" # nosec
QS_QUERY = "query"
QS_EXEC_ID = "xid"
def challenge_types():
@@ -96,6 +99,88 @@ class InvalidStageError(SentryIgnoredException):
"""Error raised when a challenge from a stage is not valid"""
class FlowContainer:
"""Allow for multiple concurrent flow executions in the same session"""
def __init__(self, request: HttpRequest, exec_id: str | None = None) -> None:
self.request = request
self.exec_id = exec_id
@staticmethod
def new(request: HttpRequest):
exec_id = str(uuid4())
request.session[SESSION_KEY_PLAN_CONTAINER % exec_id] = {}
return FlowContainer(request, exec_id)
def exists(self) -> bool:
"""Check if flow exists in container/session"""
return SESSION_KEY_PLAN in self.session
def save(self):
self.request.session.modified = True
@property
def session(self):
# Backwards compatibility: store session plan/etc directly in session
if not self.exec_id:
return self.request.session
self.request.session.setdefault(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {})
return self.request.session.get(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {})
@property
def plan(self) -> FlowPlan:
return self.session.get(SESSION_KEY_PLAN)
def to_redirect(
self,
request: HttpRequest,
flow: Flow,
allowed_silent_types: list[StageView] | None = None,
**get_params,
) -> HttpResponse:
get_params[QS_EXEC_ID] = self.exec_id
return self.plan.to_redirect(
request, flow, allowed_silent_types=allowed_silent_types, **get_params
)
@plan.setter
def plan(self, value: FlowPlan):
self.session[SESSION_KEY_PLAN] = value
self.request.session.modified = True
self.save()
@property
def application_pre(self):
return self.session.get(SESSION_KEY_APPLICATION_PRE)
@property
def get(self) -> QueryDict:
return self.session.get(SESSION_KEY_GET)
@get.setter
def get(self, value: QueryDict):
self.session[SESSION_KEY_GET] = value
self.save()
@property
def post(self) -> QueryDict:
return self.session.get(SESSION_KEY_POST)
@post.setter
def post(self, value: QueryDict):
self.session[SESSION_KEY_POST] = value
self.save()
@property
def history(self) -> list[FlowPlan]:
return self.session.get(SESSION_KEY_HISTORY)
@history.setter
def history(self, value: list[FlowPlan]):
self.session[SESSION_KEY_HISTORY] = value
self.save()
@method_decorator(xframe_options_sameorigin, name="dispatch")
class FlowExecutorView(APIView):
"""Flow executor, passing requests to Stage Views"""
@@ -103,8 +188,9 @@ class FlowExecutorView(APIView):
permission_classes = [AllowAny]
flow: Flow = None
plan: FlowPlan | None = None
container: FlowContainer
current_binding: FlowStageBinding | None = None
current_stage: Stage
current_stage_view: View
@@ -160,10 +246,12 @@ class FlowExecutorView(APIView):
if QS_KEY_TOKEN in get_params:
plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
if plan:
self.request.session[SESSION_KEY_PLAN] = plan
container = FlowContainer.new(request)
container.plan = plan
# Early check if there's an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session:
self.plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
self.container = FlowContainer(request, request.GET.get(QS_EXEC_ID))
if self.container.exists():
self.plan: FlowPlan = self.container.plan
if self.plan.flow_pk != self.flow.pk.hex:
self._logger.warning(
"f(exec): Found existing plan for other flow, deleting plan",
@@ -176,13 +264,14 @@ class FlowExecutorView(APIView):
self._logger.debug("f(exec): Continuing existing plan")
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params
self.container.get = get_params
# Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan:
request.session[SESSION_KEY_HISTORY] = []
self.container.history = []
self._logger.debug("f(exec): No active Plan found, initiating planner")
try:
self.plan = self._initiate_plan()
self.container.plan = self.plan
except FlowNonApplicableException as exc:
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
return self.handle_invalid_flow(exc)
@@ -255,12 +344,19 @@ class FlowExecutorView(APIView):
request=OpenApiTypes.NONE,
parameters=[
OpenApiParameter(
name="query",
name=QS_QUERY,
location=OpenApiParameter.QUERY,
required=True,
description="Querystring as received",
type=OpenApiTypes.STR,
)
),
OpenApiParameter(
name=QS_EXEC_ID,
location=OpenApiParameter.QUERY,
required=False,
description="Flow execution ID",
type=OpenApiTypes.STR,
),
],
operation_id="flows_executor_get",
)
@@ -287,8 +383,8 @@ class FlowExecutorView(APIView):
span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.dispatch(request)
return to_stage_response(request, stage_response)
except Exception as exc: # noqa
return to_stage_response(request, stage_response, self.container.exec_id)
except Exception as exc:
return self.handle_exception(exc)
@extend_schema(
@@ -306,12 +402,19 @@ class FlowExecutorView(APIView):
),
parameters=[
OpenApiParameter(
name="query",
name=QS_QUERY,
location=OpenApiParameter.QUERY,
required=True,
description="Querystring as received",
type=OpenApiTypes.STR,
)
),
OpenApiParameter(
name=QS_EXEC_ID,
location=OpenApiParameter.QUERY,
required=True,
description="Flow execution ID",
type=OpenApiTypes.STR,
),
],
operation_id="flows_executor_solve",
)
@@ -338,14 +441,15 @@ class FlowExecutorView(APIView):
span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.dispatch(request)
return to_stage_response(request, stage_response)
return to_stage_response(request, stage_response, self.container.exec_id)
except Exception as exc: # noqa
return self.handle_exception(exc)
def _initiate_plan(self) -> FlowPlan:
planner = FlowPlanner(self.flow)
plan = planner.plan(self.request)
self.request.session[SESSION_KEY_PLAN] = plan
container = FlowContainer.new(self.request)
container.plan = plan
try:
# Call the has_stages getter to check that
# there are no issues with the class we might've gotten
@@ -369,7 +473,7 @@ class FlowExecutorView(APIView):
except FlowNonApplicableException as exc:
self._logger.warning("f(exec): Flow restart not applicable to current user", exc=exc)
return self.handle_invalid_flow(exc)
self.request.session[SESSION_KEY_PLAN] = plan
self.container.plan = plan
kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug})
return redirect_with_qs("authentik_api:flow-executor", self.request.GET, **kwargs)
@@ -391,9 +495,13 @@ class FlowExecutorView(APIView):
)
self.cancel()
if next_param and not is_url_absolute(next_param):
return to_stage_response(self.request, redirect_with_qs(next_param))
return to_stage_response(
self.request, redirect_with_qs(next_param), self.container.exec_id
)
return to_stage_response(
self.request, self.stage_invalid(error_message=_("Invalid next URL"))
self.request,
self.stage_invalid(error_message=_("Invalid next URL")),
self.container.exec_id,
)
def stage_ok(self) -> HttpResponse:
@@ -407,7 +515,7 @@ class FlowExecutorView(APIView):
self.current_stage_view.cleanup()
self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan))
self.plan.pop()
self.request.session[SESSION_KEY_PLAN] = self.plan
self.container.plan = self.plan
if self.plan.bindings:
self._logger.debug(
"f(exec): Continuing with next stage",
@@ -450,6 +558,7 @@ class FlowExecutorView(APIView):
def cancel(self):
"""Cancel current flow execution"""
# TODO: Clean up container
keys_to_delete = [
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,
@@ -472,8 +581,8 @@ class CancelView(View):
def get(self, request: HttpRequest) -> HttpResponse:
"""View which canels the currently active plan"""
if SESSION_KEY_PLAN in request.session:
del request.session[SESSION_KEY_PLAN]
if FlowContainer(request, request.GET.get(QS_EXEC_ID)).exists():
del request.session[SESSION_KEY_PLAN_CONTAINER % request.GET.get(QS_EXEC_ID)]
LOGGER.debug("Canceled current plan")
return redirect("authentik_flows:default-invalidation")
@@ -521,19 +630,12 @@ class ToDefaultFlow(View):
def dispatch(self, request: HttpRequest) -> HttpResponse:
flow = self.get_flow()
# If user already has a pending plan, clear it so we don't have to later.
if SESSION_KEY_PLAN in self.request.session:
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
if plan.flow_pk != flow.pk.hex:
LOGGER.warning(
"f(def): Found existing plan for other flow, deleting plan",
flow_slug=flow.slug,
)
del self.request.session[SESSION_KEY_PLAN]
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
get_qs = request.GET.copy()
get_qs[QS_EXEC_ID] = str(uuid4())
return redirect_with_qs("authentik_core:if-flow", get_qs, flow_slug=flow.slug)
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
def to_stage_response(request: HttpRequest, source: HttpResponse, xid: str) -> HttpResponse:
"""Convert normal HttpResponse into JSON Response"""
if (
isinstance(source, HttpResponseRedirect)
@@ -552,6 +654,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
RedirectChallenge(
{
"to": str(redirect_url),
"xid": xid,
}
)
)
@@ -560,6 +663,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
ShellChallenge(
{
"body": source.render().content.decode("utf-8"),
"xid": xid,
}
)
)
@@ -569,6 +673,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
ShellChallenge(
{
"body": source.content.decode("utf-8"),
"xid": xid,
}
)
)
@@ -600,4 +705,6 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
raise Http404 from None
return plan.to_redirect(request, stage.configure_flow)
container = FlowContainer.new(request)
container.plan = plan
return container.to_redirect(request, stage.configure_flow)

View File

@@ -7,6 +7,7 @@ from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow
from authentik.flows.views.executor import QS_EXEC_ID
class FlowInterfaceView(InterfaceView):
@@ -17,6 +18,7 @@ class FlowInterfaceView(InterfaceView):
kwargs["flow"] = flow
kwargs["flow_background_url"] = flow.background_url(self.request)
kwargs["inspector"] = "inspector" in self.request.GET
kwargs["xid"] = self.request.GET.get(QS_EXEC_ID)
return super().get_context_data(**kwargs)
def compat_needs_sfe(self) -> bool:

View File

@@ -152,7 +152,7 @@ worker:
processes: 1
threads: 2
consumer_listen_timeout: "seconds=30"
task_max_retries: 20
task_max_retries: 5
task_default_time_limit: "minutes=10"
lock_purge_interval: "minutes=1"
task_purge_interval: "days=1"

View File

@@ -3,9 +3,10 @@
import re
import socket
from ipaddress import ip_address, ip_network
from smtplib import SMTPException
from textwrap import indent
from types import CodeType
from typing import Any
from typing import TYPE_CHECKING, Any
from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError
@@ -29,6 +30,10 @@ from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
from authentik.stages.authenticator import devices_for_user
from authentik.stages.email.utils import TemplateEmailMessage
if TYPE_CHECKING:
from authentik.stages.email.models import EmailStage
LOGGER = get_logger()
@@ -57,11 +62,12 @@ class BaseEvaluator:
self._globals = {
"ak_call_policy": self.expr_func_call_policy,
"ak_create_event": self.expr_event_create,
"ak_create_jwt": self.expr_create_jwt,
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
"ak_logger": get_logger(self._filename).bind(),
"ak_send_email": self.expr_send_email,
"ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
"ak_create_jwt": self.expr_create_jwt,
"ip_address": ip_address,
"ip_network": ip_network,
"list_flatten": BaseEvaluator.expr_flatten,
@@ -216,6 +222,81 @@ class BaseEvaluator:
access_token.save()
return access_token.token
def expr_send_email(
self,
address: str | list[str],
subject: str,
body: str | None = None,
stage: "EmailStage | None" = None,
template: str | None = None,
context: dict | None = None,
) -> bool:
"""Send an email using authentik's email system
Args:
address: Email address(es) to send to. Can be:
- Single email: "user@example.com"
- List of emails: ["user1@example.com", "user2@example.com"]
subject: Email subject
body: Email body (plain text/HTML). Mutually exclusive with template.
stage: EmailStage instance to use for settings. If None, uses global settings.
template: Template name to render. Mutually exclusive with body.
context: Additional context variables for template rendering.
Returns:
bool: True if email was queued successfully, False otherwise
"""
# Deferred imports to avoid circular import issues
from authentik.stages.email.tasks import send_mails
if body and template:
raise ValueError("body and template parameters are mutually exclusive")
if not body and not template:
raise ValueError("Either body or template parameter must be provided")
# Normalize address parameter to list of (name, email) tuples
if isinstance(address, str):
# Single email address
to_addresses = [("", address)]
elif isinstance(address, list):
if not address:
raise ValueError("Address list cannot be empty")
# List of email strings
to_addresses = [("", email) for email in address]
else:
raise ValueError("Address must be a string or list of strings")
try:
if template is not None:
# Use all available context from the evaluator for template rendering
template_context = self._context.copy()
# Add any custom context passed to the function
if context:
template_context.update(context)
# Use template rendering
message = TemplateEmailMessage(
subject=subject,
to=to_addresses,
template_name=template,
template_context=template_context,
)
else:
# Use plain body
message = TemplateEmailMessage(
subject=subject,
to=to_addresses,
body=body,
)
send_mails(stage, message)
return True
except (SMTPException, ConnectionError, ValidationError, ValueError) as exc:
LOGGER.warning("Failed to send email", exc=exc, addresses=to_addresses, subject=subject)
return False
def wrap_expression(self, expression: str) -> str:
"""Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())

View File

@@ -112,6 +112,7 @@ def get_logger_config():
"hpack": "WARNING",
"httpx": "WARNING",
"azure": "WARNING",
"channels_postgres": "WARNING",
}
for handler_name, level in handler_level_map.items():
base_config["loggers"][handler_name] = {

View File

@@ -3,19 +3,15 @@
from asyncio.exceptions import CancelledError
from typing import Any
from channels_redis.core import ChannelFull
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError
from django.db import DatabaseError, InternalError, OperationalError, ProgrammingError
from django.http.response import Http404
from django_redis.exceptions import ConnectionInterrupted
from docker.errors import DockerException
from dramatiq.errors import Retry
from h11 import LocalProtocolError
from ldap3.core.exceptions import LDAPException
from psycopg.errors import Error
from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError
from rest_framework.exceptions import APIException
from sentry_sdk import HttpTransport, get_current_scope
from sentry_sdk import init as sentry_sdk_init
@@ -23,7 +19,6 @@ from sentry_sdk.api import set_tag
from sentry_sdk.integrations.argv import ArgvIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.dramatiq import DramatiqIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.socket import SocketIntegration
from sentry_sdk.integrations.stdlib import StdlibIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration
@@ -59,13 +54,7 @@ ignored_classes = (
ProgrammingError,
SuspiciousOperation,
ValidationError,
# Redis errors
RedisConnectionError,
ConnectionInterrupted,
RedisError,
ResponseError,
# websocket errors
ChannelFull,
WebSocketException,
LocalProtocolError,
# rest_framework error
@@ -112,7 +101,6 @@ def sentry_init(**sentry_init_kwargs):
ArgvIntegration(),
DjangoIntegration(transaction_style="function_name", cache_spans=True),
DramatiqIntegration(),
RedisIntegration(),
SocketIntegration(),
StdlibIntegration(),
ThreadingIntegration(propagate_hub=True),
@@ -159,9 +147,7 @@ def before_send(event: dict, hint: dict) -> dict | None:
if event["logger"] in [
"asyncio",
"multiprocessing",
"django_redis",
"django.security.DisallowedHost",
"django_redis.cache",
"paramiko.transport",
]:
return None

View File

@@ -2,7 +2,6 @@ from django.core.paginator import Paginator
from django.db.models import Model, QuerySet
from django.db.models.query import Q
from django.utils.text import slugify
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import Actor
from dramatiq.composition import group
from dramatiq.errors import Retry
@@ -22,6 +21,7 @@ from authentik.lib.sync.outgoing.exceptions import (
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.errors import exception_to_dict
from authentik.lib.utils.reflection import class_to_path, path_to_class
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task
@@ -61,7 +61,7 @@ class SyncTasks:
provider_pk: int,
sync_objects: Actor[[str, int, int, bool], None],
):
task: Task = CurrentTask.get_task()
task = CurrentTask.get_task()
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
provider_pk=provider_pk,
@@ -118,7 +118,7 @@ class SyncTasks:
override_dry_run=False,
**filter,
):
task: Task = CurrentTask.get_task()
task = CurrentTask.get_task()
_object_type: type[Model] = path_to_class(object_type)
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
@@ -173,7 +173,7 @@ class SyncTasks:
except TransientSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, user=obj)
task.warning(
f"Failed to sync {str(obj)} due to " f"transient error: {str(exc)}",
f"Failed to sync {str(obj)} due to transient error: {str(exc)}",
obj=sanitize_item(obj),
exception=exception_to_dict(exc),
)
@@ -207,7 +207,7 @@ class SyncTasks:
provider_pk: int,
raw_op: str,
):
task: Task = CurrentTask.get_task()
task = CurrentTask.get_task()
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
)
@@ -281,7 +281,7 @@ class SyncTasks:
action: str,
pk_set: list[int],
):
task: Task = CurrentTask.get_task()
task = CurrentTask.get_task()
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
)

View File

@@ -1,5 +1,7 @@
"""Test Evaluator base functions"""
from unittest.mock import patch
from django.test import RequestFactory, TestCase
from django.urls import reverse
from jwt import decode
@@ -77,3 +79,163 @@ class TestEvaluator(TestCase):
jwt, provider.client_secret, algorithms=["HS256"], audience=provider.client_id
)
self.assertEqual(decoded["preferred_username"], user.username)
@patch("authentik.stages.email.tasks.send_mails")
def test_expr_send_email_with_body(self, mock_send_mails):
"""Test ak_send_email with body parameter"""
user = create_test_user()
evaluator = BaseEvaluator(generate_id())
evaluator._context = {"user": user}
# Test sending email with body
result = evaluator.evaluate(
"return ak_send_email('test@example.com', 'Test Subject', body='Test Body')"
)
self.assertTrue(result)
mock_send_mails.assert_called_once()
# Verify the call arguments - send_mails is called with (stage, message)
args, kwargs = mock_send_mails.call_args
stage, message = args
# Check that global settings are used (stage is None)
self.assertIsNone(stage)
# Check message properties
self.assertEqual(message.subject, "Test Subject")
self.assertEqual(message.to, ["test@example.com"])
self.assertEqual(message.body, "Test Body")
@patch("authentik.stages.email.tasks.send_mails")
def test_expr_send_email_with_template(self, mock_send_mails):
"""Test ak_send_email with template parameter"""
user = create_test_user()
evaluator = BaseEvaluator(generate_id())
evaluator._context = {"user": user}
# Test sending email with template
result = evaluator.evaluate(
"return ak_send_email('test@example.com', 'Test Subject', "
"template='email/password_reset.html')"
)
self.assertTrue(result)
mock_send_mails.assert_called_once()
def test_expr_send_email_validation_errors(self):
"""Test ak_send_email validation errors"""
evaluator = BaseEvaluator(generate_id())
# Test error when both body and template are provided
with self.assertRaises(ValueError) as cm:
evaluator.evaluate(
"return ak_send_email('test@example.com', 'Test', "
"body='Body', template='template.html')"
)
self.assertIn("mutually exclusive", str(cm.exception))
# Test error when neither body nor template are provided
with self.assertRaises(ValueError) as cm:
evaluator.evaluate("return ak_send_email('test@example.com', 'Test')")
self.assertIn("Either body or template parameter must be provided", str(cm.exception))
@patch("authentik.stages.email.tasks.send_mails")
def test_expr_send_email_with_custom_stage(self, mock_send_mails):
"""Test ak_send_email with custom EmailStage"""
from authentik.stages.email.models import EmailStage
user = create_test_user()
custom_stage = EmailStage(
name="custom-stage", use_global_settings=False, from_address="custom@example.com"
)
evaluator = BaseEvaluator(generate_id())
evaluator._context = {"user": user, "custom_stage": custom_stage}
# Test sending email with custom stage
result = evaluator.evaluate(
"return ak_send_email('test@example.com', 'Test Subject', "
"body='Test Body', stage=custom_stage)"
)
self.assertTrue(result)
mock_send_mails.assert_called_once()
# Verify the custom stage was used
args, kwargs = mock_send_mails.call_args
stage, message = args
self.assertEqual(stage, custom_stage)
self.assertFalse(stage.use_global_settings)
@patch("authentik.stages.email.tasks.send_mails")
def test_expr_send_email_with_context(self, mock_send_mails):
"""Test ak_send_email with custom context parameter"""
user = create_test_user()
evaluator = BaseEvaluator(generate_id())
evaluator._context = {"user": user, "request_id": "123"}
# Test sending email with template and custom context
result = evaluator.evaluate(
"return ak_send_email('test@example.com', 'Test Subject', "
"template='email/password_reset.html', "
"context={'url': 'http://localhost', 'expires': '2026-01-01'})"
)
self.assertTrue(result)
mock_send_mails.assert_called_once()
# Verify the call arguments - send_mails is called with (stage, message)
args, kwargs = mock_send_mails.call_args
stage, message = args
# Check that global settings are used (stage is None)
self.assertIsNone(stage)
self.assertEqual(message.subject, "Test Subject")
self.assertEqual(message.to, ["test@example.com"])
self.assertIn("2026-01-01", message.body)
self.assertIn("http://localhost", message.body)
@patch("authentik.stages.email.tasks.send_mails")
def test_expr_send_email_multiple_addresses(self, mock_send_mails):
"""Test ak_send_email with multiple email addresses"""
user = create_test_user()
evaluator = BaseEvaluator(generate_id())
evaluator._context = {"user": user}
# Test sending email to multiple addresses
result = evaluator.evaluate(
"return ak_send_email(['user1@example.com', 'user2@example.com'], "
"'Test Subject', body='Test Body')"
)
self.assertTrue(result)
mock_send_mails.assert_called_once()
# Verify the call arguments - send_mails is called with (stage, message)
args, kwargs = mock_send_mails.call_args
stage, message = args
# Check that global settings are used (stage is None)
self.assertIsNone(stage)
# Check message properties - should have multiple recipients
self.assertEqual(message.subject, "Test Subject")
self.assertEqual(message.to, ["user1@example.com", "user2@example.com"])
self.assertEqual(message.body, "Test Body")
def test_expr_send_email_multiple_addresses_validation(self):
"""Test ak_send_email validation with multiple addresses"""
evaluator = BaseEvaluator(generate_id())
# Test error when empty list is provided
with self.assertRaises(ValueError) as cm:
evaluator.evaluate("return ak_send_email([], 'Test', body='Body')")
self.assertIn("Address list cannot be empty", str(cm.exception))
# Test error when invalid type is provided
with self.assertRaises(ValueError) as cm:
evaluator.evaluate("return ak_send_email(123, 'Test', body='Body')")
self.assertIn("Address must be a string or list of strings", str(cm.exception))

View File

@@ -3,7 +3,9 @@
from dataclasses import asdict, dataclass, field
from datetime import datetime
from enum import IntEnum
from hashlib import sha256
from typing import Any
from uuid import UUID
from asgiref.sync import async_to_sync
from channels.exceptions import DenyConnection
@@ -18,8 +20,15 @@ from structlog.stdlib import BoundLogger, get_logger
from authentik.outposts.apps import GAUGE_OUTPOSTS_CONNECTED, GAUGE_OUTPOSTS_LAST_UPDATE
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
OUTPOST_GROUP = "group_outpost_%(outpost_pk)s"
OUTPOST_GROUP_INSTANCE = "group_outpost_%(outpost_pk)s_%(instance)s"
def build_outpost_group(outpost_pk: str | UUID) -> str:
return sha256(f"{connection.schema_name}/group_outpost_{str(outpost_pk)}".encode()).hexdigest()
def build_outpost_group_instance(outpost_pk: str | UUID, instance: str) -> str:
return sha256(
f"{connection.schema_name}/group_outpost_{str(outpost_pk)}_{instance}".encode()
).hexdigest()
class WebsocketMessageInstruction(IntEnum):
@@ -64,26 +73,24 @@ class OutpostConsumer(JsonWebsocketConsumer):
def connect(self):
uuid = self.scope["url_route"]["kwargs"]["pk"]
user = self.scope["user"]
outpost = (
self.outpost: Outpost | None = (
get_objects_for_user(user, "authentik_outposts.view_outpost").filter(pk=uuid).first()
)
if not outpost:
if self.outpost is None:
raise DenyConnection()
self.logger = self.logger.bind(outpost=outpost)
self.logger = self.logger.bind(outpost=self.outpost)
try:
self.accept()
except RuntimeError as exc:
self.logger.warning("runtime error during accept", exc=exc)
raise DenyConnection() from None
self.outpost = outpost
query = QueryDict(self.scope["query_string"].decode())
self.instance_uid = query.get("instance_uuid", self.channel_name)
async_to_sync(self.channel_layer.group_add)(
OUTPOST_GROUP % {"outpost_pk": str(self.outpost.pk)}, self.channel_name
build_outpost_group(self.outpost.pk), self.channel_name
)
async_to_sync(self.channel_layer.group_add)(
OUTPOST_GROUP_INSTANCE
% {"outpost_pk": str(self.outpost.pk), "instance": self.instance_uid},
build_outpost_group_instance(self.outpost.pk, self.instance_uid),
self.channel_name,
)
GAUGE_OUTPOSTS_CONNECTED.labels(
@@ -96,12 +103,11 @@ class OutpostConsumer(JsonWebsocketConsumer):
def disconnect(self, code):
if self.outpost:
async_to_sync(self.channel_layer.group_discard)(
OUTPOST_GROUP % {"outpost_pk": str(self.outpost.pk)}, self.channel_name
build_outpost_group(self.outpost.pk), self.channel_name
)
if self.instance_uid:
async_to_sync(self.channel_layer.group_discard)(
OUTPOST_GROUP_INSTANCE
% {"outpost_pk": str(self.outpost.pk), "instance": self.instance_uid},
build_outpost_group_instance(self.outpost.pk, self.instance_uid),
self.channel_name,
)
if self.outpost and self.instance_uid:

View File

@@ -13,6 +13,7 @@ from urllib3.exceptions import HTTPError
from yaml import dump_all
from authentik.events.logs import LogEvent, capture_logs
from authentik.lib.utils.reflection import class_to_path
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
@@ -105,7 +106,7 @@ class KubernetesController(BaseController):
LogEvent(
log_level="info",
event=f"{reconcile_key.title()}: Disabled",
logger=str(type(self)),
logger=class_to_path(self.__class__),
)
)
continue
@@ -144,7 +145,7 @@ class KubernetesController(BaseController):
LogEvent(
log_level="info",
event=f"{reconcile_key.title()}: Disabled",
logger=str(type(self)),
logger=class_to_path(self.__class__),
)
)
continue

View File

@@ -12,7 +12,6 @@ from channels.layers import get_channel_layer
from django.core.cache import cache
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from docker.constants import DEFAULT_UNIX_SOCKET
from dramatiq.actor import actor
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
@@ -21,7 +20,7 @@ from structlog.stdlib import get_logger
from yaml import safe_load
from authentik.lib.config import CONFIG
from authentik.outposts.consumer import OUTPOST_GROUP
from authentik.outposts.consumer import build_outpost_group
from authentik.outposts.controllers.base import BaseController, ControllerException
from authentik.outposts.controllers.docker import DockerClient
from authentik.outposts.controllers.kubernetes import KubernetesClient
@@ -41,7 +40,7 @@ from authentik.providers.rac.controllers.docker import RACDockerController
from authentik.providers.rac.controllers.kubernetes import RACKubernetesController
from authentik.providers.radius.controllers.docker import RadiusDockerController
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "goauthentik.io/outposts/teardown/%s"
@@ -108,7 +107,7 @@ def outpost_service_connection_monitor(connection_pk: Any):
@actor(description=_("Create/update/monitor/delete the deployment of an Outpost."))
def outpost_controller(outpost_pk: str, action: str = "up", from_cache: bool = False):
"""Create/update/monitor/delete the deployment of an Outpost"""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
self.set_uid(outpost_pk)
logs = []
if from_cache:
@@ -142,7 +141,7 @@ def outpost_token_ensurer():
"""
Periodically ensure that all Outposts have valid Service Accounts and Tokens
"""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
all_outposts = Outpost.objects.all()
for outpost in all_outposts:
_ = outpost.token
@@ -161,7 +160,7 @@ def outpost_send_update(pk: Any):
_ = outpost.token
outpost.build_user_permissions(outpost.user)
layer = get_channel_layer()
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
group = build_outpost_group(outpost.pk)
LOGGER.debug("sending update", channel=group, outpost=outpost)
async_to_sync(layer.group_send)(group, {"type": "event.update"})
@@ -169,7 +168,7 @@ def outpost_send_update(pk: Any):
@actor(description=_("Checks the local environment and create Service connections."))
def outpost_connection_discovery():
"""Checks the local environment and create Service connections."""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
if not CONFIG.get_bool("outposts.discover"):
self.info("Outpost integration discovery is disabled")
return
@@ -213,7 +212,7 @@ def outpost_session_end(session_id: str):
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)}
group = build_outpost_group(outpost.pk)
async_to_sync(layer.group_send)(
group,
{

View File

@@ -45,7 +45,7 @@ class TestOutpostWS(TransactionTestCase):
communicator = WebsocketCommunicator(
URLRouter(websocket.websocket_urlpatterns),
f"/ws/outpost/{self.outpost.pk}/",
{b"authorization": f"Bearer {self.token}".encode()},
[(b"authorization", f"Bearer {self.token}".encode())],
)
connected, _ = await communicator.connect()
self.assertTrue(connected)
@@ -56,7 +56,7 @@ class TestOutpostWS(TransactionTestCase):
communicator = WebsocketCommunicator(
URLRouter(websocket.websocket_urlpatterns),
f"/ws/outpost/{self.outpost.pk}/",
{b"authorization": f"Bearer {self.token}".encode()},
[(b"authorization", f"Bearer {self.token}".encode())],
)
connected, _ = await communicator.connect()
self.assertTrue(connected)
@@ -83,7 +83,7 @@ class TestOutpostWS(TransactionTestCase):
communicator = WebsocketCommunicator(
URLRouter(websocket.websocket_urlpatterns),
f"/ws/outpost/{self.outpost.pk}/",
{b"authorization": f"Bearer {self.token}".encode()},
[(b"authorization", f"Bearer {self.token}".encode())],
)
connected, _ = await communicator.connect()
self.assertTrue(connected)

View File

@@ -11,11 +11,14 @@ from authentik.outposts.api.service_connections import (
from authentik.outposts.channels import TokenOutpostMiddleware
from authentik.outposts.consumer import OutpostConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware
from authentik.tenants.channels import TenantsAwareMiddleware
websocket_urlpatterns = [
path(
"ws/outpost/<uuid:pk>/",
ChannelsLoggingMiddleware(TokenOutpostMiddleware(OutpostConsumer.as_asgi())),
ChannelsLoggingMiddleware(
TenantsAwareMiddleware(TokenOutpostMiddleware(OutpostConsumer.as_asgi()))
),
),
]

View File

@@ -10,9 +10,9 @@ from rest_framework.serializers import PrimaryKeyRelatedField
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.users import PartialGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.models import PolicyBinding, PolicyBindingModel
@@ -61,8 +61,8 @@ class PolicyBindingSerializer(ModelSerializer):
)
policy_obj = PolicySerializer(required=False, read_only=True, source="policy")
group_obj = GroupSerializer(required=False, read_only=True, source="group")
user_obj = UserSerializer(required=False, read_only=True, source="user")
group_obj = PartialGroupSerializer(required=False, read_only=True, source="group")
user_obj = PartialUserSerializer(required=False, read_only=True, source="user")
class Meta:
model = PolicyBinding
@@ -124,4 +124,5 @@ class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
serializer_class = PolicyBindingSerializer
search_fields = ["policy__name"]
filterset_class = PolicyBindingFilter
ordering = ["target", "order"]
ordering = ["order", "pk"]
ordering_fields = ["order", "target__uuid", "pk"]

View File

@@ -66,6 +66,7 @@ class OAuth2ProviderSerializer(ProviderSerializer):
"access_code_validity",
"access_token_validity",
"refresh_token_validity",
"refresh_token_threshold",
"include_claims_in_id_token",
"signing_key",
"encryption_key",

View File

@@ -23,6 +23,8 @@ SCOPE_OPENID_PROFILE = "profile"
SCOPE_OPENID_EMAIL = "email"
SCOPE_OFFLINE_ACCESS = "offline_access"
UI_LOCALES = "ui_locales"
# https://www.iana.org/assignments/oauth-parameters/auth-parameters.xhtml#pkce-code-challenge-method
PKCE_METHOD_PLAIN = "plain"
PKCE_METHOD_S256 = "S256"

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.12 on 2025-09-25 15:26
import authentik.lib.utils.time
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0029_oauth2provider__backchannel_logout_uris"),
]
operations = [
migrations.AddField(
model_name="oauth2provider",
name="refresh_token_threshold",
field=models.TextField(
default="seconds=0",
help_text="When refreshing a token, if the refresh token is valid for less than this duration, it will be renewed. When set to seconds=0, token will always be renewed. (Format: hours=1;minutes=2;seconds=3).",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
]

View File

@@ -238,6 +238,16 @@ class OAuth2Provider(WebfingerProvider, Provider):
"(Format: hours=1;minutes=2;seconds=3)."
),
)
refresh_token_threshold = models.TextField(
default="seconds=0",
validators=[timedelta_string_validator],
help_text=_(
"When refreshing a token, if the refresh token is valid for less than "
"this duration, it will be renewed. "
"When set to seconds=0, token will always be renewed. "
"(Format: hours=1;minutes=2;seconds=3)."
),
)
sub_mode = models.TextField(
choices=SubModes.choices,

View File

@@ -1,14 +1,13 @@
"""OAuth2 Provider Tasks"""
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.lib.utils.http import get_http_session
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.oauth2.utils import create_logout_token
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
@@ -31,7 +30,7 @@ def send_backchannel_logout_request(
Returns:
bool: True if the request was sent successfully, False otherwise
"""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
LOGGER.debug("Sending back-channel logout request", provider_pk=provider_pk, sub=sub)
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()

View File

@@ -1,6 +1,7 @@
"""Test authorize view"""
from unittest.mock import MagicMock, patch
from urllib.parse import parse_qs, urlparse
from django.test import RequestFactory
from django.urls import reverse
@@ -670,3 +671,55 @@ class TestAuthorize(OAuthTestCase):
)
parsed = OAuthAuthorizationParams.from_request(request)
self.assertNotIn(SCOPE_OFFLINE_ACCESS, parsed.scope)
def test_ui_locales(self):
"""Test OIDC ui_locales authorization"""
flow = create_test_flow()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=flow,
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
self.client.logout()
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"client_id": "test",
"state": state,
"redirect_uri": "foo://localhost",
"ui_locales": "invalid fr",
},
)
parsed = parse_qs(urlparse(response.url).query)
self.assertEqual(parsed["locale"], ["fr"])
def test_ui_locales_invalid(self):
"""Test OIDC ui_locales authorization"""
flow = create_test_flow()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=flow,
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "foo://localhost")],
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
self.client.logout()
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"client_id": "test",
"state": state,
"redirect_uri": "foo://localhost",
"ui_locales": "invalid",
},
)
parsed = parse_qs(urlparse(response.url).query)
self.assertNotIn("locale", parsed)

View File

@@ -376,3 +376,63 @@ class TestToken(OAuthTestCase):
)
self.assertEqual(response.status_code, 400)
self.assertTrue(Event.objects.filter(action=EventAction.SUSPICIOUS_REQUEST).exists())
@apply_blueprint("system/providers-oauth2.yaml")
def test_refresh_token_view_threshold(self):
"""test request param"""
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://local.invalid")],
signing_key=self.keypair,
refresh_token_threshold="hours=1", # nosec
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
managed__in=[
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-profile",
"goauthentik.io/providers/oauth2/scope-offline_access",
]
)
)
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
self.app.save()
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
user = create_test_admin_user()
token: RefreshToken = RefreshToken.objects.create(
provider=provider,
user=user,
token=generate_id(),
_id_token=dumps({}),
auth_time=timezone.now(),
_scope="offline_access",
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
"refresh_token": token.token,
"redirect_uri": "http://local.invalid",
},
HTTP_AUTHORIZATION=f"Basic {header}",
HTTP_ORIGIN="http://local.invalid",
)
self.assertEqual(response["Access-Control-Allow-Credentials"], "true")
self.assertEqual(response["Access-Control-Allow-Origin"], "http://local.invalid")
access: AccessToken = AccessToken.objects.filter(user=user, provider=provider).first()
self.assertJSONEqual(
response.content.decode(),
{
"access_token": access.token,
"token_type": TOKEN_TYPE,
"expires_in": 3600,
"id_token": provider.encode(
access.id_token.to_dict(),
),
"scope": "offline_access",
},
)
self.validate_jwt(access, provider)

View File

@@ -8,7 +8,12 @@ from jwt import decode
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, Token, TokenIntents, UserTypes
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.core.tests.utils import (
create_test_admin_user,
create_test_cert,
create_test_flow,
create_test_user,
)
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.constants import (
GRANT_TYPE_CLIENT_CREDENTIALS,
@@ -182,6 +187,47 @@ class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
def test_successful_two_tokens(self):
"""test successful when two app passwords with the same key exist"""
Token.objects.create(
identifier="sa-token-two",
user=create_test_user(),
intent=TokenIntents.INTENT_APP_PASSWORD,
expiring=False,
key=self.token.key,
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
"client_id": self.provider.client_id,
"username": "sa",
"password": self.token.key,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["token_type"], TOKEN_TYPE)
_, alg = self.provider.jwt_key
jwt = decode(
body["access_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
jwt = decode(
body["id_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
def test_successful_password(self):
"""test successful (password grant)"""
response = self.client.post(

View File

@@ -5,13 +5,14 @@ from datetime import timedelta
from json import dumps
from re import error as RegexError
from re import fullmatch
from urllib.parse import parse_qs, urlencode, urlparse, urlsplit, urlunsplit
from urllib.parse import parse_qs, quote, urlencode, urlparse, urlsplit, urlunparse, urlunsplit
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
from django.conf import settings
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.http.response import Http404, HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils import timezone, translation
from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
@@ -41,6 +42,7 @@ from authentik.providers.oauth2.constants import (
SCOPE_OFFLINE_ACCESS,
SCOPE_OPENID,
TOKEN_TYPE,
UI_LOCALES,
)
from authentik.providers.oauth2.errors import (
AuthorizeError,
@@ -387,6 +389,45 @@ class AuthorizationFlowInitView(BufferedPolicyAccessView):
request.context["oauth_response_type"] = self.params.response_type
return request
def dispatch_with_language(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Activate language from OIDC specific ui_locales parameter, picking the earliest one
available"""
selected_language = None
if UI_LOCALES in self.request.GET:
languages = str(self.request.GET[UI_LOCALES]).split(" ")
for language in languages:
if translation.check_for_language(language):
selected_language = translation.get_supported_language_variant(language)
LOGGER.debug(
"Activating language from oidc ui_locales", locale=selected_language
)
break
translation.activate(selected_language)
response = super().dispatch(request, *args, **kwargs)
if selected_language:
response.set_cookie(
settings.LANGUAGE_COOKIE_NAME,
selected_language,
max_age=settings.LANGUAGE_COOKIE_AGE,
path=settings.LANGUAGE_COOKIE_PATH,
domain=settings.LANGUAGE_COOKIE_DOMAIN,
secure=settings.LANGUAGE_COOKIE_SECURE,
httponly=settings.LANGUAGE_COOKIE_HTTPONLY,
samesite=settings.LANGUAGE_COOKIE_SAMESITE,
)
if isinstance(response, HttpResponseRedirect):
parsed_url = urlparse(response.url)
args = parse_qs(parsed_url.query)
args["locale"] = selected_language
response["Location"] = urlunparse(
parsed_url._replace(query=urlencode(args, quote_via=quote, doseq=True))
)
return response
def dispatch(self, request: HttpRequest, *args, **kwargs):
# Activate language before parsing params (error messages should be localised)
return self.dispatch_with_language(request, *args, **kwargs)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Start FlowPLanner, return to flow executor shell"""
# Require a login event to be set, otherwise make the user re-login

View File

@@ -340,7 +340,7 @@ class TokenParams:
if not user:
raise TokenError("invalid_grant")
token: Token = Token.filter_not_expired(
key=password, intent=TokenIntents.INTENT_APP_PASSWORD
key=password, intent=TokenIntents.INTENT_APP_PASSWORD, user=user
).first()
if not token or token.user.uid != user.uid:
raise TokenError("invalid_grant")
@@ -684,32 +684,8 @@ class TokenView(View):
)
access_token.save()
refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity)
refresh_token = RefreshToken(
user=self.params.refresh_token.user,
scope=self.params.refresh_token.scope,
expires=refresh_token_expiry,
provider=self.provider,
auth_time=self.params.refresh_token.auth_time,
session=self.params.refresh_token.session,
)
id_token = IDToken.new(
self.provider,
refresh_token,
self.request,
)
id_token.nonce = self.params.refresh_token.id_token.nonce
id_token.at_hash = access_token.at_hash
refresh_token.id_token = id_token
refresh_token.save()
# Mark old token as revoked
self.params.refresh_token.revoked = True
self.params.refresh_token.save()
return {
res = {
"access_token": access_token.token,
"refresh_token": refresh_token.token,
"token_type": TOKEN_TYPE,
"scope": " ".join(access_token.scope),
"expires_in": int(
@@ -718,6 +694,37 @@ class TokenView(View):
"id_token": access_token.id_token.to_jwt(self.provider),
}
refresh_token_threshold = timedelta_from_string(self.provider.refresh_token_threshold)
if (
refresh_token_threshold.total_seconds() == 0
or (now - self.params.refresh_token.expires) > refresh_token_threshold
):
refresh_token_expiry = now + timedelta_from_string(self.provider.refresh_token_validity)
refresh_token = RefreshToken(
user=self.params.refresh_token.user,
scope=self.params.refresh_token.scope,
expires=refresh_token_expiry,
provider=self.provider,
auth_time=self.params.refresh_token.auth_time,
session=self.params.refresh_token.session,
)
id_token = IDToken.new(
self.provider,
refresh_token,
self.request,
)
id_token.nonce = self.params.refresh_token.id_token.nonce
id_token.at_hash = access_token.at_hash
refresh_token.id_token = id_token
refresh_token.save()
# Mark old token as revoked
self.params.refresh_token.revoked = True
self.params.refresh_token.save()
res["refresh_token"] = refresh_token.token
return res
def create_client_credentials_response(self) -> dict[str, Any]:
"""See https://datatracker.ietf.org/doc/html/rfc6749#section-4.4"""
now = timezone.now()

View File

@@ -5,7 +5,7 @@ from channels.layers import get_channel_layer
from django.utils.translation import gettext_lazy as _
from dramatiq.actor import actor
from authentik.outposts.consumer import OUTPOST_GROUP
from authentik.outposts.consumer import build_outpost_group
from authentik.outposts.models import Outpost, OutpostType
from authentik.providers.oauth2.id_token import hash_session_key
@@ -15,7 +15,7 @@ def proxy_on_logout(session_id: str):
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)}
group = build_outpost_group(outpost.pk)
async_to_sync(layer.group_send)(
group,
{

View File

@@ -3,7 +3,7 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.providers.rac.api.endpoints import EndpointSerializer
@@ -16,7 +16,7 @@ class ConnectionTokenSerializer(ModelSerializer):
provider_obj = RACProviderSerializer(source="provider", read_only=True)
endpoint_obj = EndpointSerializer(source="endpoint", read_only=True)
user = GroupMemberSerializer(source="session.user", read_only=True)
user = PartialUserSerializer(source="session.user", read_only=True)
class Meta:
model = ConnectionToken

View File

@@ -1,28 +1,46 @@
"""RAC Client consumer"""
from hashlib import sha256
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.exceptions import ChannelFull, DenyConnection
from channels.generic.websocket import AsyncWebsocketConsumer
from django.db import connection
from django.http.request import QueryDict
from structlog.stdlib import BoundLogger, get_logger
from authentik.outposts.consumer import OUTPOST_GROUP_INSTANCE
from authentik.outposts.consumer import build_outpost_group_instance
from authentik.outposts.models import Outpost, OutpostState, OutpostType
from authentik.providers.rac.models import ConnectionToken, RACProvider
# Global broadcast group, which messages are sent to when the outpost connects back
# to authentik for a specific connection
# The `RACClientConsumer` consumer adds itself to this group on connection,
# and removes itself once it has been assigned a specific outpost channel
RAC_CLIENT_GROUP = "group_rac_client"
# A group for all connections in a given authentik session ID
# A disconnect message is sent to this group when the session expires/is deleted
RAC_CLIENT_GROUP_SESSION = "group_rac_client_%(session)s"
# A group for all connections with a specific token, which in almost all cases
# is just one connection, however this is used to disconnect the connection
# when the token is deleted
RAC_CLIENT_GROUP_TOKEN = "group_rac_token_%(token)s" # nosec
def build_rac_client_group() -> str:
"""
Global broadcast group, which messages are sent to when the outpost connects back
to authentik for a specific connection
The `RACClientConsumer` consumer adds itself to this group on connection,
and removes itself once it has been assigned a specific outpost channel
"""
return sha256(f"{connection.schema_name}/group_rac_client".encode()).hexdigest()
def build_rac_client_group_session(session_key: str) -> str:
"""
A group for all connections in a given authentik session ID
A disconnect message is sent to this group when the session expires/is deleted
"""
return sha256(f"{connection.schema_name}/group_rac_client_{session_key}".encode()).hexdigest()
def build_rac_client_group_token(token: str) -> str:
"""
A group for all connections with a specific token, which in almost all cases
is just one connection, however this is used to disconnect the connection
when the token is deleted
"""
return sha256(f"{connection.schema_name}/group_rac_token_{token}".encode()).hexdigest()
# Step 1: Client connects to this websocket endpoint
# Step 2: We prepare all the connection args for Guac
@@ -45,22 +63,23 @@ class RACClientConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.logger = get_logger()
await self.accept("guacamole")
await self.channel_layer.group_add(RAC_CLIENT_GROUP, self.channel_name)
await self.channel_layer.group_add(build_rac_client_group(), self.channel_name)
await self.channel_layer.group_add(
RAC_CLIENT_GROUP_SESSION % {"session": self.scope["session"].session_key},
build_rac_client_group_session(self.scope["session"].session_key),
self.channel_name,
)
await self.init_outpost_connection()
async def disconnect(self, code):
self.logger.debug("Disconnecting")
# Tell the outpost we're disconnecting
await self.channel_layer.send(
self.dest_channel_id,
{
"type": "event.disconnect",
},
)
if self.dest_channel_id:
# Tell the outpost we're disconnecting
await self.channel_layer.send(
self.dest_channel_id,
{
"type": "event.disconnect",
},
)
@database_sync_to_async
def init_outpost_connection(self):
@@ -109,10 +128,8 @@ class RACClientConsumer(AsyncWebsocketConsumer):
if len(states) < 1:
continue
self.logger.debug("Sending out connection broadcast")
async_to_sync(self.channel_layer.group_send)(
OUTPOST_GROUP_INSTANCE % {"outpost_pk": str(outpost.pk), "instance": states[0].uid},
msg,
)
group = build_outpost_group_instance(outpost.pk, states[0].uid)
async_to_sync(self.channel_layer.group_send)(group, msg)
if self.provider and self.provider.delete_token_on_disconnect:
self.logger.info("Deleting connection token to prevent reconnect", token=self.token)
self.token.delete()
@@ -157,7 +174,7 @@ class RACClientConsumer(AsyncWebsocketConsumer):
self.dest_channel_id = outpost_channel
# Since we have a specific outpost channel now, we can remove
# ourselves from the global broadcast group
await self.channel_layer.group_discard(RAC_CLIENT_GROUP, self.channel_name)
await self.channel_layer.group_discard(build_rac_client_group(), self.channel_name)
async def event_send(self, event: dict):
"""Handler called by outpost websocket that sends data to this specific

View File

@@ -3,7 +3,7 @@
from channels.exceptions import ChannelFull
from channels.generic.websocket import AsyncWebsocketConsumer
from authentik.providers.rac.consumer_client import RAC_CLIENT_GROUP
from authentik.providers.rac.consumer_client import build_rac_client_group
class RACOutpostConsumer(AsyncWebsocketConsumer):
@@ -15,7 +15,7 @@ class RACOutpostConsumer(AsyncWebsocketConsumer):
self.dest_channel_id = self.scope["url_route"]["kwargs"]["channel"]
await self.accept()
await self.channel_layer.group_send(
RAC_CLIENT_GROUP,
build_rac_client_group(),
{
"type": "event.outpost.connected",
"outpost_channel": self.channel_name,

View File

@@ -9,8 +9,8 @@ from django.dispatch import receiver
from authentik.core.models import AuthenticatedSession
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
from authentik.providers.rac.consumer_client import (
RAC_CLIENT_GROUP_SESSION,
RAC_CLIENT_GROUP_TOKEN,
build_rac_client_group_session,
build_rac_client_group_token,
)
from authentik.providers.rac.models import ConnectionToken, Endpoint
@@ -19,10 +19,7 @@ from authentik.providers.rac.models import ConnectionToken, Endpoint
def user_session_deleted(sender, instance: AuthenticatedSession, **_):
layer = get_channel_layer()
async_to_sync(layer.group_send)(
RAC_CLIENT_GROUP_SESSION
% {
"session": instance.session.session_key,
},
build_rac_client_group_session(instance.session.session_key),
{"type": "event.disconnect", "reason": "session_logout"},
)
@@ -32,10 +29,7 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
"""Disconnect session when connection token is deleted"""
layer = get_channel_layer()
async_to_sync(layer.group_send)(
RAC_CLIENT_GROUP_TOKEN
% {
"token": instance.token,
},
build_rac_client_group_token(instance.token),
{"type": "event.disconnect", "reason": "token_delete"},
)

View File

@@ -12,6 +12,7 @@ from authentik.providers.rac.consumer_outpost import RACOutpostConsumer
from authentik.providers.rac.views import RACInterface, RACStartView
from authentik.root.asgi_middleware import AuthMiddlewareStack
from authentik.root.middleware import ChannelsLoggingMiddleware
from authentik.tenants.channels import TenantsAwareMiddleware
urlpatterns = [
path(
@@ -29,11 +30,15 @@ urlpatterns = [
websocket_urlpatterns = [
path(
"ws/rac/<str:token>/",
ChannelsLoggingMiddleware(AuthMiddlewareStack(RACClientConsumer.as_asgi())),
ChannelsLoggingMiddleware(
TenantsAwareMiddleware(AuthMiddlewareStack(RACClientConsumer.as_asgi()))
),
),
path(
"ws/outpost_rac/<str:channel>/",
ChannelsLoggingMiddleware(TokenOutpostMiddleware(RACOutpostConsumer.as_asgi())),
ChannelsLoggingMiddleware(
TenantsAwareMiddleware(TokenOutpostMiddleware(RACOutpostConsumer.as_asgi()))
),
),
]

View File

@@ -97,6 +97,7 @@ class TestAuthNRequest(TestCase):
pre_authentication_flow=create_test_flow(),
signing_kp=self.cert,
verification_kp=self.cert,
signed_assertion=True,
)
def test_signed_valid(self):
@@ -171,6 +172,7 @@ class TestAuthNRequest(TestCase):
self.provider.sign_assertion = True
self.provider.sign_response = True
self.provider.save()
self.source.signed_response = True
http_request = get_request("/")
# First create an AuthNRequest

View File

@@ -4,7 +4,7 @@ from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.users import PartialGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
from authentik.providers.scim.models import SCIMProviderGroup
@@ -13,7 +13,7 @@ from authentik.providers.scim.models import SCIMProviderGroup
class SCIMProviderGroupSerializer(ModelSerializer):
"""SCIMProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
group_obj = PartialGroupSerializer(source="group", read_only=True)
class Meta:

View File

@@ -3,7 +3,7 @@
from rest_framework import mixins
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
@@ -13,7 +13,7 @@ from authentik.providers.scim.models import SCIMProviderUser
class SCIMProviderUserSerializer(ModelSerializer):
"""SCIMProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
user_obj = PartialUserSerializer(source="user", read_only=True)
class Meta:

View File

@@ -1,5 +1,6 @@
"""common RBAC serializers"""
from django.contrib.auth.models import Permission
from django.db.models import Q, QuerySet
from django.db.transaction import atomic
from django_filters.filters import CharFilter, ChoiceFilter
@@ -15,9 +16,9 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import User, UserTypes
from authentik.core.models import Group, User, UserTypes
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignResultSerializer, PermissionAssignSerializer
from authentik.rbac.decorators import permission_required
@@ -37,15 +38,15 @@ class UserObjectPermissionSerializer(ModelSerializer):
fields = ["id", "codename", "model", "app_label", "object_pk", "name"]
class UserAssignedObjectPermissionSerializer(GroupMemberSerializer):
class UserAssignedObjectPermissionSerializer(PartialUserSerializer):
"""Users assigned object permission serializer"""
permissions = UserObjectPermissionSerializer(many=True, source="userobjectpermission_set")
is_superuser = BooleanField()
class Meta:
model = GroupMemberSerializer.Meta.model
fields = GroupMemberSerializer.Meta.fields + ["permissions", "is_superuser"]
model = PartialUserSerializer.Meta.model
fields = PartialUserSerializer.Meta.fields + ["permissions", "is_superuser"]
class UserAssignedPermissionFilter(FilterSet):
@@ -54,26 +55,56 @@ class UserAssignedPermissionFilter(FilterSet):
model = ChoiceFilter(choices=model_choices(), method="filter_model", required=True)
object_pk = CharFilter(method="filter_object_pk")
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
data = self.form.cleaned_data
model: str = data["model"]
object_pk: str | None = data.get("object_pk", None)
app, _, model = model.partition(".")
superuser_pks = (
Group.objects.filter(is_superuser=True).values_list("users", flat=True).distinct()
)
permissions = Permission.objects.filter(
content_type__app_label=app,
content_type__model=model,
)
user_pks_with_model_permission = (
permissions.order_by().values_list("user", flat=True).distinct()
)
user_pks_with_object_permission = []
if object_pk:
user_pks_with_object_permission = (
UserObjectPermission.objects.filter(
permission__in=permissions,
object_pk=object_pk,
)
.order_by()
.values_list("user", flat=True)
.distinct()
)
return queryset.filter(
Q(pk__in=superuser_pks)
| Q(pk__in=user_pks_with_model_permission)
| Q(pk__in=user_pks_with_object_permission)
)
def filter_model(self, queryset: QuerySet, name, value: str) -> QuerySet:
"""Filter by object type"""
app, _, model = value.partition(".")
return queryset.filter(
Q(
user_permissions__content_type__app_label=app,
user_permissions__content_type__model=model,
)
| Q(
userobjectpermission__permission__content_type__app_label=app,
userobjectpermission__permission__content_type__model=model,
)
| Q(ak_groups__is_superuser=True)
).distinct()
# Actual filtering is handled by the above method where both `model` and `object_pk` are
# available. Don't do anything here, this method is only left here to avoid overriding too
# much of filter_queryset.
return queryset
def filter_object_pk(self, queryset: QuerySet, name, value: str) -> QuerySet:
"""Filter by object primary key"""
return queryset.filter(
Q(userobjectpermission__object_pk=value) | Q(ak_groups__is_superuser=True),
).distinct()
# Actual filtering is handled by the above method where both `model` and `object_pk` are
# available. Don't do anything here, this method is only left here to avoid overriding too
# much of filter_queryset.
return queryset
class UserAssignedPermissionViewSet(ListModelMixin, GenericViewSet):
@@ -83,7 +114,7 @@ class UserAssignedPermissionViewSet(ListModelMixin, GenericViewSet):
ordering = ["username"]
# The filtering is done in the filterset,
# which has a required filter that does the heavy lifting
queryset = User.objects.all()
queryset = User.objects.all().prefetch_related("userobjectpermission_set")
filterset_class = UserAssignedPermissionFilter
@permission_required("authentik_core.assign_user_permissions")

View File

@@ -0,0 +1,35 @@
from typing import Any
from channels_postgres.core import PostgresChannelLayer as BasePostgresChannelLayer
from channels_postgres.db import DatabaseLayer as BaseDatabaseLayer
from django.conf import settings
from psycopg_pool import AsyncConnectionPool
from authentik.root.db.base import DatabaseWrapper
class DatabaseLayer(BaseDatabaseLayer):
async def get_db_pool(self, db_params: dict[str, Any]) -> AsyncConnectionPool:
db_wrapper = DatabaseWrapper(settings.CHANNEL_LAYERS["default"]["CONFIG"])
db_params = db_wrapper.get_connection_params()
db_params.pop("cursor_factory")
db_params.pop("context")
return await super().get_db_pool(db_params)
class PostgresChannelLayer(BasePostgresChannelLayer):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.django_db = DatabaseLayer(self.django_db.psycopg_options, self.db_params)
@property
def db_params(self):
db_wrapper = DatabaseWrapper(settings.CHANNEL_LAYERS["default"]["CONFIG"])
db_params = db_wrapper.get_connection_params()
db_params.pop("cursor_factory")
db_params.pop("context")
return db_params
@db_params.setter
def db_params(self, value):
pass

View File

@@ -292,7 +292,7 @@ class ChannelsLoggingMiddleware:
except DenyConnection:
return await send({"type": "websocket.close"})
except Exception as exc:
if settings.DEBUG:
if settings.DEBUG or settings.TEST:
raise exc
LOGGER.warning("Exception in ASGI application", exc=exc)
return await send({"type": "websocket.close"})

View File

@@ -11,8 +11,6 @@ from django.dispatch import Signal
from django.http import HttpRequest, HttpResponse
from django.views import View
from django_prometheus.exports import ExportToDjangoView
from django_redis import get_redis_connection
from redis.exceptions import RedisError
monitoring_set = Signal()
@@ -44,19 +42,17 @@ class LiveView(View):
class ReadyView(View):
"""View for readiness probe, always returns Http 200, unless sql or redis is down"""
"""View for readiness probe, always returns Http 200, unless sql is down"""
def check_db(self):
for db_conn in connections.all():
# Force connection reload
db_conn.connect()
_ = db_conn.cursor()
def dispatch(self, request: HttpRequest) -> HttpResponse:
try:
for db_conn in connections.all():
# Force connection reload
db_conn.connect()
_ = db_conn.cursor()
self.check_db()
except OperationalError: # pragma: no cover
return HttpResponse(status=503)
try:
redis_conn = get_redis_connection()
redis_conn.ping()
except RedisError: # pragma: no cover
return HttpResponse(status=503)
return HttpResponse(status=200)

View File

@@ -10,7 +10,7 @@ from sentry_sdk import set_tag
from xmlsec import enable_debug_trace
from authentik import authentik_version
from authentik.lib.config import CONFIG, django_db_config, redis_url
from authentik.lib.config import CONFIG, django_db_config
from authentik.lib.logging import get_logger_config, structlog_configure
from authentik.lib.sentry import sentry_init
from authentik.lib.utils.reflection import get_env
@@ -64,6 +64,7 @@ SHARED_APPS = [
"pgactivity",
"pglock",
"channels",
"channels_postgres",
"django_dramatiq_postgres",
"authentik.tasks",
]
@@ -72,6 +73,7 @@ TENANT_APPS = [
"django.contrib.contenttypes",
"django.contrib.sessions",
"pgtrigger",
"django_postgres_cache",
"authentik.admin",
"authentik.api",
"authentik.core",
@@ -103,6 +105,7 @@ TENANT_APPS = [
"authentik.sources.plex",
"authentik.sources.saml",
"authentik.sources.scim",
"authentik.sources.telegram",
"authentik.stages.authenticator",
"authentik.stages.authenticator_duo",
"authentik.stages.authenticator_email",
@@ -185,6 +188,7 @@ SPECTACULAR_SETTINGS = {
"POSTPROCESSING_HOOKS": [
"authentik.api.schema.postprocess_schema_responses",
"authentik.api.schema.postprocess_schema_pagination",
"authentik.api.schema.postprocess_schema_remove_unused",
"drf_spectacular.hooks.postprocess_schema_enums",
],
}
@@ -224,20 +228,11 @@ REST_FRAMEWORK = {
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db")),
"TIMEOUT": CONFIG.get_int("cache.timeout", 300),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"KEY_PREFIX": "authentik_cache",
"BACKEND": "django_postgres_cache.backend.DatabaseCache",
"KEY_FUNCTION": "django_tenants.cache.make_key",
"REVERSE_KEY_FUNCTION": "django_tenants.cache.reverse_key",
}
}
DJANGO_REDIS_SCAN_ITERSIZE = 1000
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
SESSION_ENGINE = "authentik.core.sessions"
# Configured via custom SessionMiddleware
# SESSION_COOKIE_SAMESITE = "None"
@@ -295,16 +290,6 @@ TEMPLATES = [
ASGI_APPLICATION = "authentik.root.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer",
"CONFIG": {
"hosts": [CONFIG.get("channel.url") or redis_url(CONFIG.get("redis.db"))],
"prefix": "authentik_channels_",
},
},
}
# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
@@ -317,6 +302,16 @@ DATABASE_ROUTERS = (
"django_tenants.routers.TenantSyncRouter",
)
CHANNEL_LAYERS = {
"default": {
"BACKEND": "authentik.root.channels.PostgresChannelLayer",
"CONFIG": {
**DATABASES["default"],
"TIME_ZONE": None,
},
},
}
# Email
# These values should never actually be used, emails are only sent from email stages, which
# loads the config directly from CONFIG
@@ -398,8 +393,6 @@ DRAMATIQ = {
).total_seconds(),
"middlewares": (
("django_dramatiq_postgres.middleware.FullyQualifiedActorName", {}),
# TODO: fixme
# ("dramatiq.middleware.prometheus.Prometheus", {}),
("django_dramatiq_postgres.middleware.DbConnectionMiddleware", {}),
("dramatiq.middleware.age_limit.AgeLimit", {}),
(
@@ -416,10 +409,13 @@ DRAMATIQ = {
("dramatiq.middleware.pipelines.Pipelines", {}),
(
"dramatiq.middleware.retries.Retries",
{"max_retries": CONFIG.get_int("worker.task_max_retries") if not TEST else 0},
{
"max_retries": CONFIG.get_int("worker.task_max_retries") if not TEST else 0,
"max_backoff": 60 * 60 * 1000, # 1 hour
},
),
("dramatiq.results.middleware.Results", {"store_results": True}),
("django_dramatiq_postgres.middleware.CurrentTask", {}),
("authentik.tasks.middleware.CurrentTask", {}),
("authentik.tasks.middleware.TenantMiddleware", {}),
("authentik.tasks.middleware.RelObjMiddleware", {}),
("authentik.tasks.middleware.MessagesMiddleware", {}),

View File

@@ -62,6 +62,11 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
"""Configure test environment settings"""
settings.TEST = True
settings.DRAMATIQ["test"] = True
settings.CHANNEL_LAYERS["default"]["CONFIG"] = {
**settings.DATABASES["default"],
**settings.DATABASES["default"]["TEST"],
"TIME_ZONE": None,
}
# Test-specific configuration
test_config = {

View File

@@ -2,7 +2,6 @@
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from structlog.stdlib import get_logger
@@ -10,7 +9,7 @@ from authentik.lib.config import CONFIG
from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.sync import KerberosSync
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
CACHE_KEY_STATUS = "goauthentik.io/sources/kerberos/status/"
@@ -33,7 +32,7 @@ def kerberos_connectivity_check(pk: str):
description=_("Sync Kerberos source."),
)
def kerberos_sync(pk: str):
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
source: KerberosSource = KerberosSource.objects.filter(enabled=True, pk=pk).first()
if not source:
return

View File

@@ -4,7 +4,6 @@ from uuid import uuid4
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from dramatiq.composition import group
from dramatiq.message import Message
@@ -21,6 +20,7 @@ from authentik.sources.ldap.sync.forward_delete_users import UserLDAPForwardDele
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task
LOGGER = get_logger()
@@ -53,7 +53,7 @@ def ldap_connectivity_check(pk: str | None = None):
)
def ldap_sync(source_pk: str):
"""Sync a single source"""
task: Task = CurrentTask.get_task()
task = CurrentTask.get_task()
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk, enabled=True).first()
if not source:
return
@@ -127,7 +127,7 @@ def ldap_sync_paginator(
)
def ldap_sync_page(source_pk: str, sync_class: str, page_cache_key: str):
"""Synchronization of an LDAP Source"""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first()
if not source:
# Because the source couldn't be found, we don't have a UID

View File

@@ -3,14 +3,13 @@
from json import dumps
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from requests import RequestException
from structlog.stdlib import get_logger
from authentik.lib.utils.http import get_http_session
from authentik.sources.oauth.models import OAuthSource
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
LOGGER = get_logger()
@@ -21,7 +20,7 @@ LOGGER = get_logger()
)
)
def update_well_known_jwks():
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
session = get_http_session()
for source in OAuthSource.objects.all().exclude(oidc_well_known_url=""):
try:

View File

@@ -1,7 +1,6 @@
"""Plex tasks"""
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from requests import RequestException
@@ -9,13 +8,13 @@ from authentik.events.models import Event, EventAction
from authentik.lib.utils.errors import exception_to_string
from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.plex import PlexAuth
from authentik.tasks.models import Task
from authentik.tasks.middleware import CurrentTask
@actor(description=_("Check the validity of a Plex source."))
def check_plex_token(source_pk: str):
"""Check the validity of a Plex source."""
self: Task = CurrentTask.get_task()
self = CurrentTask.get_task()
sources = PlexSource.objects.filter(pk=source_pk)
if not sources.exists():
return

View File

@@ -1,10 +1,12 @@
"""SAMLSource API Views"""
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
@@ -17,6 +19,18 @@ from authentik.sources.saml.processors.metadata import MetadataProcessor
class SAMLSourceSerializer(SourceSerializer):
"""SAMLSource Serializer"""
def validate(self, attrs: dict):
if attrs.get("verification_kp"):
if not attrs.get("signed_assertion") and not attrs.get("signed_response"):
raise ValidationError(
_(
"With a Verification Certificate selected, at least one of"
" 'Verify Assertion Signature' or 'Verify Response Signature' "
"must be selected."
)
)
return super().validate(attrs)
class Meta:
model = SAMLSource
fields = SourceSerializer.Meta.fields + [
@@ -34,6 +48,8 @@ class SAMLSourceSerializer(SourceSerializer):
"signature_algorithm",
"temporary_user_delete_after",
"encryption_kp",
"signed_assertion",
"signed_response",
]
@@ -65,6 +81,8 @@ class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
"digest_algorithm",
"signature_algorithm",
"temporary_user_delete_after",
"signed_assertion",
"signed_response",
]
search_fields = ["name", "slug"]
ordering = ["name"]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.12 on 2025-09-26 04:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_saml", "0020_alter_samlsource_name_id_policy"),
]
operations = [
migrations.AddField(
model_name="samlsource",
name="signed_assertion",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="samlsource",
name="signed_response",
field=models.BooleanField(default=False),
),
]

View File

@@ -200,6 +200,9 @@ class SAMLSource(Source):
default=RSA_SHA256,
)
signed_assertion = models.BooleanField(default=True)
signed_response = models.BooleanField(default=False)
@property
def component(self) -> str:
return "ak-source-saml-form"

View File

@@ -116,27 +116,43 @@ class ResponseProcessor:
def _verify_signed(self):
"""Verify SAML Response's Signature"""
signature_nodes = self._root.xpath(
"/samlp:Response/saml:Assertion/ds:Signature", namespaces=NS_MAP
)
if len(signature_nodes) != 1:
signatures = []
if self._source.signed_response:
signature_nodes = self._root.xpath("/samlp:Response/ds:Signature", namespaces=NS_MAP)
if len(signature_nodes) != 1:
raise InvalidSignature("No Signature exists in the Response element.")
signatures.extend(signature_nodes)
if self._source.signed_assertion:
signature_nodes = self._root.xpath(
"/samlp:Response/saml:Assertion/ds:Signature", namespaces=NS_MAP
)
if len(signature_nodes) != 1:
raise InvalidSignature("No Signature exists in the Assertion element.")
signatures.extend(signature_nodes)
if len(signatures) == 0:
raise InvalidSignature()
signature_node = signature_nodes[0]
xmlsec.tree.add_ids(self._root, ["ID"])
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
self._source.verification_kp.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
)
ctx.key = key
for signature_node in signatures:
xmlsec.tree.add_ids(self._root, ["ID"])
ctx.set_enabled_key_data([xmlsec.constants.KeyDataX509])
try:
ctx.verify(signature_node)
except xmlsec.Error as exc:
raise InvalidSignature() from exc
LOGGER.debug("Successfully verified signature")
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
self._source.verification_kp.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
)
ctx.key = key
ctx.set_enabled_key_data([xmlsec.constants.KeyDataX509])
try:
ctx.verify(signature_node)
except xmlsec.Error as exc:
raise InvalidSignature() from exc
LOGGER.debug("Successfully verified signature")
def _verify_request_id(self):
if self._source.allow_idp_initiated:

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
<?xml version="1.0"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfxb5fecb6f-64b7-d4ca-f07e-55bd982b57d7" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#pfxb5fecb6f-64b7-d4ca-f07e-55bd982b57d7"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>paAKPyWfoctUwcvhymHb5M+nYg8=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>HtALAH0jNS3uVdOVTezLNDP10AXqDps+4Xky0dCEh5mwursQpOWIOc3MKcooTg14VWQYQr96bNfk4vBbhU1FWNp+An/iM3eZFuVMrjVfvQzEAa/0y7QG2MHkyScwpP165HRtNgZdTkipJmScQcfk9Nq6trx2f7w7Unno1F+GBGU8I4mVVgHzmFrZ9I+6JEEPuCjqy7qISUmNo4QkVGq1g6AuncNP8iUgX7zdNpikRh6u7PtIdpjfKah+GXfvTuvFQYHFT4v8sEvOHhnhjL4SJ4A16DzbyFUZMUlqyUweoV/ssn0fnGWaddb1KnyvRwH+i8sbDKATZfmxuK49fJLgFkaMyDX8TbA5qlDuMxS+9ZgpP6otaX0yeFpdB4G0QclUMqEmGhB9qRfE85NXHkmlHYBElG5mWdU7GfxmsIEj83Yuhy2UsUGFQIFym7BMpnCBIuNK16pCajnFgDf6+uxQYsBmemsDthDn6GgnNJjkau8xTXldoNYp+vKmG7oz7tBVRZa3aVBjMQKZ70L5Ur+Sctz7qEQDaYd0G20JoJSKW8Wz9bas1ib/gEOijM2TA928IbIRqKTNidy8hRDqBYo28HOIZy2UDoa0J3iTRriCRGdaJ8y25FsvWiTHKfxvNxG5ebzM9bNRyiNwlIp7xBR6yyTr2lsNacUnI4SQTH8ISS0=</ds:SignatureValue>
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloXDTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLlyrbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5EFUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8uvm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq614ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOpmTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCqlDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5kyPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/GcpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE28Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusOLT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDRUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OIAVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/OnS7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HAC8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1ZpoMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YEpF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTuvr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMClpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfxcb30156d-48bf-1ccc-1146-985b0c2398c8" Version="2.0" IssueInstant="2014-07-17T01:01:48Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#pfxcb30156d-48bf-1ccc-1146-985b0c2398c8"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>pj+OoLug/973FTHn1Vx6ctDHzUo=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>crl9PymGe4wAlFsapr0t3WxmUlKAImxEZyKH7sTQSU7/uTgPfYOxoqkN11h+sJgPp4f4NuCEVxVBFTXQ8kNkrI1CTpsU9xDxIHr5v9tEPrj/neWjQf+EvbWOgvzh04W1En/YyCYzHXsLrI/z/OCbJux4u29/82avYvoZjds09bXXcOYZrpEloE0kluSeKe4hCYPkbFgZCso3WCXYrJIfTWMngRd+bil8hN7aGgIya3zFUK4UXGuRmtAXSMTOPUE5SBijQXUaBMYyeBidGezYst0qi2f2HMht28qlit0oHhZA+qSYJyfdE5d0/Wx72CcPTl0tkMNGr77CVBWpWtMm28hTAaAj1S79c1YHB1PVjX2tg7lSC+ntGITK0G+wBaYgMPNkp/6smeEMNd4g40ScpkplId04gCYcj8SBJ3X+9h2dK2zBntw03oJ/m6enkujKJp+3Hxt9ImBEhzkiVdtTxcPBmY0ngnpIi7tLKAyexk8Ks4QXAHBHSD2Td/JwHjwzKg5RSxj81oGumVCWr4+URRnOYKRlcb/4pLdfyxI1A8JD05NJ/4vn67BYkdZg1N2ylnK1sXqTHOlXrkClylvjR04ueyQGWPo1u/poAxFi8NE79rpTvlYLjkD81oBzoQ41JI0kRLm50+afiVL0hqJMjTiwASTgX1nAfTH8Ax6E938=</ds:SignatureValue>
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloXDTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLlyrbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5EFUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8uvm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq614ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOpmTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCqlDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5kyPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/GcpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE28Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusOLT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDRUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OIAVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/OnS7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HAC8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1ZpoMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YEpF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTuvr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMClpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="pfx26f982d1-6aca-5b26-994d-a8888512f267" Version="2.0" IssueInstant="2014-07-17T01:01:48Z">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#pfx26f982d1-6aca-5b26-994d-a8888512f267"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>RKRMiMTyqeDeP1ezjyUmUJj6fm8=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>SvGe1hku9iTEtqdXhJJ8F+NLtBtQo1Ak8d2kSdNnkWR7DErBMDQom8mgzWxtkAutC7peJN60zYo8yyY+HSqNAQAVFWlHLocbI3+X+g2ZjEk6vJtjVtkRKopyukHymZ3Htg9kPdsPxmYH3rHHY2Q111nj9ClzEG0kYrdtU39GZNEAHH7Y8EZvYg9nqqpTovBMXxquY4PhkiGPpHyvhaqmxX0I+AfBiCqGilsiydkbdnTaj+5Fzsp05BDZtJqsaozTe2o2ykAjyST8S8SAfstsjS145VRnfuAazSFVEtKlRh/DTalZeIxTZxWZNF0t/kD83lI/D7I2tbDycSFO2ZX4csXEQ4O7r8alHNDel8eycUQ+BLFU9/IuUhBqAkkUZ4S59GzEn9rOw4q1Vyn91Lo5HEYP8pGCMnKgpO5QGJeGTv0UrenNMwSFYm7tMjPazPDlRjZ2ftDoa23PjL6Ub6mLUcdH1a2I7xur4QFk6UUeBZHsTN6lpkNSZ4byCoOAcplLKZLYc5C4Te9sul05w0JpXdo6GyrBKc7YnjHdwjZlBTf8GNepDaJrsvnmbesI05zlSuE/vMnQ2VEcQPReg2rALHHbSee89aHSYYdHYdNcl/Fj9oevtC9vENnWsLzAnFeKu/Qxe8Ty9yC9fI4gb5nHlhTxUuGL2WK+4/3rEPAhb9g=</ds:SignatureValue>
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloXDTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLlyrbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5EFUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8uvm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq614ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOpmTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCqlDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5kyPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/GcpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE28Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusOLT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDRUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OIAVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/OnS7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HAC8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1ZpoMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YEpF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTuvr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMClpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
<saml:AudienceRestriction>
<saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
<saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAw
HTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloX
DTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVk
IENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYt
c2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLly
rbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5E
FUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8u
vm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa
9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq6
14ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOp
mTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCq
lDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5k
yPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/G
cpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE2
8Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusO
LT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJD
RUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNp
Z25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OI
AVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/own
7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1
QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HA
C8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2
tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1Z
poMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3
Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YE
pF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTu
vr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMC
lpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m
46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=
-----END CERTIFICATE-----

View File

@@ -9,7 +9,7 @@ from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response, load_fixture
from authentik.sources.saml.exceptions import InvalidEncryption
from authentik.sources.saml.exceptions import InvalidEncryption, InvalidSignature
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.response import ResponseProcessor
@@ -125,3 +125,136 @@ class TestResponseProcessor(TestCase):
parser = ResponseProcessor(self.source, request)
with self.assertRaises(InvalidEncryption):
parser.parse()
def test_verification_assertion(self):
"""Test verifying signature inside assertion"""
key = load_fixture("fixtures/signature_cert.pem")
kp = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=key,
)
self.source.verification_kp = kp
self.source.signed_assertion = True
self.source.signed_response = False
request = self.factory.post(
"/",
data={
"SAMLResponse": b64encode(
load_fixture("fixtures/response_signed_assertion.xml").encode()
).decode()
},
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
parser = ResponseProcessor(self.source, request)
parser.parse()
def test_verification_response(self):
"""Test verifying signature inside response"""
key = load_fixture("fixtures/signature_cert.pem")
kp = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=key,
)
self.source.verification_kp = kp
self.source.signed_response = True
self.source.signed_assertion = False
request = self.factory.post(
"/",
data={
"SAMLResponse": b64encode(
load_fixture("fixtures/response_signed_response.xml").encode()
).decode()
},
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
parser = ResponseProcessor(self.source, request)
parser.parse()
def test_verification_response_and_assertion(self):
"""Test verifying signature inside response and assertion"""
key = load_fixture("fixtures/signature_cert.pem")
kp = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=key,
)
self.source.verification_kp = kp
self.source.signed_assertion = True
self.source.signed_response = True
request = self.factory.post(
"/",
data={
"SAMLResponse": b64encode(
load_fixture("fixtures/response_signed_response_and_assertion.xml").encode()
).decode()
},
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
parser = ResponseProcessor(self.source, request)
parser.parse()
def test_verification_wrong_signature(self):
"""Test invalid signature fails"""
key = load_fixture("fixtures/signature_cert.pem")
kp = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=key,
)
self.source.verification_kp = kp
self.source.signed_assertion = True
request = self.factory.post(
"/",
data={
"SAMLResponse": b64encode(
# Same as response_signed_assertion.xml but the role name is altered
load_fixture("fixtures/response_signed_error.xml").encode()
).decode()
},
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
parser = ResponseProcessor(self.source, request)
with self.assertRaisesMessage(InvalidSignature, ""):
parser.parse()
def test_verification_no_signature(self):
"""Test rejecting response without signature when signed_assertion is True"""
key = load_fixture("fixtures/signature_cert.pem")
kp = CertificateKeyPair.objects.create(
name=generate_id(),
certificate_data=key,
)
self.source.verification_kp = kp
self.source.signed_assertion = True
request = self.factory.post(
"/",
data={
"SAMLResponse": b64encode(
load_fixture("fixtures/response_success.xml").encode()
).decode()
},
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
parser = ResponseProcessor(self.source, request)
with self.assertRaisesMessage(InvalidSignature, ""):
parser.parse()

View File

@@ -4,14 +4,14 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.users import PartialGroupSerializer
from authentik.sources.scim.models import SCIMSourceGroup
class SCIMSourceGroupSerializer(SourceSerializer):
"""SCIMSourceGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
group_obj = PartialGroupSerializer(source="group", read_only=True)
class Meta:

View File

@@ -2,7 +2,7 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.groups import PartialUserSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.scim.models import SCIMSourceUser
@@ -11,7 +11,7 @@ from authentik.sources.scim.models import SCIMSourceUser
class SCIMSourceUserSerializer(SourceSerializer):
"""SCIMSourceUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
user_obj = PartialUserSerializer(source="user", read_only=True)
class Meta:

View File

View File

@@ -0,0 +1,31 @@
"""Telegram source property mappings API"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.telegram.models import TelegramSourcePropertyMapping
class TelegramSourcePropertyMappingSerializer(PropertyMappingSerializer):
"""TelegramSourcePropertyMapping Serializer"""
class Meta(PropertyMappingSerializer.Meta):
model = TelegramSourcePropertyMapping
class TelegramSourcePropertyMappingFilter(PropertyMappingFilterSet):
"""Filter for TelegramSourcePropertyMapping"""
class Meta(PropertyMappingFilterSet.Meta):
model = TelegramSourcePropertyMapping
class TelegramSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""TelegramSourcePropertyMapping Viewset"""
queryset = TelegramSourcePropertyMapping.objects.all()
serializer_class = TelegramSourcePropertyMappingSerializer
filterset_class = TelegramSourcePropertyMappingFilter
search_fields = ["name"]
ordering = ["name"]

View File

@@ -0,0 +1,41 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.telegram.models import TelegramSource
class TelegramSourceSerializer(SourceSerializer):
class Meta:
model = TelegramSource
fields = SourceSerializer.Meta.fields + [
"bot_username",
"bot_token",
"request_message_access",
"pre_authentication_flow",
]
extra_kwargs = {
"bot_token": {"write_only": True},
}
class TelegramSourceViewSet(UsedByMixin, ModelViewSet):
queryset = TelegramSource.objects.all()
serializer_class = TelegramSourceSerializer
lookup_field = "slug"
filterset_fields = [
"pbm_uuid",
"name",
"slug",
"enabled",
"authentication_flow",
"enrollment_flow",
"policy_engine_mode",
"user_matching_mode",
"group_matching_mode",
"bot_username",
"request_message_access",
]
search_fields = ["name", "slug"]
ordering = ["name"]

View File

@@ -0,0 +1,33 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import (
GroupSourceConnectionSerializer,
GroupSourceConnectionViewSet,
UserSourceConnectionSerializer,
UserSourceConnectionViewSet,
)
from authentik.sources.telegram.models import (
GroupTelegramSourceConnection,
UserTelegramSourceConnection,
)
class UserTelegramSourceConnectionSerializer(UserSourceConnectionSerializer):
class Meta(UserSourceConnectionSerializer.Meta):
model = UserTelegramSourceConnection
fields = UserSourceConnectionSerializer.Meta.fields
class UserTelegramSourceConnectionViewSet(UserSourceConnectionViewSet, ModelViewSet):
queryset = UserTelegramSourceConnection.objects.all()
serializer_class = UserTelegramSourceConnectionSerializer
class GroupTelegramSourceConnectionSerializer(GroupSourceConnectionSerializer):
class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupTelegramSourceConnection
class GroupTelegramSourceConnectionViewSet(GroupSourceConnectionViewSet, ModelViewSet):
queryset = GroupTelegramSourceConnection.objects.all()
serializer_class = GroupTelegramSourceConnectionSerializer

View File

@@ -0,0 +1,9 @@
from authentik.blueprints.apps import ManagedAppConfig
class TelegramConfig(ManagedAppConfig):
name = "authentik.sources.telegram"
label = "authentik_sources_telegram"
verbose_name = "authentik Sources.Telegram"
mountpoint = "source/telegram/"
default = True

View File

@@ -0,0 +1,118 @@
# Generated by Django 5.1.12 on 2025-09-24 07:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_core", "0050_user_last_updated_and_more"),
("authentik_flows", "0028_flowtoken_revoke_on_execution"),
]
operations = [
migrations.CreateModel(
name="GroupTelegramSourceConnection",
fields=[
(
"groupsourceconnection_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.groupsourceconnection",
),
),
],
options={
"verbose_name": "Group Telegram Source Connection",
"verbose_name_plural": "Group Telegram Source Connections",
},
bases=("authentik_core.groupsourceconnection",),
),
migrations.CreateModel(
name="TelegramSourcePropertyMapping",
fields=[
(
"propertymapping_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "Telegram Source Property Mapping",
"verbose_name_plural": "Telegram Source Property Mappings",
},
bases=("authentik_core.propertymapping",),
),
migrations.CreateModel(
name="UserTelegramSourceConnection",
fields=[
(
"usersourceconnection_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.usersourceconnection",
),
),
],
options={
"verbose_name": "User Telegram Source Connection",
"verbose_name_plural": "User Telegram Source Connections",
},
bases=("authentik_core.usersourceconnection",),
),
migrations.CreateModel(
name="TelegramSource",
fields=[
(
"source_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.source",
),
),
("bot_username", models.TextField(help_text="Telegram bot username")),
("bot_token", models.TextField(help_text="Telegram bot token")),
(
"request_message_access",
models.BooleanField(
default=False, help_text="Request access to send messages from your bot."
),
),
(
"pre_authentication_flow",
models.ForeignKey(
help_text="Flow used before authentication.",
on_delete=django.db.models.deletion.CASCADE,
related_name="telegram_source_pre_authentication",
to="authentik_flows.flow",
),
),
],
options={
"verbose_name": "Telegram Source",
"verbose_name_plural": "Telegram Sources",
},
bases=("authentik_core.source",),
),
]

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