Compare commits

..

190 Commits

Author SHA1 Message Date
Teffen Ellis
b7a478cc42 Bump package. Fix publish. 2026-02-26 16:10:16 +01:00
dependabot[bot]
f1c02de959 ci: bump actions/setup-go from 6.2.0 to 6.3.0 in /.github/actions/setup (#20595)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](7a3fe6cf4c...4b73464bb3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.3.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>
2026-02-26 13:13:21 +01:00
Dominic R
c54011bd8a website/integrations: ak_groups -> groups (#20579) 2026-02-26 08:59:10 +00:00
Dominic R
393a4aa75d providers/scim: ak_groups -> groups in tests (#20580) 2026-02-26 04:20:03 +01:00
dependabot[bot]
e6e3c87dc3 core: bump astral-sh/uv from 0.10.4 to 0.10.6 in /lifecycle/container (#20557)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.10.4 to 0.10.6.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.10.4...0.10.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 17:29:33 +01:00
dependabot[bot]
56f96985e3 web: bump the goauthentik group across 1 directory with 3 updates (#20551)
Bumps the goauthentik group with 3 updates in the /web directory: [@goauthentik/esbuild-plugin-live-reload](https://github.com/goauthentik/authentik/tree/HEAD/packages/esbuild-plugin-live-reload), [@goauthentik/eslint-config](https://github.com/goauthentik/authentik/tree/HEAD/packages/eslint-config) and [@goauthentik/prettier-config](https://github.com/goauthentik/authentik/tree/HEAD/packages/prettier-config).


Updates `@goauthentik/esbuild-plugin-live-reload` from 1.5.0 to 1.6.0
- [Release notes](https://github.com/goauthentik/authentik/releases)
- [Commits](https://github.com/goauthentik/authentik/commits/HEAD/packages/esbuild-plugin-live-reload)

Updates `@goauthentik/eslint-config` from 1.2.1 to 1.3.0
- [Release notes](https://github.com/goauthentik/authentik/releases)
- [Commits](https://github.com/goauthentik/authentik/commits/HEAD/packages/eslint-config)

Updates `@goauthentik/prettier-config` from 3.4.0 to 3.4.1
- [Release notes](https://github.com/goauthentik/authentik/releases)
- [Commits](https://github.com/goauthentik/authentik/commits/HEAD/packages/prettier-config)

---
updated-dependencies:
- dependency-name: "@goauthentik/esbuild-plugin-live-reload"
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: goauthentik
- dependency-name: "@goauthentik/eslint-config"
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: goauthentik
- dependency-name: "@goauthentik/prettier-config"
  dependency-version: 3.4.1
  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>
2026-02-25 15:01:44 +01:00
dependabot[bot]
d186f8b38e core: bump github.com/getsentry/sentry-go from 0.42.0 to 0.43.0 (#20552)
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.42.0 to 0.43.0.
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.42.0...v0.43.0)

---
updated-dependencies:
- dependency-name: github.com/getsentry/sentry-go
  dependency-version: 0.43.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>
2026-02-25 15:00:42 +01:00
authentik-automation[bot]
466f0f4137 core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1771856193 (#20475)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-25 13:55:25 +00:00
dependabot[bot]
223a8ffbb6 core: bump axllent/mailpit from v1.29.1 to v1.29.2 in /tests/e2e (#20553)
Bumps axllent/mailpit from v1.29.1 to v1.29.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:43:39 +01:00
dependabot[bot]
4831497369 core: bump goauthentik/selenium from 144.0-ak-0.40.0 to 145.0-ak-0.40.0 in /tests/e2e (#20554)
core: bump goauthentik/selenium in /tests/e2e

Bumps [goauthentik/selenium](https://github.com/SeleniumHQ/docker-selenium) from 144.0-ak-0.40.0 to 145.0-ak-0.40.0.
- [Release notes](https://github.com/SeleniumHQ/docker-selenium/releases)
- [Commits](https://github.com/SeleniumHQ/docker-selenium/commits)

---
updated-dependencies:
- dependency-name: goauthentik/selenium
  dependency-version: 145.0-ak-0.40.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:43:31 +01:00
dependabot[bot]
675cce81b0 core: bump library/golang from 889885d to d0a3e4b in /lifecycle/container (#20556)
core: bump library/golang in /lifecycle/container

Bumps library/golang from `889885d` to `d0a3e4b`.

---
updated-dependencies:
- dependency-name: library/golang
  dependency-version: 1.26.0-trixie
  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>
2026-02-25 14:43:15 +01:00
Tom Klingenberg
4692079320 website/integrations: gitea: ak_groups -> groups (#20565)
* Fix condition checks for user groups in Gitea integration

update deprecated filter for gitea groups.

Signed-off-by: Tom Klingenberg <76167763+dronebeelinux@users.noreply.github.com>

* Apply suggestions from code review

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

---------

Signed-off-by: Tom Klingenberg <76167763+dronebeelinux@users.noreply.github.com>
Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-25 13:42:00 +00:00
dependabot[bot]
fd06ad2e5a lifecycle/aws: bump aws-cdk from 2.1106.1 to 2.1107.0 in /lifecycle/aws (#20493)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 13:40:59 +00:00
Simonyi Gergő
9e79ba29cc ci: add reason change to versions repo bump (#20562)
add `reason` change to versions repo bump
2026-02-25 14:32:32 +01:00
Jens L.
849c37806d internal: make http timeouts configurable (#20472)
* internal: make http timeouts configurable

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

* Changed formatting to match the rest of the doc

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-25 14:21:13 +01:00
dependabot[bot]
9f3d1c3f90 core: bump aws-cdk-lib from 2.239.0 to 2.240.0 (#20494)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:15:58 +01:00
dependabot[bot]
b4f66f43e5 core: bump goauthentik/fips-debian from d6def0a to 7b82e24 in /lifecycle/container (#20497)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:15:49 +01:00
dependabot[bot]
d3775be5d2 core: bump goauthentik/fips-python from bccefee to de8ad64 in /lifecycle/container (#20496)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:15:37 +01:00
authentik-automation[bot]
74329eb46b core, web: update translations (#20543)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-25 14:13:41 +01:00
dependabot[bot]
e0c36da8f9 core: bump library/nginx from 341bf0f to 0d1b1f0 in /website (#20559)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:12:04 +01:00
dependabot[bot]
b152cb4323 core: bump library/node from 43d1f7a to c58d9e7 in /website (#20558)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:11:57 +01:00
Dominic R
4c8916adde docs: fix typos and wording in docs and integrations (#20550) 2026-02-25 09:23:39 +00:00
dependabot[bot]
9b142e12e0 web: bump the eslint group across 1 directory with 5 updates (#20492)
* web: bump the eslint group across 1 directory with 5 updates

Bumps the eslint group with 5 updates in the /web directory:

| Package | From | To |
| --- | --- | --- |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.2` | `10.0.1` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.56.0` | `8.56.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.56.0` | `8.56.1` |
| [eslint](https://github.com/eslint/eslint) | `9.39.2` | `10.0.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.56.0` | `8.56.1` |



Updates `@eslint/js` from 9.39.2 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

Updates `@typescript-eslint/eslint-plugin` from 8.56.0 to 8.56.1
- [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.56.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.56.0 to 8.56.1
- [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.56.1/packages/parser)

Updates `eslint` from 9.39.2 to 10.0.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.2...v10.0.2)

Updates `typescript-eslint` from 8.56.0 to 8.56.1
- [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.56.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: eslint
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.56.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.56.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: eslint
  dependency-version: 10.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: eslint
- dependency-name: typescript-eslint
  dependency-version: 8.56.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: eslint
...

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

* Bump packages. Fix ESLint version ranges.

* Bump version. Update description.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-25 02:41:08 +00:00
Teffen Ellis
d30a18e0a5 web: Packagify Logger (#20541)
* Prep logger for use outside web workspace.

* Bump. Prep.

* Add to publish list.

* Update deps.

* Add package directory.
2026-02-25 02:03:25 +00:00
Dewi Roberts
61c594301f website/docs: remove bad logs redirect (#20522)
* Remove bad redirect

* Remove space
2026-02-25 01:12:08 +00:00
Dewi Roberts
54373cfea5 website/docs: revamp enterprise section (#20379)
* Begin

* WIP

* WIP

* WIP

* Fix link

* Fix spellig and links

* Enterprise vs enterprise plus

* Changes based on Tana's comment

* Update website/docs/enterprise/enterprise-features.md

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

* Update website/docs/enterprise/enterprise-features.md

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

* Update website/docs/enterprise/enterprise-features.md

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

* Update website/docs/enterprise/enterprise-features.md

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

* Apply suggestions

* Apply suggestion from Eric

* Update doc title after discussion with Tana

* Fix links

* Update website/docs/enterprise/manage-enterprise.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/enterprise/manage-enterprise.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Apply suggestions

* US dollars

* Apply Fletcher's suggestions

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-25 01:11:52 +00:00
Ken Sternberg
c29427cfbe web/flow: Tidy identification stage (#20261)
* web: Add InvalidationFlow to Radius Provider dialogues

## What

- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
  - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
    to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.

* This (temporary) change is needed to prevent the unit tests from failing.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes

* Revert "This (temporary) change is needed to prevent the unit tests from failing."

This reverts commit dddde09be5.

* website: fix bad escaping of URLs in release notes

## What

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

v2024.6.4 had entries that looked like this:

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

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

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

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

## Notes

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

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

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

* web/flow: refactor FlowExecutor so that client-side stage selection is separate from stage execution

# What

Extracts and normalizes the *massive* switch/case statement into a table, eliminating as much repetition as possible. Where the server-side stage token and the client-side component have the same tag, only one is required. There were three different patterns for prop definitions, and those have been regularized into an expression with a compile-time type check, and the most common one can be omitted from the stage definition table.

# Why

1.  Because it’s hella cleaner. Stages are clear and easy to spot in the table (especially when it’s alphabetically ordered, OMG). Stages that disagree in name with their components, stages that take props different from the “standard” set, and stages that need `import` statements, are all easy to identify.
2.  Because identifying what we *do* with our web components is critical to their success, and to the success of the styling system the authentik web team envisions. FlowExecutor provides selection and execution of stages, but it also provides the inspector, the locale selector, headers, footers, customizations, and branding. Clearing away clutter to make that easier to see makes future refactoring for compatibility mode and dark theme handling much easier.

* web/flow: clean up state representation in FlowExecutor

# What

Cleans up the state and lifecycle of FlowExecutor.

*As state lifecycle*, the two fields `challenge` and `flowInfo` are synonymous: they are modified at the same time, once in the setter, and once in `updated()`; flowInfo is always a derived consequence of that current challenge. Making `challenge` the property that we are monitoring and `flowInfo` a simple accessor on `challenge` eliminates duplication of state management.

Lit automatically schedules a re-render whenever `challenge` is changed; the `requestUpdate()` is therefore not needed.

With that, the only thing left is where or when to change the document title. That too is moved to `updated()` and happens without checking for need; it does no harm to replace a string with its own value, the performance loss is so small as to be non-existent, it will not confuse the browser or the environment. Eliminating an `if` and reducing the code surface to a pattern check is a win.

FlowExecutor now has only three states: Loading, Challenge Available, and… Inspector? Let’s see what we can do about cleaning these up as well. Loading and Challenge do not seem synonymous: the challenge should not be altered until the fetch is complete, to prevent blank displays.

* web/flow: dedupe the set error flow state

# What

Extracts the logic for setting the flow state to FlowError.

# Why

It was just duplication. Trying to clean up state management is easier when special state handling is isolated into a single method.

* web/flow: dedupe the creation of fresh FlowApi instances

# What

Generates a single instance of FlowApi() that the FlowExecutor can use over the course of its lifetime.

# Why

Looking at the code generated by OpenApi, it’s clear that the parameters with which the API commits network transactions are immutable after construction; likewise, our particular invocation of `DEFAULT_GLOBALS` is also immutable with respect to a single instance of the FlowExecutor. With that in mind, there’s no reason to keep rebuilding the same network transaction object over and over; just instantiate it and live with it. In the conflict between rules-of-thumb “Never store what you can express” and “Extract repetitious expressions into instances,” the latter rule wins here.

* Intermediate. Gonna check against results.

* web/flow: extract inspector into standalone lifecycle

# What

Removes all of the code from `FlowExecutor` related to the inspector and isolates it into its own component. The lifecycle of FlowExecutor’s inspector handling has been adjusted to maintain the existing behavior.

# How

FlowExecutor is reduced to merely presenting the button:

- In `FlowExecutor`:
  - Remove all the controls and references to FlowInspector
    - Remove the capabilities check
    - Remove the inspector listener
    - Remove the render guards
    - Remove the “inspect” PropVariant (and remove it from `FlowExecutorSelections`)
    - Remove the inspector toggle
    - Remove the inspector renderer
  - Always dispatch FlowAdvance events (if the inspector is not present they will be ignored)
  - Adjust `ak-stage-redirect` to not take “promptUser” as an attribute
  - Replace the whole render-inspector-button clause with `ak-flow-inspector-button`
  - Adjust CSS to use `ak-flow-inspector-button` instead of `.inspector-button`

RedirectStage now queries the context for inspector availability and state:

- In `stages/RedirectStage`:
  - Change `promptUser` from a property accessor to a simple accessor that queries the parent context for the inspector state
  - Remove the `@property` clause

FlowInspectorButton takes over these responsibilities, isolating this separate concern into a single file:

- Manages loaded, available, and open states
- Does the capabilities check
- Listens for FlowInspectorChangeEvents on the window object
- Renders nothing at all if the inspector is inaccessible or if the inspector is present and covering up the button
- On connectedCallback checks if the URL indicate the inspector should already be open
- Manages loading the FlowInspector on demand and toggles the drawer on state change

To my great surprise, `FlowInspector` itself required no changes.

* Initial experiment to move stages into the light.

* web/flow: clean up state representation in FlowExecutor (#20027)

* web/flow: clean up state representation in FlowExecutor

# What

Cleans up the state and lifecycle of FlowExecutor.

*As state lifecycle*, the two fields `challenge` and `flowInfo` are synonymous: they are modified at the same time, once in the setter, and once in `updated()`; flowInfo is always a derived consequence of that current challenge. Making `challenge` the property that we are monitoring and `flowInfo` a simple accessor on `challenge` eliminates duplication of state management.

Lit automatically schedules a re-render whenever `challenge` is changed; the `requestUpdate()` is therefore not needed.

With that, the only thing left is where or when to change the document title. That too is moved to `updated()` and happens without checking for need; it does no harm to replace a string with its own value, the performance loss is so small as to be non-existent, it will not confuse the browser or the environment. Eliminating an `if` and reducing the code surface to a pattern check is a win.

FlowExecutor now has only three states: Loading, Challenge Available, and… Inspector? Let’s see what we can do about cleaning these up as well. Loading and Challenge do not seem synonymous: the challenge should not be altered until the fetch is complete, to prevent blank displays.

* web/flow: dedupe the set error flow state (#20029)

* web/flow: dedupe the set error flow state

# What

Extracts the logic for setting the flow state to FlowError.

# Why

It was just duplication. Trying to clean up state management is easier when special state handling is isolated into a single method.

* Protected.

---------

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

---------

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

* Fix types.

* web: Flesh out module driven tag names.

* Experiment continues: first-tier into the light.

* web/flow: optimize table for type safety

# What

Separate out the “here’s how a stage is defined” from “Here’s how a stage is represented internally.” This gives us a nice central store of where to define how the server-side componentName relates to a client-side customElementName while also guaranteeing that the componenName or supplied customElementName exists and corresponds. Type safety has been preserved system-wide (thanks, @GirlBossRush!)

* Prettier is still having opinions.

* web/flow: re-arrange IdentificationStage for maintainability

# What

Every conditional section of the IdentificationStage has been separated out into its own individual render function. Where possible, the information passed to the renderer has been reduced to a bare minimum (i.e if the function only needed the `passwordlessUrl`, that’s the only thing that’s passed to it), which helps highlight some inconsistencies in the API.

# No change

This is a purely maintenance-level change to the code, to make it obvious what needs to be plumbed/corrected in order to expose our dialogs to password managers. No functionality has been changed.

# Why

Figuring out how to turn our web components into proper elements, where what they contain is not isolated from the view of password managers, requires pulling out the functionality into small, readable components.

# Future work

Doing this has exposed several fundamental issues:

- auto-redirect is a state change from one LoginChallenge to another under a collection of conditions available on the challenge, triggered when FlowExecutor writes a new challenge. “Which challenge?” in FlowExecutor ought to be handling this, not handing it off to IdentificationStage.

- Everything about Captcha is about Captcha. It ought to be in its own little state managing class, perhaps as a lit controller.

- The same is true about WebAuthn.

- `host` is doing very little work; at best, it’s receiving a “change this” or “submit that” message, which is an Event. Look forward to that.

* Tidy.

* Removed the cache; it's extra code for no benefit whatsoever; the table is constructed ONCE at start-up, there's never going to be a cache hit.  The FlowExecutorStageFactory produces StageMappings (StageMapping[]), which is itself a warehouse of singular server-component -> client-component relationships, fetching the client from the bundle as needed.  The StageMapping only does the fetch once per instance, so (for example) a password failure will reinstantiate a PasswordStage, but it will not fetch it a second time.

* Removed comments about the cache.  Added comments about where to find the FlowExecutor stage table. Moved the import of WebAuthnAuthenticticatorRegisterState from FlowExecutor.ts to FlowExecutorStages.ts; both files are bundled together, so this is a no-op functionally, but it's easier to confirm that StageEntries without import expressions (STageModuleCallbacks) have their stages bundled (pre-imported) if the import statement is in the same file.

* Of COURSE prettier had opinions!

* Since the check for `this.can(CapabilitiesEnum.CanDebug))` has been moved into the FlowInspectorButton, FlowExecutor no longer needs the capabilities check at all.

* Move the inspector into its own folder.

* web: Flesh out stage mapping error handling. (#20292)

Co-authored-by: Ken Sternberg <ken@goauthentik.io>

* Weird merge bug: same function appeared twice.

* Added some visibility keys, as per @GirlBossRush

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-24 16:35:32 -08:00
Simonyi Gergő
6912bbec77 website/docs: fix upgrade link in release notes (#20540)
fix upgrade link in release notes
2026-02-25 00:08:10 +00:00
Simonyi Gergő
b43276d39a website/docs: fix upgrade link in 2026.2 release notes (#20539)
fix upgrade link in `2026.2` release notes
2026-02-25 00:03:09 +00:00
Simonyi Gergő
80c3d73912 website/docs: update supported versions (#20534)
update supported versions
2026-02-24 22:16:56 +00:00
Simonyi Gergő
36f4d25f95 website/docs: create draft release notes for 2026.5 (#20529)
* create draft release notes for `2026.5`

* fixup! create draft release notes for `2026.5`
2026-02-24 22:40:17 +01:00
Diego Bravo
6269da916b Fix redirect URI in Seafile integration documentation (#20532)
If you just follow orders exactly as said here it'll lead you to an error because of the strict policy having a slash at the end but in the seahub_settings.py in the docs tells you to put it without it. This minor change can help people to not encounter this minimal error sometimes difficult to see.

Signed-off-by: Diego Bravo <10383549+DystopianRescuer@users.noreply.github.com>
2026-02-24 21:12:21 +00:00
Simonyi Gergő
884e662277 website/docs: autogenerate release notes (#20527)
* autogenerate minor changes and API changes

* lint

* spellcheck

This is in a commit message, so technically it's not correct, but
at this point I don't care :))

* finalize release notes for `2026.2`
2026-02-24 19:03:17 +00:00
Jens L.
34c5b7add3 providers/oauth2: add jti claim (#20484)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-24 18:38:11 +01:00
Jens L.
37e701e458 providers/oauth2: deactivate locale after testing (#20518)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-24 15:47:55 +01:00
Jens L.
edf5ec972f policies: fix PolicyEngineMode ALL with static binding optimization (#20430)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-24 15:47:12 +01:00
Jens L.
00722115bf website/docs: fix linux setup docs (#20508)
* docs: add auth config steps

* tweak

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

* Changed wording

* Fix broken link

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-24 13:18:55 +01:00
Simonyi Gergő
f5adf8d4c7 web: fix Edit Policy button on Flow view page (#20511)
fix Edit Policy button on Flow view page
2026-02-24 12:02:26 +00:00
Connor Peshek
d2b234cf86 endpoints: fix infinite recursion in stage with unsupported connector (#20485)
* stages: fix infinite recursion

* respect mode

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

* add tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2026-02-24 13:01:31 +01:00
Simonyi Gergő
a5df6820ce enterprise: add ES384 to enterprise license algorithms (#20507)
add `ES384` to enterprise license algorithms
2026-02-24 10:51:14 +00:00
Ken Sternberg
8d2b463a9a web/flow: fix typo in RedirectStage (#20488) 2026-02-24 11:15:32 +01:00
Dominic R
9da1014271 website/docs: fix GitHub social-login wording and capitalization (#20489)
Update the GitHub social-login guide to consistently reference GitHub Developer Settings and correct provider wording.

Standardize GitHub capitalization across the page text and inline policy comments.
2026-02-24 09:17:59 +00:00
dependabot[bot]
21be5fec43 web: bump knip from 5.84.1 to 5.85.0 in /web (#20464)
* web: bump knip from 5.84.1 to 5.85.0 in /web

Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 5.84.1 to 5.85.0.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@5.85.0/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 5.85.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Update prettier packages.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-24 03:15:58 +00:00
Dewi Roberts
a49764dee0 website/integrations: standardize resource sections and update template (#20423)
Standardize resource sections and update template

Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-24 02:27:37 +00:00
dependabot[bot]
089cc011f1 core, web: bump ajv from 6.12.6 to 6.14.0 in /packages/eslint-config (#20478)
* core, web: bump ajv from 6.12.6 to 6.14.0 in /packages/eslint-config

Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
...

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

* Bump packages. Fix order.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-24 03:12:39 +01:00
Connor Peshek
8b6be1d997 sources/saml: improve exception handling for saml response parsing (#20125)
improve exception handling
2026-02-23 15:31:35 -06:00
Ken Sternberg
179a9b76f2 web/flow: separate flow inspector lifecycle from flow executor lifecycle (#20063)
* web: Add InvalidationFlow to Radius Provider dialogues

## What

- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
  - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
    to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.

* This (temporary) change is needed to prevent the unit tests from failing.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes

* Revert "This (temporary) change is needed to prevent the unit tests from failing."

This reverts commit dddde09be5.

* website: fix bad escaping of URLs in release notes

## What

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

v2024.6.4 had entries that looked like this:

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

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

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

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

## Notes

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

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

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

* web/flow: refactor FlowExecutor so that client-side stage selection is separate from stage execution

# What

Extracts and normalizes the *massive* switch/case statement into a table, eliminating as much repetition as possible. Where the server-side stage token and the client-side component have the same tag, only one is required. There were three different patterns for prop definitions, and those have been regularized into an expression with a compile-time type check, and the most common one can be omitted from the stage definition table.

# Why

1.  Because it’s hella cleaner. Stages are clear and easy to spot in the table (especially when it’s alphabetically ordered, OMG). Stages that disagree in name with their components, stages that take props different from the “standard” set, and stages that need `import` statements, are all easy to identify.
2.  Because identifying what we *do* with our web components is critical to their success, and to the success of the styling system the authentik web team envisions. FlowExecutor provides selection and execution of stages, but it also provides the inspector, the locale selector, headers, footers, customizations, and branding. Clearing away clutter to make that easier to see makes future refactoring for compatibility mode and dark theme handling much easier.

* web/flow: clean up state representation in FlowExecutor

# What

Cleans up the state and lifecycle of FlowExecutor.

*As state lifecycle*, the two fields `challenge` and `flowInfo` are synonymous: they are modified at the same time, once in the setter, and once in `updated()`; flowInfo is always a derived consequence of that current challenge. Making `challenge` the property that we are monitoring and `flowInfo` a simple accessor on `challenge` eliminates duplication of state management.

Lit automatically schedules a re-render whenever `challenge` is changed; the `requestUpdate()` is therefore not needed.

With that, the only thing left is where or when to change the document title. That too is moved to `updated()` and happens without checking for need; it does no harm to replace a string with its own value, the performance loss is so small as to be non-existent, it will not confuse the browser or the environment. Eliminating an `if` and reducing the code surface to a pattern check is a win.

FlowExecutor now has only three states: Loading, Challenge Available, and… Inspector? Let’s see what we can do about cleaning these up as well. Loading and Challenge do not seem synonymous: the challenge should not be altered until the fetch is complete, to prevent blank displays.

* web/flow: dedupe the set error flow state

# What

Extracts the logic for setting the flow state to FlowError.

# Why

It was just duplication. Trying to clean up state management is easier when special state handling is isolated into a single method.

* web/flow: dedupe the creation of fresh FlowApi instances

# What

Generates a single instance of FlowApi() that the FlowExecutor can use over the course of its lifetime.

# Why

Looking at the code generated by OpenApi, it’s clear that the parameters with which the API commits network transactions are immutable after construction; likewise, our particular invocation of `DEFAULT_GLOBALS` is also immutable with respect to a single instance of the FlowExecutor. With that in mind, there’s no reason to keep rebuilding the same network transaction object over and over; just instantiate it and live with it. In the conflict between rules-of-thumb “Never store what you can express” and “Extract repetitious expressions into instances,” the latter rule wins here.

* Intermediate. Gonna check against results.

* web/flow: extract inspector into standalone lifecycle

# What

Removes all of the code from `FlowExecutor` related to the inspector and isolates it into its own component. The lifecycle of FlowExecutor’s inspector handling has been adjusted to maintain the existing behavior.

# How

FlowExecutor is reduced to merely presenting the button:

- In `FlowExecutor`:
  - Remove all the controls and references to FlowInspector
    - Remove the capabilities check
    - Remove the inspector listener
    - Remove the render guards
    - Remove the “inspect” PropVariant (and remove it from `FlowExecutorSelections`)
    - Remove the inspector toggle
    - Remove the inspector renderer
  - Always dispatch FlowAdvance events (if the inspector is not present they will be ignored)
  - Adjust `ak-stage-redirect` to not take “promptUser” as an attribute
  - Replace the whole render-inspector-button clause with `ak-flow-inspector-button`
  - Adjust CSS to use `ak-flow-inspector-button` instead of `.inspector-button`

RedirectStage now queries the context for inspector availability and state:

- In `stages/RedirectStage`:
  - Change `promptUser` from a property accessor to a simple accessor that queries the parent context for the inspector state
  - Remove the `@property` clause

FlowInspectorButton takes over these responsibilities, isolating this separate concern into a single file:

- Manages loaded, available, and open states
- Does the capabilities check
- Listens for FlowInspectorChangeEvents on the window object
- Renders nothing at all if the inspector is inaccessible or if the inspector is present and covering up the button
- On connectedCallback checks if the URL indicate the inspector should already be open
- Manages loading the FlowInspector on demand and toggles the drawer on state change

To my great surprise, `FlowInspector` itself required no changes.

* web/flow: clean up state representation in FlowExecutor (#20027)

* web/flow: clean up state representation in FlowExecutor

# What

Cleans up the state and lifecycle of FlowExecutor.

*As state lifecycle*, the two fields `challenge` and `flowInfo` are synonymous: they are modified at the same time, once in the setter, and once in `updated()`; flowInfo is always a derived consequence of that current challenge. Making `challenge` the property that we are monitoring and `flowInfo` a simple accessor on `challenge` eliminates duplication of state management.

Lit automatically schedules a re-render whenever `challenge` is changed; the `requestUpdate()` is therefore not needed.

With that, the only thing left is where or when to change the document title. That too is moved to `updated()` and happens without checking for need; it does no harm to replace a string with its own value, the performance loss is so small as to be non-existent, it will not confuse the browser or the environment. Eliminating an `if` and reducing the code surface to a pattern check is a win.

FlowExecutor now has only three states: Loading, Challenge Available, and… Inspector? Let’s see what we can do about cleaning these up as well. Loading and Challenge do not seem synonymous: the challenge should not be altered until the fetch is complete, to prevent blank displays.

* web/flow: dedupe the set error flow state (#20029)

* web/flow: dedupe the set error flow state

# What

Extracts the logic for setting the flow state to FlowError.

# Why

It was just duplication. Trying to clean up state management is easier when special state handling is isolated into a single method.

* Protected.

---------

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

---------

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

* Fix types.

* web: Flesh out module driven tag names.

* web/flow: optimize table for type safety

# What

Separate out the “here’s how a stage is defined” from “Here’s how a stage is represented internally.” This gives us a nice central store of where to define how the server-side componentName relates to a client-side customElementName while also guaranteeing that the componenName or supplied customElementName exists and corresponds. Type safety has been preserved system-wide (thanks, @GirlBossRush!)

* Prettier is still having opinions.

* Tidy.

* Removed the cache; it's extra code for no benefit whatsoever; the table is constructed ONCE at start-up, there's never going to be a cache hit.  The FlowExecutorStageFactory produces StageMappings (StageMapping[]), which is itself a warehouse of singular server-component -> client-component relationships, fetching the client from the bundle as needed.  The StageMapping only does the fetch once per instance, so (for example) a password failure will reinstantiate a PasswordStage, but it will not fetch it a second time.

* Removed comments about the cache.  Added comments about where to find the FlowExecutor stage table. Moved the import of WebAuthnAuthenticticatorRegisterState from FlowExecutor.ts to FlowExecutorStages.ts; both files are bundled together, so this is a no-op functionally, but it's easier to confirm that StageEntries without import expressions (STageModuleCallbacks) have their stages bundled (pre-imported) if the import statement is in the same file.

* Of COURSE prettier had opinions!

* Since the check for `this.can(CapabilitiesEnum.CanDebug))` has been moved into the FlowInspectorButton, FlowExecutor no longer needs the capabilities check at all.

* Move the inspector into its own folder.

* web: Flesh out stage mapping error handling. (#20292)

Co-authored-by: Ken Sternberg <ken@goauthentik.io>

* Weird merge bug: same function appeared twice.

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-23 12:59:40 -08:00
Ken Sternberg
254bfd2a60 web/maintenance: no unknown attributes part 2 (#19014)
* web: Add InvalidationFlow to Radius Provider dialogues

## What

- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
  - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
    to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.

* This (temporary) change is needed to prevent the unit tests from failing.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes

* Revert "This (temporary) change is needed to prevent the unit tests from failing."

This reverts commit dddde09be5.

* website: fix bad escaping of URLs in release notes

## What

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

v2024.6.4 had entries that looked like this:

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

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

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

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

## Notes

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

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

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

* web/maintenance/no-unknown-attributes-1

# What

This commit is a collection of fixes and adaptations discovered while running lit-analyzer in a stricter role than usual. These fixes are to 9 of the existing issues; there are 16 more that will be addressed in the next two pull requests.

The following issues were uncovered.

- `ak-slug-input` does not take `autocomplete`.
- `ak-wizard-page-type-create` does not take, or use, the `name` attribute. It also has no `value` of its own, so it is not processed as a form object.
- `ak-endpoints-device-access-groups-form` does not take a `pk` attribute. It takes an `.instancePk` property.
- `ak-provider-oauth2-redirect-uri` is only used in one place, and that place uses the term `input-id` for the key. The component was expected `inputId`. Since it is a string and therefore an attribute, kebab-case is the appropriate fix here.
- `input-mode` is not a valid attribute. The attribute is `inputmode`, and the property is `inputMode`. It may not be undefined. If it is defined, the default is `text`. I have fixed this in the attribute and in the two Forms that used it.
- `form-associated-element` had both `name` and `type` as readonly. Since they are native attributes, they can be attributes or they can be readonly. They can’t be both. I have made them read-write.
- `user-source-settings-page` is only used in one place, and that place uses the term `input-id` for the key. The component was expected `inputId`. Since it is a string and therefore an attribute, kebab-case is the appropriate fix here.

These guideposts will be placed on the PR.

* Update web/src/admin/providers/oauth2/OAuth2ProviderRedirectURI.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>

* Update web/src/components/ak-text-input.ts

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>

* web/maintenance/no-unknown-attributes-2

# What

This commit is a collection of fixes and adaptations discovered while running lit-analyzer in a stricter role than usual.

- `src/admin/endpoints/connectors/agent/AgentConnectorSetup.ts`

After talking to @beryju, we determined that these labels aren’t shown and aren’t used.

- `src/admin/admin-overview/AdminOverviewPage.ts`
- `src/admin/admin-overview/DashboardUserPage.ts`
- `src/elements/cards/AggregatePromiseCard.ts`
- `src/elements/cards/stories/AggregatePromiseCard.stories.ts`

The `Promise` version of our card is not used by any client code. The Dashboard pages that were importing it want the vanilla `AggregateCard` instead.

- `./src/flow/stages/identification/IdentificationStage.ts`

Anchors do not have a `name` attribute, I cannot find any code using the name attributes as lookups, nor any CSS that might use the name attributes as guides. `ak-flow-password-input` is always required; the flag is unsupported and unnecessary.

- `./src/flow/stages/password/PasswordStage.ts`

Anchors do not have a `name` attribute, I cannot find any code using the name attributes as lookups, nor any CSS that might use the name attributes as guides. `ak-flow-password-input` is always required; the flag is unsupported and unnecessary.

- `src/user/user-settings/UserSettingsPage.ts`

This change to the `UserSettingsPage`:

``` diff
-                                userId=${ifPresent(currentUser?.pk)}
+                                user-id=${ifPresent(currentUser?.pk)}
```

… corresponds correctly with:

``` typescript
    @property({ type: Number, attribute: "user-id" })
    public userId?: number;
```

I find it odd (and remarkable) that nobody has complained about this yet. I even went so far as to [confirm my understanding](https://codepen.io/kensternberg-authentik/pen/raLNBwO) and, yes:

- when an attribute is truthy, property syntax does not set the field
- when an attribute is deliberately given a kebab-case name, using the camelCase variant does not set the field

However, when the attribute is truthy, attribute names are case-insensitive: ‘user-id’ and ‘User-Id’ in client code would work just fine.

## Note

A large enough number of warnings remain. Some of those are due to `lit-analyzer` not being updated to recognize newly Baseline global DOM properties like `inert` or `popover`. The rest are from RapiDoc and QrCode, which do not supply sufficient documentation or metadata for Lit-anaylzer to read correctly.

* web/bug/hidden-secrets-not-propagating

# What

This commit updates ak-secret-text-input, adding the `name` attribute to all valid input fields and updating the value writer to match those of known-working components, to ensure that either variety of the display is fully and correctly updated with the content of the hidden secret.

# Why

The hidden input field is the one that HorizontalFormElement was expecting to read its value from, but that field never received a `name` because it wasn’t present when the field was first updated.

HorizontalFormElement writes the `name` field to the first `<input>` it finds. That was the “dummy” input field, which has no working value.

Form ignored the input element because the value it read came with an undefined name.

Object-oriented state management sometimes bites.

* Turns out, I was wrong. Someone *does* use the `name` attribute: the tests.  MDN says that `name`
is incorrect, and we should use `id` instead.  I have compromised; I have switched to using the
Open UI Automation ID instead, since that's what we're doing: automated tests.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes

---------

Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2026-02-23 12:55:26 -08:00
Tana M Berry
991f5fc536 website/docs: add info about make install and recovery key (#20447)
* add info about make install and recovery key

* fix formatting on troubleshooting tip

* Apply suggestion from @dominic-r

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

* tweak to bump

* tweak

* tweaked words abouot make install per jens

* build

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2026-02-23 13:18:59 -06:00
dependabot[bot]
72a9a1ab76 web: bump ajv from 6.12.6 to 6.14.0 in /web (#20479)
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 17:31:00 +01:00
Brolywood
17ab3a4b73 providers/proxy: preserve URL-encoded path characters in redirect (#20476)
Use r.URL.EscapedPath() instead of r.URL.Path when building the
redirect URL in redirectToStart(). The decoded Path field converts
%2F to /, which url.JoinPath then collapses via path.Clean, stripping
encoded slashes from the URL. EscapedPath() preserves the original
encoding, fixing 301 redirects that break apps like RabbitMQ which
use %2F in their API paths.
2026-02-23 17:30:47 +01:00
Jens L.
93e916c8ad policies: measure policy process from manager (#20477)
* policies: measure policy process from manager

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

* fix constructor

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-23 17:30:36 +01:00
Simonyi Gergő
52e2460927 enterprise: monkey patch pyjwt to accept mismatching key (#20402)
* monkey patch pyjwt to accept mismatching key

* restore `_validate_curve` after monkeypatch

* add explanatory comment

* next year is 2027, dummy
2026-02-23 14:55:57 +01:00
Alexander Tereshkin
9ba7b373b1 enterprise/lifecycle: use datetime instead of date to track review cycles (#20283)
* enterprise/lifecycle: use datetime instead of date to track review cycles (fix for #20265)

* Update authentik/enterprise/lifecycle/api/iterations.py

Co-authored-by: Jens L. <jens@beryju.org>
Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>

* enterprise/lifecycle: replace extend_schema_field with type annotations

---------

Signed-off-by: Alexander Tereshkin <96586+atereshkin@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens L. <jens@beryju.org>
2026-02-23 14:55:44 +01:00
Simonyi Gergő
6e04a4264e root: run npm i with npm@11.10.1 in all subdirectories (#20471)
run `npm i` with `npm@11.10.1` in all subdirectories
2026-02-23 13:34:19 +01:00
Michael Beigelmacher
d9df013a48 providers/oauth2: device code flow client id via auth header (#20457)
* Use `extract_client_auth` which can get client id from either HTTP
Authorization header or POST body

* Update documentation to reflect allow sending client id via header

* Add tests for using HTTP Basic Auth to pass in client id
2026-02-23 13:18:07 +01:00
dependabot[bot]
278c8e7098 core: bump goauthentik/fips-debian from 4419749 to d6def0a in /lifecycle/container (#20467)
core: bump goauthentik/fips-debian in /lifecycle/container

Bumps goauthentik/fips-debian from `4419749` to `d6def0a`.

---
updated-dependencies:
- dependency-name: goauthentik/fips-debian
  dependency-version: trixie-slim-fips
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 12:21:48 +01:00
dependabot[bot]
0055518f99 core: bump goauthentik/fips-python from d973c46 to bccefee in /lifecycle/container (#20466)
core: bump goauthentik/fips-python in /lifecycle/container

Bumps goauthentik/fips-python from `d973c46` to `bccefee`.

---
updated-dependencies:
- dependency-name: goauthentik/fips-python
  dependency-version: 3.14.3-slim-trixie-fips
  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>
2026-02-23 12:19:00 +01:00
dependabot[bot]
1be23e2ae9 core, web: bump ajv from 6.12.6 to 6.14.0 in /packages/prettier-config (#20462)
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-22 17:44:20 +01:00
Jens L.
f62479c2c7 ci: bump and fix daily (#20461)
* ci: bump and fix daily

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

* run daily ci when changed

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

* fix missing dir

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

* download to correct path

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-22 15:29:35 +01:00
Daniil Kamakin
7a28837622 website/integrations: fix Vaultwarden SSO_SCOPES syntax (#20459)
fix Vaultwarden SSO_SCOPES syntax

Remove literal quotes to prevent injection errors and remove redundant openid scope

Signed-off-by: Daniil Kamakin <54929583+dkamakin@users.noreply.github.com>
2026-02-22 12:44:42 +00:00
Jens L.
1031b050c2 stages/user_login: log correct user when session binding is broken (#20094)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-21 18:37:59 +01:00
Ken Sternberg
395e6829d0 web/flow: generate a single API object for network transactions and use it for the lifetime of the FlowExecutor (#20030)
* web: Add InvalidationFlow to Radius Provider dialogues

## What

- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
  - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
    to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.

* This (temporary) change is needed to prevent the unit tests from failing.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes

* Revert "This (temporary) change is needed to prevent the unit tests from failing."

This reverts commit dddde09be5.

* website: fix bad escaping of URLs in release notes

## What

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

v2024.6.4 had entries that looked like this:

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

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

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

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

## Notes

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

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

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

* web/flow: refactor FlowExecutor so that client-side stage selection is separate from stage execution

# What

Extracts and normalizes the *massive* switch/case statement into a table, eliminating as much repetition as possible. Where the server-side stage token and the client-side component have the same tag, only one is required. There were three different patterns for prop definitions, and those have been regularized into an expression with a compile-time type check, and the most common one can be omitted from the stage definition table.

# Why

1.  Because it’s hella cleaner. Stages are clear and easy to spot in the table (especially when it’s alphabetically ordered, OMG). Stages that disagree in name with their components, stages that take props different from the “standard” set, and stages that need `import` statements, are all easy to identify.
2.  Because identifying what we *do* with our web components is critical to their success, and to the success of the styling system the authentik web team envisions. FlowExecutor provides selection and execution of stages, but it also provides the inspector, the locale selector, headers, footers, customizations, and branding. Clearing away clutter to make that easier to see makes future refactoring for compatibility mode and dark theme handling much easier.

* web/flow: clean up state representation in FlowExecutor

# What

Cleans up the state and lifecycle of FlowExecutor.

*As state lifecycle*, the two fields `challenge` and `flowInfo` are synonymous: they are modified at the same time, once in the setter, and once in `updated()`; flowInfo is always a derived consequence of that current challenge. Making `challenge` the property that we are monitoring and `flowInfo` a simple accessor on `challenge` eliminates duplication of state management.

Lit automatically schedules a re-render whenever `challenge` is changed; the `requestUpdate()` is therefore not needed.

With that, the only thing left is where or when to change the document title. That too is moved to `updated()` and happens without checking for need; it does no harm to replace a string with its own value, the performance loss is so small as to be non-existent, it will not confuse the browser or the environment. Eliminating an `if` and reducing the code surface to a pattern check is a win.

FlowExecutor now has only three states: Loading, Challenge Available, and… Inspector? Let’s see what we can do about cleaning these up as well. Loading and Challenge do not seem synonymous: the challenge should not be altered until the fetch is complete, to prevent blank displays.

* web/flow: dedupe the set error flow state

# What

Extracts the logic for setting the flow state to FlowError.

# Why

It was just duplication. Trying to clean up state management is easier when special state handling is isolated into a single method.

* web/flow: dedupe the creation of fresh FlowApi instances

# What

Generates a single instance of FlowApi() that the FlowExecutor can use over the course of its lifetime.

# Why

Looking at the code generated by OpenApi, it’s clear that the parameters with which the API commits network transactions are immutable after construction; likewise, our particular invocation of `DEFAULT_GLOBALS` is also immutable with respect to a single instance of the FlowExecutor. With that in mind, there’s no reason to keep rebuilding the same network transaction object over and over; just instantiate it and live with it. In the conflict between rules-of-thumb “Never store what you can express” and “Extract repetitious expressions into instances,” the latter rule wins here.

* web/flow: clean up state representation in FlowExecutor (#20027)

* web/flow: clean up state representation in FlowExecutor

# What

Cleans up the state and lifecycle of FlowExecutor.

*As state lifecycle*, the two fields `challenge` and `flowInfo` are synonymous: they are modified at the same time, once in the setter, and once in `updated()`; flowInfo is always a derived consequence of that current challenge. Making `challenge` the property that we are monitoring and `flowInfo` a simple accessor on `challenge` eliminates duplication of state management.

Lit automatically schedules a re-render whenever `challenge` is changed; the `requestUpdate()` is therefore not needed.

With that, the only thing left is where or when to change the document title. That too is moved to `updated()` and happens without checking for need; it does no harm to replace a string with its own value, the performance loss is so small as to be non-existent, it will not confuse the browser or the environment. Eliminating an `if` and reducing the code surface to a pattern check is a win.

FlowExecutor now has only three states: Loading, Challenge Available, and… Inspector? Let’s see what we can do about cleaning these up as well. Loading and Challenge do not seem synonymous: the challenge should not be altered until the fetch is complete, to prevent blank displays.

* web/flow: dedupe the set error flow state (#20029)

* web/flow: dedupe the set error flow state

# What

Extracts the logic for setting the flow state to FlowError.

# Why

It was just duplication. Trying to clean up state management is easier when special state handling is isolated into a single method.

* Protected.

---------

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

---------

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

* Fix types.

* web: Flesh out module driven tag names.

* web/flow: optimize table for type safety

# What

Separate out the “here’s how a stage is defined” from “Here’s how a stage is represented internally.” This gives us a nice central store of where to define how the server-side componentName relates to a client-side customElementName while also guaranteeing that the componenName or supplied customElementName exists and corresponds. Type safety has been preserved system-wide (thanks, @GirlBossRush!)

* Tidy.

* Removed the cache; it's extra code for no benefit whatsoever; the table is constructed ONCE at start-up, there's never going to be a cache hit.  The FlowExecutorStageFactory produces StageMappings (StageMapping[]), which is itself a warehouse of singular server-component -> client-component relationships, fetching the client from the bundle as needed.  The StageMapping only does the fetch once per instance, so (for example) a password failure will reinstantiate a PasswordStage, but it will not fetch it a second time.

* Removed comments about the cache.  Added comments about where to find the FlowExecutor stage table. Moved the import of WebAuthnAuthenticticatorRegisterState from FlowExecutor.ts to FlowExecutorStages.ts; both files are bundled together, so this is a no-op functionally, but it's easier to confirm that StageEntries without import expressions (STageModuleCallbacks) have their stages bundled (pre-imported) if the import statement is in the same file.

* web: Flesh out stage mapping error handling. (#20292)

Co-authored-by: Ken Sternberg <ken@goauthentik.io>

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-20 18:11:58 -08:00
Ken Sternberg
6e91af6dfc web/flow: refactor flow executor so component selection is in an easy-to-maintain table (#19999)
* web: Add InvalidationFlow to Radius Provider dialogues

## What

- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
  - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
    to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.

* This (temporary) change is needed to prevent the unit tests from failing.

\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes

* Revert "This (temporary) change is needed to prevent the unit tests from failing."

This reverts commit dddde09be5.

* website: fix bad escaping of URLs in release notes

## What

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

v2024.6.4 had entries that looked like this:

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

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

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

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

## Notes

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

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

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

* web/flow: refactor FlowExecutor so that client-side stage selection is separate from stage execution

# What

Extracts and normalizes the *massive* switch/case statement into a table, eliminating as much repetition as possible. Where the server-side stage token and the client-side component have the same tag, only one is required. There were three different patterns for prop definitions, and those have been regularized into an expression with a compile-time type check, and the most common one can be omitted from the stage definition table.

# Why

1.  Because it’s hella cleaner. Stages are clear and easy to spot in the table (especially when it’s alphabetically ordered, OMG). Stages that disagree in name with their components, stages that take props different from the “standard” set, and stages that need `import` statements, are all easy to identify.
2.  Because identifying what we *do* with our web components is critical to their success, and to the success of the styling system the authentik web team envisions. FlowExecutor provides selection and execution of stages, but it also provides the inspector, the locale selector, headers, footers, customizations, and branding. Clearing away clutter to make that easier to see makes future refactoring for compatibility mode and dark theme handling much easier.

* web/flow: clean up state representation in FlowExecutor (#20027)

* web/flow: clean up state representation in FlowExecutor

# What

Cleans up the state and lifecycle of FlowExecutor.

*As state lifecycle*, the two fields `challenge` and `flowInfo` are synonymous: they are modified at the same time, once in the setter, and once in `updated()`; flowInfo is always a derived consequence of that current challenge. Making `challenge` the property that we are monitoring and `flowInfo` a simple accessor on `challenge` eliminates duplication of state management.

Lit automatically schedules a re-render whenever `challenge` is changed; the `requestUpdate()` is therefore not needed.

With that, the only thing left is where or when to change the document title. That too is moved to `updated()` and happens without checking for need; it does no harm to replace a string with its own value, the performance loss is so small as to be non-existent, it will not confuse the browser or the environment. Eliminating an `if` and reducing the code surface to a pattern check is a win.

FlowExecutor now has only three states: Loading, Challenge Available, and… Inspector? Let’s see what we can do about cleaning these up as well. Loading and Challenge do not seem synonymous: the challenge should not be altered until the fetch is complete, to prevent blank displays.

* web/flow: dedupe the set error flow state (#20029)

* web/flow: dedupe the set error flow state

# What

Extracts the logic for setting the flow state to FlowError.

# Why

It was just duplication. Trying to clean up state management is easier when special state handling is isolated into a single method.

* Protected.

---------

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

---------

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

* Fix types.

* web: Flesh out module driven tag names.

* web/flow: optimize table for type safety

# What

Separate out the “here’s how a stage is defined” from “Here’s how a stage is represented internally.” This gives us a nice central store of where to define how the server-side componentName relates to a client-side customElementName while also guaranteeing that the componenName or supplied customElementName exists and corresponds. Type safety has been preserved system-wide (thanks, @GirlBossRush!)

* Tidy.

* Removed the cache; it's extra code for no benefit whatsoever; the table is constructed ONCE at start-up, there's never going to be a cache hit.  The FlowExecutorStageFactory produces StageMappings (StageMapping[]), which is itself a warehouse of singular server-component -> client-component relationships, fetching the client from the bundle as needed.  The StageMapping only does the fetch once per instance, so (for example) a password failure will reinstantiate a PasswordStage, but it will not fetch it a second time.

* Removed comments about the cache.  Added comments about where to find the FlowExecutor stage table. Moved the import of WebAuthnAuthenticticatorRegisterState from FlowExecutor.ts to FlowExecutorStages.ts; both files are bundled together, so this is a no-op functionally, but it's easier to confirm that StageEntries without import expressions (STageModuleCallbacks) have their stages bundled (pre-imported) if the import statement is in the same file.

* web: Flesh out stage mapping error handling. (#20292)

Co-authored-by: Ken Sternberg <ken@goauthentik.io>

* Restore fallback to use token if neither tag nor import are present.

* Bad check.

---------

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2026-02-20 17:35:33 -08:00
Kofl
d2c06e6f0d website/integrations: gatus: fix config block (#20446)
Update OIDC configuration formatting

The original documentation caused an error with the current gatus version. It has been fixed with the correct formatting.

Signed-off-by: Kofl <thomas@kofler.tk>
2026-02-20 15:12:17 -05:00
dependabot[bot]
934f783bc7 core: bump msgraph-sdk from 1.54.0 to 1.55.0 (#20432)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:46:18 +01:00
dependabot[bot]
a74ea64431 core: bump aws-cdk-lib from 2.238.0 to 2.239.0 (#20434)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:46:17 +01:00
dependabot[bot]
462864d57e core: bump constructs from 10.5.0 to 10.5.1 (#20433)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:45:59 +01:00
dependabot[bot]
3838078761 core: bump goauthentik/fips-python from c272691 to d973c46 in /lifecycle/container (#20437)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:45:44 +01:00
dependabot[bot]
e0a7d0fe1c core: bump goauthentik/fips-debian from b0917af to 4419749 in /lifecycle/container (#20438)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 17:45:35 +01:00
Ken Sternberg
ab981dec86 web/admin/bugfix: Edit Stage not working. Invoking IdentificationStageForm not working (#20429)
* web/admin/bugfix: Edit Stage not working. Invoking IdentificationStageForm not working.

## What

1.  Fix the field being referenced by Flows -\> \[One Flow\] -\> StageBindings -\> \[Edit Stage\] to use the PK for the *stage*, rather than the *binding*.
2.  Added a check in `StrictUnsafe`: if the property is “wrapped” and untyped, treat it as an attribute, not a property.
3.  Edit the `ak-bound-stages-list` target attribute to be an attribute, not a property.

## Why

1.  This looks like a simple typo. To avoid this in the future, *we need tests*.
2.  `ModelForm` uses both a converter and get/set accessors to manage the pk (primary key) of the object it is being invoked to edit: the first because Django primary keys can be either strings or numbers, and the latter because we have special transactional requirements when a primary key changes. Lit’s magic for handling this creates some weirdness around JavaScript prototyping (`wrapped` becomes the only key on the object; all the other keys become delegated to a prototype object), so `hasOwn()` can’t be used; we just have to check for `wrapped` and `!type`.
3.  PKs are either strings or numbers, and ModelForm has a smart converter. There’s no need to shove the values around as properties and, in fact, that’ll break some things because there’s a working `attribute` field on ModelForm! Removing the `.` property marker both avoids this issue and makes visible exactly what item-id is being referenced.

* Forced update of package lock.  AGAIN.

* Sigh

* Sigh. Again.

* Sigh. But this time, with an empty cache.

* Prettier and its opinions.

* Clearing the cache broke relationships inside SFE. That has been updated.

* WTF, over?
2026-02-20 08:44:51 -08:00
dependabot[bot]
d8f78ff653 core: bump ruff from 0.15.1 to 0.15.2 (#20435)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 16:43:42 +00:00
Jens L.
055d1302e1 enterprise/providers/microsoft_entra: only check upn when set (#20441)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-20 17:13:36 +01:00
dependabot[bot]
3bdf3d6745 core: bump selenium from 4.40.0 to 4.41.0 (#20436)
Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.40.0 to 4.41.0.
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/commits)

---
updated-dependencies:
- dependency-name: selenium
  dependency-version: 4.41.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>
2026-02-20 17:09:40 +01:00
Kofl
f56d3aba31 website/docs: change permission name from 'Can view Admin interface' to 'Can access…' (#20412)
* Update permission name from 'Can view Admin interface' to 'Can access admin interface'

based on the current 2025.12 release

Signed-off-by: Kofl <thomas@kofler.tk>

* Fix other references to old permission name

---------

Signed-off-by: Kofl <thomas@kofler.tk>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-20 09:28:22 +00:00
Andreas Brain
a52ecd30b7 website/integrations: add OIDC and update SAML instructions for Zammad (#20421)
* website/integrations: add OIDC method for Zammad

* Minor changes

* Add configuration verification

* Update index.md

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update SAML instructions for Zammad

* wip

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-20 09:27:29 +00:00
kgoode517
05ad1f0363 website/integrations: update wazuh acs url (#20401)
* Update index.mdx

Minor changes for readiblity and updated ui in authentik

Signed-off-by: kgoode517 <Kgoode517@yahoo.com>

* Apply suggestion from @dewi-tik

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: kgoode517 <Kgoode517@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-19 22:15:24 +00:00
Teffen Ellis
c35bfe9f04 web: Center footer links. (#20345)
* web: Center footer links.

* Refine track resizing behavior.

* Fix odd scenario.

* Tidy padding.
2026-02-19 18:55:55 +01:00
authentik-automation[bot]
2804fd2d57 core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1771349690 (#20367)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-19 12:35:14 +00:00
dependabot[bot]
ae803d47d6 ci: bump tj-actions/changed-files from 47.0.3 to 47.0.4 (#20374)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 47.0.3 to 47.0.4.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](28b28f6e4e...7dee1b0c15)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 47.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-19 12:35:09 +00:00
dependabot[bot]
bc0f4984b5 ci: bump helm/kind-action from 1.13.0 to 1.14.0 (#20375)
Bumps [helm/kind-action](https://github.com/helm/kind-action) from 1.13.0 to 1.14.0.
- [Release notes](https://github.com/helm/kind-action/releases)
- [Commits](92086f6be0...ef37e7f390)

---
updated-dependencies:
- dependency-name: helm/kind-action
  dependency-version: 1.14.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>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2026-02-19 12:35:04 +00:00
dependabot[bot]
122cee049a core: bump library/golang from 1.25.5-trixie to 1.26.0-trixie in /lifecycle/container (#20381)
* core: bump library/golang in /lifecycle/container

Bumps library/golang from 1.25.5-trixie to 1.26.0-trixie.

---
updated-dependencies:
- dependency-name: library/golang
  dependency-version: 1.26.0-trixie
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* bump & fix

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

* bump docs too

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>
2026-02-19 12:35:00 +00:00
dependabot[bot]
ebc06a8453 core: bump goauthentik/fips-debian from 10dadf1 to b0917af in /lifecycle/container (#20382)
core: bump goauthentik/fips-debian in /lifecycle/container

Bumps goauthentik/fips-debian from `10dadf1` to `b0917af`.

---
updated-dependencies:
- dependency-name: goauthentik/fips-debian
  dependency-version: trixie-slim-fips
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 12:34:56 +00:00
dependabot[bot]
3f6afde8b1 core: bump goauthentik/fips-python from 3.14.2-slim-trixie-fips to 3.14.3-slim-trixie-fips in /lifecycle/container (#20383)
core: bump goauthentik/fips-python in /lifecycle/container

Bumps goauthentik/fips-python from 3.14.2-slim-trixie-fips to 3.14.3-slim-trixie-fips.

---
updated-dependencies:
- dependency-name: goauthentik/fips-python
  dependency-version: 3.14.3-slim-trixie-fips
  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>
2026-02-19 12:34:52 +00:00
dependabot[bot]
58b8b8b708 core: bump twilio from 9.10.1 to 9.10.2 (#20384)
Bumps [twilio](https://github.com/twilio/twilio-python) from 9.10.1 to 9.10.2.
- [Release notes](https://github.com/twilio/twilio-python/releases)
- [Changelog](https://github.com/twilio/twilio-python/blob/main/CHANGES.md)
- [Commits](https://github.com/twilio/twilio-python/compare/9.10.1...9.10.2)

---
updated-dependencies:
- dependency-name: twilio
  dependency-version: 9.10.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>
2026-02-19 12:34:48 +00:00
dependabot[bot]
1132dbef02 core: bump astral-sh/uv from 0.9.18 to 0.10.4 in /lifecycle/container (#20385)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.9.18 to 0.10.4.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.9.18...0.10.4)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.10.4
  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>
2026-02-19 12:34:44 +00:00
dependabot[bot]
2083177e0c web: bump the storybook group across 1 directory with 5 updates (#20386)
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 10.2.9 to 10.2.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/v10.2.10/code/addons/docs)

Updates `@storybook/addon-links` from 10.2.9 to 10.2.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/v10.2.10/code/addons/links)

Updates `@storybook/web-components` from 10.2.9 to 10.2.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/v10.2.10/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 10.2.9 to 10.2.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/v10.2.10/code/frameworks/web-components-vite)

Updates `storybook` from 10.2.9 to 10.2.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/v10.2.10/code/core)

---
updated-dependencies:
- dependency-name: "@storybook/addon-docs"
  dependency-version: 10.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-version: 10.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-version: 10.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-version: 10.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 10.2.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>
2026-02-19 12:34:40 +00:00
dependabot[bot]
0b164c7f56 web: bump globby from 16.1.0 to 16.1.1 in /web (#20387)
Bumps [globby](https://github.com/sindresorhus/globby) from 16.1.0 to 16.1.1.
- [Release notes](https://github.com/sindresorhus/globby/releases)
- [Commits](https://github.com/sindresorhus/globby/compare/v16.1.0...v16.1.1)

---
updated-dependencies:
- dependency-name: globby
  dependency-version: 16.1.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>
2026-02-19 12:34:36 +00:00
dependabot[bot]
e87318715a lifecycle/aws: bump aws-cdk from 2.1106.0 to 2.1106.1 in /lifecycle/aws (#20403)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1106.0 to 2.1106.1.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1106.1/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1106.1
  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>
2026-02-19 12:34:32 +00:00
dependabot[bot]
81887a0b37 core: bump psycopg[c,pool] from 3.3.2 to 3.3.3 (#20404)
Bumps [psycopg[c,pool]](https://github.com/psycopg/psycopg) from 3.3.2 to 3.3.3.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.2...3.3.3)

---
updated-dependencies:
- dependency-name: psycopg[c,pool]
  dependency-version: 3.3.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>
2026-02-19 12:34:28 +00:00
dependabot[bot]
924a5af625 web: bump knip from 5.83.1 to 5.84.1 in /web (#20406)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 5.83.1 to 5.84.1.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@5.84.1/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 5.84.1
  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>
2026-02-19 12:34:24 +00:00
dependabot[bot]
c2043f9823 web: bump @types/node from 25.2.3 to 25.3.0 in /web (#20407)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.2.3 to 25.3.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: 25.3.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>
2026-02-19 12:34:20 +00:00
Ken Sternberg
2eedae7011 web/admin: maintenance: centralize types that are used across stages (#20398)
## What

Re-arranges where types are declared and how they’re accessed. In some cases, refine the types to provide stronger build-time guarantees.

## Why

A lot of our stages use bits and pieces of types from a base state, from the executor, and in some cases from redundant sources.

Union types are great for some things, but Typescript can get really hung up on passing a union type as a function parameter when there’s some low-level non-conformance across all the types in the union. Because OpenAPI doesn’t do abstractions, this commit introduces a `StageChallengeLike`, which abstracts all of the Challenge types, asserting that there is a unifying parent class that contains the minimum collection fields found across all challenges, and this lets us address the different challenge types and their corresponding components before worrying about refining the type for construction and deployment.

On the other hand, sometimes you want to be able to assert that *all* of the member of a union correspond to some shape of data, and you can always use `Pick<>` on the FormChallenge type to assert that at build time.

Other than that, this centralizes the types into locations in the codebase with well-known names.
2026-02-18 13:40:40 -08:00
Kofl
7f92b4249d website/integrations: beszel: remove slug reference (#20393)
Update authentik configuration steps

Removed the noting of 'slug' from the authentik configuration steps, as its not required on the Beszel site

Signed-off-by: Kofl <thomas@kofler.tk>
2026-02-18 20:02:36 +00:00
Ken Sternberg
1df540105a web/admin: maintenance: give dialogs default exports (#20397)
## What

Provide dynamically imported dialogs and forms a default export.

Some minor cleanup of types: `PropertyValues` -\> `PropertyValues<this>` provides stronger type guarantees.

## Why

We define stages and other dynamic processes on the server side using a token, the server-side component name. Client-side elements are instantiated with a constructor and identified with an element tag name.

Dynamically importing components automatically registered the element tag name with the constructor, enabling custom elements to work. Without the default export, the registration goes to the browser but the identity of the component’s underlying constructor is lost. Browsers provide a reverse lookup: given a component’s constructor it can provide the registration tag. By having default exports, we allow dynamic imports to record the constructor, retrieve the tag, and dynamically construct the templates without having to manually maintain the tag/constructor relationship (which is already complicated enough by that server-side component/client-side element relationship).

## Testing

This is purely internal maintenance; it’s about hardening the build, not changing behavior. If it lints and builds cleanly, the only real test is that nothing is borken afterwards.
2026-02-18 18:32:32 +00:00
Teffen Ellis
6a7162fd3b web: Fix element property names with custom attributes. (#20396) 2026-02-18 18:27:56 +00:00
Jens L.
e7ea15c791 enterprise/providers/microsoft_entra: fix dangling comma (#20391)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-18 18:37:33 +01:00
Ken Sternberg
d3b69b25f1 web/admin: bug: stage update forms not rendering, several modal form buttons missing (#20373)
## What

Names being passed to the browser were being incorrectly rendered. This commit updates the code in `StrictUnsafe` so that after the correct-use assertion is passed, the elementProperties are checked to see if the attribute has been named differently from the typed attribute field, and if so, retrieves the attribute name and passes it, rather than the field name, to the browser.

## Why

Since we have a lot of components with similar interfaces, it makes sense to try and check that they’re being used correctly and that the types associated with them are correct. Plus Lit, unlike React, doesn’t have a self-erasing syntax: every Lit element *is* an element, whereas JSX is an esoteric function call syntax that happens to look like XML. JavaScript templates aren’t as pretty as JSX, but they get the job done just as readily.

But in this case, cleverness bit us: we want to use the component’s JavaScript field names and types to validate that we’re using it correctly and passing the right types, but in the end we’re constructing a tag that will trigger the browser to construct the component and use it– and the field names don’t always correspond to the attribute name. Lit has a syntax for mapping the one to the other and stores it in the `elementProperties` field.

This code checks that, after we’ve determined the correct prefix for an property field that has been passed into the component, that we’ve also checked and extracted the correct *attribute name* for that property field. Most of the time it will be the same as the property field, but it muts always be checked.
2026-02-18 08:15:58 -08:00
Jens L.
3ca055f3f4 lifecycle: bump rac guacd base image (#20390)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-18 16:28:57 +01:00
Simonyi Gergő
1db9363ae2 web: revert tree-sitter removal from lockfile (#20377)
This partially reverts commit a5ef7e8db4.
2026-02-18 13:33:45 +01:00
Jens L.
60a70e7065 root: fix dependabot config for docker (#20380)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-18 13:30:36 +01:00
nsw42
fbdeee56ff website/docs: Fix broken link to flow executor (#20364)
Fix broken link

I obviously can't test this, but it looks like the redirects should work.

Signed-off-by: nsw42 <nsw42@users.noreply.github.com>
2026-02-17 18:17:38 +00:00
Simonyi Gergő
833f45fce0 core: add cause to ak_groups deprecation event and logs (#20361)
add cause to `ak_groups` deprecation event and logs
2026-02-17 18:59:35 +01:00
Simonyi Gergő
76f302cb17 rbac: fix object permission request (#20304)
fix object permission request
2026-02-17 18:32:57 +01:00
Jens L.
cb9e4e003c enterprise/providers/ws_federation: fix incorrect metadata download URL (#20173)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-02-17 18:20:47 +01:00
authentik-automation[bot]
bd22b451c2 core, web: update translations (#20303)
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>
2026-02-17 18:18:14 +01:00
authentik-automation[bot]
a5ef7e8db4 stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#20305)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-17 18:17:49 +01:00
dependabot[bot]
fe0f559cd2 core: bump django-countries from 7.6.1 to 8.2.0 (#19459)
* core: bump django-countries from 7.6.1 to 8.2.0

Bumps [django-countries](https://github.com/SmileyChris/django-countries) from 7.6.1 to 8.2.0.
- [Changelog](https://github.com/SmileyChris/django-countries/blob/main/CHANGES.md)
- [Commits](https://github.com/SmileyChris/django-countries/compare/v7.6.1...v8.2.0)

---
updated-dependencies:
- dependency-name: django-countries
  dependency-version: 8.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
# Conflicts:
#	pyproject.toml
#	uv.lock

* re-gen schema

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

* fix

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>
2026-02-17 18:13:41 +01:00
dependabot[bot]
3d33d1b8df web: bump the storybook group across 1 directory with 5 updates (#20130)
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 10.2.7 to 10.2.8
- [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/v10.2.8/code/addons/docs)

Updates `@storybook/addon-links` from 10.2.7 to 10.2.8
- [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/v10.2.8/code/addons/links)

Updates `@storybook/web-components` from 10.2.7 to 10.2.8
- [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/v10.2.8/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 10.2.7 to 10.2.8
- [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/v10.2.8/code/frameworks/web-components-vite)

Updates `storybook` from 10.2.7 to 10.2.8
- [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/v10.2.8/code/core)

---
updated-dependencies:
- dependency-name: "@storybook/addon-docs"
  dependency-version: 10.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-version: 10.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-version: 10.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-version: 10.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-version: 10.2.8
  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>
2026-02-17 16:26:58 +01:00
dependabot[bot]
c6fd8638eb web: bump pino from 10.3.0 to 10.3.1 in /web (#20133)
Bumps [pino](https://github.com/pinojs/pino) from 10.3.0 to 10.3.1.
- [Release notes](https://github.com/pinojs/pino/releases)
- [Commits](https://github.com/pinojs/pino/compare/v10.3.0...v10.3.1)

---
updated-dependencies:
- dependency-name: pino
  dependency-version: 10.3.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>
2026-02-17 16:26:32 +01:00
dependabot[bot]
ef8a32554a core: bump github.com/pires/go-proxyproto from 0.10.0 to 0.11.0 (#20182)
Bumps [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) from 0.10.0 to 0.11.0.
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.10.0...v0.11.0)

---
updated-dependencies:
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.11.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>
2026-02-17 14:43:46 +00:00
dependabot[bot]
cbfb0e345a web: bump @patternfly/elements from 4.2.0 to 4.3.1 in /web (#20185)
Bumps [@patternfly/elements](https://github.com/patternfly/patternfly-elements/tree/HEAD/elements) from 4.2.0 to 4.3.1.
- [Release notes](https://github.com/patternfly/patternfly-elements/releases)
- [Changelog](https://github.com/patternfly/patternfly-elements/blob/main/elements/CHANGELOG.md)
- [Commits](https://github.com/patternfly/patternfly-elements/commits/@patternfly/elements@4.3.1/elements)

---
updated-dependencies:
- dependency-name: "@patternfly/elements"
  dependency-version: 4.3.1
  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>
2026-02-17 14:43:41 +00:00
dependabot[bot]
d9cb896508 lifecycle/aws: bump aws-cdk from 2.1105.0 to 2.1106.0 in /lifecycle/aws (#20272)
Bumps [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk) from 2.1105.0 to 2.1106.0.
- [Release notes](https://github.com/aws/aws-cdk-cli/releases)
- [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1106.0/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-version: 2.1106.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>
2026-02-17 14:43:36 +00:00
dependabot[bot]
af50e99b2c web: bump chromedriver from 145.0.1 to 145.0.3 in /web (#20313)
Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 145.0.1 to 145.0.3.
- [Commits](https://github.com/giggio/node-chromedriver/compare/145.0.1...145.0.3)

---
updated-dependencies:
- dependency-name: chromedriver
  dependency-version: 145.0.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>
2026-02-17 14:43:32 +00:00
dependabot[bot]
3ba3c11157 web: bump @sentry/browser from 10.38.0 to 10.39.0 in /web in the sentry group across 1 directory (#20340)
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.38.0 to 10.39.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.38.0...10.39.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-version: 10.39.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>
2026-02-17 14:43:28 +00:00
dependabot[bot]
2a87e22806 web: bump mermaid from 11.12.2 to 11.12.3 in /web (#20359)
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.12.2 to 11.12.3.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.12.2...mermaid@11.12.3)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.12.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>
2026-02-17 15:41:22 +01:00
dependabot[bot]
e094798ece ci: bump tj-actions/changed-files from 47.0.2 to 47.0.3 (#20357)
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 47.0.2 to 47.0.3.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](8cba46e29c...28b28f6e4e)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 47.0.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>
2026-02-17 15:26:58 +01:00
dependabot[bot]
ea3a5df47f core: bump constructs from 10.4.5 to 10.5.0 (#20358)
Bumps [constructs](https://github.com/aws/constructs) from 10.4.5 to 10.5.0.
- [Release notes](https://github.com/aws/constructs/releases)
- [Commits](https://github.com/aws/constructs/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: constructs
  dependency-version: 10.5.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>
2026-02-17 15:26:28 +01:00
dependabot[bot]
67aa050b41 core: bump sentry-sdk from 2.52.0 to 2.53.0 (#20341)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.52.0 to 2.53.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.52.0...2.53.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.53.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>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-17 13:06:24 +00:00
Dominic R
b27a877887 lifecycle/container: fix rust builds and pin toolchain version (#20300)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-17 12:50:29 +00:00
dependabot[bot]
98ea361a82 core: bump openapitools/openapi-generator-cli from v7.19.0 to v7.20.0 in /scripts/api (#20339)
core: bump openapitools/openapi-generator-cli in /scripts/api

Bumps openapitools/openapi-generator-cli from v7.19.0 to v7.20.0.

---
updated-dependencies:
- dependency-name: openapitools/openapi-generator-cli
  dependency-version: v7.20.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 13:23:51 +01:00
dependabot[bot]
f93c3ce1e7 core: bump uvicorn[standard] from 0.40.0 to 0.41.0 (#20342)
Bumps [uvicorn[standard]](https://github.com/Kludex/uvicorn) from 0.40.0 to 0.41.0.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.40.0...0.41.0)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-version: 0.41.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>
2026-02-17 13:23:41 +01:00
dependabot[bot]
c5cafa8457 ci: bump actions/stale from 10.1.1 to 10.2.0 (#20343)
Bumps [actions/stale](https://github.com/actions/stale) from 10.1.1 to 10.2.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](997185467f...b5d41d4e1d)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.2.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>
2026-02-17 13:22:46 +01:00
Simonyi Gergő
e94352ba00 ci: fix setup altering package-lock (#20348) 2026-02-17 13:10:30 +01:00
Dominic R
e056dbdadd website/docs, integrations: fix language (#20338) 2026-02-17 09:03:07 +00:00
Dewi Roberts
be256354c6 website/docs: rac: update rac provider docs (#20225)
* WIP

* Sentence

* Delete image

* WIP

* adjust wording

---------

Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-17 01:56:52 +00:00
Alex
3b77963fd2 website/integrations: update Vikunja docs to post-1.0 syntax (#20269)
* website/integrations: update Vikunja docs to post-1.0 syntax

* improve

---------

Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-17 00:58:37 +00:00
Dewi Roberts
0d67860a64 website/docs: add okta source doc (#20296)
* Begin

* Add steps

* Apply suggestions

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

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Apply suggestion from @dominic-r

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

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-17 00:45:24 +00:00
jhuesser
db62c4aaf1 website/integrations: add VMware Cloud Director integration guide (#20324)
* website/integrations: add vCloud Director integration guide

* Initial changes

* add more details to configuration verification.

* Update website/integrations/hypervisors-orchestrators/vmware-cloud-director/index.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: jhuesser <github@jhuesser.ch>

* Update website/integrations/hypervisors-orchestrators/vmware-cloud-director/index.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: jhuesser <github@jhuesser.ch>

* Update website/integrations/hypervisors-orchestrators/vmware-cloud-director/index.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: jhuesser <github@jhuesser.ch>

* Update website/integrations/hypervisors-orchestrators/vmware-cloud-director/index.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: jhuesser <github@jhuesser.ch>

* Update website/integrations/hypervisors-orchestrators/vmware-cloud-director/index.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: jhuesser <github@jhuesser.ch>

* Update website/integrations/hypervisors-orchestrators/vmware-cloud-director/index.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: jhuesser <github@jhuesser.ch>

* Small language change

* rewrite key configuration steps

* fix anrchor

---------

Signed-off-by: jhuesser <github@jhuesser.ch>
Co-authored-by: dewi-tik <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-16 19:12:04 -05:00
Teffen Ellis
61a75e6a0a web: Flow Executor layout fixes (#20134)
* Fix footer alignment.

* Fix loading position in compatibility mode.

* Apply min height only when placeholder content is present.

* Fix alignment in compatibility mode.

* Add compatibility mode host selectors.

* Fix nullish challenge height. Clarify selector behavior.

* Add type defintion

* Fix padding.

* Fix misapplication of pf-* class to container.

* Fix huge base64 encoded attribute.

* Clean up layering issues, order of styles.

* Disable dev override.

* Document parts.
2026-02-16 20:29:32 +01:00
Tana M Berry
ff611c845f website/integrations: fix all links to Bindings docs (#20214)
fix link to Bindings docs

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
2026-02-16 08:06:26 -06:00
Rohit Awate
74871afaaa website/docs: Clarify encryption key requirement for Paperless (#20316)
* Clarify encryption key requirement in documentation

Added note to ensure no encryption key is selected.

Signed-off-by: Rohit Awate <rohitawate121@gmail.com>

* Apply suggestion from @dewi-tik

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Rohit Awate <rohitawate121@gmail.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-16 12:59:31 +00:00
dependabot[bot]
1c73621094 core: bump pytest-django from 4.11.1 to 4.12.0 (#20311)
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.11.1 to 4.12.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.11.1...v4.12.0)

---
updated-dependencies:
- dependency-name: pytest-django
  dependency-version: 4.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>
2026-02-16 13:56:32 +01:00
dependabot[bot]
71dd924e1b core: bump gunicorn from 25.0.3 to 25.1.0 (#20312)
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 25.0.3 to 25.1.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/25.0.3...25.1.0)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-version: 25.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>
2026-02-16 13:56:26 +01:00
dependabot[bot]
2b2ad10ab1 ci: bump int128/docker-manifest-create-action from 2.14.0 to 2.16.0 (#20314)
Bumps [int128/docker-manifest-create-action](https://github.com/int128/docker-manifest-create-action) from 2.14.0 to 2.16.0.
- [Release notes](https://github.com/int128/docker-manifest-create-action/releases)
- [Commits](1a059c021f...8aac06098a)

---
updated-dependencies:
- dependency-name: int128/docker-manifest-create-action
  dependency-version: 2.16.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>
2026-02-16 13:55:54 +01:00
dependabot[bot]
7d1f7625e0 core: bump axllent/mailpit from v1.29.0 to v1.29.1 in /tests/e2e (#20310)
Bumps axllent/mailpit from v1.29.0 to v1.29.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 13:54:55 +01:00
Marc 'risson' Schmitt
68d4d6d346 root: do not rely on npm cli for version bump (#20276) 2026-02-16 08:58:09 +00:00
Dewi Roberts
2393532b07 website/integrations: seafile: fix issues (#20295)
* Addresses issues in doc

* Apply suggestion from @dominic-r

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

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-15 18:06:45 +00:00
Simonyi Gergő
52686eaacd website/docs: add affine to release notes (#20299)
* add affine to release notes

* use built-in github linking

* add missing credits to Arcane integration
2026-02-15 12:42:00 -05:00
kgoode517
6d7ad1fa0c website/integrations: pangolin: add subject mode (#20306)
* Update index.mdx

This change is response to a know issue with the sso needing to map to the users username not email which is default. This clarification should be provided to prevent errors see the following. https://github.com/fosrl/pangolin/issues/639

Signed-off-by: kgoode517 <Kgoode517@yahoo.com>

* Apply suggestion from @dominic-r

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

---------

Signed-off-by: kgoode517 <Kgoode517@yahoo.com>
Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-15 15:29:21 +00:00
authentik-automation[bot]
41637963ce core, web: update translations (#20291)
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>
2026-02-14 07:33:57 +01:00
Ken Sternberg
128f057e91 web: updated package-lock.json to include missing tree-sitter references (#20244)
web: updated package-lock.json to include missing tree-sitter references.
2026-02-14 07:32:59 +01:00
dependabot[bot]
bcc2f8a612 web: bump @types/react from 19.2.13 to 19.2.14 in /web in the react group across 1 directory (#20219)
web: bump @types/react in /web in the react group across 1 directory

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.2.13 to 19.2.14
- [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.2.14
  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>
2026-02-14 05:59:51 +00:00
dependabot[bot]
29724ef6e1 core, web: bump qs from 6.14.1 to 6.14.2 in /packages/docusaurus-config (#20277)
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 06:30:06 +01:00
Dewi Roberts
851915b02a stage/identification: recovery: make wording more generic (#20209)
Make wording more generic
2026-02-14 06:29:31 +01:00
dependabot[bot]
533630a5ec web: bump @fortawesome/fontawesome-free from 7.1.0 to 7.2.0 in /web (#20187)
Bumps [@fortawesome/fontawesome-free](https://github.com/FortAwesome/Font-Awesome) from 7.1.0 to 7.2.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.1.0...7.2.0)

---
updated-dependencies:
- dependency-name: "@fortawesome/fontawesome-free"
  dependency-version: 7.2.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>
2026-02-14 06:29:09 +01:00
dependabot[bot]
25dc3fe462 web: bump @types/node from 25.2.2 to 25.2.3 in /web (#20186)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.2.2 to 25.2.3.
- [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: 25.2.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>
2026-02-14 06:28:42 +01:00
dependabot[bot]
03e9e33d86 web: bump country-flag-icons from 1.6.12 to 1.6.13 in /web (#20184)
Bumps [country-flag-icons](https://gitlab.com/catamphetamine/country-flag-icons) from 1.6.12 to 1.6.13.
- [Changelog](https://gitlab.com/catamphetamine/country-flag-icons/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/country-flag-icons/compare/v1.6.12...v1.6.13)

---
updated-dependencies:
- dependency-name: country-flag-icons
  dependency-version: 1.6.13
  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>
2026-02-14 06:28:32 +01:00
Teffen Ellis
8c32593448 web: Apply CSpell corrections. (#20190) 2026-02-14 02:47:23 +01:00
Teffen Ellis
f252087a9c website/docs: Custom CSS (#19991)
* website/docs: Custom CSS

* Revise.

* Fix paths.

* Update links.

* Update header capitalization

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-13 18:31:15 +01:00
authentik-automation[bot]
15ad260333 core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1770992049 (#20285)
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>
2026-02-13 14:43:27 +00:00
Marcelo Elizeche Landó
b76539e73f stage/invitation: Send invite via email UI (#19823)
* first approach

* add cc and bcc support, better ui

* remove unnecessary data return

* add template support

* fix linting

* do the ui

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

* display invite info in InvitationSendEmailForm.ts

* Select the invitation template by default

* Fix linting

* fix tests

* Add tests, clean code

* Add docs

* fix link

* Make the UI less disgusting

* Make the UI less disgusting

* Apply suggestions from code review

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

* small formatting fix

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

* Use writeToClipboard function, better wording for CC and BCC

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Marcelo Elizeche Landó <marce@melizeche.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2026-02-13 11:00:31 -03:00
Simonyi Gergő
c205a41cb5 root: remove unused django-cte (#20090) 2026-02-13 14:10:22 +01:00
dependabot[bot]
32f2d3ad30 core: bump ruff from 0.15.0 to 0.15.1 (#20273)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 12:54:07 +00:00
authentik-automation[bot]
a9e382d6c5 core, web: update translations (#20271)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-13 13:48:42 +01:00
dependabot[bot]
a35005416b ci: bump docker/build-push-action from 6.19.1 to 6.19.2 (#20274)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 13:45:02 +01:00
Alexander Tereshkin
3348ab34c3 enterprise/lifecycle: fix multiple reviews showing up in "Reviews" when the user is a member of multiple reviewer groups (#20266) 2026-02-13 13:34:00 +01:00
Marc 'risson' Schmitt
81b4256e3c ci: fix binary outpost build on release (#20248) 2026-02-13 13:33:35 +01:00
Dominic R
cc798f4425 web: add pretty names for lifecycle review events in event logs (#20264) 2026-02-12 21:38:33 +00:00
Dominic R
9903fd4d95 web: fix italic formatting in lifecycle rule help text (#20263)
* web: fix italic formatting in lifecycle rule help text

* r
2026-02-12 16:07:19 -05:00
Marc 'risson' Schmitt
20c2a33155 website/docs: 2025.8.6 release notes (#20243) 2026-02-12 15:54:07 +00:00
Marc 'risson' Schmitt
f1ee092930 website/docs: 2025.12.4 release notes (#20226) 2026-02-12 15:52:03 +00:00
Marc 'risson' Schmitt
611ddc2904 website/docs: 2025.10.4 release notes (#20242) 2026-02-12 15:50:06 +00:00
authentik-automation[bot]
aeb2457767 security: CVE-2026-25748 (#20240)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 15:17:01 +00:00
authentik-automation[bot]
97b6c9533f security: CVE-2026-25922 (#20241)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 14:51:19 +00:00
authentik-automation[bot]
c880c9f4ab security: CVE-2026-25227 (#20239)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 14:45:50 +00:00
Simonyi Gergő
af36cdc597 ci: fix release testing (#20207)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-12 12:39:00 +00:00
Teffen Ellis
4bd9b08cfb core: Apply CSpell corrections. (#20191) 2026-02-12 12:52:01 +01:00
authentik-automation[bot]
976df9e7da core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1770842608 (#20213)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 12:50:38 +01:00
authentik-automation[bot]
be64ed4281 core, web: update translations (#20215)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-12 12:49:35 +01:00
dependabot[bot]
5790316616 core: bump library/node from 25.6.0-trixie to 25.6.1-trixie in /website (#20220)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 12:48:35 +01:00
dependabot[bot]
53c376e4e9 core: bump google-api-python-client from 2.189.0 to 2.190.0 (#20217)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 12:48:22 +01:00
dependabot[bot]
38bde992b7 core: bump webauthn from 2.7.0 to 2.7.1 (#20218)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 12:47:43 +01:00
dependabot[bot]
c3353c1bf7 ci: bump docker/build-push-action from 6.18.0 to 6.19.1 (#20221)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 12:47:26 +01:00
tumpech
56f0df9d89 website/integrations: Update Komga instructions to add "email_verified" attribute to "email" claim. (#20135)
* Add email_verified to komga

* Fix minor spelling issues in Komga docs.

* Add email scope verification link

* Update index.md

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: dewi-tik <dewi@goauthentik.io>
2026-02-12 09:28:28 +00:00
Teffen Ellis
8aeedb6380 website: Apply CSpell corrections. (#20189)
* website: Apply CSpell corrections.

* Lint/spelling fix

---------

Co-authored-by: dewi-tik <dewi@goauthentik.io>
2026-02-11 21:57:36 +01:00
Connor Peshek
858a040dfb providers/saml: send logoutResponse on sp-init logout (#17691)
* providers/saml: send logoutResponse on sp-init logout

* Use first updated to fix multiple submits

* add backchannel logoutResponse

* tests

---------

Signed-off-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: connor peshek <connorpeshek@connors-MacBook-Pro.local>
2026-02-11 14:18:39 -06:00
Dewi Roberts
0329b6e1ab website/docs: ssf: update SSF documentation (#20195)
* Update SSF documentation

* Fix tags

* Update website/docs/add-secure-apps/providers/ssf/create-ssf-provider.md

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

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

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Apply suggestions from code review

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

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2026-02-11 19:44:47 +00:00
Tana M Berry
26eb34e17e website/docs: draft of new WS-Fed provider docs (#20091)
* first draft

* add table of parms

* tweak

* add section about certs

* a little more content

* more info on wa

* new procedurla file and edit sidebar

* tweaks

* dewi and jens edits

* tweak to remove bullet

* add docs link to the Rel Notes

* dewi edits thx

* ooops missed that last edit
2026-02-11 10:34:39 -06:00
Dewi Roberts
9d41d41b4f website/docs: add email verification scope doc (#20141)
* WIP

* Add link to 2025.10 release notes

* Apply suggestions from code review

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2026-02-11 15:55:25 +00:00
Georg
37432f43ba website/docs: correct reference to overriden S3 variable (#20156)
docs: correct reference to overriden S3 variable

Fixes: c30d1a478d ("files: rework (#17535)")

Signed-off-by: Georg Pfuetzenreuter <georg.pfuetzenreuter@suse.com>
2026-02-11 15:43:49 +00:00
Dewi Roberts
655e25e0d5 website/docs: rac: fixes the property mapping formatting (#20200)
Fixes the property mapping formatting
2026-02-11 10:20:45 -05:00
Simonyi Gergő
0356a30d65 api: fix test_build_schema (#20196)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-02-11 14:57:47 +01:00
Marc 'risson' Schmitt
15db2713ab ci: ensure schema is up-to-date (#20194) 2026-02-11 13:48:24 +00:00
authentik-automation[bot]
d9efce1002 core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1770771214 (#20178)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-11 13:42:51 +00:00
authentik-automation[bot]
a1a22978b3 core, web: update translations (#20177)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2026-02-11 14:13:40 +01:00
dependabot[bot]
95c9e5476e core: bump cachetools from 7.0.0 to 7.0.1 (#20183)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 14:09:13 +01:00
dependabot[bot]
93cf6e2cb1 core: bump cryptography from 46.0.4 to 46.0.5 (#20171)
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.4 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.4...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 10:18:52 +01:00
Simonyi Gergő
0dd8ee073a core: fix test_docker.sh (#20179)
Broken by 646a0d3692
2026-02-11 09:54:40 +01:00
authentik-automation[bot]
7cb789e777 root: bump version to 2026.5.0-rc1 (#20174)
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>
2026-02-11 01:43:16 +01:00
565 changed files with 14998 additions and 12897 deletions

View File

@@ -115,13 +115,20 @@ runs:
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
PR_NUMBER: ${{ steps.should_run.outputs.pr_number }}
REASON: ${{ steps.should_run.outputs.reason }}
run: |
set -e -o pipefail
PR_NUMBER="${{ steps.should_run.outputs.pr_number }}"
# Get PR details
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.user.login')
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT
# Determine which labels to process
if [ "${REASON}" = "label_added_to_merged_pr" ]; then
if [ "${{ steps.should_run.outputs.reason }}" = "label_added_to_merged_pr" ]; then
# Only process the specific label that was just added
if [ "${{ github.event_name }}" = "issues" ]; then
LABEL_NAME="${{ github.event.label.name }}"
@@ -145,13 +152,13 @@ runs:
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
PR_NUMBER: '${{ steps.should_run.outputs.pr_number }}'
COMMIT_SHA: '${{ steps.should_run.outputs.merge_commit_sha }}'
PR_TITLE: ${{ github.event.pull_request.title }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
LABELS: '${{ steps.pr_details.outputs.labels }}'
run: |
set -e -o pipefail
PR_NUMBER='${{ steps.should_run.outputs.pr_number }}'
COMMIT_SHA='${{ steps.should_run.outputs.merge_commit_sha }}'
PR_TITLE='${{ steps.pr_details.outputs.pr_title }}'
PR_AUTHOR='${{ steps.pr_details.outputs.pr_author }}'
LABELS='${{ steps.pr_details.outputs.labels }}'
echo "Processing PR #$PR_NUMBER (reason: ${{ steps.should_run.outputs.reason }})"
echo "Found backport labels: $LABELS"

View File

@@ -89,8 +89,6 @@ if should_push:
_cache_tag = "buildcache"
if image_arch:
_cache_tag += f"-{image_arch}"
if is_release:
_cache_tag += f"-{version_family}"
cache_to = f"type=registry,ref={get_attest_image_names(image_tags)}:{_cache_tag},mode=max"

View File

@@ -8,11 +8,6 @@ inputs:
postgresql_version:
description: "Optional postgresql image tag"
default: "16"
working-directory:
description: |
Optional working directory if this repo isn't in the root of the actions workspace.
When set, needs to contain a trailing slash
default: ""
runs:
using: "composite"
@@ -34,25 +29,24 @@ runs:
if: ${{ contains(inputs.dependencies, 'python') }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5
with:
python-version-file: "${{ inputs.working-directory }}pyproject.toml"
python-version-file: "pyproject.toml"
- name: Install Python deps
if: ${{ contains(inputs.dependencies, 'python') }}
shell: bash
working-directory: ${{ inputs.working-directory }}
run: uv sync --all-extras --dev --frozen
- name: Setup node
if: ${{ contains(inputs.dependencies, 'node') }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v4
with:
node-version-file: ${{ inputs.working-directory }}web/package.json
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: ${{ inputs.working-directory }}web/package-lock.json
cache-dependency-path: web/package-lock.json
registry-url: 'https://registry.npmjs.org'
- name: Setup go
if: ${{ contains(inputs.dependencies, 'go') }}
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v5
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v5
with:
go-version-file: "${{ inputs.working-directory }}go.mod"
go-version-file: "go.mod"
- name: Setup docker cache
if: ${{ contains(inputs.dependencies, 'runtime') }}
uses: AndreKurait/docker-cache@0fe76702a40db986d9663c24954fc14c6a6031b7
@@ -61,7 +55,6 @@ runs:
- name: Setup dependencies
if: ${{ contains(inputs.dependencies, 'runtime') }}
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/compose.yml up -d
@@ -69,7 +62,6 @@ runs:
- name: Generate config
if: ${{ contains(inputs.dependencies, 'python') }}
shell: uv run python {0}
working-directory: ${{ inputs.working-directory }}
run: |
from authentik.lib.generators import generate_id
from yaml import safe_dump

View File

@@ -38,6 +38,21 @@ updates:
#endregion
#region Rust
- package-ecosystem: rust-toolchain
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "core:"
labels:
- dependencies
#endregion
#region Web
- package-ecosystem: npm
@@ -234,7 +249,7 @@ updates:
- package-ecosystem: docker
directories:
- /
- /lifecycle/container
- /website
schedule:
interval: daily

View File

@@ -80,7 +80,7 @@ jobs:
make gen-client-ts
make gen-client-go
- name: Build Docker Image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
id: push
with:
context: .

View File

@@ -90,7 +90,7 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: int128/docker-manifest-create-action@1a059c021f1d5e9f2bd39de745d5dd3a0ef6df90 # v2
- uses: int128/docker-manifest-create-action@8aac06098a12365ccdf99372dcfb453ccce8a0b0 # v2
id: build
with:
tags: ${{ matrix.tag }}

View File

@@ -21,7 +21,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -96,7 +96,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile

View File

@@ -6,6 +6,10 @@ on:
schedule:
# Every night at 3am
- cron: "0 3 * * *"
pull_request:
paths:
# Needs to refer to itself
- .github/workflows/ci-main-daily.yml
jobs:
test-container:
@@ -15,14 +19,14 @@ jobs:
matrix:
version:
- docs
- version-2025-4
- version-2025-2
- version-2025-12
- version-2025-10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- run: |
current="$(pwd)"
dir="/tmp/authentik/${{ matrix.version }}"
mkdir -p $dir
cd $dir
wget https://${{ matrix.version }}.goauthentik.io/compose.yml
${current}/scripts/test_docker.sh
mkdir -p "${dir}/lifecycle/container"
cd "${dir}"
wget "https://${{ matrix.version }}.goauthentik.io/docker-compose.yml" -O "${dir}/lifecycle/container/compose.yml"
"${current}/scripts/test_docker.sh"

View File

@@ -42,6 +42,16 @@ jobs:
uses: ./.github/actions/setup
- name: run job
run: uv run make ci-${{ matrix.job }}
test-gen-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: generate schema
run: make migrate gen-build
- name: ensure schema is up-to-date
run: git diff --exit-code -- schema.yml blueprints/schema.json
test-migrations:
runs-on: ubuntu-latest
steps:
@@ -160,7 +170,7 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
- name: run integration
run: |
uv run coverage run manage.py test tests/integration
@@ -277,6 +287,7 @@ jobs:
if: always()
needs:
- lint
- test-gen-build
- test-migrations
- test-migrations-from-stable
- test-unittest

View File

@@ -111,7 +111,7 @@ jobs:
run: make gen-client-go
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: lifecycle/container/${{ matrix.type }}.Dockerfile

View File

@@ -32,7 +32,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -14,7 +14,7 @@ jobs:
if: ${{ env.GH_APP_ID != '' }}
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
env:
GH_APP_ID: ${{ secrets.GH_APP_ID }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Delete 'dev' containers older than a week
uses: snok/container-retention-policy@3b0972b2276b171b212f8c4efbca59ebba26eceb # v3.0.1
with:

View File

@@ -9,6 +9,7 @@ on:
- packages/eslint-config/**
- packages/prettier-config/**
- packages/docusaurus-config/**
- packages/logger-js/**
- packages/esbuild-plugin-live-reload/**
workflow_dispatch:
@@ -29,6 +30,7 @@ jobs:
- packages/eslint-config
- packages/prettier-config
- packages/docusaurus-config
- packages/logger-js
- packages/esbuild-plugin-live-reload
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
@@ -40,7 +42,7 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@8cba46e29c11878d930bca7870bb54394d3e8b21 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
with:
files: |
${{ matrix.package }}/package.json

View File

@@ -32,7 +32,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:
@@ -60,7 +60,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
with:

View File

@@ -51,7 +51,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
@@ -119,7 +119,7 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
id: push
with:
push: true

View File

@@ -70,7 +70,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- id: get-user-id
name: Get GitHub app user ID
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
@@ -91,7 +91,6 @@ jobs:
# ID from https://api.github.com/users/authentik-automation[bot]
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'
git pull
git commit -a -m "release: ${{ inputs.version }}" --allow-empty
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
git push --follow-tags
@@ -118,7 +117,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
repositories: helm
- id: get-user-id
name: Get GitHub app user ID
@@ -160,7 +159,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
repositories: version
- id: get-user-id
name: Get GitHub app user ID
@@ -175,7 +174,7 @@ jobs:
if: "${{ inputs.release_reason == 'feature' }}"
run: |
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}"
reason="${{ inputs.release_reason }}"
reason="{{ inputs.release_reason }}"
jq \
--arg version "${{ inputs.version }}" \
--arg changelog "See ${changelog_url}" \
@@ -187,7 +186,7 @@ jobs:
if: "${{ inputs.release_reason != 'feature' }}"
run: |
changelog_url="https://docs.goauthentik.io/docs/releases/${{ needs.check-inputs.outputs.major_version }}#fixed-in-$(echo -n ${{ inputs.version}} | sed 's/\.//g')"
reason="${{ inputs.release_reason }}"
reason="{{ inputs.release_reason }}"
jq \
--arg version "${{ inputs.version }}" \
--arg changelog "See ${changelog_url}" \

View File

@@ -18,8 +18,8 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with:
repo-token: ${{ steps.generate_token.outputs.token }}
days-before-stale: 60

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIV_KEY }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v5
if: ${{ github.event_name != 'pull_request' }}
with:

View File

@@ -34,6 +34,7 @@ packages/docusaurus-config @goauthentik/frontend
packages/esbuild-plugin-live-reload @goauthentik/frontend
packages/eslint-config @goauthentik/frontend
packages/prettier-config @goauthentik/frontend
packages/logger-js @goauthentik/frontend
packages/tsconfig @goauthentik/frontend
# Web
web/ @goauthentik/frontend

View File

@@ -18,10 +18,10 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
(.x being the latest patch release for each version)
| Version | Supported |
| --------- | --------- |
| 2025.12.x | ✅ |
| 2026.2.x | ✅ |
| Version | Supported |
| ---------- | ---------- |
| 2025.12.x | ✅ |
| 2026.2.x | ✅ |
## Reporting a Vulnerability
@@ -60,40 +60,6 @@ authentik reserves the right to reclassify CVSS as necessary. To determine sever
| 7.0 8.9 | High |
| 9.0 10.0 | Critical |
## Intended functionality
The following capabilities are part of intentional system design and should not be reported as security vulnerabilities:
- Expressions (property mappings/policies/prompts) can execute arbitrary Python code without safeguards.
This is expected behavior. Any user with permission to create or modify objects containing expression fields can write code that is executed within authentik. If a vulnerability allows a user without the required permissions to write or modify code and have it executed, that would be a valid security report.
However, the fact that expressions are executed as part of normal operations is not considered a privilege escalation or security vulnerability.
- Blueprints can access all files on the filesystem.
This access is intentional to allow legitimate configuration and deployment tasks. It does not represent a security problem by itself.
- Importing blueprints allows arbitrary modification of application objects.
This is intended functionality. This behavior reflects the privileged design of blueprint imports. It is "exploitable" when importing blueprints from untrusted sources without reviewing the blueprint beforehand. However, any method to create, modify or execute blueprints without the required permissions would be a valid security report.
- Flow imports may contain objects other than flows (such as policies, users, groups, etc.)
This is expected behavior as flow imports are blueprint files.
- Prompt HTML is not escaped.
Prompts intentionally allow raw HTML, including script tags, so they can be used to create interactive or customized user interface elements. Because of this, scripts within prompts may affect or interact with the surrounding page as designed.
- Open redirects that do not include tokens or other sensitive information are not considered a security vulnerability.
Redirects that only change navigation flow and do not expose session tokens, API keys, or other confidential data are considered acceptable and do not require reporting.
- Outgoing network requests are not filtered.
The destinations of outgoing network requests (HTTP, TCP, etc.) made by authentik to configurable endpoints through objects such as OAuth Sources, SSO Providers, and others are not validated. Depending on your threat model, these requests should be restricted at the network level using appropriate firewall or network policies.
## Disclosure process
1. Report from Github or Issue is reported via Email as listed above.

View File

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

View File

@@ -94,7 +94,7 @@ class Backend:
Args:
file_path: Relative file path
request: Optional Django HttpRequest for fully qualifed URL building
request: Optional Django HttpRequest for fully qualified URL building
use_cache: whether to retrieve the URL from cache
Returns:

View File

@@ -71,7 +71,7 @@ def postprocess_schema_responses(
def postprocess_schema_query_params(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
"""Optimize pagination parameters, instead of redeclaring parameters for each endpoint
declare them globally and refer to them"""
LOGGER.debug("Deduplicating query parameters")
for path in result["paths"].values():

View File

@@ -272,7 +272,7 @@ class Importer:
and entry.state != BlueprintEntryDesiredState.MUST_CREATED
):
self.logger.debug(
"Initialise serializer with instance",
"Initialize serializer with instance",
model=model,
instance=model_instance,
pk=model_instance.pk,
@@ -290,7 +290,7 @@ class Importer:
)
else:
self.logger.debug(
"Initialised new serializer instance",
"Initialized new serializer instance",
model=model,
**cleanse_dict(updated_identifiers),
)

View File

@@ -47,12 +47,7 @@ class ApplicationSerializer(ModelSerializer):
"""Application Serializer"""
launch_url = SerializerMethodField()
provider_obj = ProviderSerializer(
source="get_provider",
required=False,
read_only=True,
allow_null=True,
)
provider_obj = ProviderSerializer(source="get_provider", required=False, read_only=True)
backchannel_providers_obj = ProviderSerializer(
source="backchannel_providers", required=False, read_only=True, many=True
)
@@ -159,14 +154,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
return queryset
def _get_allowed_applications(
self, pagined_apps: Iterator[Application], user: User | None = None
self, paginated_apps: Iterator[Application], user: User | None = None
) -> list[Application]:
applications = []
request = self.request._request
if user:
request = copy(request)
request.user = user
for application in pagined_apps:
for application in paginated_apps:
engine = PolicyEngine(application, request.user, request)
engine.build()
if engine.passing:

View File

@@ -17,6 +17,7 @@ from django.contrib.sessions.base_session import AbstractBaseSession
from django.core.validators import validate_slug
from django.db import models
from django.db.models import Q, QuerySet, options
from django.db.models.constants import LOOKUP_SEP
from django.http import HttpRequest
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -44,7 +45,6 @@ from authentik.lib.models import (
DomainlessFormattedURLValidator,
SerializerModel,
)
from authentik.lib.utils.inheritance import get_deepest_child
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import PolicyBindingModel
from authentik.rbac.models import Role
@@ -803,7 +803,25 @@ class Application(SerializerModel, PolicyBindingModel):
"""Get casted provider instance. Needs Application queryset with_provider"""
if not self.provider:
return None
return get_deepest_child(self.provider)
candidates = []
base_class = Provider
for subclass in base_class.objects.get_queryset()._get_subclasses_recurse(base_class):
parent = self.provider
for level in subclass.split(LOOKUP_SEP):
try:
parent = getattr(parent, level)
except AttributeError:
break
if parent in candidates:
continue
idx = subclass.count(LOOKUP_SEP)
if type(parent) is not base_class:
idx += 1
candidates.insert(idx, parent)
if not candidates:
return None
return candidates[-1]
def backchannel_provider_for[T: Provider](self, provider_type: type[T], **kwargs) -> T | None:
"""Get Backchannel provider for a specific type"""
@@ -1115,11 +1133,7 @@ class ExpiringModel(models.Model):
default the object is deleted. This is less efficient compared
to bulk deleting objects, but classes like Token() need to change
values instead of being deleted."""
try:
return self.delete(*args, **kwargs)
except self.DoesNotExist:
# Object has already been deleted, so this should be fine
return None
return self.delete(*args, **kwargs)
@classmethod
def filter_not_expired(cls, **kwargs) -> QuerySet[Self]:

View File

@@ -24,8 +24,7 @@ from authentik.root.ws.consumer import build_device_group
# Arguments: user: User, password: str
password_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest,
# stage: Stage, context: dict[str, any]
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
login_failed = Signal()
LOGGER = get_logger()

View File

@@ -63,7 +63,7 @@ class TestPropertyMappingAPI(APITestCase):
PropertyMappingSerializer().validate_expression("/")
def test_types(self):
"""Test PropertyMappigns's types endpoint"""
"""Test PropertyMapping's types endpoint"""
response = self.client.get(
reverse("authentik_api:propertymapping-types"),
)

View File

@@ -78,7 +78,7 @@ def generate_key_id_legacy(key_data: str) -> str:
"""Generate Key ID using MD5 (legacy format for backwards compatibility)."""
if not key_data:
return ""
return md5(key_data.encode("utf-8"), usedforsecurity=False).hexdigest() # nosec
return md5(key_data.encode("utf-8")).hexdigest() # nosec
class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):

View File

@@ -1,11 +1,8 @@
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.endpoints.api.connectors import ConnectorSerializer
from authentik.endpoints.controller import Capabilities
from authentik.endpoints.models import Connector, EndpointStage
from authentik.endpoints.models import EndpointStage
from authentik.flows.api.stages import StageSerializer
@@ -14,13 +11,6 @@ class EndpointStageSerializer(StageSerializer):
connector_obj = ConnectorSerializer(source="connector", read_only=True)
def validate_connector(self, connector: Connector) -> Connector:
conn: Connector = Connector.objects.get_subclass(pk=connector.pk)
controller = conn.controller(conn)
if Capabilities.STAGE_ENDPOINTS not in controller.capabilities():
raise ValidationError(_("Selected connector is not compatible with this stage."))
return connector
class Meta:
model = EndpointStage
fields = StageSerializer.Meta.fields + [

View File

@@ -18,10 +18,7 @@ from authentik.rbac.decorators import permission_required
class EnrollmentTokenSerializer(ModelSerializer):
device_group_obj = DeviceAccessGroupSerializer(
source="device_group",
read_only=True,
required=False,
allow_null=True,
source="device_group", read_only=True, required=False
)
def __init__(self, *args, **kwargs) -> None:

View File

@@ -37,8 +37,6 @@ class AgentEnrollmentAuth(BaseAuthentication):
token = EnrollmentToken.filter_not_expired(key=key).first()
if not token:
raise PermissionDenied()
if not token.connector.enabled:
raise PermissionDenied()
CTX_AUTH_VIA.set("endpoint_token_enrollment")
return (DeviceUser(), token)
@@ -53,8 +51,6 @@ class AgentAuth(BaseAuthentication):
device_token = DeviceToken.filter_not_expired(key=key).first()
if not device_token:
raise PermissionDenied()
if not device_token.device.connector.enabled:
raise PermissionDenied()
if device_token.device.device.is_expired:
raise PermissionDenied()
CTX_AUTH_VIA.set("endpoint_token")

View File

@@ -8,7 +8,7 @@ from rest_framework.fields import CharField
from authentik.core.api.utils import PassiveSerializer
from authentik.endpoints.connectors.agent.models import AgentConnector, EnrollmentToken
from authentik.endpoints.controller import BaseController, Capabilities
from authentik.endpoints.controller import BaseController
from authentik.endpoints.facts import OSFamily
@@ -48,8 +48,8 @@ class AgentConnectorController(BaseController[AgentConnector]):
def vendor_identifier() -> str:
return "goauthentik.io/platform"
def capabilities(self) -> list[Capabilities]:
return [Capabilities.STAGE_ENDPOINTS]
def supported_enrollment_methods(self):
return []
def generate_mdm_config(
self, target_platform: OSFamily, request: HttpRequest, token: EnrollmentToken

View File

@@ -58,16 +58,6 @@ class TestAgentAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
def test_enroll_disabled(self):
self.connector.enabled = False
self.connector.save()
response = self.client.post(
reverse("authentik_api:agentconnector-enroll"),
data={"device_serial": generate_id(), "device_name": "bar"},
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 403)
def test_enroll_token_delete(self):
response = self.client.post(
reverse("authentik_api:agentconnector-enroll"),
@@ -114,16 +104,6 @@ class TestAgentAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
@reconcile_app("authentik_crypto")
def test_config_disabled(self):
self.connector.enabled = False
self.connector.save()
response = self.client.get(
reverse("authentik_api:agentconnector-agent-config"),
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
)
self.assertEqual(response.status_code, 403)
def test_check_in(self):
response = self.client.post(
reverse("authentik_api:agentconnector-check-in"),
@@ -132,16 +112,6 @@ class TestAgentAPI(APITestCase):
)
self.assertEqual(response.status_code, 204)
def test_check_in_disabled(self):
self.connector.enabled = False
self.connector.save()
response = self.client.post(
reverse("authentik_api:agentconnector-check-in"),
data=CHECK_IN_DATA_VALID,
HTTP_AUTHORIZATION=f"Bearer+agent {self.device_token.key}",
)
self.assertEqual(response.status_code, 403)
def test_check_in_token_expired(self):
self.device_token.expiring = True
self.device_token.expires = now() - timedelta(hours=1)

View File

@@ -8,15 +8,13 @@ from authentik.lib.sentry import SentryIgnoredException
MERGED_VENDOR = "goauthentik.io/@merged"
class Capabilities(models.TextChoices):
class EnrollmentMethods(models.TextChoices):
# Automatically enrolled through user action
ENROLL_AUTOMATIC_USER = "enroll_automatic_user"
AUTOMATIC_USER = "automatic_user"
# Automatically enrolled through connector integration
ENROLL_AUTOMATIC_API = "enroll_automatic_api"
AUTOMATIC_API = "automatic_api"
# Manually enrolled with user interaction (user scanning a QR code for example)
ENROLL_MANUAL_USER = "enroll_manual_user"
# Supported for use with Endpoints stage
STAGE_ENDPOINTS = "stage_endpoints"
MANUAL_USER = "manual_user"
class ConnectorSyncException(SentryIgnoredException):
@@ -36,7 +34,7 @@ class BaseController[T: "Connector"]:
def vendor_identifier() -> str:
raise NotImplementedError
def capabilities(self) -> list[Capabilities]:
def supported_enrollment_methods(self) -> list[EnrollmentMethods]:
return []
def stage_view_enrollment(self) -> StageView | None:

View File

@@ -1,4 +1,4 @@
from authentik.endpoints.models import Connector, EndpointStage, StageMode
from authentik.endpoints.models import EndpointStage, StageMode
from authentik.flows.stage import StageView
PLAN_CONTEXT_ENDPOINT_CONNECTOR = "endpoint_connector"
@@ -8,10 +8,7 @@ class EndpointStageView(StageView):
def _get_inner(self) -> StageView | None:
stage: EndpointStage = self.executor.current_stage
connector: Connector = stage.connector
if not connector.enabled:
return None
inner_stage: type[StageView] | None = connector.stage
inner_stage: type[StageView] | None = stage.connector.stage
if not inner_stage:
return None
return inner_stage(self.executor, request=self.request)

View File

@@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.endpoints.controller import Capabilities
from authentik.endpoints.controller import EnrollmentMethods
from authentik.endpoints.models import Connector
LOGGER = get_logger()
@@ -17,11 +17,11 @@ def endpoints_sync(connector_pk: Any):
connector: Connector | None = (
Connector.objects.filter(pk=connector_pk).select_subclasses().first()
)
if not connector or not connector.enabled:
if not connector:
return
controller = connector.controller
ctrl = controller(connector)
if Capabilities.AUTOMATIC_API not in ctrl.capabilities():
if EnrollmentMethods.AUTOMATIC_API not in ctrl.supported_enrollment_methods():
return
LOGGER.info("Syncing connector", connector=connector.name)
ctrl.sync_endpoints()

View File

@@ -1,41 +0,0 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
from authentik.endpoints.connectors.agent.models import AgentConnector
from authentik.endpoints.models import StageMode
from authentik.enterprise.endpoints.connectors.fleet.models import FleetConnector
from authentik.lib.generators import generate_id
class TestAPI(APITestCase):
def setUp(self):
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_endpoint_stage_agent(self):
connector = AgentConnector.objects.create(name=generate_id())
res = self.client.post(
reverse("authentik_api:stages-endpoint-list"),
data={
"name": generate_id(),
"connector": str(connector.pk),
"mode": StageMode.REQUIRED,
},
)
self.assertEqual(res.status_code, 201)
def test_endpoint_stage_fleet(self):
connector = FleetConnector.objects.create(name=generate_id())
res = self.client.post(
reverse("authentik_api:stages-endpoint-list"),
data={
"name": generate_id(),
"connector": str(connector.pk),
"mode": StageMode.REQUIRED,
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(
res.content, {"connector": ["Selected connector is not compatible with this stage."]}
)

View File

@@ -3,7 +3,6 @@ from hmac import compare_digest
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseBadRequest, QueryDict
from authentik.common.oauth.constants import QS_LOGIN_HINT
from authentik.endpoints.connectors.agent.auth import (
agent_auth_issue_token,
check_device_policies,
@@ -15,7 +14,7 @@ from authentik.enterprise.policy import EnterprisePolicyAccessView
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_DEVICE, FlowPlanner
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
from authentik.flows.stage import StageView
from authentik.providers.oauth2.utils import HttpResponseRedirectScheme
QS_AGENT_IA_TOKEN = "ak-auth-ia-token" # nosec
@@ -65,14 +64,14 @@ class AgentInteractiveAuth(EnterprisePolicyAccessView):
planner = FlowPlanner(self.connector.authorization_flow)
planner.allow_empty_flows = True
context = {
PLAN_CONTEXT_DEVICE: self.device,
PLAN_CONTEXT_DEVICE_AUTH_TOKEN: self.auth_token,
}
if QS_LOGIN_HINT in request.GET:
context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = request.GET[QS_LOGIN_HINT]
try:
plan = planner.plan(self.request, context)
plan = planner.plan(
self.request,
{
PLAN_CONTEXT_DEVICE: self.device,
PLAN_CONTEXT_DEVICE_AUTH_TOKEN: self.auth_token,
},
)
except FlowNonApplicableException:
return self.handle_no_permission_authenticated()
plan.append_stage(in_memory_stage(AgentAuthFulfillmentStage))
@@ -85,6 +84,7 @@ class AgentInteractiveAuth(EnterprisePolicyAccessView):
class AgentAuthFulfillmentStage(StageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
device: Device = self.executor.plan.context.pop(PLAN_CONTEXT_DEVICE)
auth_token: DeviceAuthenticationToken = self.executor.plan.context.pop(

View File

@@ -6,7 +6,7 @@ from requests import RequestException
from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.endpoints.controller import BaseController, Capabilities, ConnectorSyncException
from authentik.endpoints.controller import BaseController, ConnectorSyncException, EnrollmentMethods
from authentik.endpoints.facts import (
DeviceFacts,
OSFamily,
@@ -43,8 +43,8 @@ class FleetController(BaseController[DBC]):
def vendor_identifier() -> str:
return "fleetdm.com"
def capabilities(self) -> list[Capabilities]:
return [Capabilities.ENROLL_AUTOMATIC_API]
def supported_enrollment_methods(self) -> list[EnrollmentMethods]:
return [EnrollmentMethods.AUTOMATIC_API]
def _url(self, path: str) -> str:
return f"{self.connector.url}{path}"

View File

@@ -331,7 +331,7 @@ class GoogleWorkspaceGroupTests(TestCase):
).exists()
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 7)
self.assertEqual(len(http.requests()), 5)
def test_sync_discover_multiple(self):
"""Test group discovery"""
@@ -372,7 +372,7 @@ class GoogleWorkspaceGroupTests(TestCase):
).exists()
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 7)
self.assertEqual(len(http.requests()), 5)
# Change response to trigger update
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",

View File

@@ -309,7 +309,7 @@ class GoogleWorkspaceUserTests(TestCase):
).exists()
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 7)
self.assertEqual(len(http.requests()), 5)
def test_sync_discover_multiple(self):
"""Test user discovery, running multiple times"""
@@ -352,7 +352,7 @@ class GoogleWorkspaceUserTests(TestCase):
).exists()
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 7)
self.assertEqual(len(http.requests()), 5)
# Change response, which will trigger a discovery update
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",

View File

@@ -81,8 +81,6 @@ class SignInProcessor:
self.sign_in_request = sign_in_request
self.saml_processor = AssertionProcessor(self.provider, self.request, AuthNRequest())
self.saml_processor.provider.audience = self.sign_in_request.wtrealm
if self.provider.signing_kp:
self.saml_processor.provider.sign_assertion = True
def create_response_token(self):
root = Element(f"{{{NS_WS_FED_TRUST}}}RequestSecurityTokenResponse", nsmap=NS_MAP)
@@ -150,8 +148,7 @@ class SignInProcessor:
def response(self) -> dict[str, str]:
root = self.create_response_token()
assertion = root.xpath("//saml:Assertion", namespaces=NS_MAP)[0]
if self.provider.signing_kp:
self.saml_processor._sign(assertion)
self.saml_processor._sign(assertion)
str_token = etree.tostring(root).decode("utf-8") # nosec
return delete_none_values(
{

View File

@@ -3,7 +3,8 @@
from django.urls import path
from authentik.enterprise.providers.ws_federation.api.providers import WSFederationProviderViewSet
from authentik.enterprise.providers.ws_federation.views import MetadataDownload, WSFedEntryView
from authentik.enterprise.providers.ws_federation.views import WSFedEntryView
from authentik.providers.saml.views.metadata import MetadataDownload
urlpatterns = [
path(

View File

@@ -93,13 +93,11 @@ def on_login_failed(
credentials: dict[str, str],
request: HttpRequest,
stage: Stage | None = None,
context: dict[str, Any] | None = None,
**kwargs,
):
"""Failed Login, authentik custom event"""
user = User.objects.filter(username=credentials.get("username")).first()
context = context or {}
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **context).from_http(
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(
request, user
)

View File

@@ -207,9 +207,3 @@ class TestEvents(TestCase):
"username": user.username,
},
)
def test_invalid_string(self):
"""Test creating an event with invalid unicode string data"""
event = Event.new("unittest", foo="foo bar \u0000 baz")
event.save()
self.assertEqual(event.context["foo"], "foo bar baz")

View File

@@ -36,10 +36,6 @@ ALLOWED_SPECIAL_KEYS = re.compile(
)
def cleanse_str(raw: Any) -> str:
return str(raw).replace("\u0000", "")
def cleanse_item(key: str, value: Any) -> Any:
"""Cleanse a single item"""
if isinstance(value, dict):
@@ -70,7 +66,7 @@ def cleanse_dict(source: dict[Any, Any]) -> dict[Any, Any]:
def model_to_dict(model: Model) -> dict[str, Any]:
"""Convert model to dict"""
name = cleanse_str(model)
name = str(model)
if hasattr(model, "name"):
name = model.name
return {
@@ -137,11 +133,11 @@ def sanitize_item(value: Any) -> Any: # noqa: PLR0911, PLR0912
if isinstance(value, ASN):
return ASN_CONTEXT_PROCESSOR.asn_to_dict(value)
if isinstance(value, Path):
return cleanse_str(value)
return str(value)
if isinstance(value, Exception):
return cleanse_str(value)
return str(value)
if isinstance(value, YAMLTag):
return cleanse_str(value)
return str(value)
if isinstance(value, Enum):
return value.value
if isinstance(value, type):
@@ -165,7 +161,7 @@ def sanitize_item(value: Any) -> Any: # noqa: PLR0911, PLR0912
raise ValueError("JSON can't represent timezone-aware times.")
return value.isoformat()
if isinstance(value, timedelta):
return cleanse_str(value.total_seconds())
return str(value.total_seconds())
if callable(value):
return {
"type": "callable",
@@ -178,8 +174,8 @@ def sanitize_item(value: Any) -> Any: # noqa: PLR0911, PLR0912
try:
return DjangoJSONEncoder().default(value)
except TypeError:
return cleanse_str(value)
return cleanse_str(value)
return str(value)
return str(value)
def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:

View File

@@ -29,12 +29,6 @@ class RefreshOtherFlowsAfterAuthentication(Flag[bool], key="flows_refresh_others
visibility = "public"
class ContinuousLogin(Flag[bool], key="flows_continuous_login"):
default = False
visibility = "public"
class AuthentikFlowsConfig(ManagedAppConfig):
"""authentik flows app config"""

View File

@@ -182,5 +182,3 @@ storage:
# backend: file # or s3
# file: {}
# s3: {}
skip_migrations: false

View File

@@ -103,7 +103,6 @@ class SyncTasks:
)
users_tasks.run().wait(timeout=provider.get_object_sync_time_limit_ms(User))
group_tasks.run().wait(timeout=provider.get_object_sync_time_limit_ms(Group))
self._sync_cleanup(provider, task)
except TransientSyncException as exc:
self.logger.warning("transient sync exception", exc=exc)
task.warning("Sync encountered a transient exception. Retrying", exc=exc)
@@ -112,35 +111,6 @@ class SyncTasks:
task.error(exc)
return
def _sync_cleanup(self, provider: OutgoingSyncProvider, task: Task):
"""Delete remote objects that are no longer in scope"""
for object_type in (User, Group):
try:
client = provider.client_for_model(object_type)
except TransientSyncException:
continue
in_scope_pks = set(provider.get_object_qs(object_type).values_list("pk", flat=True))
stale = client.connection_type.objects.filter(provider=provider).exclude(
**{f"{client.connection_type_query}__pk__in": in_scope_pks}
)
for connection in stale:
try:
client.delete(connection.scim_id)
task.info(
f"Deleted out-of-scope {object_type._meta.verbose_name}",
scim_id=connection.scim_id,
)
except NotFoundSyncException:
pass
except TransientSyncException as exc:
self.logger.warning("transient error during cleanup", exc=exc)
self.logger.warning(
"Cleanup encountered a transient exception. Retrying", exc=exc
)
raise Retry() from exc
except DryRunRejected as exc:
self.logger.info("Rejected dry-run cleanup event", exc=exc)
def sync_objects(
self,
object_type: str,

View File

@@ -1,119 +0,0 @@
"""Tests for inheritance helpers."""
from contextlib import contextmanager
from django.db import connection, models
from django.test import TransactionTestCase
from django.test.utils import isolate_apps
from authentik.lib.utils.inheritance import get_deepest_child
@contextmanager
def temporary_inheritance_models():
"""Create a temporary multi-table inheritance graph for testing."""
with isolate_apps("authentik.lib.tests"):
class GrandParent(models.Model):
class Meta:
app_label = "tests"
def __str__(self) -> str:
return f"GrandParent({self.pk})"
class Parent(GrandParent):
class Meta:
app_label = "tests"
def __str__(self) -> str:
return f"Parent({self.pk})"
class Child(Parent):
class Meta:
app_label = "tests"
def __str__(self) -> str:
return f"Child({self.pk})"
class GrandChild(Child):
class Meta:
app_label = "tests"
def __str__(self) -> str:
return f"GrandChild({self.pk})"
with connection.schema_editor() as schema_editor:
schema_editor.create_model(GrandParent)
schema_editor.create_model(Parent)
schema_editor.create_model(Child)
schema_editor.create_model(GrandChild)
try:
yield GrandParent, Parent, Child, GrandChild
finally:
with connection.schema_editor() as schema_editor:
schema_editor.delete_model(GrandChild)
schema_editor.delete_model(Child)
schema_editor.delete_model(Parent)
schema_editor.delete_model(GrandParent)
class TestInheritanceUtils(TransactionTestCase):
"""Tests for helper functions in authentik.lib.utils.inheritance."""
def test_get_deepest_child_grandparent_to_parent(self):
"""GrandParent -> Parent."""
with temporary_inheritance_models() as (GrandParent, Parent, _Child, _GrandChild):
parent = Parent.objects.create()
grandparent = GrandParent.objects.get(pk=parent.pk)
resolved = get_deepest_child(grandparent)
self.assertIsInstance(resolved, Parent)
self.assertEqual(resolved.pk, parent.pk)
def test_get_deepest_child_grandparent_to_child(self):
"""GrandParent -> Child."""
with temporary_inheritance_models() as (GrandParent, _Parent, Child, _GrandChild):
child = Child.objects.create()
grandparent = GrandParent.objects.get(pk=child.pk)
resolved = get_deepest_child(grandparent)
self.assertIsInstance(resolved, Child)
self.assertEqual(resolved.pk, child.pk)
def test_get_deepest_child_grandparent_to_grandchild(self):
"""GrandParent -> GrandChild."""
with temporary_inheritance_models() as (GrandParent, _Parent, _Child, GrandChild):
grandchild = GrandChild.objects.create()
grandparent = GrandParent.objects.get(pk=grandchild.pk)
resolved = get_deepest_child(grandparent)
self.assertIsInstance(resolved, GrandChild)
self.assertEqual(resolved.pk, grandchild.pk)
def test_get_deepest_child_parent_to_child(self):
"""Parent -> Child (start from non-root)."""
with temporary_inheritance_models() as (_GrandParent, Parent, Child, _GrandChild):
child = Child.objects.create()
parent = Parent.objects.get(pk=child.pk)
resolved = get_deepest_child(parent)
self.assertIsInstance(resolved, Child)
self.assertEqual(resolved.pk, child.pk)
def test_get_deepest_child_no_queries_with_preloaded_relations(self):
"""No extra queries when the inheritance chain is fully select_related."""
with temporary_inheritance_models() as (GrandParent, _Parent, _Child, GrandChild):
grandchild = GrandChild.objects.create()
grandparent = GrandParent.objects.select_related("parent__child__grandchild").get(
pk=grandchild.pk
)
with self.assertNumQueries(0):
resolved = get_deepest_child(grandparent)
self.assertIsInstance(resolved, GrandChild)

View File

@@ -1,41 +0,0 @@
from django.db.models import Model, OneToOneField, OneToOneRel
def get_deepest_child(parent: Model) -> Model:
"""
In multiple table inheritance, given any ancestor object, get the deepest child object.
See https://docs.djangoproject.com/en/dev/topics/db/models/#multi-table-inheritance
This function does not query the database if `select_related` has been performed on all
subclasses of `parent`'s model.
"""
# Almost verbatim copy from django-model-utils, see
# https://github.com/jazzband/django-model-utils/blob/5.0.0/model_utils/managers.py#L132
one_to_one_rels = [
field for field in parent._meta.get_fields() if isinstance(field, OneToOneRel)
]
submodel_fields = [
rel
for rel in one_to_one_rels
if isinstance(rel.field, OneToOneField)
and issubclass(rel.field.model, parent._meta.model)
and parent._meta.model is not rel.field.model
and rel.parent_link
]
submodel_accessors = [submodel_field.get_accessor_name() for submodel_field in submodel_fields]
# End Copy
child = None
for submodel in submodel_accessors:
try:
child = getattr(parent, submodel)
break
except AttributeError:
continue
if not child:
return parent
return get_deepest_child(child)

View File

@@ -185,10 +185,8 @@ class KubernetesObjectReconciler[T]:
patch = self.get_patch()
if patch is not None:
try:
current_json = ApiClient().sanitize_for_serialization(current)
except AttributeError:
current_json = asdict(current)
current_json = ApiClient().sanitize_for_serialization(current)
try:
if apply_patch(current_json, patch) != current_json:
raise NeedsUpdate()

View File

@@ -163,5 +163,4 @@ def outpost_pre_delete_cleanup(sender, instance: Outpost, **_):
@receiver(pre_delete, sender=AuthenticatedSession)
def outpost_logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
"""Catch logout by expiring sessions being deleted"""
if Outpost.objects.exists():
outpost_session_end.send(instance.session.session_key)
outpost_session_end.send(instance.session.session_key)

View File

@@ -7,6 +7,7 @@ from socket import gethostname
from typing import Any
from urllib.parse import urlparse
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
@@ -158,7 +159,7 @@ def outpost_send_update(pk: Any):
layer = get_channel_layer()
group = build_outpost_group(outpost.pk)
LOGGER.debug("sending update", channel=group, outpost=outpost)
layer.group_send_blocking(group, {"type": "event.update"})
async_to_sync(layer.group_send)(group, {"type": "event.update"})
@actor(description=_("Checks the local environment and create Service connections."))
@@ -209,7 +210,7 @@ def outpost_session_end(session_id: str):
for outpost in Outpost.objects.all():
LOGGER.info("Sending session end signal to outpost", outpost=outpost)
group = build_outpost_group(outpost.pk)
layer.group_send_blocking(
async_to_sync(layer.group_send)(
group,
{
"type": "event.session.end",

View File

@@ -1,19 +1,31 @@
"""Shared logout stages for SAML and OIDC providers"""
from django.http import HttpResponse
from rest_framework.fields import CharField, DictField, ListField
from rest_framework.fields import CharField, ListField
from authentik.common.oauth.constants import PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.stage import ChallengeStageView
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS
class LogoutURL(PassiveSerializer):
"""Data for a single logout URL"""
url = CharField()
provider_name = CharField(required=False, allow_null=True)
binding = CharField(required=False, allow_null=True)
saml_request = CharField(required=False, allow_null=True)
saml_response = CharField(required=False, allow_null=True)
saml_relay_state = CharField(required=False, allow_null=True)
class IframeLogoutChallenge(Challenge):
"""Challenge for iframe logout"""
component = CharField(default="ak-provider-iframe-logout")
logout_urls = ListField(child=DictField(), default=list)
logout_urls = ListField(child=LogoutURL(), default=list)
class IframeLogoutChallengeResponse(ChallengeResponse):

View File

@@ -2,7 +2,6 @@
from base64 import b64encode
from json import loads
from urllib.parse import quote
from django.urls import reverse
@@ -97,16 +96,3 @@ class TesOAuth2DeviceBackchannel(OAuthTestCase):
self.assertEqual(res.status_code, 200)
body = loads(res.content.decode())
self.assertEqual(body["expires_in"], 60)
def test_backchannel_client_id_via_auth_header_urlencoded(self):
"""Test URL-encoded client IDs in Basic auth"""
self.provider.client_id = "test/client+id"
self.provider.save()
creds = b64encode(f"{quote(self.provider.client_id, safe='')}:".encode()).decode()
res = self.client.post(
reverse("authentik_providers_oauth2:device"),
HTTP_AUTHORIZATION=f"Basic {creds}",
)
self.assertEqual(res.status_code, 200)
body = loads(res.content.decode())
self.assertEqual(body["expires_in"], 60)

View File

@@ -2,7 +2,6 @@
from base64 import b64encode
from json import dumps
from urllib.parse import quote
from django.test import RequestFactory
from django.urls import reverse
@@ -29,7 +28,6 @@ from authentik.providers.oauth2.models import (
ScopeMapping,
)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.utils import extract_client_auth
from authentik.providers.oauth2.views.token import TokenParams
@@ -117,20 +115,6 @@ class TestToken(OAuthTestCase):
params = TokenParams.parse(request, provider, provider.client_id, provider.client_secret)
self.assertEqual(params.provider, provider)
def test_extract_client_auth_basic_auth_percent_decodes(self):
"""test percent-decoding of client credentials in Basic auth"""
header = b64encode(
f"{quote('client/id', safe='')}:{quote('secret+/==', safe='')}".encode()
).decode()
request = self.factory.post("/", HTTP_AUTHORIZATION=f"Basic {header}")
self.assertEqual(extract_client_auth(request), ("client/id", "secret+/=="))
def test_extract_client_auth_basic_auth_preserves_raw_plus(self):
"""test compatibility with clients that still send raw plus characters"""
header = b64encode(b"client:secret+plus").decode()
request = self.factory.post("/", HTTP_AUTHORIZATION=f"Basic {header}")
self.assertEqual(extract_client_auth(request), ("client", "secret+plus"))
def test_auth_code_view(self):
"""test request param"""
provider = OAuth2Provider.objects.create(

View File

@@ -2,7 +2,6 @@
from base64 import b64encode
from json import loads
from urllib.parse import quote
from django.test import RequestFactory
from django.urls import reverse
@@ -179,41 +178,6 @@ class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
def test_successful_basic_auth_urlencoded_client_secret(self):
"""test successful with URL-encoded Basic auth credentials"""
client_secret = b64encode(f"sa:{self.token.key}".encode()).decode()
header = b64encode(
f"{quote(self.provider.client_id, safe='')}:{quote(client_secret, safe='')}".encode()
).decode()
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}",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
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

@@ -7,7 +7,7 @@ from binascii import Error
from hashlib import sha256
from hmac import compare_digest
from typing import Any
from urllib.parse import unquote, urlparse
from urllib.parse import urlparse
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.http.response import HttpResponseRedirect
@@ -122,10 +122,6 @@ def extract_client_auth(request: HttpRequest) -> tuple[str, str]:
try:
user_pass = b64decode(b64_user_pass).decode("utf-8").partition(":")
client_id, _, client_secret = user_pass
# RFC 6749 requires client credentials in Basic auth to be form-encoded first.
# We only percent-decode here so raw `+` characters keep their previous meaning.
client_id = unquote(client_id)
client_secret = unquote(client_secret)
except ValueError, Error:
client_id = client_secret = "" # nosec
else:

View File

@@ -432,7 +432,7 @@ class AuthorizationFlowInitView(BufferedPolicyAccessView):
return response
def dispatch(self, request: HttpRequest, *args, **kwargs):
# Activate language before parsing params (error messages should be localised)
# Activate language before parsing params (error messages should be localized)
return self.dispatch_with_language(request, *args, **kwargs)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@@ -368,7 +368,7 @@ class TokenParams:
) -> tuple[dict, OAuthSource] | tuple[None, None]:
# Fully decode the JWT without verifying the signature, so we can get access to
# the header.
# Get the Key ID from the header, and use that to optimise our source query to only find
# Get the Key ID from the header, and use that to optimize our source query to only find
# sources that have a JWK for that Key ID
# The Key ID doesn't have a fixed format, but must match between an issued JWT
# and whatever is returned by the JWKS endpoint

View File

@@ -27,8 +27,6 @@ class TraefikMiddlewareSpecForwardAuth:
trustForwardHeader: bool = field(default=True)
maxResponseBodySize: int = field(default=1024 * 1024 * 4)
@dataclass(slots=True)
class TraefikMiddlewareSpec:
@@ -142,7 +140,6 @@ class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
],
authResponseHeadersRegex="",
trustForwardHeader=True,
maxResponseBodySize=1024 * 1024 * 4,
)
),
)

View File

@@ -0,0 +1,13 @@
"""Proxy provider signals"""
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from authentik.core.models import AuthenticatedSession
from authentik.providers.proxy.tasks import proxy_on_logout
@receiver(pre_delete, sender=AuthenticatedSession)
def logout_proxy_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
"""Catch logout by expiring sessions being deleted"""
proxy_on_logout.send(instance.session.session_key)

View File

@@ -0,0 +1,26 @@
"""proxy provider tasks"""
from asgiref.sync import async_to_sync
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 build_outpost_group
from authentik.outposts.models import Outpost, OutpostType
from authentik.providers.oauth2.id_token import hash_session_key
@actor(description=_("Terminate session on Proxy outpost."))
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 = build_outpost_group(outpost.pk)
async_to_sync(layer.group_send)(
group,
{
"type": "event.provider.specific",
"sub_type": "logout",
"session_id": hashed_session_id,
},
)

View File

@@ -213,6 +213,7 @@ class SAMLProviderSerializer(ProviderSerializer):
"sign_assertion",
"sign_response",
"sign_logout_request",
"sign_logout_response",
"sp_binding",
"sls_binding",
"logout_method",
@@ -232,7 +233,7 @@ class SAMLMetadataSerializer(PassiveSerializer):
"""SAML Provider Metadata serializer"""
metadata = CharField(read_only=True)
download_url = CharField(read_only=True, required=False, allow_null=True)
download_url = CharField(read_only=True, required=False)
class SAMLProviderImportSerializer(PassiveSerializer):

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-24 18:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0020_samlprovider_logout_method_and_more"),
]
operations = [
migrations.AddField(
model_name="samlprovider",
name="sign_logout_response",
field=models.BooleanField(default=False),
),
]

View File

@@ -227,6 +227,7 @@ class SAMLProvider(Provider):
sign_assertion = models.BooleanField(default=True)
sign_response = models.BooleanField(default=False)
sign_logout_request = models.BooleanField(default=False)
sign_logout_response = models.BooleanField(default=False)
@property
def launch_url(self) -> str | None:

View File

@@ -1,11 +1,12 @@
"""SAML Logout stages for automatic injection"""
from django.http import HttpResponse
from rest_framework.fields import BooleanField, CharField
from rest_framework.fields import BooleanField, CharField, ChoiceField
from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeResponse, HttpChallengeResponse
from authentik.flows.stage import ChallengeStageView
from authentik.providers.saml.models import SAMLBindings
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS
LOGGER = get_logger()
@@ -19,14 +20,17 @@ class NativeLogoutChallenge(Challenge):
"""Challenge for native browser logout"""
component = CharField(default="ak-provider-saml-native-logout")
post_url = CharField(required=False)
saml_request = CharField(required=False)
relay_state = CharField(required=False)
provider_name = CharField(required=False)
binding = CharField(required=False)
redirect_url = CharField(required=False)
is_complete = BooleanField(required=False, default=False)
post_url = CharField(required=False)
redirect_url = CharField(required=False)
saml_binding = ChoiceField(choices=SAMLBindings.choices, required=False)
saml_request = CharField(required=False)
saml_response = CharField(required=False)
saml_relay_state = CharField(required=False)
class NativeLogoutChallengeResponse(ChallengeResponse):
"""Response for native browser logout"""

View File

@@ -0,0 +1,196 @@
"""LogoutResponse processor"""
import base64
from urllib.parse import quote, urlencode
import xmlsec
from lxml import etree
from lxml.etree import Element, SubElement
from authentik.common.saml.constants import (
DIGEST_ALGORITHM_TRANSLATION_MAP,
NS_MAP,
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
SIGN_ALGORITHM_TRANSFORM_MAP,
)
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.utils import get_random_id
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode
from authentik.providers.saml.utils.time import get_time_string
class LogoutResponseProcessor:
"""Generate a SAML LogoutResponse"""
provider: SAMLProvider
logout_request: LogoutRequest
destination: str | None
relay_state: str | None
_issue_instant: str
_response_id: str
def __init__(
self,
provider: SAMLProvider,
logout_request: LogoutRequest,
destination: str | None = None,
relay_state: str | None = None,
):
self.provider = provider
self.logout_request = logout_request
self.destination = destination
self.relay_state = relay_state or (logout_request.relay_state if logout_request else None)
self._issue_instant = get_time_string()
self._response_id = get_random_id()
def get_issuer(self) -> Element:
"""Get Issuer element"""
issuer = Element(f"{{{NS_SAML_ASSERTION}}}Issuer")
issuer.text = self.provider.issuer
return issuer
def build(self, status: str = "Success") -> Element:
"""Build a SAML LogoutResponse as etree Element"""
response = Element(f"{{{NS_SAML_PROTOCOL}}}LogoutResponse", nsmap=NS_MAP)
response.attrib["Version"] = "2.0"
response.attrib["IssueInstant"] = self._issue_instant
response.attrib["ID"] = self._response_id
if self.destination:
response.attrib["Destination"] = self.destination
if self.logout_request and self.logout_request.id:
response.attrib["InResponseTo"] = self.logout_request.id
response.append(self.get_issuer())
# Add Status element
status_element = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status")
status_code = SubElement(status_element, f"{{{NS_SAML_PROTOCOL}}}StatusCode")
status_code.attrib["Value"] = f"urn:oasis:names:tc:SAML:2.0:status:{status}"
return response
def build_response(self, status: str = "Success") -> str:
"""Build and sign LogoutResponse, return as XML string (not encoded)"""
response = self.build(status)
if self.provider.signing_kp and self.provider.sign_logout_response:
self._add_signature(response)
self._sign_response(response)
return etree.tostring(response).decode()
def encode_post(self, status: str = "Success") -> str:
"""Encode LogoutResponse for POST binding"""
response = self.build(status)
if self.provider.signing_kp and self.provider.sign_logout_response:
self._add_signature(response)
self._sign_response(response)
return base64.b64encode(etree.tostring(response)).decode()
def encode_redirect(self, status: str = "Success") -> str:
"""Encode LogoutResponse for Redirect binding"""
response = self.build(status)
# Note: For redirect binding, signatures are added as query parameters, not in XML
xml_str = etree.tostring(response, encoding="UTF-8", xml_declaration=True)
return deflate_and_base64_encode(xml_str.decode("UTF-8"))
def get_redirect_url(self, status: str = "Success") -> str:
"""Build complete logout response URL for redirect binding with signature if needed"""
encoded_response = self.encode_redirect(status)
params = {
"SAMLResponse": encoded_response,
}
if self.relay_state:
params["RelayState"] = self.relay_state
if self.provider.signing_kp and self.provider.sign_logout_response:
sig_alg = self.provider.signature_algorithm
params["SigAlg"] = sig_alg
# Build the string to sign
query_string = self._build_signable_query_string(params)
signature = self._sign_query_string(query_string)
params["Signature"] = base64.b64encode(signature).decode()
# Some SP's use query params on their sls endpoint
if not self.destination:
raise ValueError("destination is required for redirect URL")
separator = "&" if "?" in self.destination else "?"
return f"{self.destination}{separator}{urlencode(params)}"
def _add_signature(self, element: Element):
"""Add signature placeholder to element"""
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
signature = xmlsec.template.create(
element,
xmlsec.constants.TransformExclC14N,
sign_algorithm_transform,
ns=xmlsec.constants.DSigNs,
)
element.insert(1, signature) # Insert after Issuer
def _sign_response(self, response: Element):
"""Sign the response element"""
digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get(
self.provider.digest_algorithm, xmlsec.constants.TransformSha1
)
xmlsec.tree.add_ids(response, ["ID"])
signature_node = xmlsec.tree.find_node(response, xmlsec.constants.NodeSignature)
ref = xmlsec.template.add_reference(
signature_node,
digest_algorithm_transform,
uri="#" + response.attrib["ID"],
)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped)
xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N)
key_info = xmlsec.template.ensure_key_info(signature_node)
xmlsec.template.add_x509_data(key_info)
ctx = xmlsec.SignatureContext()
ctx.key = xmlsec.Key.from_memory(
self.provider.signing_kp.key_data, # Use key_data for the private key
xmlsec.constants.KeyDataFormatPem,
)
ctx.key.load_cert_from_memory(
self.provider.signing_kp.certificate_data, xmlsec.constants.KeyDataFormatPem
)
ctx.sign(signature_node)
def _build_signable_query_string(self, params: dict) -> str:
"""Build query string for signing (order matters per SAML spec)"""
# SAML spec requires specific order: SAMLResponse, RelayState, SigAlg
# Values must be URL-encoded individually before concatenation
ordered = []
if "SAMLResponse" in params:
ordered.append(f"SAMLResponse={quote(params['SAMLResponse'], safe='')}")
if "RelayState" in params:
ordered.append(f"RelayState={quote(params['RelayState'], safe='')}")
if "SigAlg" in params:
ordered.append(f"SigAlg={quote(params['SigAlg'], safe='')}")
return "&".join(ordered)
def _sign_query_string(self, query_string: str) -> bytes:
"""Sign the query string for redirect binding"""
signature_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha256
)
key = xmlsec.Key.from_memory(
self.provider.signing_kp.key_data,
xmlsec.constants.KeyDataFormatPem,
None,
)
ctx = xmlsec.SignatureContext()
ctx.key = key
return ctx.sign_binary(query_string.encode("utf-8"), signature_algorithm_transform)

View File

@@ -175,16 +175,16 @@ def handle_flow_pre_user_logout(
logout_data = {
"post_url": session.provider.sls_url,
"saml_request": form_data["SAMLRequest"],
"relay_state": form_data["RelayState"],
"saml_relay_state": form_data["RelayState"],
"provider_name": session.provider.name,
"binding": SAMLBindings.POST,
"saml_binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"redirect_url": logout_url,
"provider_name": session.provider.name,
"binding": SAMLBindings.REDIRECT,
"saml_binding": SAMLBindings.REDIRECT,
}
native_sessions.append(logout_data)

View File

@@ -5,8 +5,11 @@ from django.contrib.auth import get_user_model
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
LOGGER = get_logger()
User = get_user_model()
@@ -78,3 +81,86 @@ def send_post_logout_request(provider: SAMLProvider, processor: LogoutRequestPro
)
return True
@actor(description="Send SAML LogoutResponse to a Service Provider (backchannel)")
def send_saml_logout_response(
provider_pk: int,
sls_url: str,
logout_request_id: str | None = None,
relay_state: str | None = None,
):
"""Send SAML LogoutResponse to a Service Provider using backchannel (server-to-server)"""
provider = SAMLProvider.objects.filter(pk=provider_pk).first()
if not provider:
LOGGER.error(
"Provider not found for SAML logout response",
provider_pk=provider_pk,
)
return False
LOGGER.debug(
"Sending backchannel SAML logout response",
provider=provider.name,
sls_url=sls_url,
)
# Create a minimal LogoutRequest object for the response processor
# We only need the ID and relay_state for building the response
logout_request = None
if logout_request_id:
logout_request = LogoutRequest()
logout_request.id = logout_request_id
logout_request.relay_state = relay_state
# Build the logout response
processor = LogoutResponseProcessor(
provider=provider,
logout_request=logout_request,
destination=sls_url,
relay_state=relay_state,
)
encoded_response = processor.encode_post()
form_data = {
"SAMLResponse": encoded_response,
}
if relay_state:
form_data["RelayState"] = relay_state
# Send the logout response to the SP
try:
response = requests.post(
sls_url,
data=form_data,
timeout=10,
headers={
"Content-Type": "application/x-www-form-urlencoded",
},
allow_redirects=True,
)
response.raise_for_status()
LOGGER.info(
"Successfully sent backchannel logout response to SP",
provider=provider.name,
sls_url=sls_url,
status_code=response.status_code,
)
return True
except requests.exceptions.RequestException as exc:
LOGGER.warning(
"Failed to send backchannel logout response to SP",
provider=provider.name,
sls_url=sls_url,
error=str(exc),
)
Event.new(
EventAction.CONFIGURATION_ERROR,
provider=provider,
message=f"Backchannel logout response failed: {str(exc)}",
).save()
return False

View File

@@ -69,7 +69,7 @@ class TestNativeLogoutStageView(TestCase):
{
"redirect_url": "https://sp1.example.com/sls?SAMLRequest=encoded",
"provider_name": "test-provider-1",
"binding": "redirect",
"saml_binding": "redirect",
}
]
stage_view = NativeLogoutStageView(
@@ -85,7 +85,7 @@ class TestNativeLogoutStageView(TestCase):
# Should return a NativeLogoutChallenge
self.assertIsInstance(challenge, NativeLogoutChallenge)
self.assertEqual(challenge.initial_data["binding"], "redirect")
self.assertEqual(challenge.initial_data["saml_binding"], "redirect")
self.assertEqual(challenge.initial_data["provider_name"], "test-provider-1")
self.assertIn("redirect_url", challenge.initial_data)
@@ -102,9 +102,9 @@ class TestNativeLogoutStageView(TestCase):
{
"post_url": "https://sp2.example.com/sls",
"saml_request": "encoded_saml_request",
"relay_state": "https://idp.example.com/flow/test-flow",
"saml_relay_state": "https://idp.example.com/flow/test-flow",
"provider_name": "test-provider-2",
"binding": "post",
"saml_binding": "post",
}
]
stage_view = NativeLogoutStageView(
@@ -120,11 +120,11 @@ class TestNativeLogoutStageView(TestCase):
# Should return a NativeLogoutChallenge
self.assertIsInstance(challenge, NativeLogoutChallenge)
self.assertEqual(challenge.initial_data["binding"], "post")
self.assertEqual(challenge.initial_data["saml_binding"], "post")
self.assertEqual(challenge.initial_data["provider_name"], "test-provider-2")
self.assertEqual(challenge.initial_data["post_url"], "https://sp2.example.com/sls")
self.assertIn("saml_request", challenge.initial_data)
self.assertIn("relay_state", challenge.initial_data)
self.assertIn("saml_relay_state", challenge.initial_data)
def test_get_challenge_all_complete(self):
"""Test get_challenge when all providers are done"""

View File

@@ -0,0 +1,139 @@
"""logout response tests"""
from defusedxml import ElementTree
from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.common.saml.constants import (
NS_SAML_ASSERTION,
NS_SAML_PROTOCOL,
NS_SIGNATURE,
)
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.logout_request_parser import LogoutRequest
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
class TestLogoutResponse(TestCase):
"""Test LogoutResponse processor"""
@apply_blueprint("system/providers-saml.yaml")
def setUp(self):
cert = create_test_cert()
self.provider: SAMLProvider = SAMLProvider.objects.create(
authorization_flow=create_test_flow(),
acs_url="http://testserver/source/saml/provider/acs/",
sls_url="http://testserver/source/saml/provider/sls/",
signing_kp=cert,
verification_kp=cert,
)
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
self.provider.save()
def test_build_response(self):
"""Test building a LogoutResponse"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="test-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
response_xml = processor.build_response(status="Success")
# Parse and verify
root = ElementTree.fromstring(response_xml)
self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
self.assertEqual(root.attrib["Version"], "2.0")
self.assertEqual(root.attrib["Destination"], self.provider.sls_url)
self.assertEqual(root.attrib["InResponseTo"], "test-request-id")
# Check Issuer
issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer")
self.assertEqual(issuer.text, self.provider.issuer)
# Check Status
status = root.find(f".//{{{NS_SAML_PROTOCOL}}}StatusCode")
self.assertEqual(status.attrib["Value"], "urn:oasis:names:tc:SAML:2.0:status:Success")
def test_build_response_signed(self):
"""Test building a signed LogoutResponse"""
self.provider.sign_logout_response = True
self.provider.save()
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="test-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
response_xml = processor.build_response(status="Success")
# Parse and verify signature is present
root = ElementTree.fromstring(response_xml)
signature = root.find(f".//{{{NS_SIGNATURE}}}Signature")
self.assertIsNotNone(signature)
# Verify signature structure
signed_info = signature.find(f"{{{NS_SIGNATURE}}}SignedInfo")
self.assertIsNotNone(signed_info)
signature_value = signature.find(f"{{{NS_SIGNATURE}}}SignatureValue")
self.assertIsNotNone(signature_value)
self.assertIsNotNone(signature_value.text)
def test_no_inresponseto(self):
"""Test building response without a logout request omits InResponseTo attribute"""
processor = LogoutResponseProcessor(self.provider, None, destination=self.provider.sls_url)
response_xml = processor.build_response(status="Success")
root = ElementTree.fromstring(response_xml)
self.assertEqual(root.tag, f"{{{NS_SAML_PROTOCOL}}}LogoutResponse")
self.assertNotIn("InResponseTo", root.attrib)
def test_no_destination(self):
"""Test building response without destination"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
)
processor = LogoutResponseProcessor(self.provider, logout_request, destination=None)
response_xml = processor.build_response(status="Success")
root = ElementTree.fromstring(response_xml)
self.assertNotIn("Destination", root.attrib)
def test_relay_state_from_logout_request(self):
"""Test that relay_state is taken from logout_request if not provided"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="request-relay-state",
)
processor = LogoutResponseProcessor(
self.provider, logout_request, destination=self.provider.sls_url
)
self.assertEqual(processor.relay_state, "request-relay-state")
def test_relay_state_override(self):
"""Test that explicit relay_state overrides logout_request relay_state"""
logout_request = LogoutRequest(
id="test-request-id",
issuer="test-sp",
relay_state="request-relay-state",
)
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
relay_state="explicit-relay-state",
)
self.assertEqual(processor.relay_state, "explicit-relay-state")

View File

@@ -0,0 +1,291 @@
"""Tests for SAML provider tasks"""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from requests.exceptions import ConnectionError, HTTPError
from authentik.common.saml.constants import SAML_NAME_ID_FORMAT_EMAIL
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.tasks import (
send_post_logout_request,
send_saml_logout_request,
send_saml_logout_response,
)
class TestSendSamlLogoutResponse(TestCase):
"""Tests for send_saml_logout_response task"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_response(self, mock_post):
"""Test successful POST to SP returns True"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state="https://sp.example.com/return",
)
self.assertTrue(result)
mock_post.assert_called_once()
# Verify the POST was made with correct data
call_kwargs = mock_post.call_args[1]
self.assertEqual(call_kwargs["timeout"], 10)
self.assertEqual(
call_kwargs["headers"]["Content-Type"], "application/x-www-form-urlencoded"
)
# Verify form data contains SAMLResponse and RelayState
form_data = call_kwargs["data"]
self.assertIn("SAMLResponse", form_data)
self.assertIn("RelayState", form_data)
self.assertEqual(form_data["RelayState"], "https://sp.example.com/return")
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_response_no_relay_state(self, mock_post):
"""Test successful POST without relay_state"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state=None,
)
self.assertTrue(result)
# Verify form data does not contain RelayState
form_data = mock_post.call_args[1]["data"]
self.assertIn("SAMLResponse", form_data)
self.assertNotIn("RelayState", form_data)
def test_provider_not_found(self):
"""Test returns False when provider doesn't exist"""
result = send_saml_logout_response(
provider_pk=99999, # Non-existent provider
sls_url="https://sp.example.com/sls",
logout_request_id="test-request-id",
relay_state=None,
)
self.assertFalse(result)
@patch("authentik.providers.saml.tasks.Event")
@patch("authentik.providers.saml.tasks.requests.post")
def test_http_error_creates_event(self, mock_post, mock_event_class):
"""Test HTTP error creates an error event"""
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = HTTPError("500 Server Error")
mock_post.return_value = mock_response
mock_event = MagicMock()
mock_event_class.new.return_value = mock_event
result = send_saml_logout_response(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id="test-request-id",
relay_state=None,
)
self.assertFalse(result)
# Verify error event was created
mock_event_class.new.assert_called_once()
call_kwargs = mock_event_class.new.call_args[1]
self.assertIn("Backchannel logout response failed", call_kwargs["message"])
mock_event.save.assert_called_once()
class TestSendSamlLogoutRequest(TestCase):
"""Tests for send_saml_logout_request task"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_logout_request(self, mock_post):
"""Test successful POST logout request returns True"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
result = send_saml_logout_request(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
self.assertTrue(result)
mock_post.assert_called_once()
# Verify the POST was made with correct data
call_kwargs = mock_post.call_args[1]
self.assertEqual(call_kwargs["timeout"], 10)
self.assertEqual(
call_kwargs["headers"]["Content-Type"], "application/x-www-form-urlencoded"
)
# Verify form data contains SAMLRequest
form_data = call_kwargs["data"]
self.assertIn("SAMLRequest", form_data)
def test_provider_not_found(self):
"""Test returns False when provider doesn't exist"""
result = send_saml_logout_request(
provider_pk=99999, # Non-existent provider
sls_url="https://sp.example.com/sls",
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
self.assertFalse(result)
@patch("authentik.providers.saml.tasks.requests.post")
def test_http_error_raises(self, mock_post):
"""Test HTTP error raises exception (no try/catch in send_post_logout_request)"""
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = HTTPError("500 Server Error")
mock_post.return_value = mock_response
with self.assertRaises(HTTPError):
send_saml_logout_request(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
class TestSendPostLogoutRequest(TestCase):
"""Tests for send_post_logout_request function"""
def setUp(self):
"""Set up test fixtures"""
self.cert = create_test_cert()
self.flow = create_test_flow()
self.provider = SAMLProvider.objects.create(
name="test-provider",
authorization_flow=self.flow,
acs_url="https://sp.example.com/acs",
sls_url="https://sp.example.com/sls",
issuer="https://idp.example.com",
signing_kp=self.cert,
)
@patch("authentik.providers.saml.tasks.requests.post")
def test_successful_post(self, mock_post):
"""Test successful POST returns True"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
result = send_post_logout_request(self.provider, processor)
self.assertTrue(result)
mock_post.assert_called_once()
@patch("authentik.providers.saml.tasks.requests.post")
def test_with_relay_state(self, mock_post):
"""Test POST includes RelayState when present"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
relay_state="https://sp.example.com/return",
)
result = send_post_logout_request(self.provider, processor)
self.assertTrue(result)
# Verify RelayState is included
form_data = mock_post.call_args[1]["data"]
self.assertIn("RelayState", form_data)
self.assertEqual(form_data["RelayState"], "https://sp.example.com/return")
@patch("authentik.providers.saml.tasks.requests.post")
def test_connection_error_raises(self, mock_post):
"""Test connection error raises exception"""
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
mock_post.side_effect = ConnectionError("Connection refused")
processor = LogoutRequestProcessor(
provider=self.provider,
user=None,
destination=self.provider.sls_url,
name_id="test@example.com",
name_id_format=SAML_NAME_ID_FORMAT_EMAIL,
session_index="test-session-123",
)
with self.assertRaises(ConnectionError):
send_post_logout_request(self.provider, processor)

View File

@@ -14,7 +14,9 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLBindings, SAMLLogoutMethods, SAMLProvider
from authentik.providers.saml.processors.logout_request import LogoutRequestProcessor
from authentik.providers.saml.views.flows import PLAN_CONTEXT_SAML_RELAY_STATE
from authentik.providers.saml.views.flows import (
PLAN_CONTEXT_SAML_RELAY_STATE,
)
from authentik.providers.saml.views.sp_slo import (
SPInitiatedSLOBindingPOSTView,
SPInitiatedSLOBindingRedirectView,
@@ -91,33 +93,32 @@ class TestSPInitiatedSLOViews(TestCase):
self.assertEqual(logout_request.issuer, self.provider.issuer)
self.assertEqual(logout_request.session_index, "test-session-123")
def test_redirect_view_handles_logout_response_with_plan_context(self):
"""Test that redirect view always redirects to plan context URL, ignoring RelayState"""
plan_relay_state = "https://idp.example.com/flow/return"
def test_redirect_view_handles_logout_response_with_relay_state(self):
"""Test that redirect view handles logout response with RelayState"""
# Use raw URL (no encoding needed)
relay_state = "https://idp.example.com/flow/return"
# Create request with SAML logout response
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLResponse": "dummy-response",
"RelayState": "https://somewhere-else.example.com/return",
"RelayState": relay_state,
},
)
plan = FlowPlan(flow_pk="test-flow")
plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = plan_relay_state
request.session = {SESSION_KEY_PLAN: plan}
request.session = {}
request.brand = self.brand
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
response = view.dispatch(request, application_slug=self.application.slug)
# Should redirect to plan context URL, not the request's RelayState
# Should redirect to relay state URL
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, plan_relay_state)
self.assertEqual(response.url, relay_state)
def test_redirect_view_ignores_relay_state_without_plan(self):
"""Test that redirect view ignores RelayState and falls back to root when no plan context"""
def test_redirect_view_handles_logout_response_plain_relay_state(self):
"""Test that redirect view handles logout response with plain RelayState"""
relay_state = "https://sp.example.com/plain"
# Create request with SAML logout response
@@ -135,9 +136,9 @@ class TestSPInitiatedSLOViews(TestCase):
view.setup(request, application_slug=self.application.slug)
response = view.dispatch(request, application_slug=self.application.slug)
# Should ignore relay_state and redirect to root (no plan context)
# Should redirect to plain relay state
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
self.assertEqual(response.url, relay_state)
def test_redirect_view_handles_logout_response_no_relay_state_with_plan_context(self):
"""Test that redirect view uses plan context fallback when no RelayState"""
@@ -229,30 +230,29 @@ class TestSPInitiatedSLOViews(TestCase):
self.assertEqual(logout_request.issuer, self.provider.issuer)
self.assertEqual(logout_request.session_index, "test-session-123")
def test_post_view_handles_logout_response_with_plan_context(self):
"""Test that POST view always redirects to plan context URL, ignoring RelayState"""
plan_relay_state = "https://idp.example.com/flow/return"
def test_post_view_handles_logout_response_with_relay_state(self):
"""Test that POST view handles logout response with RelayState"""
# Use raw URL (no encoding needed)
relay_state = "https://idp.example.com/flow/return"
# Create POST request with SAML logout response
request = self.factory.post(
f"/slo/post/{self.application.slug}/",
{
"SAMLResponse": "dummy-response",
"RelayState": "https://somewhere-else.example.com/return",
"RelayState": relay_state,
},
)
plan = FlowPlan(flow_pk="test-flow")
plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = plan_relay_state
request.session = {SESSION_KEY_PLAN: plan}
request.session = {}
request.brand = self.brand
view = SPInitiatedSLOBindingPOSTView()
view.setup(request, application_slug=self.application.slug)
response = view.dispatch(request, application_slug=self.application.slug)
# Should redirect to plan context URL, not the request's RelayState
# Should redirect to relay state URL
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, plan_relay_state)
self.assertEqual(response.url, relay_state)
def test_post_view_handles_logout_response_no_relay_state_with_plan_context(self):
"""Test that POST view uses plan context fallback when no RelayState"""
@@ -419,7 +419,7 @@ class TestSPInitiatedSLOViews(TestCase):
view.resolve_provider_application()
def test_relay_state_decoding_failure(self):
"""Test that arbitrary path RelayState is ignored and redirects to root"""
"""Test handling of RelayState that's a path"""
# Create request with relay state that is a path
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
@@ -435,73 +435,9 @@ class TestSPInitiatedSLOViews(TestCase):
view.setup(request, application_slug=self.application.slug)
response = view.dispatch(request, application_slug=self.application.slug)
# Should ignore relay_state and redirect to root (no plan context)
# Should treat it as plain URL and redirect to it
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
def test_redirect_view_blocks_external_relay_state(self):
"""Test that redirect view ignores external malicious URL and redirects to root"""
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLResponse": "dummy-response",
"RelayState": "https://evil.com/phishing",
},
)
request.session = {}
request.brand = self.brand
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
response = view.dispatch(request, application_slug=self.application.slug)
# Should ignore relay_state and redirect to root (no plan context)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
def test_redirect_view_ignores_relay_state_uses_plan_context(self):
"""Test that redirect view always uses plan context URL regardless of RelayState"""
plan_relay_state = "https://authentik.example.com/if/flow/logout/"
request = self.factory.get(
f"/slo/redirect/{self.application.slug}/",
{
"SAMLResponse": "dummy-response",
"RelayState": "https://evil.com/phishing",
},
)
plan = FlowPlan(flow_pk="test-flow")
plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = plan_relay_state
request.session = {SESSION_KEY_PLAN: plan}
request.brand = self.brand
view = SPInitiatedSLOBindingRedirectView()
view.setup(request, application_slug=self.application.slug)
response = view.dispatch(request, application_slug=self.application.slug)
# Should always use plan context value, ignoring malicious RelayState
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, plan_relay_state)
def test_post_view_ignores_external_relay_state(self):
"""Test that POST view ignores external RelayState and redirects to root"""
request = self.factory.post(
f"/slo/post/{self.application.slug}/",
{
"SAMLResponse": "dummy-response",
"RelayState": "https://evil.com/phishing",
},
)
request.session = {}
request.brand = self.brand
view = SPInitiatedSLOBindingPOSTView()
view.setup(request, application_slug=self.application.slug)
response = view.dispatch(request, application_slug=self.application.slug)
# Should ignore relay_state and redirect to root (no plan context)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
self.assertEqual(response.url, "/some/invalid/path")
class TestSPInitiatedSLOLogoutMethods(TestCase):

View File

@@ -15,10 +15,22 @@ from authentik.flows.stage import SessionEndStage
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.iframe_logout import IframeLogoutStageView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLProvider, SAMLSession
from authentik.providers.saml.models import (
SAMLBindings,
SAMLLogoutMethods,
SAMLProvider,
SAMLSession,
)
from authentik.providers.saml.native_logout import NativeLogoutStageView
from authentik.providers.saml.processors.logout_request_parser import LogoutRequestParser
from authentik.providers.saml.processors.logout_response_processor import LogoutResponseProcessor
from authentik.providers.saml.tasks import send_saml_logout_response
from authentik.providers.saml.utils.encoding import nice64
from authentik.providers.saml.views.flows import (
PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS,
PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS,
PLAN_CONTEXT_SAML_LOGOUT_REQUEST,
PLAN_CONTEXT_SAML_RELAY_STATE,
REQUEST_KEY_RELAY_STATE,
@@ -29,24 +41,6 @@ from authentik.providers.saml.views.flows import (
LOGGER = get_logger()
def _get_redirect_url(request: HttpRequest, relay_state: str = "") -> str:
"""Get the safe redirect URL from the plan context, logging a warning if the
incoming relay_state doesn't match the stored value."""
stored_relay_state = ""
if SESSION_KEY_PLAN in request.session:
plan: FlowPlan = request.session[SESSION_KEY_PLAN]
stored_relay_state = plan.context.get(PLAN_CONTEXT_SAML_RELAY_STATE, "")
if relay_state and relay_state != stored_relay_state:
LOGGER.warning(
"SAML logout relay_state mismatch, possible open redirect attempt",
received_relay_state=relay_state,
stored_relay_state=stored_relay_state,
)
return stored_relay_state
class SPInitiatedSLOView(PolicyAccessView):
"""Handle SP-initiated SAML Single Logout requests"""
@@ -86,7 +80,102 @@ class SPInitiatedSLOView(PolicyAccessView):
**self.plan_context,
},
)
plan.append_stage(in_memory_stage(SessionEndStage))
if self.provider.sls_url:
# Get logout request and extract relay state
logout_request = self.plan_context.get(PLAN_CONTEXT_SAML_LOGOUT_REQUEST)
relay_state = logout_request.relay_state if logout_request else None
# Store relay state for the logout response
plan.context[PLAN_CONTEXT_SAML_RELAY_STATE] = relay_state
if self.provider.logout_method == SAMLLogoutMethods.FRONTCHANNEL_NATIVE:
# Native mode - user will be redirected/posted away from authentik
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
)
if self.provider.sls_binding == SAMLBindings.POST:
logout_response = processor.encode_post()
logout_data = {
"post_url": self.provider.sls_url,
"saml_response": logout_response,
"saml_relay_state": relay_state,
"provider_name": self.provider.name,
"saml_binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"redirect_url": logout_url,
"provider_name": self.provider.name,
"saml_binding": SAMLBindings.REDIRECT,
}
plan.context[PLAN_CONTEXT_SAML_LOGOUT_NATIVE_SESSIONS] = [logout_data]
plan.append_stage(in_memory_stage(NativeLogoutStageView))
elif self.provider.logout_method == SAMLLogoutMethods.BACKCHANNEL:
# Backchannel mode - server sends logout response directly to SP in background
# No user interaction needed
if self.provider.sls_binding != SAMLBindings.POST:
LOGGER.warning(
"Backchannel logout requires POST binding, but provider is configured "
"with %s binding",
self.provider.sls_binding,
provider=self.provider,
)
# Queue the logout response to be sent in the background
# This doesn't block the user's logout from completing
send_saml_logout_response.send(
provider_pk=self.provider.pk,
sls_url=self.provider.sls_url,
logout_request_id=logout_request.id if logout_request else None,
relay_state=relay_state,
)
LOGGER.debug(
"Queued backchannel logout response",
provider=self.provider,
sls_url=self.provider.sls_url,
)
# Just end the session - no user interaction needed
plan.append_stage(in_memory_stage(SessionEndStage))
else:
# Iframe mode (default for FRONTCHANNEL_IFRAME) - user stays on authentik
processor = LogoutResponseProcessor(
self.provider,
logout_request,
destination=self.provider.sls_url,
)
logout_response = processor.build_response()
if self.provider.sls_binding == SAMLBindings.POST:
logout_data = {
"url": self.provider.sls_url,
"saml_response": nice64(logout_response),
"saml_relay_state": relay_state,
"provider_name": self.provider.name,
"binding": SAMLBindings.POST,
}
else:
logout_url = processor.get_redirect_url()
logout_data = {
"url": logout_url,
"provider_name": self.provider.name,
"binding": SAMLBindings.REDIRECT,
}
plan.context[PLAN_CONTEXT_SAML_LOGOUT_IFRAME_SESSIONS] = [logout_data]
plan.append_stage(in_memory_stage(IframeLogoutStageView))
plan.append_stage(in_memory_stage(SessionEndStage))
else:
# No SLS URL configured, just end session
plan.append_stage(in_memory_stage(SessionEndStage))
# Remove samlsession from database
auth_session = AuthenticatedSession.from_request(self.request, self.request.user)
@@ -114,9 +203,17 @@ class SPInitiatedSLOBindingRedirectView(SPInitiatedSLOView):
# IDP SLO, so we want to redirect to our next provider
if REQUEST_KEY_SAML_RESPONSE in request.GET:
relay_state = request.GET.get(REQUEST_KEY_RELAY_STATE, "")
redirect_url = _get_redirect_url(request, relay_state)
if redirect_url:
return redirect(redirect_url)
if relay_state:
return redirect(relay_state)
# No RelayState provided, try to get return URL from plan context
if SESSION_KEY_PLAN in request.session:
plan: FlowPlan = request.session[SESSION_KEY_PLAN]
relay_state = plan.context.get(PLAN_CONTEXT_SAML_RELAY_STATE)
if relay_state:
return redirect(relay_state)
# No relay state and no plan context - redirect to root
return redirect("authentik_core:root-redirect")
# For SAML logout requests, use the parent dispatch with auth checks
@@ -157,9 +254,17 @@ class SPInitiatedSLOBindingPOSTView(SPInitiatedSLOView):
# IDP SLO, so we want to redirect to our next provider
if REQUEST_KEY_SAML_RESPONSE in request.POST:
relay_state = request.POST.get(REQUEST_KEY_RELAY_STATE, "")
redirect_url = _get_redirect_url(request, relay_state)
if redirect_url:
return redirect(redirect_url)
if relay_state:
return redirect(relay_state)
# No RelayState provided, try to get return URL from plan context
if SESSION_KEY_PLAN in request.session:
plan: FlowPlan = request.session[SESSION_KEY_PLAN]
relay_state = plan.context.get(PLAN_CONTEXT_SAML_RELAY_STATE)
if relay_state:
return redirect(relay_state)
# No relay state and no plan context - redirect to root
return redirect("authentik_core:root-redirect")
# For SAML logout requests, use the parent dispatch with auth checks

View File

@@ -65,7 +65,7 @@ class EnterpriseUser(BaseModel):
employeeNumber: str | None = Field(
None,
description="Numeric or alphanumeric identifier assigned to a person, "
"typically based on order of hire or association with anorganization.",
"typically based on order of hire or association with an organization.",
)
costCenter: str | None = Field(None, description="Identifies the name of a cost center.")
organization: str | None = Field(None, description="Identifies the name of an organization.")
@@ -73,7 +73,7 @@ class EnterpriseUser(BaseModel):
department: str | None = Field(
None,
description="Numeric or alphanumeric identifier assigned to a person,"
" typically based on order of hire or association with anorganization.",
" typically based on order of hire or association with an organization.",
)
manager: Manager | None = Field(
None,

View File

@@ -52,10 +52,10 @@ class SCIMApplicationPoliciesTests(TestCase):
email=f"{uid}@goauthentik.io",
)
self.users[1].ak_groups.add(self.group1)
self.users[2].ak_groups.add(self.group2)
self.users[4].ak_groups.add(self.group1)
self.users[4].ak_groups.add(self.group2)
self.users[1].groups.add(self.group1)
self.users[2].groups.add(self.group2)
self.users[4].groups.add(self.group1)
self.users[4].groups.add(self.group2)
def test_no_group_policy(self):
"""Test with no group policy set"""

View File

@@ -10,7 +10,6 @@ from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderGroup
from authentik.providers.scim.tasks import scim_sync
class SCIMGroupTests(TestCase):
@@ -206,80 +205,3 @@ class SCIMGroupTests(TestCase):
self.assertEqual(mock.request_history[1].method, "POST")
self.assertEqual(mock.request_history[2].method, "GET")
self.assertNotIn("PUT", [req.method for req in mock.request_history])
def _create_stale_provider_group(self, scim_id: str) -> Group:
"""Create a group that is outside the provider's scope (via group_filters) with an
existing SCIMProviderGroup, simulating a previously synced group now out of scope."""
self.app.backchannel_providers.remove(self.provider)
anchor = Group.objects.create(name=generate_id())
stale = Group.objects.create(name=generate_id())
self.app.backchannel_providers.add(self.provider)
self.provider.group_filters.set([anchor])
SCIMProviderGroup.objects.create(provider=self.provider, group=stale, scim_id=scim_id)
return stale
@Mocker()
def test_sync_cleanup_stale_group_delete(self, mock: Mocker):
"""Stale (out-of-scope) groups are deleted during full sync cleanup"""
scim_id = generate_id()
mock.get("https://localhost/ServiceProviderConfig", json={})
mock.post("https://localhost/Groups", json={"id": generate_id()})
mock.delete(f"https://localhost/Groups/{scim_id}", status_code=204)
self._create_stale_provider_group(scim_id)
scim_sync.send(self.provider.pk).get_result()
delete_reqs = [r for r in mock.request_history if r.method == "DELETE"]
self.assertEqual(len(delete_reqs), 1)
self.assertEqual(delete_reqs[0].url, f"https://localhost/Groups/{scim_id}")
self.assertFalse(
SCIMProviderGroup.objects.filter(provider=self.provider, scim_id=scim_id).exists()
)
@Mocker()
def test_sync_cleanup_stale_group_not_found(self, mock: Mocker):
"""Stale group cleanup handles 404 from the remote gracefully"""
scim_id = generate_id()
mock.get("https://localhost/ServiceProviderConfig", json={})
mock.post("https://localhost/Groups", json={"id": generate_id()})
mock.delete(f"https://localhost/Groups/{scim_id}", status_code=404)
self._create_stale_provider_group(scim_id)
scim_sync.send(self.provider.pk).get_result()
delete_reqs = [r for r in mock.request_history if r.method == "DELETE"]
self.assertEqual(len(delete_reqs), 1)
self.assertFalse(
SCIMProviderGroup.objects.filter(provider=self.provider, scim_id=scim_id).exists()
)
@Mocker()
def test_sync_cleanup_stale_group_transient_error(self, mock: Mocker):
"""Stale group cleanup logs and retries on transient HTTP errors"""
scim_id = generate_id()
mock.get("https://localhost/ServiceProviderConfig", json={})
mock.post("https://localhost/Groups", json={"id": generate_id()})
mock.delete(f"https://localhost/Groups/{scim_id}", status_code=429)
self._create_stale_provider_group(scim_id)
scim_sync.send(self.provider.pk)
delete_reqs = [r for r in mock.request_history if r.method == "DELETE"]
self.assertEqual(len(delete_reqs), 1)
@Mocker()
def test_sync_cleanup_stale_group_dry_run(self, mock: Mocker):
"""Stale group cleanup skips HTTP DELETE in dry_run mode"""
self.provider.dry_run = True
self.provider.save()
scim_id = generate_id()
mock.get("https://localhost/ServiceProviderConfig", json={})
self._create_stale_provider_group(scim_id)
scim_sync.send(self.provider.pk)
delete_reqs = [r for r in mock.request_history if r.method == "DELETE"]
self.assertEqual(len(delete_reqs), 0)

View File

@@ -1,19 +1,17 @@
"""SCIM User tests"""
from json import loads
from unittest.mock import patch
from django.test import TestCase
from jsonschema import validate
from requests_mock import Mocker
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User, UserTypes
from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.lib.sync.outgoing.base import SAFE_METHODS
from authentik.lib.sync.outgoing.exceptions import TransientSyncException
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMProviderUser
from authentik.providers.scim.tasks import scim_sync, scim_sync_objects, sync_tasks
from authentik.providers.scim.tasks import scim_sync, scim_sync_objects
from authentik.tasks.models import Task
from authentik.tenants.models import Tenant
@@ -539,104 +537,3 @@ class SCIMUserTests(TestCase):
self.assertEqual(mock.call_count, 2)
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[1].method, "POST")
def _create_stale_provider_user(self, scim_id: str, uid: str) -> User:
"""Create a service-account user (excluded from provider scope) with an existing
SCIMProviderUser, simulating a previously synced user that is now out of scope."""
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
type=UserTypes.SERVICE_ACCOUNT,
)
SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
return user
@Mocker()
def test_sync_cleanup_stale_user_delete(self, mock: Mocker):
"""Stale (out-of-scope) users are deleted during full sync cleanup"""
scim_id = generate_id()
uid = generate_id()
mock.get("https://localhost/ServiceProviderConfig", json={})
mock.delete(f"https://localhost/Users/{scim_id}", status_code=204)
self._create_stale_provider_user(scim_id, uid)
scim_sync.send(self.provider.pk).get_result()
delete_reqs = [r for r in mock.request_history if r.method == "DELETE"]
self.assertEqual(len(delete_reqs), 1)
self.assertEqual(delete_reqs[0].url, f"https://localhost/Users/{scim_id}")
self.assertFalse(
SCIMProviderUser.objects.filter(provider=self.provider, scim_id=scim_id).exists()
)
@Mocker()
def test_sync_cleanup_stale_user_not_found(self, mock: Mocker):
"""Stale user cleanup handles 404 from the remote gracefully"""
scim_id = generate_id()
uid = generate_id()
mock.get("https://localhost/ServiceProviderConfig", json={})
mock.delete(f"https://localhost/Users/{scim_id}", status_code=404)
self._create_stale_provider_user(scim_id, uid)
scim_sync.send(self.provider.pk).get_result()
delete_reqs = [r for r in mock.request_history if r.method == "DELETE"]
self.assertEqual(len(delete_reqs), 1)
self.assertFalse(
SCIMProviderUser.objects.filter(provider=self.provider, scim_id=scim_id).exists()
)
@Mocker()
def test_sync_cleanup_stale_user_transient_error(self, mock: Mocker):
"""Stale user cleanup logs and retries on transient HTTP errors"""
scim_id = generate_id()
uid = generate_id()
mock.get("https://localhost/ServiceProviderConfig", json={})
mock.delete(f"https://localhost/Users/{scim_id}", status_code=429)
self._create_stale_provider_user(scim_id, uid)
scim_sync.send(self.provider.pk)
delete_reqs = [r for r in mock.request_history if r.method == "DELETE"]
self.assertEqual(len(delete_reqs), 1)
@Mocker()
def test_sync_cleanup_stale_user_dry_run(self, mock: Mocker):
"""Stale user cleanup skips HTTP DELETE in dry_run mode"""
self.provider.dry_run = True
self.provider.save()
scim_id = generate_id()
uid = generate_id()
mock.get("https://localhost/ServiceProviderConfig", json={})
self._create_stale_provider_user(scim_id, uid)
scim_sync.send(self.provider.pk)
delete_reqs = [r for r in mock.request_history if r.method == "DELETE"]
self.assertEqual(len(delete_reqs), 0)
def test_sync_cleanup_client_for_model_transient(self):
"""Cleanup silently skips an object type when client_for_model raises
TransientSyncException"""
with Mocker() as mock:
mock.get("https://localhost/ServiceProviderConfig", json={})
with patch.object(
SCIMProvider,
"client_for_model",
side_effect=TransientSyncException("connection failed"),
):
scim_sync.send(self.provider.pk).get_result()
def test_sync_transient_exception(self):
"""TransientSyncException in _sync_cleanup is caught by sync() which then
schedules a retry"""
with Mocker() as mock:
mock.get("https://localhost/ServiceProviderConfig", json={})
with patch.object(
sync_tasks,
"_sync_cleanup",
side_effect=TransientSyncException("connection failed"),
):
scim_sync.send(self.provider.pk)

View File

@@ -60,7 +60,11 @@ class LDAPSourceSerializer(SourceSerializer):
sources = sources.exclude(pk=self.instance.pk)
if sources.exists():
raise ValidationError(
_("Only a single LDAP Source with password synchronization is allowed")
{
"sync_users_password": _(
"Only a single LDAP Source with password synchronization is allowed"
)
}
)
return sync_users_password
@@ -217,7 +221,7 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
for sync_class in SYNC_CLASSES:
class_name = sync_class.name()
all_objects.setdefault(class_name, [])
for page in sync_class(source, Task()).get_objects(size_limit=10):
for page in sync_class(source).get_objects(size_limit=10):
for obj in page:
obj: dict
obj.pop("raw_attributes", None)

View File

@@ -14,7 +14,6 @@ from django.utils.translation import gettext_lazy as _
from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
from ldap3.core.exceptions import LDAPException, LDAPInsufficientAccessRightsResult, LDAPSchemaError
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.core.models import (
Group,
@@ -32,7 +31,6 @@ from authentik.tasks.schedules.common import ScheduleSpec
LDAP_TIMEOUT = 15
LDAP_UNIQUENESS = "ldap_uniq"
LDAP_DISTINGUISHED_NAME = "distinguishedName"
LOGGER = get_logger()
def flatten(value: Any) -> Any:
@@ -270,7 +268,6 @@ class LDAPSource(IncomingSyncSource):
)
if self.start_tls:
LOGGER.debug("Connection StartTLS", source=self)
conn.start_tls(read_server_info=False)
try:
successful = conn.bind()
@@ -281,9 +278,7 @@ class LDAPSource(IncomingSyncSource):
# See https://github.com/goauthentik/authentik/issues/4590
# See also https://github.com/goauthentik/authentik/issues/3399
if server_kwargs.get("get_info", ALL) == NONE:
LOGGER.warning("Failed to connect after schema downgrade", source=self, exc=exc)
raise exc
LOGGER.warning("Downgrading connection to no schema info", source=self, exc=exc)
server_kwargs["get_info"] = NONE
return self.connection(server, server_kwargs, connection_kwargs)
finally:

View File

@@ -27,7 +27,7 @@ from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task
LOGGER = get_logger()
SYNC_CLASSES: list[type[BaseLDAPSynchronizer]] = [
SYNC_CLASSES = [
UserLDAPSynchronizer,
GroupLDAPSynchronizer,
MembershipLDAPSynchronizer,

View File

@@ -1,19 +1,10 @@
"""LDAP Source API tests"""
from json import loads
from unittest.mock import MagicMock, patch
from django.db.models import Q
from django.urls import reverse
from rest_framework.exceptions import ErrorDetail
from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id, generate_key
from authentik.lib.generators import generate_key
from authentik.sources.ldap.api import LDAPSourceSerializer
from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping
from authentik.sources.ldap.tests.mock_ad import mock_ad_connection
from authentik.sources.ldap.models import LDAPSource
LDAP_PASSWORD = generate_key()
@@ -35,13 +26,12 @@ class LDAPAPITests(APITestCase):
}
)
self.assertTrue(serializer.is_valid())
self.assertEqual(serializer.errors, {})
def test_sync_users_password_invalid(self):
"""Ensure only a single source with password sync can be created"""
LDAPSource.objects.create(
name="foo",
slug=generate_id(),
slug="foo",
server_uri="ldaps://1.2.3.4",
bind_cn="",
bind_password=LDAP_PASSWORD,
@@ -51,26 +41,15 @@ class LDAPAPITests(APITestCase):
serializer = LDAPSourceSerializer(
data={
"name": "foo",
"slug": generate_id(),
"slug": " foo",
"server_uri": "ldaps://1.2.3.4",
"bind_cn": "",
"bind_password": LDAP_PASSWORD,
"base_dn": "dc=foo",
"sync_users_password": True,
"sync_users_password": False,
}
)
self.assertFalse(serializer.is_valid())
self.assertEqual(
serializer.errors,
{
"sync_users_password": [
ErrorDetail(
string="Only a single LDAP Source with password synchronization is allowed",
code="invalid",
)
]
},
)
def test_sync_users_mapping_empty(self):
"""Check that when sync_users is enabled, property mappings must be set"""
@@ -103,32 +82,3 @@ class LDAPAPITests(APITestCase):
}
)
self.assertFalse(serializer.is_valid())
@apply_blueprint("system/sources-ldap.yaml")
def test_sync_debug(self):
user = create_test_admin_user()
self.client.force_login(user)
source: LDAPSource = LDAPSource.objects.create(
name=generate_id(),
slug=generate_id(),
base_dn="dc=goauthentik,dc=io",
additional_user_dn="ou=users",
additional_group_dn="ou=groups",
)
source.user_property_mappings.set(
LDAPSourcePropertyMapping.objects.filter(
Q(managed__startswith="goauthentik.io/sources/ldap/default")
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
)
)
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
res = self.client.get(
reverse("authentik_api:ldapsource-debug", kwargs={"slug": source.slug})
)
self.assertEqual(res.status_code, 200)
body = loads(res.content.decode())
self.assertIn("users", body)
self.assertIn("groups", body)
self.assertIn("membership", body)

View File

@@ -59,11 +59,7 @@ class OAuthSourceSerializer(SourceSerializer):
def validate(self, attrs: dict) -> dict:
session = get_http_session()
provider_type_name = attrs.get(
"provider_type",
self.instance.provider_type if self.instance else None,
)
source_type = registry.find_type(provider_type_name)
source_type = registry.find_type(attrs["provider_type"])
well_known = attrs.get("oidc_well_known_url") or source_type.oidc_well_known_url
inferred_oidc_jwks_url = None
@@ -105,15 +101,16 @@ class OAuthSourceSerializer(SourceSerializer):
config = jwks_config.json()
attrs["oidc_jwks"] = config
provider_type = registry.find_type(attrs.get("provider_type", ""))
for url in [
"authorization_url",
"access_token_url",
"profile_url",
]:
if getattr(source_type, url, None) is None:
if getattr(provider_type, url, None) is None:
if url not in attrs:
raise ValidationError(
f"{url} is required for provider {source_type.verbose_name}"
f"{url} is required for provider {provider_type.verbose_name}"
)
return attrs

View File

@@ -1,31 +0,0 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.sources.oauth.models import OAuthSource
class TestOAuthSourceAPI(APITestCase):
def setUp(self):
self.source = OAuthSource.objects.create(
name=generate_id(),
slug=generate_id(),
provider_type="openidconnect",
authorization_url="",
profile_url="",
consumer_key=generate_id(),
)
self.user = create_test_admin_user()
def test_patch_no_type(self):
self.client.force_login(self.user)
res = self.client.patch(
reverse("authentik_api:oauthsource-detail", kwargs={"slug": self.source.slug}),
{
"authorization_url": f"https://{generate_id()}",
"profile_url": f"https://{generate_id()}",
"access_token_url": f"https://{generate_id()}",
},
)
self.assertEqual(res.status_code, 200)

View File

@@ -14,24 +14,6 @@ class SAMLException(SentryIgnoredException):
return self.default_message
class MissingSAMLResponse(SAMLException):
"""Exception raised when request does not contain SAML Response."""
default_message = "Request does not contain a SAML response."
class UnsupportedNameIDFormat(SAMLException):
"""Exception raised when SAML Response contains NameID Format not supported."""
default_message = "The NameID Format in the SAML Response is not supported."
class MismatchedRequestID(SAMLException):
"""Exception raised when the returned request ID doesn't match the saved ID."""
default_message = "The SAML Response ID does not match the original request ID."
class InvalidEncryption(SAMLException):
"""Encryption of XML Object is either missing or invalid."""
@@ -42,3 +24,21 @@ class InvalidSignature(SAMLException):
"""Signature of XML Object is either missing or invalid."""
default_message = "The signature of the SAML object is either missing or invalid."
class MismatchedRequestID(SAMLException):
"""Exception raised when the returned request ID doesn't match the saved ID."""
default_message = "The SAML Response ID does not match the original request ID."
class MissingSAMLResponse(SAMLException):
"""Exception raised when request does not contain SAML Response."""
default_message = "Request does not contain a SAML response."
class UnsupportedNameIDFormat(SAMLException):
"""Exception raised when SAML Response contains NameID Format not supported."""
default_message = "The NameID Format in the SAML Response is not supported."

View File

@@ -4,6 +4,7 @@ from urllib.parse import parse_qsl, urlparse, urlunparse
from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import SuspiciousOperation
from django.http import Http404, HttpRequest, HttpResponse
from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect
@@ -37,7 +38,9 @@ from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSI
from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import (
InvalidEncryption,
InvalidSignature,
MismatchedRequestID,
MissingSAMLResponse,
UnsupportedNameIDFormat,
)
@@ -156,7 +159,15 @@ class ACSView(View):
processor = ResponseProcessor(source, request)
try:
processor.parse()
except (InvalidSignature, MissingSAMLResponse, VerificationError, ValueError) as exc:
except (
InvalidEncryption,
InvalidSignature,
MismatchedRequestID,
MissingSAMLResponse,
SuspiciousOperation,
VerificationError,
ValueError,
) as exc:
return bad_request_message(request, str(exc))
try:

View File

@@ -38,7 +38,6 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
from authentik.stages.authenticator_webauthn.stage import PLAN_CONTEXT_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD_ARGS
LOGGER = get_logger()
if TYPE_CHECKING:
@@ -144,11 +143,7 @@ def validate_challenge_code(code: str, stage_view: StageView, user: User) -> Dev
credentials={"username": user.username},
request=stage_view.request,
stage=stage_view.executor.current_stage,
context={
PLAN_CONTEXT_METHOD_ARGS: {
"device_class": DeviceClasses.TOTP.value,
}
},
device_class=DeviceClasses.TOTP.value,
)
raise ValidationError(
_("Invalid Token. Please ensure the time on your device is accurate and try again.")
@@ -220,13 +215,9 @@ def validate_challenge_webauthn(
credentials={"username": user.username},
request=stage_view.request,
stage=stage_view.executor.current_stage,
context={
PLAN_CONTEXT_METHOD_ARGS: {
"device": device,
"device_class": DeviceClasses.WEBAUTHN.value,
"device_type": device.device_type,
},
},
device=device,
device_class=DeviceClasses.WEBAUTHN.value,
device_type=device.device_type,
)
raise ValidationError("Assertion failed") from exc
@@ -276,12 +267,8 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
credentials={"username": user.username},
request=stage_view.request,
stage=stage_view.executor.current_stage,
context={
PLAN_CONTEXT_METHOD_ARGS: {
"device_class": DeviceClasses.DUO.value,
"duo_response": response,
}
},
device_class=DeviceClasses.DUO.value,
duo_response=response,
)
raise ValidationError("Duo denied access", code="denied")
return device

File diff suppressed because one or more lines are too long

View File

@@ -58,7 +58,7 @@ def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str, key: s
# [reCAPTCHA](https://developers.google.com/recaptcha/docs/verify#error_code_reference)
# [hCaptcha](https://docs.hcaptcha.com/#siteverify-error-codes-table)
# [Turnstile](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#error-codes)
retriable_error_codes = [
retryable_error_codes = [
"missing-input-response",
"invalid-input-response",
"timeout-or-duplicate",
@@ -66,7 +66,7 @@ def verify_captcha_token(stage: CaptchaStage, token: str, remote_ip: str, key: s
"already-seen-response",
]
if set(error_codes).issubset(set(retriable_error_codes)):
if set(error_codes).issubset(set(retryable_error_codes)):
error_message = _("Invalid captcha response. Retrying may solve this issue.")
else:
error_message = _("Invalid captcha response")

View File

@@ -80,7 +80,7 @@ class ConsentStageView(ChallengeStageView):
def should_always_prompt(self) -> bool:
"""Check if the current request should require a prompt for non consent reasons,
i.e. this stage injected from another stage, mode is always requireed or no application
i.e. this stage injected from another stage, mode is always required or no application
is set."""
current_stage: ConsentStage = self.executor.current_stage
# Make this StageView work when injected, in which case `current_stage` is an instance

View File

@@ -40,6 +40,10 @@ class EmailTemplates(models.TextChoices):
"email/event_notification.html",
_("Event Notification"),
)
INVITATION = (
"email/invitation.html",
_("Invitation"),
)
def get_template_choices():

View File

@@ -0,0 +1,55 @@
{% extends "email/base.html" %}
{% load i18n %}
{% load humanize %}
{% block content %}
<tr>
<td align="center">
<h1>
{% blocktrans %}
You're Invited!
{% endblocktrans %}
</h1>
</td>
</tr>
<tr>
<td align="center">
<table border="0">
<tr>
<td align="center" style="max-width: 300px; padding: 20px 0; color: #212124;">
{% blocktrans %}
You have been invited to join {{ host }}. Click the button below to get started.
{% endblocktrans %}
</td>
</tr>
{% if expires %}
<tr>
<td align="center" style="max-width: 300px; padding: 10px 0; color: #212124; font-size: 12px;">
{% blocktrans with expires=expires|naturaltime %}
This invitation expires {{ expires }}.
{% endblocktrans %}
</td>
</tr>
{% endif %}
<tr>
<td align="center" class="btn btn-primary">
<a id="confirm" href="{{ url }}" rel="noopener noreferrer" target="_blank">{% trans 'Accept Invitation' %}</a>
</td>
</tr>
</table>
</td>
</tr>
{% endblock %}
{% block sub_content %}
<tr>
<td style="padding: 20px; font-size: 12px; color: #212124;" align="center">
{% blocktrans %}
If you cannot click the button above, please copy and paste the following URL into your browser:
{% endblocktrans %}
<br>
<a href="{{ url }}" rel="noopener noreferrer" target="_blank">{{ url }}</a>
</td>
</tr>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% load i18n %}
{% load humanize %}
{% blocktrans %}You're Invited!{% endblocktrans %}
{% blocktrans %}You have been invited to join {{ host }}. Use the link below to get started.{% endblocktrans %}
{% trans 'Accept Invitation' %}: {{ url }}
{% if expires %}
{% blocktrans with expires=expires|naturaltime %}This invitation expires {{ expires }}.{% endblocktrans %}
{% endif %}
{% blocktrans %}If you cannot click the link above, please copy and paste the following URL into your browser:{% endblocktrans %}
{{ url }}

View File

@@ -54,7 +54,7 @@ class TestEmailStageTemplates(FlowTestCase):
chmod(file2, 0o000) # Remove all permissions so we can't read the file
choices = get_template_choices()
self.assertEqual(choices[-1][0], Path(file).name)
self.assertEqual(len(choices), 5)
self.assertEqual(len(choices), 6)
unlink(file)
unlink(file2)

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