Compare commits

...

57 Commits

Author SHA1 Message Date
3adf79c493 release: 2025.2.2 2025-03-17 19:34:52 +01:00
f478593826 website/docs: prepare for 2025.2.2 (cherry-pick #13552) (#13553)
website/docs: prepare for 2025.2.2 (#13552)

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-03-17 19:24:20 +01:00
edf4de7271 stages/identification: check captcha after checking authentication (cherry-pick #13533) (#13551)
stages/identification: check captcha after checking authentication (#13533)

Co-authored-by: Jens L. <jens@goauthentik.io>
2025-03-17 17:23:55 +00:00
db43869e25 sources/oauth: fix duplicate authentication (cherry-pick #13322) (#13535)
sources/oauth: fix duplicate authentication (#13322)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-03-14 15:54:22 +00:00
8a668af5f6 providers/rac: fix signals and Endpoint caching (cherry-pick #13529) (#13531)
providers/rac: fix signals and Endpoint caching (#13529)

* fix RAC signals

And possibly other things by not using `ManagedAppConfig`. This was
broken by 2128e7f45f.

* invalidate Endpoint cache on update or delete

This will result in more invalidations, but it will also fix some
invalid Endpoint instances from showing up in Endpoint lists.

Since an Endpoint can be tied to a Policy, some invalid results can
still show up if the result of the Policy changes (either because the
Policy itself changes or because data checked by that Policy changes).

Even with those potentially invalid results, I believe the caching
itself is advantageous as long as the API provides an option for
`superuser_full_list`.

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2025-03-14 16:38:23 +01:00
eef233fd11 web/user: show admin interface button on mobile (cherry-pick #13421) (#13518)
web/user: show admin interface button on mobile (#13421)

Co-authored-by: Jens L. <jens@goauthentik.io>
2025-03-14 00:17:43 +00:00
833b350c42 web/flows: fix missing padding on authenticator_validate card (cherry-pick #13420) (#13519)
web/flows: fix missing padding on authenticator_validate card (#13420)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-03-14 00:17:01 +00:00
b388265d98 providers/SCIM: fix object exists error for users, attempt to look up user ID in remote system (#13437)
* providers/scim: handle ObjectExistsSyncException when filtering is supported by remote system

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

* unrelated: correctly check for backchannel application in SCIM view page

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

* unrelated: fix missing ignore paths in codespell

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	pyproject.toml
2025-03-13 17:51:36 +00:00
faefd9776d sources/oauth: ignore missing well-known keys (cherry-pick #13468) (#13470)
sources/oauth: ignore missing well-known keys (#13468)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-03-12 13:56:27 +00:00
a5ee159189 web/admin: fix display bug for assigned users in application bindings in the wizard (cherry-pick #13435) (#13452)
web/admin: fix display bug for assigned users in application bindings in the wizard (#13435)

* 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.

* web/admin: fix display bug for assigned users in application bindings in the wizard

## What

Modifies the type-of-binding detection algorithm to check if there's a user field and
that it's a number.

## Why

The original type-of-binding detector checked if the field was set and asserted that it was a string
of at least one character. Unfortunately, this doesn't work for `user`, where the primary key is an
integer. Changing the algorithm to "It's really a string with something in it, *or* it's a number,"
works.

## Testing

- Ensure you have at least one user you can use, and that user has a username.
- Navigate through the Application Wizard until you reach the binding page.
- Create a user binding
- See that the user shows up in the table.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2025-03-11 18:09:27 +00:00
35c739ee84 lib/config: fix conn_max_age parsing (cherry-pick #13370) (#13415)
lib/config: fix conn_max_age parsing (#13370)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-03-06 14:05:41 +00:00
e9764333ea stages/authenticator_email: Fix Enroll dropdown in the MFA Devices page (cherry-pick #13404) (#13414)
stages/authenticator_email: Fix Enroll dropdown in the MFA Devices page (#13404)

Implement missing ui_user_settings() in AuthenticatorEmailStage

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-03-06 12:15:56 +00:00
22af17be2c web/user: ensure modal container on user-settings page is min-height: 100% (cherry-pick #13402) (#13413)
web/user: ensure modal container on user-settings page is min-height: 100% (#13402)

* 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.

* web/admin: ensure modal container on user-settings page is min-height: 100%

## What

Add a min-height and auto-scroll directives to the CSS for the main section of the user-settings
page.

```
+                .pf-c-page__main {
+                    min-height: 100vw;
+                    overflow-y: auto;
```

## Why

Without this, Safari refused to render any pop-up modals that were "centered" on the viewport but
were "beneath" the rendered content space of the container. As a result, users could not create new
access tokens or app passwords. This is arguably incorrect behavior on Safari's part, but 🤷‍♀️.
Adding `overflow-y: auto` on the container means that if the page is not long enough to host the
pop-up, it will be accessible via scrolling.

## Testing

- Using Safari, Visit the User->User Settings, click "Tokens and App Passwords" tab, and click
  "Create Token" or "Create App Password"
- Observe that the dialog is now accessible.

## Related Issue:

- [Unable to create API token in Safari
  #12891](https://github.com/goauthentik/authentik/issues/12891)

* Fix a really stupid typo.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2025-03-06 12:15:46 +00:00
679bf17d6f website/docs: fix build (#13385)
* website/docs: updated debugging docs (#12809)

* lifecycle: much improved debugging experience

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

* Optimised images with calibre/image-actions

* start documenting container debugging

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

* add user: root

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

* update example override file

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

* update env var

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

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

* fix

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>

* website/docs: fix build

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-03-04 16:57:20 +01:00
cbfa51fb31 providers/proxy: kubernetes outpost: fix reconcile when only annotations changed (cherry-pick #13372) (#13384)
providers/proxy: kubernetes outpost: fix reconcile when only annotations changed (#13372)

* providers/proxy: kubernetes outpost: fix reconcile when only annotations changed



* fixup



---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-03-04 16:48:25 +01:00
5f8c21cc88 website/docs: update the 2025.2 rel notes (cherry-pick #13213) (#13222)
website/docs: update the 2025.2 rel notes (#13213)

* removed rc notice, added links to docs

* remved todo about SSF preview banner

* update sidebar and security



* add api diff



* fix format



* fix link

* bolded H3s

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-03-04 16:16:30 +01:00
69b3d1722b *: fix stage incorrectly being inserted instead of appended (cherry-pick #13304) (#13327)
*: fix stage incorrectly being inserted instead of appended (#13304)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-03-03 13:11:50 +00:00
fa4ce1d629 enterprise/stages/source: fix dispatch method signature (cherry-pick #13321) (#13326)
enterprise/stages/source: fix dispatch method signature (#13321)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-28 22:43:08 +00:00
e4a392834f website/docs: prepare for 2025.2.1 (cherry-pick #13277) (#13279)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-02-26 21:23:04 +01:00
31fe0e5923 release: 2025.2.1 2025-02-26 20:54:52 +01:00
8b619635ea stages/authenticator_email: fix session cleanup test b (cherry-pick #13264) (#13276)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
fix session cleanup test b (#13264)
2025-02-26 20:46:05 +01:00
1f1db523c0 stages/email: Fix email stage serialization (cherry-pick #13256) (#13273)
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Fix email stage serialization (#13256)
2025-02-26 20:44:50 +01:00
bbc23e1d77 core: add pre-hydrated relative URL (cherry-pick #13243) (#13246)
core: add pre-hydrated relative URL (#13243)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-25 11:25:29 +01:00
c30b7ee3e9 website/docs: fix missing breaking entry for 2025.2 release notes (cherry-pick #13223) (#13224)
website/docs: fix missing breaking entry for 2025.2 release notes (#13223)

* website/docs: fix missing breaking entry for 2025.2 release notes



* Update website/docs/releases/2025/v2025.2.md




---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-02-24 16:58:18 +01:00
2ba79627bc stages/authenticator_email: Email Authenticator Stage Documentation (cherry-pick #12853) (#13218)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-02-24 14:57:55 +01:00
198cbe1d9d website/docs: add paragraph about impossible travel (cherry-pick #13125) (#13220)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-02-24 14:55:34 +01:00
db6da159d5 website/docs: remove mention of wizard (cherry-pick #13126) (#13219)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
2025-02-24 14:54:48 +01:00
9862e32078 website/docs: add info about new perms for super-user in groups (cherry-pick #13188) (#13217)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-24 14:49:15 +01:00
a7714e2892 website/docs: add new SSF provider docs (cherry-pick #13102) (#13215)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
2025-02-24 14:49:05 +01:00
073e1d241b website/docs: remove Enterprise badge from RAC docs (cherry-pick #13069) (#13216)
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-02-24 14:47:56 +01:00
5c5cc1c7da release: 2025.2.0 2025-02-24 12:55:17 +01:00
3dccce1095 web/user: fix display for RAC tile (cherry-pick #13211) (#13212)
web/user: fix display for RAC tile (#13211)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-24 12:18:36 +01:00
78f997fbee web/flow: fix translate extract (cherry-pick #13208) (#13210)
web/flow: fix translate extract (#13208)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-24 11:59:29 +01:00
ed83c2b0b1 release: 2025.2.0-rc3 2025-02-23 16:02:45 +01:00
af780deb27 core: add darkreader-lock (cherry-pick #13183) (#13184)
core: add darkreader-lock (#13183)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-23 04:53:09 +01:00
a4be38567f web/admin: fix default selection for binding policy (cherry-pick #13180) (#13182)
web/admin: fix default selection for binding policy (#13180)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-23 04:20:04 +01:00
39aafbb34a web/flow: grab focus to uid input field (cherry-pick #13177) (#13178)
web/flow: grab focus to uid input field (#13177)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-23 00:52:37 +01:00
07eb5fe533 web/flow: update default flow background (cherry-pick #13175) (#13176)
web/flow: update default flow background (#13175)

* web/flow: update default flow background



* Optimised images with calibre/image-actions

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-02-22 23:37:10 +01:00
301a89dd92 web/admin: only show message when not editing an application (cherry-pick #13165) (#13168)
web/admin: only show message when not editing an application (#13165)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-21 23:37:14 +01:00
cd6d0a47f3 web/user: fix race condition in user settings flow executor (cherry-pick #13163) (#13169)
web/user: fix race condition in user settings flow executor (#13163)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-21 23:36:59 +01:00
8a23eaef1e web/user: fix RAC launch not opening when clicking icon (cherry-pick #13164) (#13166)
web/user: fix RAC launch not opening when clicking icon (#13164)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-21 19:21:40 +01:00
8f285fbcc5 web: Indicate when caps-lock is active during password input. (cherry-pick #12733) (#13160)
web: Indicate when caps-lock is active during password input. (#12733)

Determining the state of the caps-lock key can be tricky as we're
dependant on a user-provided input to set a value. Thus, our initial
state defaults to not display any warning until the first keystroke.

- Revise to better use lit-html.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-02-21 17:09:35 +01:00
5d391424f7 web/user: fix post MFA creation link being invalid (cherry-pick #13157) (#13159)
web/user: fix post MFA creation link being invalid (#13157)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-21 17:01:09 +01:00
2de11f8a69 release: 2025.2.0-rc2 2025-02-20 23:47:15 +01:00
b2dcf94aba policies/geoip: fix math in impossible travel (cherry-pick #13141) (#13145)
policies/geoip: fix math in impossible travel (#13141)

* policies/geoip: fix math in impossible travel



* fix threshold



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-20 23:46:21 +01:00
adb532fc5d enterprise/stages/source: fix Source stage not executing authentication/enrollment flow (cherry-pick #12875) (#13146)
enterprise/stages/source: fix Source stage not executing authentication/enrollment flow (#12875)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-20 23:45:43 +01:00
5d3b35d1ba revert: rbac: exclude permissions for internal models (#12803) (cherry-pick #13138) (#13140)
revert: rbac: exclude permissions for internal models (#12803) (#13138)

Revert "rbac: exclude permissions for internal models (#12803)"

This reverts commit e08ccf4ca0.

Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-20 16:07:21 +01:00
433a94d9ee web/flows: fix error on interactive Captcha stage when retrying captcha (cherry-pick #13119) (#13139)
web/flows: fix error on interactive Captcha stage when retrying captcha (#13119)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-20 15:12:03 +01:00
f28d622d10 cmd: set version in outposts (cherry-pick #13116) (#13122)
cmd: set version in outposts (#13116)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-19 18:20:28 +01:00
50a68c22c5 sources/oauth: add group sync for azure_ad (cherry-pick #12894) (#13123)
sources/oauth: add group sync for azure_ad (#12894)

* sources/oauth: add group sync for azure_ad



* make group sync optional



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-19 18:20:16 +01:00
13c99c8546 web/user: fix opening application with Enter not respecting new tab setting (cherry-pick #13115) (#13118)
web/user: fix opening application with Enter not respecting new tab setting (#13115)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-19 17:57:18 +01:00
7243add30f web/admin: update Application Wizard button placement (cherry-pick #12771) (#13121)
web/admin: update Application Wizard button placement (#12771)

* 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.

* web: Make using the wizard the default for new applications

# What

1. I removed the "Wizard Hint" bar and migrated the "Create With Wizard" button down to the default
   position as "Create With Provider," moving the "Create" button to a secondary position.
   Primary coloring has been kept for both.

2. Added an alert to the "Create" legacy dialog:

> Using this form will only create an Application. In order to authenticate with the application,
> you will have to manually pair it with a Provider.

3. Updated the subtitle on the Wizard dialog:

``` diff
-    wizardDescription = msg("Create a new application");
+    wizardDescription = msg("Create a new application and configure a provider for it.");
```

4. Updated the User page so that, if the User is-a Administrator and the number of Applications in
   the system is zero, the user will be invited to create a new Application using the Wizard rather
   than the legacy Form:

```diff
     renderNewAppButton() {
         const href = paramURL("/core/applications", {
-            createForm: true,
+            createWizard: true,
         });
```

5. Fixed a bug where, on initial render, if the `this.brand` field was not available, an error would
   appear in the console. The effects were usually harmless, as brand information came quickly and
   filled in before the user could notice, but it looked bad in the debugger.

6. Fixed a bug in testing where the wizard page "Configure Policy Bindings" had been changed to
   "Configure Policy/User/Group Binding".

# Testing

Since the wizard OUID didn't change (`data-ouia-component-id="start-application-wizard"`), the E2E
tests for "Application Wizard" completed without any substantial changes to the routine or to the
tests.

``` sh
npm run test:e2e:watch -- --spec ./tests/specs/new-application-by-wizard.ts
```

# User documentation changes required.

These changes were made at the request of docs, as an initial draft to show how the page looks with
the Application Wizard as he default tool for creating new Applications.

# Developer documentation changes required.

None.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2025-02-19 17:57:03 +01:00
6611a64a62 web: bump API Client version (cherry-pick #13113) (#13114)
web: bump API Client version (#13113)

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-02-19 13:16:48 +01:00
5262f61483 providers/rac: move to open source (cherry-pick #13015) (#13112)
providers/rac: move to open source (#13015)

* move RAC to open source

* move web out of enterprise



* remove enterprise license requirements from RAC

* format



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-02-19 13:16:18 +01:00
9dcbb4af9e release: 2025.2.0-rc1 2025-02-19 02:36:48 +01:00
0665bfac58 website/docs: add 2025.2 release notes (cherry-pick #13002) (#13108)
website/docs: add 2025.2 release notes (#13002)

* website/docs: add 2025.2 release notes



* make compile



* ffs



* ffs



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-19 02:12:47 +01:00
790e0c4d80 core: clear expired database sessions (cherry-pick #13105) (#13106)
core: clear expired database sessions (#13105)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-02-18 23:22:21 +01:00
146 changed files with 3870 additions and 917 deletions

View File

@ -1,16 +1,16 @@
[bumpversion]
current_version = 2024.12.3
current_version = 2025.2.2
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
serialize =
serialize =
{major}.{minor}.{patch}-{rc_t}{rc_n}
{major}.{minor}.{patch}
message = release: {new_version}
tag_name = version/{new_version}
[bumpversion:part:rc_t]
values =
values =
rc
final
optional_value = final

View File

@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
| Version | Supported |
| --------- | --------- |
| 2024.10.x | ✅ |
| 2024.12.x | ✅ |
| 2025.2.x | ✅ |
## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.12.3"
__version__ = "2025.2.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -50,7 +50,6 @@ from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProviderGroup,
MicrosoftEntraProviderUser,
)
from authentik.enterprise.providers.rac.models import ConnectionToken
from authentik.enterprise.providers.ssf.models import StreamEvent
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
EndpointDevice,
@ -72,6 +71,7 @@ from authentik.providers.oauth2.models import (
DeviceToken,
RefreshToken,
)
from authentik.providers.rac.models import ConnectionToken
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.rbac.models import Role
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser

View File

@ -35,8 +35,7 @@ from authentik.flows.planner import (
FlowPlanner,
)
from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_values
@ -47,8 +46,9 @@ from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
LOGGER = get_logger()
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
class MessageStage(StageView):
@ -219,28 +219,28 @@ class SourceFlowManager:
}
)
flow_context.update(self.policy_context)
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
plan.context.update(flow_context)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
if stages:
for stage in stages:
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan
flow_slug = token.flow.slug
token.delete()
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=flow_slug,
)
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
if not flow:
# We only check for the flow token here if we don't have a flow, otherwise we rely on
# SESSION_KEY_SOURCE_FLOW_STAGES to delegate the usage of this token and dynamically add
# stages that deal with this token to return to another flow
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self._logger.info(
"Replacing source flow with overridden flow", flow=token.flow.slug
)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
plan.context.update(flow_context)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
if stages:
for stage in stages:
plan.append_stage(stage)
redirect = plan.to_redirect(self.request, token.flow)
token.delete()
return redirect
return bad_request_message(
self.request,
_("Configured flow does not exist."),
@ -259,6 +259,8 @@ class SourceFlowManager:
if stages:
for stage in stages:
plan.append_stage(stage)
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
plan.append_stage(stage)
return plan.to_redirect(self.request, flow)
def handle_auth(
@ -295,6 +297,8 @@ class SourceFlowManager:
# When request isn't authenticated we jump straight to auth
if not self.request.user.is_authenticated:
return self.handle_auth(connection)
# When an override flow token exists we actually still use a flow for link
# to continue the existing flow we came from
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
return self._prepare_flow(None, connection)
connection.save()

View File

@ -67,6 +67,8 @@ def clean_expired_models(self: SystemTask):
raise ImproperlyConfigured(
"Invalid session_storage setting, allowed values are db and cache"
)
if CONFIG.get("session_storage", "cache") == "db":
DBSessionStore.clear_expired()
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")

View File

@ -11,6 +11,7 @@
build: "{{ build }}",
api: {
base: "{{ base_url }}",
relBase: "{{ base_url_rel }}",
},
};
window.addEventListener("DOMContentLoaded", function () {

View File

@ -8,6 +8,8 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
<meta name="darkreader-lock">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">

View File

@ -55,7 +55,7 @@ class RedirectToAppLaunch(View):
)
except FlowNonApplicableException:
raise Http404 from None
plan.insert_stage(in_memory_stage(RedirectToAppStage))
plan.append_stage(in_memory_stage(RedirectToAppStage))
return plan.to_redirect(request, flow)

View File

@ -53,6 +53,7 @@ class InterfaceView(TemplateView):
kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
kwargs["base_url_rel"] = CONFIG.get("web.path", "/")
return super().get_context_data(**kwargs)

View File

@ -1,14 +0,0 @@
"""RAC app config"""
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEnterpriseProviderRAC(EnterpriseConfig):
"""authentik enterprise rac app config"""
name = "authentik.enterprise.providers.rac"
label = "authentik_providers_rac"
verbose_name = "authentik Enterprise.Providers.RAC"
default = True
mountpoint = ""
ws_mountpoint = "authentik.enterprise.providers.rac.urls"

View File

@ -16,7 +16,6 @@ TENANT_APPS = [
"authentik.enterprise.audit",
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.rac",
"authentik.enterprise.providers.ssf",
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
"authentik.enterprise.stages.source",

View File

@ -9,13 +9,16 @@ from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Source, User
from authentik.core.sources.flow_manager import SESSION_KEY_OVERRIDE_FLOW_TOKEN
from authentik.core.sources.flow_manager import (
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
SESSION_KEY_SOURCE_FLOW_STAGES,
)
from authentik.core.types import UILoginButton
from authentik.enterprise.stages.source.models import SourceStage
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.models import FlowToken
from authentik.flows.models import FlowToken, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
from authentik.flows.stage import ChallengeStageView
from authentik.flows.stage import ChallengeStageView, StageView
from authentik.lib.utils.time import timedelta_from_string
PLAN_CONTEXT_RESUME_TOKEN = "resume_token" # nosec
@ -49,6 +52,7 @@ class SourceStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
resume_token = self.create_flow_token()
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
return self.login_button.challenge
def create_flow_token(self) -> FlowToken:
@ -77,3 +81,19 @@ class SourceStageView(ChallengeStageView):
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return self.executor.stage_ok()
class SourceStageFinal(StageView):
"""Dynamic stage injected in the source flow manager. This is injected in the
flow the source flow manager picks (authentication or enrollment), and will run at the end.
This stage uses the override flow token to resume execution of the initial flow the
source stage is bound to."""
def dispatch(self, *args, **kwargs):
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
response = plan.to_redirect(self.request, token.flow)
token.delete()
return response

View File

@ -4,7 +4,8 @@ from django.urls import reverse
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.enterprise.stages.source.models import SourceStage
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
from authentik.enterprise.stages.source.stage import SourceStageFinal
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
@ -87,6 +88,7 @@ class TestSourceStage(FlowTestCase):
self.assertIsNotNone(flow_token)
session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN]
plan.insert_stage(in_memory_stage(SourceStageFinal), index=0)
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
session[SESSION_KEY_PLAN] = plan
session.save()
@ -96,4 +98,6 @@ class TestSourceStage(FlowTestCase):
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
self.assertStageRedirects(
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
)

View File

@ -76,10 +76,10 @@ class FlowPlan:
self.bindings.append(binding)
self.markers.append(marker or StageMarker())
def insert_stage(self, stage: Stage, marker: StageMarker | None = None):
def insert_stage(self, stage: Stage, marker: StageMarker | None = None, index=1):
"""Insert stage into plan, as immediate next stage"""
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
self.markers.insert(1, marker or StageMarker())
self.bindings.insert(index, FlowStageBinding(stage=stage, order=0))
self.markers.insert(index, marker or StageMarker())
def redirect(self, destination: str):
"""Insert a redirect stage as next stage"""

View File

@ -282,16 +282,14 @@ class ConfigLoader:
def get_optional_int(self, path: str, default=None) -> int | None:
"""Wrapper for get that converts value into int or None if set"""
value = self.get(path, default)
value = self.get(path, UNSET)
if value is UNSET:
return default
try:
return int(value)
except (ValueError, TypeError) as exc:
if value is None or (isinstance(value, str) and value.lower() == "null"):
return default
if value is UNSET:
return default
return None
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
return default
@ -372,9 +370,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
"sslcert": config.get("postgresql.sslcert"),
"sslkey": config.get("postgresql.sslkey"),
},
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
"CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0),
"CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False),
"DISABLE_SERVER_SIDE_CURSORS": config.get_bool(
"postgresql.disable_server_side_cursors", False
),
"TEST": {
@ -383,8 +381,8 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
}
}
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
conn_max_age = config.get_optional_int("postgresql.conn_max_age", UNSET)
disable_server_side_cursors = config.get_bool("postgresql.disable_server_side_cursors", UNSET)
if config.get_bool("postgresql.use_pgpool", False):
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
if disable_server_side_cursors is not UNSET:

View File

@ -158,6 +158,18 @@ class TestConfig(TestCase):
test_obj = Test()
dumps(test_obj, indent=4, cls=AttrEncoder)
def test_get_optional_int(self):
config = ConfigLoader()
self.assertEqual(config.get_optional_int("foo", 21), 21)
self.assertEqual(config.get_optional_int("foo"), None)
config.set("foo", "21")
self.assertEqual(config.get_optional_int("foo"), 21)
self.assertEqual(config.get_optional_int("foo", 0), 21)
self.assertEqual(config.get_optional_int("foo", "null"), 21)
config.set("foo", "null")
self.assertEqual(config.get_optional_int("foo"), None)
self.assertEqual(config.get_optional_int("foo", 21), None)
@mock.patch.dict(environ, check_deprecations_env_vars)
def test_check_deprecations(self):
"""Test config key re-write for deprecated env vars"""
@ -221,6 +233,16 @@ class TestConfig(TestCase):
},
)
def test_db_conn_max_age(self):
"""Test DB conn_max_age Config"""
config = ConfigLoader()
config.set("postgresql.conn_max_age", "null")
conf = django_db_config(config)
self.assertEqual(
conf["default"]["CONN_MAX_AGE"],
None,
)
def test_db_read_replicas(self):
"""Test read replicas"""
config = ConfigLoader()

View File

@ -19,7 +19,6 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.core.models import Provider
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.providers.rac.models import RACProvider
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
@ -31,6 +30,7 @@ from authentik.outposts.models import (
)
from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.rac.models import RACProvider
from authentik.providers.radius.models import RadiusProvider

View File

@ -18,8 +18,6 @@ from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
from structlog.stdlib import get_logger
from yaml import safe_load
from authentik.enterprise.providers.rac.controllers.docker import RACDockerController
from authentik.enterprise.providers.rac.controllers.kubernetes import RACKubernetesController
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask, prefill_task
from authentik.lib.config import CONFIG
@ -41,6 +39,8 @@ from authentik.providers.ldap.controllers.docker import LDAPDockerController
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
from authentik.providers.proxy.controllers.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.providers.rac.controllers.docker import RACDockerController
from authentik.providers.rac.controllers.kubernetes import RACKubernetesController
from authentik.providers.radius.controllers.docker import RadiusDockerController
from authentik.providers.radius.controllers.kubernetes import RadiusKubernetesController
from authentik.root.celery import CELERY_APP

View File

@ -128,7 +128,7 @@ class GeoIPPolicy(Policy):
(geoip_data["lat"], geoip_data["long"]),
)
if self.check_history_distance and dist.km >= (
self.history_max_distance_km - self.distance_tolerance_km
self.history_max_distance_km + self.distance_tolerance_km
):
return PolicyResult(
False, _("Distance from previous authentication is larger than threshold.")
@ -139,7 +139,7 @@ class GeoIPPolicy(Policy):
# clamped to be at least 1 hour
rel_time_hours = max(int((_now - previous_login.created).total_seconds() / 3600), 1)
if self.check_impossible_travel and dist.km >= (
(MAX_DISTANCE_HOUR_KM * rel_time_hours) - self.distance_tolerance_km
(MAX_DISTANCE_HOUR_KM * rel_time_hours) + self.distance_tolerance_km
):
return PolicyResult(False, _("Distance is further than possible."))
return PolicyResult(True)

View File

@ -148,10 +148,10 @@ class PasswordPolicy(Policy):
user_inputs.append(request.user.email)
if request.http_request:
user_inputs.append(request.http_request.brand.branding_title)
# Only calculate result for the first 100 characters, as with over 100 char
# Only calculate result for the first 72 characters, as with over 100 char
# long passwords we can be reasonably sure that they'll surpass the score anyways
# See https://github.com/dropbox/zxcvbn#runtime-latency
results = zxcvbn(password[:100], user_inputs)
results = zxcvbn(password[:72], user_inputs)
LOGGER.debug("password failed", check="zxcvbn", score=results["score"])
result = PolicyResult(results["score"] > self.zxcvbn_score_threshold)
if not result.passing:

View File

@ -71,7 +71,7 @@ class CodeValidatorView(PolicyAccessView):
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
plan.append_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
return plan.to_redirect(self.request, self.token.provider.authorization_flow)

View File

@ -34,5 +34,5 @@ class EndSessionView(PolicyAccessView):
PLAN_CONTEXT_APPLICATION: self.application,
},
)
plan.insert_stage(in_memory_stage(SessionEndStage))
plan.append_stage(in_memory_stage(SessionEndStage))
return plan.to_redirect(self.request, self.flow)

View File

@ -36,17 +36,17 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
def reconciler_name() -> str:
return "ingress"
def _check_annotations(self, reference: V1Ingress):
def _check_annotations(self, current: V1Ingress, reference: V1Ingress):
"""Check that all annotations *we* set are correct"""
for key, value in self.get_ingress_annotations().items():
if key not in reference.metadata.annotations:
for key, value in reference.metadata.annotations.items():
if key not in current.metadata.annotations:
raise NeedsUpdate()
if reference.metadata.annotations[key] != value:
if current.metadata.annotations[key] != value:
raise NeedsUpdate()
def reconcile(self, current: V1Ingress, reference: V1Ingress):
super().reconcile(current, reference)
self._check_annotations(reference)
self._check_annotations(current, reference)
# Create a list of all expected host and tls hosts
expected_hosts = []
expected_hosts_tls = []

View File

@ -6,13 +6,12 @@ from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.enterprise.providers.rac.models import ConnectionToken
from authentik.providers.rac.api.endpoints import EndpointSerializer
from authentik.providers.rac.api.providers import RACProviderSerializer
from authentik.providers.rac.models import ConnectionToken
class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
class ConnectionTokenSerializer(ModelSerializer):
"""ConnectionToken Serializer"""
provider_obj = RACProviderSerializer(source="provider", read_only=True)

View File

@ -14,10 +14,9 @@ from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import Provider
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.enterprise.providers.rac.models import Endpoint
from authentik.policies.engine import PolicyEngine
from authentik.providers.rac.api.providers import RACProviderSerializer
from authentik.providers.rac.models import Endpoint
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
@ -28,7 +27,7 @@ def user_endpoint_cache_key(user_pk: str) -> str:
return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}"
class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer):
class EndpointSerializer(ModelSerializer):
"""Endpoint Serializer"""
provider_obj = RACProviderSerializer(source="provider", read_only=True)

View File

@ -10,7 +10,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.property_mappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField
from authentik.enterprise.providers.rac.models import RACPropertyMapping
from authentik.providers.rac.models import RACPropertyMapping
class RACPropertyMappingSerializer(PropertyMappingSerializer):

View File

@ -5,11 +5,10 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.models import RACProvider
from authentik.providers.rac.models import RACProvider
class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
class RACProviderSerializer(ProviderSerializer):
"""RACProvider Serializer"""
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")

View File

@ -0,0 +1,14 @@
"""RAC app config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikProviderRAC(ManagedAppConfig):
"""authentik rac app config"""
name = "authentik.providers.rac"
label = "authentik_providers_rac"
verbose_name = "authentik Providers.RAC"
default = True
mountpoint = ""
ws_mountpoint = "authentik.providers.rac.urls"

View File

@ -7,22 +7,22 @@ from channels.generic.websocket import AsyncWebsocketConsumer
from django.http.request import QueryDict
from structlog.stdlib import BoundLogger, get_logger
from authentik.enterprise.providers.rac.models import ConnectionToken, RACProvider
from authentik.outposts.consumer import OUTPOST_GROUP_INSTANCE
from authentik.outposts.models import Outpost, OutpostState, OutpostType
from authentik.providers.rac.models import ConnectionToken, RACProvider
# Global broadcast group, which messages are sent to when the outpost connects back
# to authentik for a specific connection
# The `RACClientConsumer` consumer adds itself to this group on connection,
# and removes itself once it has been assigned a specific outpost channel
RAC_CLIENT_GROUP = "group_enterprise_rac_client"
RAC_CLIENT_GROUP = "group_rac_client"
# A group for all connections in a given authentik session ID
# A disconnect message is sent to this group when the session expires/is deleted
RAC_CLIENT_GROUP_SESSION = "group_enterprise_rac_client_%(session)s"
RAC_CLIENT_GROUP_SESSION = "group_rac_client_%(session)s"
# A group for all connections with a specific token, which in almost all cases
# is just one connection, however this is used to disconnect the connection
# when the token is deleted
RAC_CLIENT_GROUP_TOKEN = "group_enterprise_rac_token_%(token)s" # nosec
RAC_CLIENT_GROUP_TOKEN = "group_rac_token_%(token)s" # nosec
# Step 1: Client connects to this websocket endpoint
# Step 2: We prepare all the connection args for Guac

View File

@ -3,7 +3,7 @@
from channels.exceptions import ChannelFull
from channels.generic.websocket import AsyncWebsocketConsumer
from authentik.enterprise.providers.rac.consumer_client import RAC_CLIENT_GROUP
from authentik.providers.rac.consumer_client import RAC_CLIENT_GROUP
class RACOutpostConsumer(AsyncWebsocketConsumer):

View File

@ -74,7 +74,7 @@ class RACProvider(Provider):
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.providers.rac.api.providers import RACProviderSerializer
return RACProviderSerializer
@ -100,7 +100,7 @@ class Endpoint(SerializerModel, PolicyBindingModel):
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
from authentik.providers.rac.api.endpoints import EndpointSerializer
return EndpointSerializer
@ -129,7 +129,7 @@ class RACPropertyMapping(PropertyMapping):
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.rac.api.property_mappings import (
from authentik.providers.rac.api.property_mappings import (
RACPropertyMappingSerializer,
)

View File

@ -4,18 +4,17 @@ from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.contrib.auth.signals import user_logged_out
from django.core.cache import cache
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from django.db.models.signals import post_delete, post_save, pre_delete
from django.dispatch import receiver
from django.http import HttpRequest
from authentik.core.models import User
from authentik.enterprise.providers.rac.api.endpoints import user_endpoint_cache_key
from authentik.enterprise.providers.rac.consumer_client import (
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
from authentik.providers.rac.consumer_client import (
RAC_CLIENT_GROUP_SESSION,
RAC_CLIENT_GROUP_TOKEN,
)
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint
from authentik.providers.rac.models import ConnectionToken, Endpoint
@receiver(user_logged_out)
@ -46,12 +45,8 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
)
@receiver(post_save, sender=Endpoint)
def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
"""Clear user's endpoint cache upon endpoint creation"""
if not created: # pragma: no cover
return
# Delete user endpoint cache
@receiver([post_save, post_delete], sender=Endpoint)
def post_save_post_delete_endpoint(**_):
"""Clear user's endpoint cache upon endpoint creation or deletion"""
keys = cache.keys(user_endpoint_cache_key("*"))
cache.delete_many(keys)

View File

@ -3,7 +3,7 @@
{% load authentik_core %}
{% block head %}
<script src="{% versioned_script 'dist/enterprise/rac/index-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/rac/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon_url }}">

View File

@ -1,16 +1,9 @@
"""Test RAC Provider"""
from datetime import timedelta
from time import mktime
from unittest.mock import MagicMock, patch
from django.urls import reverse
from django.utils.timezone import now
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import License
from authentik.lib.generators import generate_id
@ -20,21 +13,8 @@ class TestAPI(APITestCase):
def setUp(self) -> None:
self.user = create_test_admin_user()
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_create(self):
"""Test creation of RAC Provider"""
License.objects.create(key=generate_id())
self.client.force_login(self.user)
response = self.client.post(
reverse("authentik_api:racprovider-list"),

View File

@ -5,10 +5,10 @@ from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user
from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.rac.models import Endpoint, Protocols, RACProvider
class TestEndpointsAPI(APITestCase):

View File

@ -4,14 +4,14 @@ from django.test import TransactionTestCase
from authentik.core.models import Application, AuthenticatedSession
from authentik.core.tests.utils import create_test_admin_user
from authentik.enterprise.providers.rac.models import (
from authentik.lib.generators import generate_id
from authentik.providers.rac.models import (
ConnectionToken,
Endpoint,
Protocols,
RACPropertyMapping,
RACProvider,
)
from authentik.lib.generators import generate_id
class TestModels(TransactionTestCase):

View File

@ -1,23 +1,17 @@
"""RAC Views tests"""
from datetime import timedelta
from json import loads
from time import mktime
from unittest.mock import MagicMock, patch
from django.urls import reverse
from django.utils.timezone import now
from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import License
from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider
from authentik.lib.generators import generate_id
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.rac.models import Endpoint, Protocols, RACProvider
class TestRACViews(APITestCase):
@ -39,21 +33,8 @@ class TestRACViews(APITestCase):
provider=self.provider,
)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_no_policy(self):
"""Test request"""
License.objects.create(key=generate_id())
self.client.force_login(self.user)
response = self.client.get(
reverse(
@ -70,18 +51,6 @@ class TestRACViews(APITestCase):
final_response = self.client.get(next_url)
self.assertEqual(final_response.status_code, 200)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_app_deny(self):
"""Test request (deny on app level)"""
PolicyBinding.objects.create(
@ -89,7 +58,6 @@ class TestRACViews(APITestCase):
policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2),
order=0,
)
License.objects.create(key=generate_id())
self.client.force_login(self.user)
response = self.client.get(
reverse(
@ -99,18 +67,6 @@ class TestRACViews(APITestCase):
)
self.assertIsInstance(response, AccessDeniedResponse)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
def test_endpoint_deny(self):
"""Test request (deny on endpoint level)"""
PolicyBinding.objects.create(
@ -118,7 +74,6 @@ class TestRACViews(APITestCase):
policy=DummyPolicy.objects.create(name="deny", result=False, wait_min=1, wait_max=2),
order=0,
)
License.objects.create(key=generate_id())
self.client.force_login(self.user)
response = self.client.get(
reverse(

View File

@ -4,14 +4,14 @@ from channels.auth import AuthMiddleware
from channels.sessions import CookieMiddleware
from django.urls import path
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
from authentik.enterprise.providers.rac.api.property_mappings import RACPropertyMappingViewSet
from authentik.enterprise.providers.rac.api.providers import RACProviderViewSet
from authentik.enterprise.providers.rac.consumer_client import RACClientConsumer
from authentik.enterprise.providers.rac.consumer_outpost import RACOutpostConsumer
from authentik.enterprise.providers.rac.views import RACInterface, RACStartView
from authentik.outposts.channels import TokenOutpostMiddleware
from authentik.providers.rac.api.connection_tokens import ConnectionTokenViewSet
from authentik.providers.rac.api.endpoints import EndpointViewSet
from authentik.providers.rac.api.property_mappings import RACPropertyMappingViewSet
from authentik.providers.rac.api.providers import RACProviderViewSet
from authentik.providers.rac.consumer_client import RACClientConsumer
from authentik.providers.rac.consumer_outpost import RACOutpostConsumer
from authentik.providers.rac.views import RACInterface, RACStartView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.middleware import ChannelsLoggingMiddleware

View File

@ -10,8 +10,6 @@ from django.utils.translation import gettext as _
from authentik.core.models import Application, AuthenticatedSession
from authentik.core.views.interface import InterfaceView
from authentik.enterprise.policy import EnterprisePolicyAccessView
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint, RACProvider
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.exceptions import FlowNonApplicableException
@ -20,9 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
class RACStartView(EnterprisePolicyAccessView):
class RACStartView(PolicyAccessView):
"""Start a RAC connection by checking access and creating a connection token"""
endpoint: Endpoint
@ -46,7 +46,7 @@ class RACStartView(EnterprisePolicyAccessView):
)
except FlowNonApplicableException:
raise Http404 from None
plan.insert_stage(
plan.append_stage(
in_memory_stage(
RACFinalStage,
application=self.application,

View File

@ -61,7 +61,7 @@ class SAMLSLOView(PolicyAccessView):
PLAN_CONTEXT_APPLICATION: self.application,
},
)
plan.insert_stage(in_memory_stage(SessionEndStage))
plan.append_stage(in_memory_stage(SessionEndStage))
return plan.to_redirect(self.request, self.flow)
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:

View File

@ -1,10 +1,12 @@
"""User client"""
from django.db import transaction
from django.utils.http import urlencode
from pydantic import ValidationError
from authentik.core.models import User
from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.lib.sync.outgoing.exceptions import ObjectExistsSyncException, StopSync
from authentik.policies.utils import delete_none_values
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
@ -55,18 +57,35 @@ class SCIMUserClient(SCIMClient[User, SCIMProviderUser, SCIMUserSchema]):
def create(self, user: User):
"""Create user from scratch and create a connection object"""
scim_user = self.to_schema(user, None)
response = self._request(
"POST",
"/Users",
json=scim_user.model_dump(
mode="json",
exclude_unset=True,
),
)
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
return SCIMProviderUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
with transaction.atomic():
try:
response = self._request(
"POST",
"/Users",
json=scim_user.model_dump(
mode="json",
exclude_unset=True,
),
)
except ObjectExistsSyncException as exc:
if not self._config.filter.supported:
raise exc
users = self._request(
"GET", f"/Users?{urlencode({'filter': f'userName eq {scim_user.userName}'})}"
)
users_res = users.get("Resources", [])
if len(users_res) < 1:
raise exc
return SCIMProviderUser.objects.create(
provider=self.provider, user=user, scim_id=users_res[0]["id"]
)
else:
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
return SCIMProviderUser.objects.create(
provider=self.provider, user=user, scim_id=scim_id
)
def update(self, user: User, connection: SCIMProviderUser):
"""Update existing user"""

View File

@ -2,7 +2,7 @@
from django.apps import apps
from django.contrib.auth.models import Permission
from django.db.models import Q, QuerySet
from django.db.models import QuerySet
from django_filters.filters import ModelChoiceFilter
from django_filters.filterset import FilterSet
from django_filters.rest_framework import DjangoFilterBackend
@ -18,7 +18,6 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.blueprints.v1.importer import excluded_models
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User
from authentik.lib.validators import RequiredTogetherValidator
@ -106,13 +105,13 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
]
def get_queryset(self) -> QuerySet:
query = Q()
for model in excluded_models():
query |= Q(
content_type__app_label=model._meta.app_label,
content_type__model=model._meta.model_name,
return (
Permission.objects.all()
.select_related("content_type")
.filter(
content_type__app_label__startswith="authentik",
)
return Permission.objects.all().select_related("content_type").exclude(query)
)
class PermissionAssignSerializer(PassiveSerializer):

View File

@ -87,6 +87,7 @@ TENANT_APPS = [
"authentik.providers.ldap",
"authentik.providers.oauth2",
"authentik.providers.proxy",
"authentik.providers.rac",
"authentik.providers.radius",
"authentik.providers.saml",
"authentik.providers.scim",

View File

@ -68,8 +68,6 @@ class OAuth2Client(BaseOAuthClient):
error_desc = self.get_request_arg("error_description", None)
return {"error": error_desc or error or _("No token received.")}
args = {
"client_id": self.get_client_id(),
"client_secret": self.get_client_secret(),
"redirect_uri": callback,
"code": code,
"grant_type": "authorization_code",

View File

@ -28,7 +28,7 @@ def update_well_known_jwks(self: SystemTask):
LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text)
messages.append(f"Failed to update OIDC configuration for {source.slug}")
continue
config = well_known_config.json()
config: dict = well_known_config.json()
try:
dirty = False
source_attr_key = (
@ -40,7 +40,9 @@ def update_well_known_jwks(self: SystemTask):
for source_attr, config_key in source_attr_key:
# Check if we're actually changing anything to only
# save when something has changed
if getattr(source, source_attr, "") != config[config_key]:
if config_key not in config:
continue
if getattr(source, source_attr, "") != config.get(config_key, ""):
dirty = True
setattr(source, source_attr, config[config_key])
except (IndexError, KeyError) as exc:

View File

@ -2,6 +2,7 @@
from typing import Any
from requests import RequestException
from structlog.stdlib import get_logger
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
@ -21,10 +22,35 @@ class AzureADOAuthRedirect(OAuthRedirect):
}
class AzureADClient(UserprofileHeaderAuthClient):
"""Fetch AzureAD group information"""
def get_profile_info(self, token):
profile_data = super().get_profile_info(token)
if "https://graph.microsoft.com/GroupMember.Read.All" not in self.source.additional_scopes:
return profile_data
group_response = self.session.request(
"get",
"https://graph.microsoft.com/v1.0/me/memberOf",
headers={"Authorization": f"{token['token_type']} {token['access_token']}"},
)
try:
group_response.raise_for_status()
except RequestException as exc:
LOGGER.warning(
"Unable to fetch user profile",
exc=exc,
response=exc.response.text if exc.response else str(exc),
)
return None
profile_data["raw_groups"] = group_response.json()
return profile_data
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
"""AzureAD OAuth2 Callback"""
client_class = UserprofileHeaderAuthClient
client_class = AzureADClient
def get_user_id(self, info: dict[str, str]) -> str:
# Default try to get `id` for the Graph API endpoint
@ -53,8 +79,24 @@ class AzureADType(SourceType):
def get_base_user_properties(self, info: dict[str, Any], **kwargs) -> dict[str, Any]:
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
# Format group info
groups = []
group_id_dict = {}
for group in info.get("raw_groups", {}).get("value", []):
if group["@odata.type"] != "#microsoft.graph.group":
continue
groups.append(group["id"])
group_id_dict[group["id"]] = group
info["raw_groups"] = group_id_dict
return {
"username": info.get("userPrincipalName"),
"email": mail,
"name": info.get("displayName"),
"groups": groups,
}
def get_base_group_properties(self, source, group_id, **kwargs):
raw_group = kwargs["info"]["raw_groups"][group_id]
return {
"name": raw_group["displayName"],
}

View File

@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer
from authentik.core.types import UserSettingSerializer
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import StageInvalidException
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
@ -71,6 +72,14 @@ class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-email-form"
def ui_user_settings(self) -> UserSettingSerializer | None:
return UserSettingSerializer(
data={
"title": self.friendly_name or str(self._meta.verbose_name),
"component": "ak-user-settings-authenticator-email",
}
)
@property
def backend_class(self) -> type[BaseEmailBackend]:
"""Get the email backend class to use"""

View File

@ -300,9 +300,11 @@ class TestAuthenticatorEmailStage(FlowTestCase):
)
self.assertEqual(response.status_code, 200)
self.assertTrue(device.confirmed)
# Session key should be removed after device is saved
device.save()
self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, self.client.session)
# Get a fresh session to check if the key was removed
session = self.client.session
session.save()
session.load()
self.assertNotIn(SESSION_KEY_EMAIL_DEVICE, session)
def test_model_properties_and_methods(self):
"""Test model properties"""

View File

@ -12,6 +12,7 @@ from structlog.stdlib import get_logger
from authentik.events.models import Event, EventAction, TaskStatus
from authentik.events.system_tasks import SystemTask
from authentik.lib.utils.reflection import class_to_path, path_to_class
from authentik.root.celery import CELERY_APP
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
from authentik.stages.email.models import EmailStage
@ -32,9 +33,10 @@ def send_mails(
Celery group promise for the email sending tasks
"""
tasks = []
stage_class = stage.__class__
# Use the class path instead of the class itself for serialization
stage_class_path = class_to_path(stage.__class__)
for message in messages:
tasks.append(send_mail.s(message.__dict__, stage_class, str(stage.pk)))
tasks.append(send_mail.s(message.__dict__, stage_class_path, str(stage.pk)))
lazy_group = group(*tasks)
promise = lazy_group()
return promise
@ -61,7 +63,7 @@ def get_email_body(email: EmailMultiAlternatives) -> str:
def send_mail(
self: SystemTask,
message: dict[Any, Any],
stage_class: EmailStage | AuthenticatorEmailStage = EmailStage,
stage_class_path: str | None = None,
email_stage_pk: str | None = None,
):
"""Send Email for Email Stage. Retries are scheduled automatically."""
@ -69,9 +71,10 @@ def send_mail(
message_id = make_msgid(domain=DNS_NAME)
self.set_uid(slugify(message_id.replace(".", "_").replace("@", "_")))
try:
if not email_stage_pk:
stage: EmailStage | AuthenticatorEmailStage = stage_class(use_global_settings=True)
if not stage_class_path or not email_stage_pk:
stage = EmailStage(use_global_settings=True)
else:
stage_class = path_to_class(stage_class_path)
stages = stage_class.objects.filter(pk=email_stage_pk)
if not stages.exists():
self.set_status(

View File

@ -0,0 +1,58 @@
"""Test email stage tasks"""
from unittest.mock import patch
from django.core.mail import EmailMultiAlternatives
from django.test import TestCase
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.utils.reflection import class_to_path
from authentik.stages.authenticator_email.models import AuthenticatorEmailStage
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import get_email_body, send_mails
class TestEmailTasks(TestCase):
"""Test email stage tasks"""
def setUp(self):
self.user = create_test_admin_user()
self.stage = EmailStage.objects.create(
name="test-email",
use_global_settings=True,
)
self.auth_stage = AuthenticatorEmailStage.objects.create(
name="test-auth-email",
use_global_settings=True,
)
def test_get_email_body_html(self):
"""Test get_email_body with HTML alternative"""
message = EmailMultiAlternatives()
message.body = "plain text"
message.attach_alternative("<p>html content</p>", "text/html")
self.assertEqual(get_email_body(message), "<p>html content</p>")
def test_get_email_body_plain(self):
"""Test get_email_body with plain text only"""
message = EmailMultiAlternatives()
message.body = "plain text"
self.assertEqual(get_email_body(message), "plain text")
def test_send_mails_email_stage(self):
"""Test send_mails with EmailStage"""
message = EmailMultiAlternatives()
with patch("authentik.stages.email.tasks.send_mail") as mock_send:
send_mails(self.stage, message)
mock_send.s.assert_called_once_with(
message.__dict__, class_to_path(EmailStage), str(self.stage.pk)
)
def test_send_mails_authenticator_stage(self):
"""Test send_mails with AuthenticatorEmailStage"""
message = EmailMultiAlternatives()
with patch("authentik.stages.email.tasks.send_mail") as mock_send:
send_mails(self.auth_stage, message)
mock_send.s.assert_called_once_with(
message.__dict__, class_to_path(AuthenticatorEmailStage), str(self.auth_stage.pk)
)

View File

@ -142,38 +142,35 @@ class IdentificationChallengeResponse(ChallengeResponse):
raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user
# Password check
if current_stage.password_stage:
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
# Captcha check
if captcha_stage := current_stage.captcha_stage:
captcha_token = attrs.get("captcha_token", None)
if not captcha_token:
self.stage.logger.warning("Token not set for captcha attempt")
verify_captcha_token(captcha_stage, captcha_token, client_ip)
# Password check
if not current_stage.password_stage:
# No password stage select, don't validate the password
return attrs
password = attrs.get("password", None)
if not password:
self.stage.logger.warning("Password not set for ident+auth attempt")
try:
with start_span(
op="authentik.stages.identification.authenticate",
name="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
current_stage,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
except PermissionDenied as exc:
raise ValidationError(str(exc)) from exc
return attrs

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.12.3 Blueprint schema",
"title": "authentik 2025.2.2 Blueprint schema",
"required": [
"version",
"entries"
@ -801,6 +801,126 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.racprovider"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.endpoint"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.racpropertymapping"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
}
}
},
{
"type": "object",
"required": [
@ -3561,126 +3681,6 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.racprovider"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.racprovider"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.endpoint"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.endpoint"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_providers_rac.racpropertymapping"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_providers_rac.racpropertymapping"
}
}
},
{
"type": "object",
"required": [
@ -4663,6 +4663,7 @@
"authentik.providers.ldap",
"authentik.providers.oauth2",
"authentik.providers.proxy",
"authentik.providers.rac",
"authentik.providers.radius",
"authentik.providers.saml",
"authentik.providers.scim",
@ -4703,7 +4704,6 @@
"authentik.enterprise.audit",
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.rac",
"authentik.enterprise.providers.ssf",
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
"authentik.enterprise.stages.source",
@ -4738,6 +4738,9 @@
"authentik_providers_oauth2.scopemapping",
"authentik_providers_oauth2.oauth2provider",
"authentik_providers_proxy.proxyprovider",
"authentik_providers_rac.racprovider",
"authentik_providers_rac.endpoint",
"authentik_providers_rac.racpropertymapping",
"authentik_providers_radius.radiusprovider",
"authentik_providers_radius.radiusproviderpropertymapping",
"authentik_providers_saml.samlprovider",
@ -4807,9 +4810,6 @@
"authentik_providers_google_workspace.googleworkspaceprovidermapping",
"authentik_providers_microsoft_entra.microsoftentraprovider",
"authentik_providers_microsoft_entra.microsoftentraprovidermapping",
"authentik_providers_rac.racprovider",
"authentik_providers_rac.endpoint",
"authentik_providers_rac.racpropertymapping",
"authentik_providers_ssf.ssfprovider",
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
"authentik_stages_source.sourcestage",
@ -6046,6 +6046,216 @@
}
}
},
"model_authentik_providers_rac.racprovider": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"authentication_flow": {
"type": "string",
"format": "uuid",
"title": "Authentication flow",
"description": "Flow used for authentication when the associated application is accessed by an un-authenticated user."
},
"authorization_flow": {
"type": "string",
"format": "uuid",
"title": "Authorization flow",
"description": "Flow used when authorizing this provider."
},
"property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Property mappings"
},
"settings": {
"type": "object",
"additionalProperties": true,
"title": "Settings"
},
"connection_expiry": {
"type": "string",
"minLength": 1,
"title": "Connection expiry",
"description": "Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
},
"delete_token_on_disconnect": {
"type": "boolean",
"title": "Delete token on disconnect",
"description": "When set to true, connection tokens will be deleted upon disconnect."
}
},
"required": []
},
"model_authentik_providers_rac.racprovider_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_racprovider",
"change_racprovider",
"delete_racprovider",
"view_racprovider"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_rac.endpoint": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"provider": {
"type": "integer",
"title": "Provider"
},
"protocol": {
"type": "string",
"enum": [
"rdp",
"vnc",
"ssh"
],
"title": "Protocol"
},
"host": {
"type": "string",
"minLength": 1,
"title": "Host"
},
"settings": {
"type": "object",
"additionalProperties": true,
"title": "Settings"
},
"property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Property mappings"
},
"auth_mode": {
"type": "string",
"enum": [
"static",
"prompt"
],
"title": "Auth mode"
},
"maximum_connections": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"title": "Maximum connections"
}
},
"required": []
},
"model_authentik_providers_rac.endpoint_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_endpoint",
"change_endpoint",
"delete_endpoint",
"view_endpoint"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_rac.racpropertymapping": {
"type": "object",
"properties": {
"managed": {
"type": [
"string",
"null"
],
"minLength": 1,
"title": "Managed by authentik",
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
},
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"expression": {
"type": "string",
"title": "Expression"
},
"static_settings": {
"type": "object",
"additionalProperties": true,
"title": "Static settings"
}
},
"required": []
},
"model_authentik_providers_rac.racpropertymapping_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_racpropertymapping",
"change_racpropertymapping",
"delete_racpropertymapping",
"view_racpropertymapping"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_radius.radiusprovider": {
"type": "object",
"properties": {
@ -14215,216 +14425,6 @@
}
}
},
"model_authentik_providers_rac.racprovider": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"authentication_flow": {
"type": "string",
"format": "uuid",
"title": "Authentication flow",
"description": "Flow used for authentication when the associated application is accessed by an un-authenticated user."
},
"authorization_flow": {
"type": "string",
"format": "uuid",
"title": "Authorization flow",
"description": "Flow used when authorizing this provider."
},
"property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Property mappings"
},
"settings": {
"type": "object",
"additionalProperties": true,
"title": "Settings"
},
"connection_expiry": {
"type": "string",
"minLength": 1,
"title": "Connection expiry",
"description": "Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)"
},
"delete_token_on_disconnect": {
"type": "boolean",
"title": "Delete token on disconnect",
"description": "When set to true, connection tokens will be deleted upon disconnect."
}
},
"required": []
},
"model_authentik_providers_rac.racprovider_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_racprovider",
"change_racprovider",
"delete_racprovider",
"view_racprovider"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_rac.endpoint": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"provider": {
"type": "integer",
"title": "Provider"
},
"protocol": {
"type": "string",
"enum": [
"rdp",
"vnc",
"ssh"
],
"title": "Protocol"
},
"host": {
"type": "string",
"minLength": 1,
"title": "Host"
},
"settings": {
"type": "object",
"additionalProperties": true,
"title": "Settings"
},
"property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Property mappings"
},
"auth_mode": {
"type": "string",
"enum": [
"static",
"prompt"
],
"title": "Auth mode"
},
"maximum_connections": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"title": "Maximum connections"
}
},
"required": []
},
"model_authentik_providers_rac.endpoint_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_endpoint",
"change_endpoint",
"delete_endpoint",
"view_endpoint"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_rac.racpropertymapping": {
"type": "object",
"properties": {
"managed": {
"type": [
"string",
"null"
],
"minLength": 1,
"title": "Managed by authentik",
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
},
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"expression": {
"type": "string",
"title": "Expression"
},
"static_settings": {
"type": "object",
"additionalProperties": true,
"title": "Static settings"
}
},
"required": []
},
"model_authentik_providers_rac.racpropertymapping_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_racpropertymapping",
"change_racpropertymapping",
"delete_racpropertymapping",
"view_racpropertymapping"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_ssf.ssfprovider": {
"type": "object",
"properties": {

View File

@ -10,6 +10,7 @@ import (
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
@ -24,7 +25,8 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{

View File

@ -10,6 +10,7 @@ import (
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
@ -27,7 +28,8 @@ Optionally, you can set these:
- AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST`
var rootCmd = &cobra.Command{
Long: helpMessage,
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{

View File

@ -9,6 +9,7 @@ import (
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
@ -23,7 +24,8 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{

View File

@ -9,6 +9,7 @@ import (
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
@ -23,7 +24,8 @@ Required environment variables:
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
Long: helpMessage,
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2024.12.3",
"version": "2025.2.2",
"private": true
}

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.12.3"
version = "2025.2.2"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.12.3
version: 2025.2.2
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -39482,6 +39482,7 @@ components:
- authentik.providers.ldap
- authentik.providers.oauth2
- authentik.providers.proxy
- authentik.providers.rac
- authentik.providers.radius
- authentik.providers.saml
- authentik.providers.scim
@ -39522,7 +39523,6 @@ components:
- authentik.enterprise.audit
- authentik.enterprise.providers.google_workspace
- authentik.enterprise.providers.microsoft_entra
- authentik.enterprise.providers.rac
- authentik.enterprise.providers.ssf
- authentik.enterprise.stages.authenticator_endpoint_gdtc
- authentik.enterprise.stages.source
@ -46625,6 +46625,9 @@ components:
- authentik_providers_oauth2.scopemapping
- authentik_providers_oauth2.oauth2provider
- authentik_providers_proxy.proxyprovider
- authentik_providers_rac.racprovider
- authentik_providers_rac.endpoint
- authentik_providers_rac.racpropertymapping
- authentik_providers_radius.radiusprovider
- authentik_providers_radius.radiusproviderpropertymapping
- authentik_providers_saml.samlprovider
@ -46694,9 +46697,6 @@ components:
- authentik_providers_google_workspace.googleworkspaceprovidermapping
- authentik_providers_microsoft_entra.microsoftentraprovider
- authentik_providers_microsoft_entra.microsoftentraprovidermapping
- authentik_providers_rac.racprovider
- authentik_providers_rac.endpoint
- authentik_providers_rac.racpropertymapping
- authentik_providers_ssf.ssfprovider
- authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage
- authentik_stages_source.sourcestage

View File

@ -74,7 +74,7 @@ const interfaces = [
["user/UserInterface.ts", "user"],
["flow/FlowInterface.ts", "flow"],
["standalone/api-browser/index.ts", "standalone/api-browser"],
["enterprise/rac/index.ts", "enterprise/rac"],
["rac/index.ts", "rac"],
["standalone/loading/index.ts", "standalone/loading"],
["polyfill/poly.ts", "."],
];

8
web/package-lock.json generated
View File

@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2024.12.3-1739814462",
"@goauthentik/api": "^2024.12.3-1739965710",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@ -1814,9 +1814,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2024.12.3-1739814462",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739814462.tgz",
"integrity": "sha512-qWGsq7zP0rG1PfjZA+iimaX4cVkd1n2JA/WceTOKgBmqnomQSI7SJNkdSpD+Qdy76PI0UuQWN73PInq/3rmm5Q=="
"version": "2024.12.3-1739965710",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.12.3-1739965710.tgz",
"integrity": "sha512-16zoQWeJhAFSwttvqLRoXoQA43tMW1ZXDEihW6r8rtWtlxqPh7n36RtcWYraYiLcjmJskI90zdgz6k1kmY5AXw=="
},
"node_modules/@goauthentik/web": {
"resolved": "",

View File

@ -11,7 +11,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2024.12.3-1739814462",
"@goauthentik/api": "^2024.12.3-1739965710",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",

View File

@ -6,7 +6,7 @@ const config: KnipConfig = {
"./src/user/UserInterface.ts",
"./src/flow/FlowInterface.ts",
"./src/standalone/api-browser/index.ts",
"./src/enterprise/rac/index.ts",
"./src/rac/index.ts",
"./src/standalone/loading/index.ts",
"./src/polyfill/poly.ts",
],

View File

@ -7,6 +7,7 @@ import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/Alert.js";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
@ -21,7 +22,7 @@ import "@goauthentik/elements/forms/SearchSelect";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@ -120,7 +121,12 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
}
renderForm(): TemplateResult {
const alertMsg = msg(
"Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.",
);
return html`<form class="pf-c-form pf-m-horizontal">
${this.instance ? nothing : html`<ak-alert level="pf-m-info">${alertMsg}</ak-alert>`}
<ak-text-input
name="name"
value=${ifDefined(this.instance?.name)}

View File

@ -50,7 +50,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
}
pageDescription(): string {
return msg(
str`External applications that use ${this.brand.brandingTitle || "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
str`External applications that use ${this.brand?.brandingTitle ?? "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
);
}
pageIcon(): string {
@ -85,10 +85,6 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
];
}
renderSectionBefore(): TemplateResult {
return html`<ak-application-wizard-hint></ak-application-wizard-hint>`;
}
renderSidebarAfter(): TemplateResult {
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card">
@ -160,12 +156,21 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
}
renderObjectCreate(): TemplateResult {
return html`<ak-forms-modal .open=${getURLParam("createForm", false)}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Application")} </span>
<ak-application-form slot="form"> </ak-application-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>`;
return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}>
<button
slot="trigger"
class="pf-c-button pf-m-primary"
data-ouia-component-id="start-application-wizard"
>
${msg("Create with Provider")}
</button>
</ak-application-wizard>
<ak-forms-modal .open=${getURLParam("createForm", false)}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Application")} </span>
<ak-application-form slot="form"> </ak-application-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>`;
}
}

View File

@ -30,7 +30,7 @@ export class ApplicationWizardStep extends WizardStep {
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
// these fields and provide them to all the child classes.
wizardTitle = msg("New application");
wizardDescription = msg("Create a new application");
wizardDescription = msg("Create a new application and configure a provider for it.");
canCancel = true;
// This should be overridden in the children for more precise targeting.

View File

@ -58,7 +58,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
get bindingsAsColumns() {
return this.wizard.bindings.map((binding, index) => {
const { order, enabled, timeout } = binding;
const isSet = P.string.minLength(1);
const isSet = P.union(P.string.minLength(1), P.number);
const policy = match(binding)
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))

View File

@ -31,9 +31,9 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
@property({ type: Array })
allowedTypes: PolicyBindingCheckTarget[] = [
PolicyBindingCheckTarget.policy,
PolicyBindingCheckTarget.group,
PolicyBindingCheckTarget.user,
PolicyBindingCheckTarget.policy,
];
@property({ type: Array })

View File

@ -58,9 +58,9 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
@property({ type: Array })
allowedTypes: PolicyBindingCheckTarget[] = [
PolicyBindingCheckTarget.policy,
PolicyBindingCheckTarget.group,
PolicyBindingCheckTarget.user,
PolicyBindingCheckTarget.policy,
];
@property({ type: Array })

View File

@ -105,6 +105,22 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Maximum distance")}
name="historyMaxDistanceKm"
>
<input
type="number"
min="1"
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg(
"Maximum distance a login attempt is allowed from in kilometers.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Distance tolerance")}
name="distanceToleranceKm"
@ -133,27 +149,6 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
${msg("Amount of previous login events to check against.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Maximum distance")}
name="historyMaxDistanceKm"
>
<input
type="number"
min="1"
value="${first(this.instance?.historyMaxDistanceKm, 100)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg(
"Maximum distance a login attempt is allowed from in kilometers.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Distance settings (Impossible travel)")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="checkImpossibleTravel">
<label class="pf-c-switch">
<input

View File

@ -21,12 +21,22 @@ export class RelatedApplicationButton extends AKElement {
@property({ attribute: false })
provider?: Provider;
@property()
mode: "primary" | "backchannel" = "primary";
render(): TemplateResult {
if (this.provider?.assignedApplicationSlug) {
if (this.mode === "primary" && this.provider?.assignedApplicationSlug) {
return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}">
${this.provider.assignedApplicationName}
</a>`;
}
if (this.mode === "backchannel" && this.provider?.assignedBackchannelApplicationSlug) {
return html`<a
href="#/core/applications/${this.provider.assignedBackchannelApplicationSlug}"
>
${this.provider.assignedBackchannelApplicationName}
</a>`;
}
return html`<ak-forms-modal>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Application")} </span>

View File

@ -173,6 +173,7 @@ export class SCIMProviderViewPage extends AKElement {
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-provider-related-application
mode="backchannel"
.provider=${this.provider}
></ak-provider-related-application>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 KiB

After

Width:  |  Height:  |  Size: 628 KiB

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.12.3";
export const VERSION = "2025.2.2";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -13,6 +13,7 @@ export interface GlobalAuthentik {
build: string;
api: {
base: string;
relBase: string;
};
}
@ -27,6 +28,7 @@ export function globalAK(): GlobalAuthentik {
ak.brand = CurrentBrandFromJSON(ak.brand);
ak.config = ConfigFromJSON(ak.config);
}
const apiBase = new URL(process.env.AK_API_BASE_PATH || window.location.origin);
if (!ak) {
return {
config: ConfigFromJSON({
@ -39,7 +41,8 @@ export function globalAK(): GlobalAuthentik {
versionSubdomain: "",
build: "",
api: {
base: process.env.AK_API_BASE_PATH || window.location.origin,
base: apiBase.toString(),
relBase: apiBase.pathname,
},
};
}

View File

@ -45,6 +45,8 @@ html > form > input {
left: -2000px;
}
/*#region Icons*/
.pf-icon {
display: inline-block;
font-style: normal;
@ -54,6 +56,18 @@ html > form > input {
vertical-align: middle;
}
.pf-c-form-control {
--pf-c-form-control--m-caps-lock--BackgroundUrl: url("data:image/svg+xml;charset=utf8,%3Csvg fill='%23aaabac' viewBox='0 0 56 56' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 20.7812 37.6211 L 35.2421 37.6211 C 38.5233 37.6211 40.2577 35.6992 40.2577 32.6055 L 40.2577 28.4570 L 49.1404 28.4570 C 51.0859 28.4570 52.6329 27.3086 52.6329 25.5039 C 52.6329 24.4024 52.0703 23.5351 51.0158 22.6211 L 30.9062 4.8789 C 29.9452 4.0351 29.0546 3.4727 27.9999 3.4727 C 26.9687 3.4727 26.0780 4.0351 25.1171 4.8789 L 4.9843 22.6445 C 3.8828 23.6055 3.3671 24.4024 3.3671 25.5039 C 3.3671 27.3086 4.9140 28.4570 6.8828 28.4570 L 15.7421 28.4570 L 15.7421 32.6055 C 15.7421 35.6992 17.4999 37.6211 20.7812 37.6211 Z M 21.1562 34.0820 C 20.2655 34.0820 19.6562 33.4961 19.6562 32.6055 L 19.6562 25.7149 C 19.6562 25.1524 19.4452 24.9180 18.8828 24.9180 L 8.6640 24.9180 C 8.4999 24.9180 8.4296 24.8476 8.4296 24.7305 C 8.4296 24.6367 8.4530 24.5430 8.5702 24.4492 L 27.5077 7.9961 C 27.7187 7.8086 27.8359 7.7383 27.9999 7.7383 C 28.1640 7.7383 28.3046 7.8086 28.4921 7.9961 L 47.4532 24.4492 C 47.5703 24.5430 47.5939 24.6367 47.5939 24.7305 C 47.5939 24.8476 47.4998 24.9180 47.3356 24.9180 L 37.1406 24.9180 C 36.5780 24.9180 36.3671 25.1524 36.3671 25.7149 L 36.3671 32.6055 C 36.3671 33.4727 35.7109 34.0820 34.8671 34.0820 Z M 19.7733 52.5273 L 36.0624 52.5273 C 38.7577 52.5273 40.3046 51.0273 40.3046 48.3086 L 40.3046 44.9336 C 40.3046 42.2148 38.7577 40.6680 36.0624 40.6680 L 19.7733 40.6680 C 17.0546 40.6680 15.5077 42.2383 15.5077 44.9336 L 15.5077 48.3086 C 15.5077 51.0039 17.0546 52.5273 19.7733 52.5273 Z M 20.3124 49.2227 C 19.4921 49.2227 19.0468 48.8008 19.0468 47.9805 L 19.0468 45.2617 C 19.0468 44.4414 19.4921 43.9727 20.3124 43.9727 L 35.5233 43.9727 C 36.3202 43.9727 36.7655 44.4414 36.7655 45.2617 L 36.7655 47.9805 C 36.7655 48.8008 36.3202 49.2227 35.5233 49.2227 Z'/%3E%3C/svg%3E");
}
.pf-c-form-control.pf-m-icon.pf-m-caps-lock {
--pf-c-form-control--m-icon--BackgroundUrl: var(
--pf-c-form-control--m-caps-lock--BackgroundUrl
);
}
/*#endregion*/
.pf-c-page__header {
z-index: 0;
background-color: var(--ak-dark-background-light);

View File

@ -3,6 +3,7 @@ import type { AbstractConstructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context";
import type { LitElement } from "lit";
import { state } from "lit/decorators.js";
import type { CurrentBrand } from "@goauthentik/api";
@ -12,6 +13,7 @@ export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
) {
abstract class WithBrandProvider extends superclass {
@consume({ context: authentikBrandContext, subscribe })
@state()
public brand!: CurrentBrand;
}
return WithBrandProvider;

View File

@ -0,0 +1,27 @@
/**
* @fileoverview Utilities for DOM element interaction, focus management, and event handling.
*/
/**
* Recursively check if the target element or any of its children are active (i.e. "focused").
*
* @param targetElement The element to check if it is active.
* @param containerElement The container element to check if the target element is active within.
*/
export function isActiveElement(
targetElement: Element | null,
containerElement: Element | null,
): boolean {
// Does the container element even exist?
if (!containerElement) return false;
// Does the container element have a shadow root?
if (!("shadowRoot" in containerElement)) return false;
if (containerElement.shadowRoot === null) return false;
// Is the target element the active element?
if (containerElement.shadowRoot.activeElement === targetElement) return true;
// Let's check the children of the container element...
return isActiveElement(containerElement.shadowRoot.activeElement, containerElement);
}

View File

@ -1,36 +1,93 @@
import { AKElement } from "@goauthentik/elements/Base.js";
import { bound } from "@goauthentik/elements/decorators/bound";
import "@goauthentik/elements/forms/FormElement";
import { isActiveElement } from "@goauthentik/elements/utils/focus";
import { msg } from "@lit/localize";
import { html, nothing, render } from "lit";
import { customElement, property } from "lit/decorators.js";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Ref, createRef, ref } from "lit/directives/ref.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
/**
* A configuration object for the visibility states of the password input.
*/
interface VisibilityProps {
icon: string;
label: string;
}
/**
* Enum-like object for the visibility states of the password input.
*/
const Visibility = {
Reveal: {
icon: "fa-eye",
label: msg("Show password"),
},
Mask: {
icon: "fa-eye-slash",
label: msg("Hide password"),
},
} as const satisfies Record<string, VisibilityProps>;
@customElement("ak-flow-input-password")
export class InputPassword extends AKElement {
static get styles() {
return [PFBase, PFInputGroup, PFFormControl, PFButton];
}
//#region Properties
/**
* The ID of the input field.
*
* @attr
*/
@property({ type: String, attribute: "input-id" })
inputId = "ak-stage-password-input";
/**
* The name of the input field.
*
* @attr
*/
@property({ type: String })
name = "password";
/**
* The label for the input field.
*
* @attr
*/
@property({ type: String })
label = msg("Password");
/**
* The placeholder text for the input field.
*
* @attr
*/
@property({ type: String })
placeholder = msg("Please enter your password");
/**
* The initial value of the input field.
*
* @attr
*/
@property({ type: String, attribute: "prefill" })
passwordPrefill = "";
initialValue = "";
/**
* The errors for the input field.
*/
@property({ type: Object })
errors: Record<string, string> = {};
@ -41,113 +98,220 @@ export class InputPassword extends AKElement {
@property({ type: String })
invalid?: string;
/**
* Whether to allow the user to toggle the visibility of the password.
*
* @attr
*/
@property({ type: Boolean, attribute: "allow-show-password" })
allowShowPassword = false;
/**
* Whether the password is currently visible.
*
* @attr
*/
@property({ type: Boolean, attribute: "password-visible" })
passwordVisible = false;
/**
* Automatically grab focus after rendering.
*
* @attr
*/
@property({ type: Boolean, attribute: "grab-focus" })
grabFocus = false;
timer?: number;
//#endregion
input?: HTMLInputElement;
//#region Refs
cleanup(): void {
if (this.timer) {
console.debug("authentik/stages/password: cleared focus timer");
window.clearInterval(this.timer);
this.timer = undefined;
inputRef: Ref<HTMLInputElement> = createRef();
toggleVisibilityRef: Ref<HTMLButtonElement> = createRef();
//#endregion
//#region State
/**
* Whether the caps lock key is enabled.
*/
@state()
capsLock = false;
//#endregion
//#region Listeners
/**
* Toggle the visibility of the password field.
*
* Directly affects the DOM, so no `.requestUpdate()` required. Effect is immediately visible.
*
* @param event The event that triggered the visibility toggle.
*/
@bound
togglePasswordVisibility(event?: PointerEvent) {
event?.stopPropagation();
event?.preventDefault();
const input = this.inputRef.value;
if (!input) {
console.warn("ak-flow-password-input: unable to identify input field");
return;
}
input.type = input.type === "password" ? "text" : "password";
this.syncVisibilityToggle(input);
}
// Must support both older browsers and shadyDom; we'll keep using this in-line, but it'll still
// be in the scope of the parent element, not an independent shadowDOM.
/**
* Listen for key events, synchronizing the caps lock indicators.
*/
@bound
capsLockListener(event: KeyboardEvent) {
this.capsLock = event.getModifierState("CapsLock");
}
//#region Lifecycle
/**
* Interval ID for the focus observer.
*
* @see {@linkcode observeInputFocus}
*/
inputFocusIntervalID?: ReturnType<typeof setInterval>;
/**
* Periodically attempt to focus the input field until it is focused.
*
* This is some-what of a crude way to get autofocus, but in most cases
* the `autofocus` attribute isn't enough, due to timing within shadow doms and such.
*/
observeInputFocus(): void {
if (!this.grabFocus) {
return;
}
this.inputFocusIntervalID = setInterval(() => {
const input = this.inputRef.value;
if (!input) return;
if (isActiveElement(input, document.activeElement)) {
console.debug("authentik/stages/password: cleared focus observer");
clearInterval(this.inputFocusIntervalID);
}
input.focus();
}, 10);
console.debug("authentik/stages/password: started focus observer");
}
connectedCallback() {
super.connectedCallback();
this.observeInputFocus();
addEventListener("keydown", this.capsLockListener);
addEventListener("keyup", this.capsLockListener);
}
disconnectedCallback() {
if (this.inputFocusIntervalID) {
clearInterval(this.inputFocusIntervalID);
}
super.disconnectedCallback();
removeEventListener("keydown", this.capsLockListener);
removeEventListener("keyup", this.capsLockListener);
}
//#endregion
//#region Render
/**
* Create the render root for the password input.
*
* Must support both older browsers and shadyDom; we'll keep using this in-line,
* but it'll still be in the scope of the parent element, not an independent shadowDOM.
*/
createRenderRoot() {
return this;
}
// State is saved in the DOM, and read from the DOM. Directly affects the DOM,
// so no `.requestUpdate()` required. Effect is immediately visible.
togglePasswordVisibility(ev: PointerEvent) {
const passwordField = this.renderRoot.querySelector(`#${this.inputId}`) as HTMLInputElement;
ev.stopPropagation();
ev.preventDefault();
/**
* Render the password visibility toggle button.
*
* In the unlikely event that we want to make "show password" the _default_ behavior,
* this effect handler is broken out into its own method.
*
* The current behavior in the main {@linkcode render} method assumes the field is of type "password."
*
* To have this effect, er, take effect, call it in an {@linkcode updated} method.
*
* @param input The password field to render the visibility features for.
*/
syncVisibilityToggle(input: HTMLInputElement | undefined = this.inputRef.value): void {
if (!input) return;
if (!passwordField) {
throw new Error("ak-flow-password-input: unable to identify input field");
}
const toggleElement = this.toggleVisibilityRef.value;
passwordField.type = passwordField.type === "password" ? "text" : "password";
this.renderPasswordVisibilityFeatures(passwordField);
}
if (!toggleElement) return;
// In the unlikely event that we want to make "show password" the _default_ behavior, this
// effect handler is broken out into its own method. The current behavior in the main
// `.render()` method assumes the field is of type "password." To have this effect, er, take
// effect, call it in an `.updated()` method.
renderPasswordVisibilityFeatures(passwordField: HTMLInputElement) {
const toggleId = `#${this.inputId}-visibility-toggle`;
const visibilityToggle = this.renderRoot.querySelector(toggleId) as HTMLButtonElement;
if (!visibilityToggle) {
return;
}
const show = passwordField.type === "password";
visibilityToggle?.setAttribute(
const masked = input.type === "password";
toggleElement.setAttribute(
"aria-label",
show ? msg("Show password") : msg("Hide password"),
);
visibilityToggle?.querySelector("i")?.remove();
render(
show
? html`<i class="fas fa-eye" aria-hidden="true"></i>`
: html`<i class="fas fa-eye-slash" aria-hidden="true"></i>`,
visibilityToggle,
masked ? Visibility.Reveal.label : Visibility.Mask.label,
);
const iconElement = toggleElement.querySelector("i")!;
iconElement.classList.remove(Visibility.Mask.icon, Visibility.Reveal.icon);
iconElement.classList.add(masked ? Visibility.Reveal.icon : Visibility.Mask.icon);
}
renderInput(): HTMLInputElement {
this.input = document.createElement("input");
this.input.id = `${this.inputId}`;
this.input.type = "password";
this.input.name = this.name;
this.input.placeholder = this.placeholder;
this.input.autofocus = this.grabFocus;
this.input.autocomplete = "current-password";
this.input.classList.add("pf-c-form-control");
this.input.required = true;
this.input.value = this.passwordPrefill ?? "";
if (this.invalid) {
this.input.setAttribute("aria-invalid", this.invalid);
}
// This is somewhat of a crude way to get autofocus, but in most cases the `autofocus` attribute
// isn't enough, due to timing within shadow doms and such.
renderVisibilityToggle() {
if (!this.allowShowPassword) return nothing;
if (this.grabFocus) {
this.timer = window.setInterval(() => {
if (!this.input) {
return;
}
// Because activeElement behaves differently with shadow dom
// we need to recursively check
const rootEl = document.activeElement;
const isActive = (el: Element | null): boolean => {
if (!rootEl) return false;
if (!("shadowRoot" in rootEl)) return false;
if (rootEl.shadowRoot === null) return false;
if (rootEl.shadowRoot.activeElement === el) return true;
return isActive(rootEl.shadowRoot.activeElement);
};
if (isActive(this.input)) {
this.cleanup();
}
this.input.focus();
}, 10);
console.debug("authentik/stages/password: started focus timer");
}
return this.input;
const { label, icon } = this.passwordVisible ? Visibility.Mask : Visibility.Reveal;
return html`<button
${ref(this.toggleVisibilityRef)}
aria-label=${label}
@click=${this.togglePasswordVisibility}
class="pf-c-button pf-m-control"
type="button"
>
<i class="fas ${icon}" aria-hidden="true"></i>
</button>`;
}
renderHelperText() {
if (!this.capsLock) return nothing;
return html`<div
class="pf-c-form__helper-text"
id="helper-text-form-caps-lock-helper"
aria-live="polite"
>
<div class="pf-c-helper-text">
<div class="pf-c-helper-text__item pf-m-warning">
<span class="pf-c-helper-text__item-icon">
<i class="fas fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
</span>
<span class="pf-c-helper-text__item-text">${msg("Caps Lock is enabled.")}</span>
</div>
</div>
</div>`;
}
render() {
@ -157,22 +321,34 @@ export class InputPassword extends AKElement {
class="pf-c-form__group"
.errors=${this.errors}
>
<div class="pf-c-input-group">
${this.renderInput()}
${this.allowShowPassword
? html` <button
id="${this.inputId}-visibility-toggle"
class="pf-c-button pf-m-control ak-stage-password-toggle-visibility"
type="button"
aria-label=${msg("Show password")}
@click=${(ev: PointerEvent) => this.togglePasswordVisibility(ev)}
>
<i class="fas fa-eye" aria-hidden="true"></i>
</button>`
: nothing}
<div class="pf-c-form__group-control">
<div class="pf-c-input-group">
<input
type=${this.passwordVisible ? "text" : "password"}
id=${this.inputId}
name=${this.name}
placeholder=${this.placeholder}
autocomplete="current-password"
class="${classMap({
"pf-c-form-control": true,
"pf-m-icon": true,
"pf-m-caps-lock": this.capsLock,
})}"
required
aria-invalid=${ifDefined(this.invalid)}
value=${this.initialValue}
${ref(this.inputRef)}
/>
${this.renderVisibilityToggle()}
</div>
${this.renderHelperText()}
</div>
</ak-form-element>`;
}
//#endregion
}
declare global {

View File

@ -3,7 +3,7 @@ import "@goauthentik/elements/forms/FormElement";
import { BaseDeviceStage } from "@goauthentik/flow/stages/authenticator_validate/base";
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
import { msg } from "@lit/localize";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
@ -35,7 +35,7 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
switch (this.deviceChallenge?.deviceClass) {
case DeviceClassesEnum.Email: {
const email = this.deviceChallenge.challenge?.email;
return msg(`A code has been sent to you via email${email ? ` ${email}` : ""}`);
return msg(str`A code has been sent to you via email${email ? ` ${email}` : ""}`);
}
case DeviceClassesEnum.Sms:
return msg("A code has been sent to you via SMS.");
@ -70,52 +70,57 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
return html`<ak-empty-state loading> </ak-empty-state>`;
}
return html`<div class="pf-c-login__main-body">
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
${this.renderUserInfo()}
<div class="icon-description">
<i class="fa ${this.deviceIcon()}" aria-hidden="true"></i>
<p>${this.deviceMessage()}</p>
</div>
<ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? msg("Static token")
: msg("Authentication code")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<!-- @ts-ignore -->
<input
type="text"
name="code"
inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? "text"
: "numeric"}"
pattern="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? "[0-9a-zA-Z]*"
: "[0-9]*"}"
placeholder="${msg("Please enter your code")}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
${this.renderUserInfo()}
<div class="icon-description">
<i class="fa ${this.deviceIcon()}" aria-hidden="true"></i>
<p>${this.deviceMessage()}</p>
</div>
<ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? msg("Static token")
: msg("Authentication code")}"
required
/>
</ak-form-element>
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<!-- @ts-ignore -->
<input
type="text"
name="code"
inputmode="${this.deviceChallenge?.deviceClass ===
DeviceClassesEnum.Static
? "text"
: "numeric"}"
pattern="${this.deviceChallenge?.deviceClass ===
DeviceClassesEnum.Static
? "[0-9a-zA-Z]*"
: "[0-9]*"}"
placeholder="${msg("Please enter your code")}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
required
/>
</ak-form-element>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}
</button>
${this.renderReturnToDevicePicker()}
</div>
</form>
</div>`;
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")}
</button>
${this.renderReturnToDevicePicker()}
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -161,7 +161,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
super.disconnectedCallback();
}
get captchaDocumentContainer() {
get captchaDocumentContainer(): HTMLDivElement {
if (this._captchaDocumentContainer) {
return this._captchaDocumentContainer;
}
@ -170,7 +170,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
return this._captchaDocumentContainer;
}
get captchaFrame() {
get captchaFrame(): HTMLIFrameElement {
if (this._captchaFrame) {
return this._captchaFrame;
}
@ -326,7 +326,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
.exhaustive();
}
updated(changedProperties: PropertyValues<this>) {
firstUpdated(changedProperties: PropertyValues<this>) {
if (!(changedProperties.has("challenge") && this.challenge !== undefined)) {
return;
}

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