Compare commits

...

59 Commits

Author SHA1 Message Date
e095e9f694 release: 2023.10.7 2024-01-29 17:44:48 +01:00
10e311534f security: fix CVE-2024-23647 (cherry-pick #8345) (#8347)
security: fix CVE-2024-23647 (#8345)

* security: fix CVE-2024-23647



* add tests



* add website



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-01-29 17:42:04 +01:00
46fdb45273 root: fix redis config not being updated to match previous change
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-01-29 14:02:18 +01:00
6d4125cb90 root: fix listen trusted_proxy_cidrs config loading from environment (#8075)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	go.mod
#	go.sum
#	internal/config/struct.go
2024-01-25 14:51:25 +01:00
bc83176962 stages/authenticator_validate: use friendly_name for stage selector when enrolling (cherry-pick #8255) (#8256)
stages/authenticator_validate: use friendly_name for stage selector when enrolling (#8255)

* stages/authenticator_validate: use friendly_name for stage selector when enrolling



* fix tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-01-22 16:27:46 +01:00
0fa8432b72 rbac: fix invitations listing with restricted permissions (cherry-pick #8227) (#8229)
rbac: fix invitations listing with restricted permissions (#8227)

* rbac: fix missing permission definition for list



* core: fix users's system_permissions not including role permissions



* core: don't require permissions for users/me/



* web/admin: catch error when listing stages on invitation page fails



* Revert "rbac: fix missing permission definition for list"

This reverts commit fd7572e699.

* Revert "core: don't require permissions for users/me/"

This reverts commit 9df0dbda8a.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-01-18 23:24:58 +01:00
bb9a524b53 sources/oauth: fix URLs being overwritten by OIDC urls (cherry-pick #8147) (#8156)
sources/oauth: fix URLs being overwritten by OIDC urls (#8147)

* sources/oauth: fix URLs being overwritten by OIDC urls



* fix tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-01-13 16:37:47 +01:00
d31c05625b sources/oauth: fix azure_ad user_id and add test and fallback (cherry-pick #8146) (#8152) 2024-01-12 21:01:24 +01:00
399223b770 web/flows: fix icon for generic oauth source with dark theme (cherry-pick #8148) (#8151) 2024-01-12 21:01:11 +01:00
19197d3f9b sources/oauth: revert azure_ad profile URL change (cherry-pick #8139) (#8141)
sources/oauth: revert azure_ad profile URL change (#8139)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-01-12 16:21:59 +01:00
1cd000dfe2 release: 2023.10.6 2024-01-09 18:50:48 +01:00
00ae97944a providers/oauth2: fix CVE-2024-21637 (cherry-pick #8104) (#8105)
* providers/oauth2: fix CVE-2024-21637 (#8104)

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

* update changelog

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-01-09 18:32:03 +01:00
9f3ccfb7c7 web/flows: fix device picker incorrect foreground color (cherry-pick #8067) (#8069)
web/flows: fix device picker incorrect foreground color (#8067)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-01-05 15:31:08 +01:00
9ed9c39ac8 rbac: fix error when looking up permissions for now uninstalled apps (cherry-pick #8068) (#8070)
rbac: fix error when looking up permissions for now uninstalled apps (#8068)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-01-05 15:30:59 +01:00
30b6eeee9f outposts: disable deployment and secret reconciler for embedded outpost in code instead of in config (cherry-pick #8021) (#8024)
outposts: disable deployment and secret reconciler for embedded outpost in code instead of in config (#8021)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-30 21:40:54 +01:00
afe2621783 providers/proxy: use access token (cherry-pick #8022) (#8023)
providers/proxy: use access token (#8022)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-30 18:30:42 +01:00
8b12c6a01a outposts: fix Outpost reconcile not re-assigning managed attribute (cherry-pick #8014) (#8020)
outposts: fix Outpost reconcile not re-assigning managed attribute (#8014)

* outposts: fix Outpost reconcile not re-assigning managed attribute



* rework reconcile to find both name and managed outpost



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-30 15:37:36 +01:00
f63adfed96 core: fix PropertyMapping context not being available in request context
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-12-23 03:05:29 +01:00
9c8fec21cf providers/oauth2: remember session_id from initial token (cherry-pick #7976) (#7977)
providers/oauth2: remember session_id from initial token (#7976)

* providers/oauth2: remember session_id original token was created with for future access/refresh tokens



* providers/proxy: use hashed session as `sid`



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-23 01:20:48 +01:00
4776d2bcc5 sources/oauth: fix missing get_user_id for OIDC-like sources (Azure AD) (#7970)
* lib: add debug requests session that shows all sent requests

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

* sources/oauth: fix missing get_user_id for OIDC-like OAuth Sources

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	authentik/lib/utils/http.py
2023-12-22 00:13:42 +01:00
a15a040362 release: 2023.10.5 2023-12-21 14:18:36 +01:00
fcd6dc1d60 events: fix lint (#7700)
* events: fix lint

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

* test without explicit poetry env use?

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

* delete previous poetry env

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

* prevent invalid cached poetry envs

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

* run test-from-stable as matrix and make required

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

* fix missing postgres version

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

* sigh

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

* idk

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	.github/actions/setup/action.yml
#	.github/workflows/ci-main.yml
2023-12-19 18:40:14 +01:00
acc3b59869 events: add better fallback for sanitize_item to ensure everything can be saved as JSON (cherry-pick #7694) (#7937)
events: add better fallback for sanitize_item to ensure everything can be saved as JSON (#7694)

* events: fix events sanitizing not handling all types



* remove some leftover prints



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-19 18:31:20 +01:00
d9d5ac10e6 events: include user agent in events (cherry-pick #7693) (#7938)
events: include user agent in events (#7693)

* events: include user agent in events



* fix tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-19 18:31:06 +01:00
750669dcab stages/email: improve error handling for incorrect template syntax (cherry-pick #7758) (#7936)
stages/email: improve error handling for incorrect template syntax (#7758)

* stages/email: improve error handling for incorrect template syntax



* add tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-19 18:30:56 +01:00
88a3eed67e root: don't show warning when app has no URLs to import (cherry-pick #7765) (#7935)
root: don't show warning when app has no URLs to import (#7765)

Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-19 18:30:49 +01:00
6c214fffc4 blueprints: improve file change handler (cherry-pick #7813) (#7934)
blueprints: improve file change handler (#7813)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-19 18:30:37 +01:00
70100fc105 web/user: fix search not updating app (cherry-pick #7825) (#7933)
web/user: fix search not updating app (#7825)

web/user: fix app not updating

so when using two classes in a classMap directive, the update fails (basically saying that each class must be separated), however this error only shows when directly calling requestUpdate and is swallowed somewhere when relying on the default render cycle

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-19 18:30:23 +01:00
3c1163fabd root: Fix cache related image build issues (cherry-pick #7831) (#7932)
Fix cache related image build issues

Co-authored-by: Philipp Kolberg <philipp.kolberg@t-online.de>
2023-12-19 18:30:15 +01:00
539e8242ff web: fix overflow glitch on ak-page-header (cherry-pick #7883) (#7931)
web: fix overflow glitch on ak-page-header (#7883)

By adding 'grow' but not 'shrink' to the header section, the page was allowed to allocate
as much width as was available when the window opened, but not allowed to resize the width
if it was pushed closed by zoom, page resize, or summon sidebar.

This commit adds 'shrink' to the capabilities of the header.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2023-12-19 18:30:04 +01:00
2648333590 providers/scim: change familyName default (cherry-pick #7904) (#7930)
providers/scim: change familyName default (#7904)

* Update providers-scim.yaml



* fix: add formatted to match the givenName & familyName



* fix, update tests



---------

Signed-off-by: Antoine <antoine+github@jiveoff.fr>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
Co-authored-by: Antoine <antoine+github@jiveoff.fr>
2023-12-19 18:29:55 +01:00
fe828ef993 tests: fix flaky tests (cherry-pick #7676) (#7939)
tests: fix flaky tests (#7676)

* tests: fix flaky tests



* make test-from-stable use actual latest version



* fix checkout



* remove hardcoded seed



* ignore tests for now i guess idk



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-12-19 18:29:44 +01:00
29a6530742 web: dark/light theme fixes (#7872)
* web: fix css for user tree-view

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

* fix unrelated things

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

* fix header button colors

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

* fix missing fallback not showing default slant

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

* move global theme-dark css to only use for SSR rendered pages

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	.github/workflows/ci-main.yml
#	web/xliff/fr.xlf
2023-12-19 18:18:19 +01:00
a6b9274c4f web/admin: always show oidc well-known URL fields when they're set (#7560)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/xliff/de.xlf
#	web/xliff/en.xlf
#	web/xliff/es.xlf
#	web/xliff/fr.xlf
#	web/xliff/pl.xlf
#	web/xliff/pseudo-LOCALE.xlf
#	web/xliff/tr.xlf
#	web/xliff/zh-Hans.xlf
#	web/xliff/zh-Hant.xlf
#	web/xliff/zh_TW.xlf
2023-12-19 18:10:40 +01:00
a2a67161ac release: 2023.10.4 2023-11-21 18:38:24 +01:00
2e8263a99b web: fix locale
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-11-21 18:20:41 +01:00
6b9afed21f security: fix CVE-2023-48228 (cherry-pick #7666) (#7668)
security: fix CVE-2023-48228 (#7666)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-21 18:13:54 +01:00
1eb1f4e0b8 web/admin: fix admins not able to delete MFA devices (#7660)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/xliff/zh-Hans.xlf
2023-11-21 15:24:37 +01:00
7c3d60ec3a events: don't update internal service accounts unless needed (cherry-pick #7611) (#7640)
events: stop spam (#7611)

* events: don't log updates to internal service accounts



* dont log reputation updates



* don't actually ignore things, stop updating outpost user when not required



* prevent updating internal service account users



* fix setattr call



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-20 19:43:30 +01:00
a494c6b6e8 root: specify node and python versions in respective config files, deduplicate in CI (#7620)
* root: specify node and python versions in respective config files, deduplicate in CI

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

* fix engines missing for wdio

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

* bump setup python version

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

* actually don't bump a bunch of things

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	poetry.lock
#	website/package.json
2023-11-19 00:35:55 +01:00
6604d3577f core: bump golang from 1.21.3-bookworm to 1.21.4-bookworm (cherry-pick #7483) (#7622)
core: bump golang from 1.21.3-bookworm to 1.21.4-bookworm

Bumps golang from 1.21.3-bookworm to 1.21.4-bookworm.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-19 00:33:07 +01:00
f8bfa7e16a ci: fix permissions for release pipeline to publish binaries (cherry-pick #7512) (#7621)
ci: fix permissions for release pipeline to publish binaries (#7512)

ci: fix permissions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-19 00:31:20 +01:00
ea6cf6eabf events: fix missing model_* events when not directly authenticated (cherry-pick #7588) (#7597)
events: fix missing model_* events when not directly authenticated (#7588)

* events: fix missing model_* events when not directly authenticated



* defer accessing database



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-16 12:59:41 +01:00
769ce3ce7b providers/scim: fix missing schemas attribute for User and Group (cherry-pick #7477) (#7596)
providers/scim: fix missing schemas attribute for User and Group (#7477)

* providers/scim: fix missing schemas attribute for User and Group



* make things actually work



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-16 12:06:01 +01:00
3891fb3fa8 events: sanitize functions (cherry-pick #7587) (#7589)
events: sanitize functions (#7587)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-15 23:24:13 +01:00
41eb965350 stages/email: use uuid for email confirmation token instead of username (cherry-pick #7581) (#7584)
stages/email: use uuid for email confirmation token instead of username (#7581)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-15 21:57:05 +01:00
8d95612287 providers/proxy: Fix duplicate cookies when using file system store. (cherry-pick #7541) (#7544)
providers/proxy: Fix duplicate cookies when using file system store. (#7541)

Fix duplicate cookies when using file system store.

Co-authored-by: thijs_a <thijs@thijsalders.nl>
2023-11-13 16:02:35 +01:00
82b5274b15 release: 2023.10.3 2023-11-09 18:37:22 +01:00
af56ce3d78 core: fix worker beat toggle inverted (cherry-pick #7508) (#7509)
core: fix worker beat toggle inverted (#7508)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-09 18:36:56 +01:00
f5c6e7aeb0 Web: bugfix: broken backchannel selector (cherry-pick #7480) (#7507)
Web: bugfix: broken backchannel selector (#7480)

* web: break circular dependency between AKElement & Interface.

This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.

I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.

* web: fix broken typescript references

This built... and then it didn't?  Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.

* web: rollback dependabot's upgrade of context

The most frustrating part of this is that I RAN THIS, dammit, with the updated
context and the current Wizard, and it finished the End-to-End tests without
complaint.

* web: bugfix: broken backchannel selector

There were two bugs here, both of them introduced by me because I didn't understand the
system well enough the first time through, and because I didn't test thoroughly enough.

The first is that I was calling the wrong confirmation code; the resulting syntax survived
because `confirm()` is actually a legitimate function call in the context of the DOM Window,
a legacy survivor similar to `alert()` but with a yes/no return value. Bleah.

The second is that the confirm code doesn't appear to pass back a dictionary with the
`{ items: Array<Provider> }` list, it passes back just the `items` as an Array.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2023-11-09 17:58:38 +01:00
3809400e93 events: fix gdpr compliance always running (cherry-pick #7491) (#7505)
events: fix gdpr compliance always running

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2023-11-09 17:57:25 +01:00
1def9865cf web/flows: attempt to fix bitwareden android compatibility (cherry-pick #7455) (#7457)
web/flows: attempt to fix bitwareden android compatibility (#7455)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-06 23:58:44 +01:00
3716298639 sources/oauth: fix patreon (cherry-pick #7454) (#7456)
sources/oauth: fix patreon (#7454)

* web/admin: add note for potentially confusing consumer key/secret



* sources/oauth: fix patreon default scopes



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-06 16:36:22 +01:00
c16317d7cf providers/proxy: fix closed redis client (cherry-pick #7385) (#7429)
providers/proxy: fix closed redis client (#7385)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-03 15:46:17 +01:00
bbb8fa8269 ci: explicitly give write permissions to packages (cherry-pick #7428) (#7430)
ci: explicitly give write permissions to packages (#7428)

* ci: explicitly give write permissions to packages



* run full CI on cherry-picks



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-03 15:46:00 +01:00
e4c251a178 web/admin: fix html error on oauth2 provider page (cherry-pick #7384) (#7424)
web/admin: fix html error on oauth2 provider page (#7384)

* web: break circular dependency between AKElement & Interface.

This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.

I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.

* web: fix broken typescript references

This built... and then it didn't?  Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.

* \# Details

Extra `>` symbol screwed up the reading of the rest of the component.  Unfortunately,
too many fields in an input are optional, so it was easy for this bug to bypass any
checks by the validators.  I should have caught it myself, though.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2023-11-03 13:17:26 +01:00
0fefd5f522 stages/email: fix duplicate querystring encoding (cherry-pick #7386) (#7425)
stages/email: fix duplicate querystring encoding (#7386)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-03 13:17:18 +01:00
88057db0b0 providers/oauth2: set auth_via for token and other endpoints (cherry-pick #7417) (#7427)
providers/oauth2: set auth_via for token and other endpoints (#7417)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2023-11-03 13:17:10 +01:00
91cb6c9beb root: Improve multi arch Docker image build speed (cherry-pick #7355) (#7426)
root: Improve multi arch Docker image build speed (#7355)

* Improve multi arch Docker image build speed

Use only host architecture for GeoIP database update and for Go cross-compilation

* Speedup Go multi-arch compilation for other images

* Speedup multi-arch ldap image build

Co-authored-by: Philipp Kolberg <39984529+PKizzle@users.noreply.github.com>
2023-11-03 13:16:54 +01:00
129 changed files with 1406 additions and 489 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2023.10.2
current_version = 2023.10.7
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -2,36 +2,39 @@ name: "Setup authentik testing environment"
description: "Setup authentik testing environment"
inputs:
postgresql_tag:
postgresql_version:
description: "Optional postgresql image tag"
default: "12"
runs:
using: "composite"
steps:
- name: Install poetry
- name: Install poetry & deps
shell: bash
run: |
pipx install poetry || true
sudo apt update
sudo apt install -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
sudo apt-get update
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
- name: Setup python and restore poetry
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version-file: 'pyproject.toml'
cache: "poetry"
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: "20"
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Setup go
uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
- name: Setup dependencies
shell: bash
run: |
export PSQL_TAG=${{ inputs.postgresql_tag }}
export PSQL_TAG=${{ inputs.postgresql_version }}
docker-compose -f .github/actions/setup/docker-compose.yml up -d
poetry env use python3.11
poetry install
cd web && npm ci
- name: Generate config

View File

@ -11,6 +11,7 @@ on:
pull_request:
branches:
- main
- version-*
env:
POSTGRES_DB: authentik
@ -47,25 +48,38 @@ jobs:
- name: run migrations
run: poetry run python -m lifecycle.migrate
test-migrations-from-stable:
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }}
runs-on: ubuntu-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
psql:
- 12-alpine
- 15-alpine
- 16-alpine
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup authentik env
uses: ./.github/actions/setup
with:
postgresql_version: ${{ matrix.psql }}
- name: checkout stable
run: |
# Delete all poetry envs
rm -rf /home/runner/.cache/pypoetry
# Copy current, latest config to local
cp authentik/lib/default.yml local.env.yml
cp -R .github ..
cp -R scripts ..
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
git checkout version/$(python -c "from authentik import __version__; print(__version__)")
rm -rf .github/ scripts/
mv ../.github ../scripts .
- name: Setup authentik env (ensure stable deps are installed)
uses: ./.github/actions/setup
with:
postgresql_version: ${{ matrix.psql }}
- name: run migrations to stable
run: poetry run python -m lifecycle.migrate
- name: checkout current code
@ -75,9 +89,13 @@ jobs:
git reset --hard HEAD
git clean -d -fx .
git checkout $GITHUB_SHA
# Delete previous poetry env
rm -rf $(poetry env info --path)
poetry install
- name: Setup authentik env (ensure latest deps are installed)
uses: ./.github/actions/setup
with:
postgresql_version: ${{ matrix.psql }}
- name: migrate to latest
run: poetry run python -m lifecycle.migrate
test-unittest:
@ -96,7 +114,7 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
with:
postgresql_tag: ${{ matrix.psql }}
postgresql_version: ${{ matrix.psql }}
- name: run unittest
run: |
poetry run make test
@ -185,6 +203,9 @@ jobs:
build:
needs: ci-core-mark
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
@ -235,6 +256,9 @@ jobs:
build-arm64:
needs: ci-core-mark
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
timeout-minutes: 120
steps:
- uses: actions/checkout@v4

View File

@ -9,6 +9,7 @@ on:
pull_request:
branches:
- main
- version-*
jobs:
lint-golint:
@ -65,6 +66,9 @@ jobs:
- ldap
- radius
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
steps:
- uses: actions/checkout@v4
with:
@ -126,7 +130,7 @@ jobs:
go-version-file: "go.mod"
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Generate API

View File

@ -9,6 +9,7 @@ on:
pull_request:
branches:
- main
- version-*
jobs:
lint-eslint:
@ -23,7 +24,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- working-directory: ${{ matrix.project }}/
@ -39,7 +40,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
@ -61,7 +62,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- working-directory: ${{ matrix.project }}/
@ -77,7 +78,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
@ -109,7 +110,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/

View File

@ -9,6 +9,7 @@ on:
pull_request:
branches:
- main
- version-*
jobs:
lint-prettier:
@ -17,7 +18,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/
@ -31,7 +32,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/
@ -52,7 +53,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/

View File

@ -6,6 +6,7 @@ on:
workflow_dispatch:
permissions:
# Needed to be able to push to the next branch
contents: write
jobs:

View File

@ -7,6 +7,9 @@ on:
jobs:
build-server:
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
@ -52,6 +55,9 @@ jobs:
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
build-outpost:
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
strategy:
fail-fast: false
matrix:
@ -106,6 +112,9 @@ jobs:
build-outpost-binary:
timeout-minutes: 120
runs-on: ubuntu-latest
permissions:
# Needed to upload binaries to the release
contents: write
strategy:
fail-fast: false
matrix:
@ -122,7 +131,7 @@ jobs:
go-version-file: "go.mod"
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Build web

View File

@ -6,8 +6,8 @@ on:
workflow_dispatch:
permissions:
# Needed to update issues and PRs
issues: write
pull-requests: write
jobs:
stale:

View File

@ -19,7 +19,7 @@ jobs:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@v4
with:
node-version: "20"
node-version-file: web/package.json
registry-url: "https://registry.npmjs.org"
- name: Generate API Client
run: make gen-client-ts

View File

@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/node:21 as website-builder
@ -7,7 +9,7 @@ WORKDIR /work/website
RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
--mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
--mount=type=cache,target=/root/.npm \
--mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./website /work/website/
@ -25,7 +27,7 @@ WORKDIR /work/web
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=cache,target=/root/.npm \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./web /work/web/
@ -35,7 +37,14 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build
# Stage 3: Build go proxy
FROM docker.io/golang:1.21.3-bookworm AS go-builder
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS go-builder
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ARG GOOS=$TARGETOS
ARG GOARCH=$TARGETARCH
WORKDIR /go/src/goauthentik.io
@ -55,12 +64,12 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
ENV CGO_ENABLED=0
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /go/authentik ./cmd/server
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
GOARM="${TARGETVARIANT#v}" go build -o /go/authentik ./cmd/server
# Stage 4: MaxMind GeoIP
FROM ghcr.io/maxmind/geoipupdate:v6.0 as geoip
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v6.0 as geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
ENV GEOIPUPDATE_VERBOSE="true"
@ -82,7 +91,9 @@ ENV VENV_PATH="/ak-root/venv" \
POETRY_VIRTUALENVS_CREATE=false \
PATH="/ak-root/venv/bin:$PATH"
RUN --mount=type=cache,target=/var/cache/apt \
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
apt-get update && \
# Required for installing pip packages
apt-get install -y --no-install-recommends build-essential pkg-config libxmlsec1-dev zlib1g-dev libpq-dev

View File

@ -2,7 +2,7 @@
from os import environ
from typing import Optional
__version__ = "2023.10.2"
__version__ = "2023.10.7"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -21,7 +21,9 @@ _other_urls = []
for _authentik_app in get_apps():
try:
api_urls = import_module(f"{_authentik_app.name}.urls")
except (ModuleNotFoundError, ImportError) as exc:
except ModuleNotFoundError:
continue
except ImportError as exc:
LOGGER.warning("Could not import app's URLs", app_name=_authentik_app.name, exc=exc)
continue
if not hasattr(api_urls, "api_urlpatterns"):

View File

@ -40,7 +40,7 @@ class ManagedAppConfig(AppConfig):
meth()
self._logger.debug("Successfully reconciled", name=name)
except (DatabaseError, ProgrammingError, InternalError) as exc:
self._logger.debug("Failed to run reconcile", name=name, exc=exc)
self._logger.warning("Failed to run reconcile", name=name, exc=exc)
class AuthentikBlueprintsConfig(ManagedAppConfig):

View File

@ -75,13 +75,13 @@ class BlueprintEventHandler(FileSystemEventHandler):
return
if event.is_directory:
return
root = Path(CONFIG.get("blueprints_dir")).absolute()
path = Path(event.src_path).absolute()
rel_path = str(path.relative_to(root))
if isinstance(event, FileCreatedEvent):
LOGGER.debug("new blueprint file created, starting discovery")
blueprints_discovery.delay()
LOGGER.debug("new blueprint file created, starting discovery", path=rel_path)
blueprints_discovery.delay(rel_path)
if isinstance(event, FileModifiedEvent):
path = Path(event.src_path)
root = Path(CONFIG.get("blueprints_dir")).absolute()
rel_path = str(path.relative_to(root))
for instance in BlueprintInstance.objects.filter(path=rel_path, enabled=True):
LOGGER.debug("modified blueprint file, starting apply", instance=instance)
apply_blueprint.delay(instance.pk.hex)
@ -98,39 +98,32 @@ def blueprints_find_dict():
return blueprints
def blueprints_find():
def blueprints_find() -> list[BlueprintFile]:
"""Find blueprints and return valid ones"""
blueprints = []
root = Path(CONFIG.get("blueprints_dir"))
for path in root.rglob("**/*.yaml"):
rel_path = path.relative_to(root)
# Check if any part in the path starts with a dot and assume a hidden file
if any(part for part in path.parts if part.startswith(".")):
continue
LOGGER.debug("found blueprint", path=str(path))
with open(path, "r", encoding="utf-8") as blueprint_file:
try:
raw_blueprint = load(blueprint_file.read(), BlueprintLoader)
except YAMLError as exc:
raw_blueprint = None
LOGGER.warning("failed to parse blueprint", exc=exc, path=str(path))
LOGGER.warning("failed to parse blueprint", exc=exc, path=str(rel_path))
if not raw_blueprint:
continue
metadata = raw_blueprint.get("metadata", None)
version = raw_blueprint.get("version", 1)
if version != 1:
LOGGER.warning("invalid blueprint version", version=version, path=str(path))
LOGGER.warning("invalid blueprint version", version=version, path=str(rel_path))
continue
file_hash = sha512(path.read_bytes()).hexdigest()
blueprint = BlueprintFile(
str(path.relative_to(root)), version, file_hash, int(path.stat().st_mtime)
)
blueprint = BlueprintFile(str(rel_path), version, file_hash, int(path.stat().st_mtime))
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
blueprints.append(blueprint)
LOGGER.debug(
"parsed & loaded blueprint",
hash=file_hash,
path=str(path),
)
return blueprints
@ -138,10 +131,12 @@ def blueprints_find():
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
)
@prefill_task
def blueprints_discovery(self: MonitoredTask):
def blueprints_discovery(self: MonitoredTask, path: Optional[str] = None):
"""Find blueprints and check if they need to be created in the database"""
count = 0
for blueprint in blueprints_find():
if path and blueprint.path != path:
continue
check_blueprint_v1_file(blueprint)
count += 1
self.set_status(
@ -171,7 +166,11 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
metadata={},
)
instance.save()
LOGGER.info(
"Creating new blueprint instance from file", instance=instance, path=instance.path
)
if instance.last_applied_hash != blueprint.hash:
LOGGER.info("Applying blueprint due to changed file", instance=instance, path=instance.path)
apply_blueprint.delay(str(instance.pk))

View File

@ -38,7 +38,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
managed = ReadOnlyField()
component = SerializerMethodField()
icon = ReadOnlyField(source="get_icon")
icon = ReadOnlyField(source="icon_url")
def get_component(self, obj: Source) -> str:
"""Get object component so that we know how to edit the object"""

View File

@ -171,6 +171,11 @@ class UserSerializer(ModelSerializer):
raise ValidationError("Setting a user to internal service account is not allowed.")
return user_type
def validate(self, attrs: dict) -> dict:
if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
raise ValidationError("Can't modify internal service account users")
return super().validate(attrs)
class Meta:
model = User
fields = [
@ -228,9 +233,9 @@ class UserSelfSerializer(ModelSerializer):
def get_system_permissions(self, user: User) -> list[str]:
"""Get all system permissions assigned to the user"""
return list(
user.user_permissions.filter(
content_type__app_label="authentik_rbac", content_type__model="systempermission"
).values_list("codename", flat=True)
x.split(".", maxsplit=1)[1]
for x in user.get_all_permissions()
if x.startswith("authentik_rbac")
)
class Meta:

View File

@ -44,6 +44,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
if request:
req.http_request = request
self._context["request"] = req
req.context.update(**kwargs)
self._context.update(**kwargs)
self.dry_run = dry_run

View File

@ -17,9 +17,15 @@ class Command(BaseCommand):
"""Run worker"""
def add_arguments(self, parser):
parser.add_argument("-b", "--beat", action="store_true")
parser.add_argument(
"-b",
"--beat",
action="store_false",
help="When set, this worker will _not_ run Beat (scheduled) tasks",
)
def handle(self, **options):
LOGGER.debug("Celery options", **options)
close_old_connections()
if CONFIG.get_bool("remote_debug"):
import debugpy

View File

@ -13,7 +13,6 @@
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
<script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
<script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>

View File

@ -6,6 +6,7 @@
{% block head_before %}
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}
{% endblock %}

View File

@ -27,6 +27,7 @@ from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMGroup, SCIMUser
from authentik.stages.authenticator_static.models import StaticToken
@ -52,11 +53,13 @@ IGNORED_MODELS = (
RefreshToken,
SCIMUser,
SCIMGroup,
Reputation,
)
def should_log_model(model: Model) -> bool:
"""Return true if operation on `model` should be logged"""
# Check for silk by string so this comparison doesn't fail when silk isn't installed
if model.__module__.startswith("silk"):
return False
return model.__class__ not in IGNORED_MODELS
@ -93,21 +96,30 @@ class AuditMiddleware:
of models"""
get_response: Callable[[HttpRequest], HttpResponse]
anonymous_user: User = None
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def _ensure_fallback_user(self):
"""Defer fetching anonymous user until we have to"""
if self.anonymous_user:
return
from guardian.shortcuts import get_anonymous_user
self.anonymous_user = get_anonymous_user()
def connect(self, request: HttpRequest):
"""Connect signal for automatic logging"""
if not hasattr(request, "user"):
return
if not getattr(request.user, "is_authenticated", False):
return
self._ensure_fallback_user()
user = getattr(request, "user", self.anonymous_user)
if not user.is_authenticated:
user = self.anonymous_user
if not hasattr(request, "request_id"):
return
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
m2m_changed_handler = partial(self.m2m_changed_handler, user=request.user, request=request)
post_save_handler = partial(self.post_save_handler, user=user, request=request)
pre_delete_handler = partial(self.pre_delete_handler, user=user, request=request)
m2m_changed_handler = partial(self.m2m_changed_handler, user=user, request=request)
post_save.connect(
post_save_handler,
dispatch_uid=request.request_id,

View File

@ -217,6 +217,7 @@ class Event(SerializerModel, ExpiringModel):
"path": request.path,
"method": request.method,
"args": cleanse_dict(QueryDict(request.META.get("QUERY_STRING", ""))),
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
}
# Special case for events created during flow execution
# since they keep the http query within a wrapped query

View File

@ -13,6 +13,7 @@ from authentik.events.tasks import event_notification_handler, gdpr_cleanup
from authentik.flows.models import Stage
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.config import CONFIG
from authentik.stages.invitation.models import Invitation
from authentik.stages.invitation.signals import invitation_used
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
@ -92,4 +93,5 @@ def event_post_save_notification(sender, instance: Event, **_):
@receiver(pre_delete, sender=User)
def event_user_pre_delete_cleanup(sender, instance: User, **_):
"""If gdpr_compliance is enabled, remove all the user's events"""
gdpr_cleanup.delay(instance.pk)
if CONFIG.get_bool("gdpr_compliance", True):
gdpr_cleanup.delay(instance.pk)

View File

@ -53,7 +53,15 @@ class TestEvents(TestCase):
"""Test plain from_http"""
event = Event.new("unittest").from_http(self.factory.get("/"))
self.assertEqual(
event.context, {"http_request": {"args": {}, "method": "GET", "path": "/"}}
event.context,
{
"http_request": {
"args": {},
"method": "GET",
"path": "/",
"user_agent": "",
}
},
)
def test_from_http_clean_querystring(self):
@ -67,6 +75,7 @@ class TestEvents(TestCase):
"args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
"method": "GET",
"path": "/",
"user_agent": "",
}
},
)
@ -83,6 +92,7 @@ class TestEvents(TestCase):
"args": {"token": SafeExceptionReporterFilter.cleansed_substitute},
"method": "GET",
"path": "/",
"user_agent": "",
}
},
)

View File

@ -5,12 +5,13 @@ from dataclasses import asdict, is_dataclass
from datetime import date, datetime, time, timedelta
from enum import Enum
from pathlib import Path
from types import GeneratorType
from types import GeneratorType, NoneType
from typing import Any, Optional
from uuid import UUID
from django.contrib.auth.models import AnonymousUser
from django.core.handlers.wsgi import WSGIRequest
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.db.models.base import Model
from django.http.request import HttpRequest
@ -153,7 +154,20 @@ def sanitize_item(value: Any) -> Any:
return value.isoformat()
if isinstance(value, timedelta):
return str(value.total_seconds())
return value
if callable(value):
return {
"type": "callable",
"name": value.__name__,
"module": value.__module__,
}
# List taken from the stdlib's JSON encoder (_make_iterencode, encoder.py:415)
if isinstance(value, (bool, int, float, NoneType, list, tuple, dict)):
return value
try:
return DjangoJSONEncoder().default(value)
except TypeError:
return str(value)
return str(value)
def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:

View File

@ -167,7 +167,11 @@ class ChallengeStageView(StageView):
stage_type=self.__class__.__name__, method="get_challenge"
).time(),
):
challenge = self.get_challenge(*args, **kwargs)
try:
challenge = self.get_challenge(*args, **kwargs)
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
with Hub.current.start_span(
op="authentik.flow.stage._get_challenge",
description=self.__class__.__name__,

View File

@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, is_dict
from authentik.core.models import Provider
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
from authentik.outposts.models import (
Outpost,
OutpostConfig,
@ -47,6 +47,16 @@ class OutpostSerializer(ModelSerializer):
source="service_connection", read_only=True
)
def validate_name(self, name: str) -> str:
"""Validate name (especially for embedded outpost)"""
if not self.instance:
return name
if self.instance.managed == MANAGED_OUTPOST and name != MANAGED_OUTPOST_NAME:
raise ValidationError("Embedded outpost's name cannot be changed")
if self.instance.name == MANAGED_OUTPOST_NAME:
self.instance.managed = MANAGED_OUTPOST
return name
def validate_providers(self, providers: list[Provider]) -> list[Provider]:
"""Check that all providers match the type of the outpost"""
type_map = {

View File

@ -15,6 +15,7 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
["outpost", "uid", "version"],
)
MANAGED_OUTPOST = "goauthentik.io/outposts/embedded"
MANAGED_OUTPOST_NAME = "authentik Embedded Outpost"
class AuthentikOutpostConfig(ManagedAppConfig):
@ -35,14 +36,17 @@ class AuthentikOutpostConfig(ManagedAppConfig):
DockerServiceConnection,
KubernetesServiceConnection,
Outpost,
OutpostConfig,
OutpostType,
)
if outpost := Outpost.objects.filter(name=MANAGED_OUTPOST_NAME, managed="").first():
outpost.managed = MANAGED_OUTPOST
outpost.save()
return
outpost, updated = Outpost.objects.update_or_create(
defaults={
"name": "authentik Embedded Outpost",
"type": OutpostType.PROXY,
"name": MANAGED_OUTPOST_NAME,
},
managed=MANAGED_OUTPOST,
)
@ -51,10 +55,4 @@ class AuthentikOutpostConfig(ManagedAppConfig):
outpost.service_connection = KubernetesServiceConnection.objects.first()
elif DockerServiceConnection.objects.exists():
outpost.service_connection = DockerServiceConnection.objects.first()
outpost.config = OutpostConfig(
kubernetes_disabled_components=[
"deployment",
"secret",
]
)
outpost.save()

View File

@ -43,6 +43,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
self.api = AppsV1Api(controller.client)
self.outpost = self.controller.outpost
@property
def noop(self) -> bool:
return self.is_embedded
@staticmethod
def reconciler_name() -> str:
return "deployment"

View File

@ -24,6 +24,10 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
super().__init__(controller)
self.api = CoreV1Api(controller.client)
@property
def noop(self) -> bool:
return self.is_embedded
@staticmethod
def reconciler_name() -> str:
return "secret"

View File

@ -77,7 +77,10 @@ class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusSe
@property
def noop(self) -> bool:
return (not self._crd_exists()) or (self.is_embedded)
if not self._crd_exists():
self.logger.debug("CRD doesn't exist")
return True
return self.is_embedded
def _crd_exists(self) -> bool:
"""Check if the Prometheus ServiceMonitor exists"""

View File

@ -344,12 +344,22 @@ class Outpost(SerializerModel, ManagedModel):
user_created = False
if not user:
user: User = User.objects.create(username=self.user_identifier)
user.set_unusable_password()
user_created = True
user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
user.name = f"Outpost {self.name} Service-Account"
user.path = USER_PATH_OUTPOSTS
user.save()
attrs = {
"type": UserTypes.INTERNAL_SERVICE_ACCOUNT,
"name": f"Outpost {self.name} Service-Account",
"path": USER_PATH_OUTPOSTS,
}
dirty = False
for key, value in attrs.items():
if getattr(user, key) != value:
dirty = True
setattr(user, key, value)
if user.has_usable_password():
user.set_unusable_password()
dirty = True
if dirty:
user.save()
if user_created:
self.build_user_permissions(user)
return user

View File

@ -2,11 +2,13 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.blueprints.tests import reconcile_app
from authentik.core.models import PropertyMapping
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id
from authentik.outposts.api.outposts import OutpostSerializer
from authentik.outposts.models import OutpostType, default_outpost_config
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost, OutpostType, default_outpost_config
from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.proxy.models import ProxyProvider
@ -22,7 +24,36 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_outpost_validaton(self):
@reconcile_app("authentik_outposts")
def test_managed_name_change(self):
"""Test name change for embedded outpost"""
embedded_outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first()
self.assertIsNotNone(embedded_outpost)
response = self.client.patch(
reverse("authentik_api:outpost-detail", kwargs={"pk": embedded_outpost.pk}),
{"name": "foo"},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content, {"name": ["Embedded outpost's name cannot be changed"]}
)
@reconcile_app("authentik_outposts")
def test_managed_without_managed(self):
"""Test name change for embedded outpost"""
embedded_outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first()
self.assertIsNotNone(embedded_outpost)
embedded_outpost.managed = ""
embedded_outpost.save()
response = self.client.patch(
reverse("authentik_api:outpost-detail", kwargs={"pk": embedded_outpost.pk}),
{"name": "foo"},
)
self.assertEqual(response.status_code, 200)
embedded_outpost.refresh_from_db()
self.assertEqual(embedded_outpost.managed, MANAGED_OUTPOST)
def test_outpost_validation(self):
"""Test Outpost validation"""
valid = OutpostSerializer(
data={

View File

@ -0,0 +1,27 @@
# Generated by Django 5.0 on 2023-12-22 23:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0016_alter_refreshtoken_token"),
]
operations = [
migrations.AddField(
model_name="accesstoken",
name="session_id",
field=models.CharField(blank=True, default=""),
),
migrations.AddField(
model_name="authorizationcode",
name="session_id",
field=models.CharField(blank=True, default=""),
),
migrations.AddField(
model_name="refreshtoken",
name="session_id",
field=models.CharField(blank=True, default=""),
),
]

View File

@ -296,6 +296,7 @@ class BaseGrantModel(models.Model):
revoked = models.BooleanField(default=False)
_scope = models.TextField(default="", verbose_name=_("Scopes"))
auth_time = models.DateTimeField(verbose_name="Authentication time")
session_id = models.CharField(default="", blank=True)
@property
def scope(self) -> list[str]:

View File

@ -85,6 +85,25 @@ class TestAuthorize(OAuthTestCase):
)
OAuthAuthorizationParams.from_request(request)
def test_blocked_redirect_uri(self):
"""test missing/invalid redirect URI"""
OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
redirect_uris="data:local.invalid",
)
with self.assertRaises(RedirectUriError):
request = self.factory.get(
"/",
data={
"response_type": "code",
"client_id": "test",
"redirect_uri": "data:localhost",
},
)
OAuthAuthorizationParams.from_request(request)
def test_invalid_redirect_uri_empty(self):
"""test missing/invalid redirect URI"""
provider = OAuth2Provider.objects.create(

View File

@ -0,0 +1,258 @@
"""Test token view"""
from base64 import b64encode, urlsafe_b64encode
from hashlib import sha256
from django.test import RequestFactory
from django.urls import reverse
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.challenge import ChallengeTypes
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.constants import GRANT_TYPE_AUTHORIZATION_CODE
from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider
from authentik.providers.oauth2.tests.utils import OAuthTestCase
class TestTokenPKCE(OAuthTestCase):
"""Test token view"""
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
self.app = Application.objects.create(name=generate_id(), slug="test")
def test_pkce_missing_in_authorize(self):
"""Test PKCE with code_challenge in authorize request
and missing verifier in token request"""
flow = create_test_flow()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=flow,
redirect_uris="foo://localhost",
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
user = create_test_admin_user()
self.client.force_login(user)
challenge = generate_id()
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
# Step 1, initiate params and get redirect to flow
self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"client_id": "test",
"state": state,
"redirect_uri": "foo://localhost",
"code_challenge": challenge,
"code_challenge_method": "S256",
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": f"foo://localhost?code={code.code}&state={state}",
},
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
"code": code.code,
# Missing the code_verifier here
"redirect_uri": "foo://localhost",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
self.assertJSONEqual(
response.content,
{
"error": "invalid_grant",
"error_description": (
"The provided authorization grant or refresh token is invalid, expired, "
"revoked, does not match the redirection URI used in the authorization "
"request, or was issued to another client"
),
},
)
self.assertEqual(response.status_code, 400)
def test_pkce_missing_in_token(self):
"""Test PKCE with missing code_challenge in authorization request but verifier
set in token request"""
flow = create_test_flow()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=flow,
redirect_uris="foo://localhost",
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
user = create_test_admin_user()
self.client.force_login(user)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
# Step 1, initiate params and get redirect to flow
self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"client_id": "test",
"state": state,
"redirect_uri": "foo://localhost",
# "code_challenge": challenge,
# "code_challenge_method": "S256",
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": f"foo://localhost?code={code.code}&state={state}",
},
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
"code": code.code,
"code_verifier": generate_id(),
"redirect_uri": "foo://localhost",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
self.assertJSONEqual(
response.content,
{
"error": "invalid_grant",
"error_description": (
"The provided authorization grant or refresh token is invalid, expired, "
"revoked, does not match the redirection URI used in the authorization "
"request, or was issued to another client"
),
},
)
self.assertEqual(response.status_code, 400)
def test_pkce_correct_s256(self):
"""Test full with pkce"""
flow = create_test_flow()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=flow,
redirect_uris="foo://localhost",
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
user = create_test_admin_user()
self.client.force_login(user)
verifier = generate_id()
challenge = (
urlsafe_b64encode(sha256(verifier.encode("ascii")).digest())
.decode("utf-8")
.replace("=", "")
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
# Step 1, initiate params and get redirect to flow
self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"client_id": "test",
"state": state,
"redirect_uri": "foo://localhost",
"code_challenge": challenge,
"code_challenge_method": "S256",
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": f"foo://localhost?code={code.code}&state={state}",
},
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
"code": code.code,
"code_verifier": verifier,
"redirect_uri": "foo://localhost",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
self.assertEqual(response.status_code, 200)
def test_pkce_correct_plain(self):
"""Test full with pkce"""
flow = create_test_flow()
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=flow,
redirect_uris="foo://localhost",
access_code_validity="seconds=100",
)
Application.objects.create(name="app", slug="app", provider=provider)
state = generate_id()
user = create_test_admin_user()
self.client.force_login(user)
verifier = generate_id()
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
# Step 1, initiate params and get redirect to flow
self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
"client_id": "test",
"state": state,
"redirect_uri": "foo://localhost",
"code_challenge": verifier,
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"type": ChallengeTypes.REDIRECT.value,
"to": f"foo://localhost?code={code.code}&state={state}",
},
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
data={
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
"code": code.code,
"code_verifier": verifier,
"redirect_uri": "foo://localhost",
},
HTTP_AUTHORIZATION=f"Basic {header}",
)
self.assertEqual(response.status_code, 200)

View File

@ -188,6 +188,7 @@ def authenticate_provider(request: HttpRequest) -> Optional[OAuth2Provider]:
if client_id != provider.client_id or client_secret != provider.client_secret:
LOGGER.debug("(basic) Provider for basic auth does not exist")
return None
CTX_AUTH_VIA.set("oauth_client_secret")
return provider

View File

@ -1,6 +1,7 @@
"""authentik OAuth2 Authorization views"""
from dataclasses import dataclass, field
from datetime import timedelta
from hashlib import sha256
from json import dumps
from re import error as RegexError
from re import fullmatch
@ -74,6 +75,7 @@ PLAN_CONTEXT_PARAMS = "goauthentik.io/providers/oauth2/params"
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"}
@dataclass(slots=True)
@ -174,6 +176,10 @@ class OAuthAuthorizationParams:
self.check_scope()
self.check_nonce()
self.check_code_challenge()
if self.request:
raise AuthorizeError(
self.redirect_uri, "request_not_supported", self.grant_type, self.state
)
def check_redirect_uri(self):
"""Redirect URI validation."""
@ -211,10 +217,9 @@ class OAuthAuthorizationParams:
expected=allowed_redirect_urls,
)
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
if self.request:
raise AuthorizeError(
self.redirect_uri, "request_not_supported", self.grant_type, self.state
)
# Check against forbidden schemes
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
def check_scope(self):
"""Ensure openid scope is set in Hybrid flows, or when requesting an id_token"""
@ -282,6 +287,7 @@ class OAuthAuthorizationParams:
expires=now + timedelta_from_string(self.provider.access_code_validity),
scope=self.scope,
nonce=self.nonce,
session_id=sha256(request.session.session_key.encode("ascii")).hexdigest(),
)
if self.code_challenge and self.code_challenge_method:
@ -569,6 +575,7 @@ class OAuthFulfillmentStage(StageView):
expires=access_token_expiry,
provider=self.provider,
auth_time=auth_event.created if auth_event else now,
session_id=sha256(self.request.session.session_key.encode("ascii")).hexdigest(),
)
id_token = IDToken.new(self.provider, token, self.request)

View File

@ -6,6 +6,7 @@ from hashlib import sha256
from re import error as RegexError
from re import fullmatch
from typing import Any, Optional
from urllib.parse import urlparse
from django.http import HttpRequest, HttpResponse
from django.utils import timezone
@ -17,6 +18,7 @@ from jwt import PyJWK, PyJWT, PyJWTError, decode
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.middleware import CTX_AUTH_VIA
from authentik.core.models import (
USER_ATTRIBUTE_EXPIRES,
USER_ATTRIBUTE_GENERATED,
@ -53,6 +55,7 @@ from authentik.providers.oauth2.models import (
RefreshToken,
)
from authentik.providers.oauth2.utils import TokenResponse, cors_allow, extract_client_auth
from authentik.providers.oauth2.views.authorize import FORBIDDEN_URI_SCHEMES
from authentik.sources.oauth.models import OAuthSource
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
@ -204,6 +207,10 @@ class TokenParams:
).from_http(request)
raise TokenError("invalid_client")
# Check against forbidden schemes
if urlparse(self.redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
raise TokenError("invalid_request")
self.authorization_code = AuthorizationCode.objects.filter(code=raw_code).first()
if not self.authorization_code:
LOGGER.warning("Code does not exist", code=raw_code)
@ -221,7 +228,10 @@ class TokenParams:
raise TokenError("invalid_grant")
# Validate PKCE parameters.
if self.code_verifier:
if self.authorization_code.code_challenge:
# Authorization code had PKCE but we didn't get one
if not self.code_verifier:
raise TokenError("invalid_grant")
if self.authorization_code.code_challenge_method == PKCE_METHOD_S256:
new_code_challenge = (
urlsafe_b64encode(sha256(self.code_verifier.encode("ascii")).digest())
@ -234,6 +244,10 @@ class TokenParams:
if new_code_challenge != self.authorization_code.code_challenge:
LOGGER.warning("Code challenge not matching")
raise TokenError("invalid_grant")
# Token request had a code_verifier but code did not have a code challenge
# Prevent downgrade
if not self.authorization_code.code_challenge and self.code_verifier:
raise TokenError("invalid_grant")
def __post_init_refresh(self, raw_token: str, request: HttpRequest):
if not raw_token:
@ -448,6 +462,7 @@ class TokenView(View):
if not self.provider:
LOGGER.warning("OAuth2Provider does not exist", client_id=client_id)
raise TokenError("invalid_client")
CTX_AUTH_VIA.set("oauth_client_secret")
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
with Hub.current.start_span(
@ -482,6 +497,7 @@ class TokenView(View):
# Keep same scopes as previous token
scope=self.params.authorization_code.scope,
auth_time=self.params.authorization_code.auth_time,
session_id=self.params.authorization_code.session_id,
)
access_token.id_token = IDToken.new(
self.provider,
@ -497,6 +513,7 @@ class TokenView(View):
expires=refresh_token_expiry,
provider=self.provider,
auth_time=self.params.authorization_code.auth_time,
session_id=self.params.authorization_code.session_id,
)
id_token = IDToken.new(
self.provider,
@ -534,6 +551,7 @@ class TokenView(View):
# Keep same scopes as previous token
scope=self.params.refresh_token.scope,
auth_time=self.params.refresh_token.auth_time,
session_id=self.params.refresh_token.session_id,
)
access_token.id_token = IDToken.new(
self.provider,
@ -549,6 +567,7 @@ class TokenView(View):
expires=refresh_token_expiry,
provider=self.provider,
auth_time=self.params.refresh_token.auth_time,
session_id=self.params.refresh_token.session_id,
)
id_token = IDToken.new(
self.provider,

View File

@ -1,4 +1,6 @@
"""proxy provider tasks"""
from hashlib import sha256
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.db import DatabaseError, InternalError, ProgrammingError
@ -23,6 +25,7 @@ def proxy_set_defaults():
def proxy_on_logout(session_id: str):
"""Update outpost instances connected to a single outpost"""
layer = get_channel_layer()
hashed_session_id = sha256(session_id.encode("ascii")).hexdigest()
for outpost in Outpost.objects.filter(type=OutpostType.PROXY):
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
async_to_sync(layer.group_send)(
@ -30,6 +33,6 @@ def proxy_on_logout(session_id: str):
{
"type": "event.provider.specific",
"sub_type": "logout",
"session_id": session_id,
"session_id": hashed_session_id,
},
)

View File

@ -46,7 +46,9 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
def to_scim(self, obj: Group) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = {}
raw_scim_group = {
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
}
for mapping in (
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
):

View File

@ -15,12 +15,14 @@ from pydanticscim.user import User as BaseUser
class User(BaseUser):
"""Modified User schema with added externalId field"""
schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:User",)
externalId: Optional[str] = None
class Group(BaseGroup):
"""Modified Group schema with added externalId field"""
schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:Group",)
externalId: Optional[str] = None

View File

@ -39,7 +39,9 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
def to_scim(self, obj: User) -> SCIMUserSchema:
"""Convert authentik user into SCIM"""
raw_scim_user = {}
raw_scim_user = {
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
}
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
if not isinstance(mapping, SCIMMapping):
continue

View File

@ -61,7 +61,11 @@ class SCIMGroupTests(TestCase):
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)
@Mocker()
@ -96,7 +100,11 @@ class SCIMGroupTests(TestCase):
validate(body, loads(schema.read()))
self.assertEqual(
body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)
group.save()
self.assertEqual(mock.call_count, 4)
@ -129,7 +137,11 @@ class SCIMGroupTests(TestCase):
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)
group.delete()
self.assertEqual(mock.call_count, 4)

View File

@ -89,17 +89,22 @@ class SCIMMembershipTests(TestCase):
self.assertJSONEqual(
mocker.request_history[3].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"emails": [],
"active": True,
"externalId": user.uid,
"name": {"familyName": "", "formatted": "", "givenName": ""},
"name": {"familyName": " ", "formatted": " ", "givenName": ""},
"displayName": "",
"userName": user.username,
},
)
self.assertJSONEqual(
mocker.request_history[5].body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)
with Mocker() as mocker:
@ -118,6 +123,7 @@ class SCIMMembershipTests(TestCase):
self.assertJSONEqual(
mocker.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
@ -125,7 +131,6 @@ class SCIMMembershipTests(TestCase):
"value": [{"value": user_scim_id}],
}
],
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
},
)
@ -174,17 +179,22 @@ class SCIMMembershipTests(TestCase):
self.assertJSONEqual(
mocker.request_history[3].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"displayName": "",
"emails": [],
"externalId": user.uid,
"name": {"familyName": "", "formatted": "", "givenName": ""},
"name": {"familyName": " ", "formatted": " ", "givenName": ""},
"userName": user.username,
},
)
self.assertJSONEqual(
mocker.request_history[5].body,
{"externalId": str(group.pk), "displayName": group.name},
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)
with Mocker() as mocker:
@ -203,6 +213,7 @@ class SCIMMembershipTests(TestCase):
self.assertJSONEqual(
mocker.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
@ -210,7 +221,6 @@ class SCIMMembershipTests(TestCase):
"value": [{"value": user_scim_id}],
}
],
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
},
)
@ -230,6 +240,7 @@ class SCIMMembershipTests(TestCase):
self.assertJSONEqual(
mocker.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
@ -237,6 +248,5 @@ class SCIMMembershipTests(TestCase):
"value": [{"value": user_scim_id}],
}
],
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
},
)

View File

@ -57,7 +57,7 @@ class SCIMUserTests(TestCase):
uid = generate_id()
user = User.objects.create(
username=uid,
name=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 2)
@ -66,6 +66,7 @@ class SCIMUserTests(TestCase):
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
@ -76,11 +77,11 @@ class SCIMUserTests(TestCase):
],
"externalId": user.uid,
"name": {
"familyName": "",
"formatted": uid,
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": uid,
"displayName": f"{uid} {uid}",
"userName": uid,
},
)
@ -109,7 +110,7 @@ class SCIMUserTests(TestCase):
uid = generate_id()
user = User.objects.create(
username=uid,
name=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 2)
@ -121,6 +122,7 @@ class SCIMUserTests(TestCase):
self.assertEqual(
body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
@ -129,11 +131,11 @@ class SCIMUserTests(TestCase):
"value": f"{uid}@goauthentik.io",
}
],
"displayName": uid,
"displayName": f"{uid} {uid}",
"externalId": user.uid,
"name": {
"familyName": "",
"formatted": uid,
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"userName": uid,
@ -164,7 +166,7 @@ class SCIMUserTests(TestCase):
uid = generate_id()
user = User.objects.create(
username=uid,
name=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 2)
@ -173,6 +175,7 @@ class SCIMUserTests(TestCase):
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
@ -183,11 +186,11 @@ class SCIMUserTests(TestCase):
],
"externalId": user.uid,
"name": {
"familyName": "",
"formatted": uid,
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": uid,
"displayName": f"{uid} {uid}",
"userName": uid,
},
)
@ -227,7 +230,7 @@ class SCIMUserTests(TestCase):
)
user = User.objects.create(
username=uid,
name=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
@ -240,6 +243,7 @@ class SCIMUserTests(TestCase):
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
@ -250,11 +254,11 @@ class SCIMUserTests(TestCase):
],
"externalId": user.uid,
"name": {
"familyName": "",
"formatted": uid,
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": uid,
"displayName": f"{uid} {uid}",
"userName": uid,
},
)

View File

@ -24,7 +24,10 @@ class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
def get_app_label_verbose(self, instance: GroupObjectPermission) -> str:
"""Get app label from permission's model"""
return apps.get_app_config(instance.content_type.app_label).verbose_name
try:
return apps.get_app_config(instance.content_type.app_label).verbose_name
except LookupError:
return instance.content_type.app_label
def get_model_verbose(self, instance: GroupObjectPermission) -> str:
"""Get model label from permission's model"""

View File

@ -24,7 +24,10 @@ class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
def get_app_label_verbose(self, instance: UserObjectPermission) -> str:
"""Get app label from permission's model"""
return apps.get_app_config(instance.content_type.app_label).verbose_name
try:
return apps.get_app_config(instance.content_type.app_label).verbose_name
except LookupError:
return instance.content_type.app_label
def get_model_verbose(self, instance: UserObjectPermission) -> str:
"""Get model label from permission's model"""

View File

@ -56,6 +56,7 @@ class OAuthSourceSerializer(SourceSerializer):
"""Get source's type configuration"""
return SourceTypeSerializer(instance.source_type).data
# pylint: disable=too-many-locals
def validate(self, attrs: dict) -> dict:
session = get_http_session()
source_type = registry.find_type(attrs["provider_type"])
@ -73,9 +74,17 @@ class OAuthSourceSerializer(SourceSerializer):
config = well_known_config.json()
if "issuer" not in config:
raise ValidationError({"oidc_well_known_url": "Invalid well-known configuration"})
attrs["authorization_url"] = config.get("authorization_endpoint", "")
attrs["access_token_url"] = config.get("token_endpoint", "")
attrs["profile_url"] = config.get("userinfo_endpoint", "")
field_map = {
# authentik field to oidc field
"authorization_url": "authorization_endpoint",
"access_token_url": "token_endpoint",
"profile_url": "userinfo_endpoint",
}
for ak_key, oidc_key in field_map.items():
# Don't overwrite user-set values
if ak_key in attrs and attrs[ak_key]:
continue
attrs[ak_key] = config.get(oidc_key, "")
inferred_oidc_jwks_url = config.get("jwks_uri", "")
# Prefer user-entered URL to inferred URL to default URL

View File

@ -44,3 +44,7 @@ class TestTypeAzureAD(TestCase):
self.assertEqual(ak_context["username"], AAD_USER["userPrincipalName"])
self.assertEqual(ak_context["email"], AAD_USER["mail"])
self.assertEqual(ak_context["name"], AAD_USER["displayName"])
def test_user_id(self):
"""Test azure AD user ID"""
self.assertEqual(AzureADOAuthCallback().get_user_id(AAD_USER), AAD_USER["id"])

View File

@ -69,9 +69,6 @@ class TestOAuthSource(TestCase):
"provider_type": "openidconnect",
"consumer_key": "foo",
"consumer_secret": "foo",
"authorization_url": "http://foo",
"access_token_url": "http://foo",
"profile_url": "http://foo",
"oidc_well_known_url": url,
"oidc_jwks_url": "",
},

View File

@ -4,8 +4,8 @@ from typing import Any
from structlog.stdlib import get_logger
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
LOGGER = get_logger()
@ -20,11 +20,16 @@ class AzureADOAuthRedirect(OAuthRedirect):
}
class AzureADOAuthCallback(OAuthCallback):
class AzureADOAuthCallback(OpenIDConnectOAuth2Callback):
"""AzureAD OAuth2 Callback"""
client_class = UserprofileHeaderAuthClient
def get_user_id(self, info: dict[str, str]) -> str:
# Default try to get `id` for the Graph API endpoint
# fallback to OpenID logic in case the profile URL was changed
return info.get("id", super().get_user_id(info))
def get_user_enroll_context(
self,
info: dict[str, Any],

View File

@ -23,7 +23,7 @@ class OpenIDConnectOAuth2Callback(OAuthCallback):
client_class = UserprofileHeaderAuthClient
def get_user_id(self, info: dict[str, str]) -> str:
return info.get("sub", "")
return info.get("sub", None)
def get_user_enroll_context(
self,

View File

@ -3,8 +3,8 @@ from typing import Any
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -17,7 +17,7 @@ class OktaOAuthRedirect(OAuthRedirect):
}
class OktaOAuth2Callback(OAuthCallback):
class OktaOAuth2Callback(OpenIDConnectOAuth2Callback):
"""Okta OAuth2 Callback"""
# Okta has the same quirk as azure and throws an error if the access token
@ -25,9 +25,6 @@ class OktaOAuth2Callback(OAuthCallback):
# see https://github.com/goauthentik/authentik/issues/1910
client_class = UserprofileHeaderAuthClient
def get_user_id(self, info: dict[str, str]) -> str:
return info.get("sub", "")
def get_user_enroll_context(
self,
info: dict[str, Any],

View File

@ -12,8 +12,9 @@ class PatreonOAuthRedirect(OAuthRedirect):
"""Patreon OAuth2 Redirect"""
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
# https://docs.patreon.com/#scopes
return {
"scope": ["openid", "email", "profile"],
"scope": ["identity", "identity[email]"],
}

View File

@ -3,8 +3,8 @@ from json import dumps
from typing import Any, Optional
from authentik.sources.oauth.clients.oauth2 import UserprofileHeaderAuthClient
from authentik.sources.oauth.types.oidc import OpenIDConnectOAuth2Callback
from authentik.sources.oauth.types.registry import SourceType, registry
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -27,14 +27,11 @@ class TwitchOAuthRedirect(OAuthRedirect):
}
class TwitchOAuth2Callback(OAuthCallback):
class TwitchOAuth2Callback(OpenIDConnectOAuth2Callback):
"""Twitch OAuth2 Callback"""
client_class = TwitchClient
def get_user_id(self, info: dict[str, str]) -> str:
return info.get("sub", "")
def get_user_enroll_context(
self,
info: dict[str, Any],

View File

@ -69,7 +69,6 @@ class AuthenticatorSMSStageView(ChallengeStageView):
stage: AuthenticatorSMSStage = self.executor.current_stage
hashed_number = hash_phone_number(phone_number)
query = Q(phone_number=hashed_number) | Q(phone_number=phone_number)
print(SMSDevice.objects.filter(query, stage=stage.pk))
if SMSDevice.objects.filter(query, stage=stage.pk).exists():
raise ValidationError(_("Invalid phone number"))
# No code yet, but we have a phone number, so send a verification message

View File

@ -199,11 +199,9 @@ class AuthenticatorSMSStageTests(FlowTestCase):
sms_send_mock,
),
):
print(self.client.session[SESSION_KEY_PLAN])
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
)
print(response.content.decode())
self.assertStageResponse(
response,
self.flow,

View File

@ -38,4 +38,11 @@ class Migration(migrations.Migration):
name="statictoken",
options={"verbose_name": "Static Token", "verbose_name_plural": "Static Tokens"},
),
migrations.AlterModelOptions(
name="authenticatorstaticstage",
options={
"verbose_name": "Static Authenticator Setup Stage",
"verbose_name_plural": "Static Authenticator Setup Stages",
},
),
]

View File

@ -46,11 +46,11 @@ class AuthenticatorStaticStage(ConfigurableStage, FriendlyNamedStage, Stage):
)
def __str__(self) -> str:
return f"Static Authenticator Stage {self.name}"
return f"Static Authenticator Setup Stage {self.name}"
class Meta:
verbose_name = _("Static Authenticator Stage")
verbose_name_plural = _("Static Authenticator Stages")
verbose_name = _("Static Authenticator Setup Stage")
verbose_name_plural = _("Static Authenticator Setup Stages")
class StaticDevice(SerializerModel, ThrottlingMixin, Device):

View File

@ -300,8 +300,10 @@ class AuthenticatorValidateStageView(ChallengeStageView):
serializer = SelectableStageSerializer(
data={
"pk": stage.pk,
"name": stage.name,
"verbose_name": str(stage._meta.verbose_name),
"name": stage.friendly_name or stage.name,
"verbose_name": str(stage._meta.verbose_name)
.replace("Setup Stage", "")
.strip(),
"meta_model_name": f"{stage._meta.app_label}.{stage._meta.model_name}",
}
)

View File

@ -184,6 +184,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
"args": {},
"method": "GET",
"path": f"/api/v3/flows/executor/{flow.slug}/",
"user_agent": "",
},
},
)

View File

@ -11,6 +11,7 @@ from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
@ -26,19 +27,22 @@ class AuthenticatorValidateStageTests(FlowTestCase):
def test_not_configured_action(self):
"""Test not_configured_action"""
conf_stage = IdentificationStage.objects.create(
ident_stage = IdentificationStage.objects.create(
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
)
conf_stage = AuthenticatorStaticStage.objects.create(
name=generate_id(),
)
stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
not_configured_action=NotConfiguredAction.CONFIGURE,
)
stage.configuration_stages.set([conf_stage])
flow = create_test_flow()
FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
response = self.client.get(
@ -57,27 +61,22 @@ class AuthenticatorValidateStageTests(FlowTestCase):
self.assertStageResponse(
response,
flow,
component="ak-stage-identification",
password_fields=False,
primary_action="Continue",
user_fields=["username"],
sources=[],
show_source_labels=False,
component="ak-stage-authenticator-static",
)
def test_not_configured_action_multiple(self):
"""Test not_configured_action"""
conf_stage = IdentificationStage.objects.create(
ident_stage = IdentificationStage.objects.create(
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
)
conf_stage2 = IdentificationStage.objects.create(
conf_stage = AuthenticatorStaticStage.objects.create(
name=generate_id(),
)
conf_stage2 = AuthenticatorStaticStage.objects.create(
name=generate_id(),
user_fields=[
UserFields.USERNAME,
],
)
stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
@ -85,7 +84,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
)
stage.configuration_stages.set([conf_stage, conf_stage2])
flow = create_test_flow()
FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=ident_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=stage, order=1)
# Get initial identification stage
@ -118,12 +117,7 @@ class AuthenticatorValidateStageTests(FlowTestCase):
self.assertStageResponse(
response,
flow,
component="ak-stage-identification",
password_fields=False,
primary_action="Continue",
user_fields=["username"],
sources=[],
show_source_labels=False,
component="ak-stage-authenticator-static",
)
def test_stage_validation(self):

View File

@ -1,9 +1,11 @@
"""authentik multi-stage authentication engine"""
from datetime import timedelta
from uuid import uuid4
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.template.exceptions import TemplateSyntaxError
from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now
@ -11,11 +13,14 @@ from django.utils.translation import gettext as _
from rest_framework.fields import CharField
from rest_framework.serializers import ValidationError
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.exceptions import StageInvalidException
from authentik.flows.models import FlowDesignation, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN, QS_QUERY
from authentik.lib.utils.errors import exception_to_string
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -52,16 +57,11 @@ class EmailStageView(ChallengeStageView):
kwargs={"flow_slug": self.executor.flow.slug},
)
# Parse query string from current URL (full query string)
query_params = QueryDict(self.request.META.get("QUERY_STRING", ""), mutable=True)
# this view is only run within a flow executor, where we need to get the query string
# from the query= parameter (double encoded); but for the redirect
# we need to expand it since it'll go through the flow interface
query_params = QueryDict(self.request.GET.get(QS_QUERY), mutable=True)
query_params.pop(QS_KEY_TOKEN, None)
# Check for nested query string used by flow executor, and remove any
# kind of flow token from that
if QS_QUERY in query_params:
inner_query_params = QueryDict(query_params.get(QS_QUERY), mutable=True)
inner_query_params.pop(QS_KEY_TOKEN, None)
query_params[QS_QUERY] = inner_query_params.urlencode()
query_params.update(kwargs)
full_url = base_url
if len(query_params) > 0:
@ -75,7 +75,7 @@ class EmailStageView(ChallengeStageView):
valid_delta = timedelta(
minutes=current_stage.token_expiry + 1
) # + 1 because django timesince always rounds down
identifier = slugify(f"ak-email-stage-{current_stage.name}-{pending_user}")
identifier = slugify(f"ak-email-stage-{current_stage.name}-{str(uuid4())}")
# Don't check for validity here, we only care if the token exists
tokens = FlowToken.objects.filter(identifier=identifier)
if not tokens.exists():
@ -107,18 +107,27 @@ class EmailStageView(ChallengeStageView):
current_stage: EmailStage = self.executor.current_stage
token = self.get_token()
# Send mail to user
message = TemplateEmailMessage(
subject=_(current_stage.subject),
to=[email],
language=pending_user.locale(self.request),
template_name=current_stage.template,
template_context={
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
"user": pending_user,
"expires": token.expires,
},
)
send_mails(current_stage, message)
try:
message = TemplateEmailMessage(
subject=_(current_stage.subject),
to=[email],
language=pending_user.locale(self.request),
template_name=current_stage.template,
template_context={
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
"user": pending_user,
"expires": token.expires,
},
)
send_mails(current_stage, message)
except TemplateSyntaxError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=_("Exception occurred while rendering E-mail template"),
error=exception_to_string(exc),
template=current_stage.template,
).from_http(self.request)
raise StageInvalidException from exc
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify
@ -139,7 +148,11 @@ class EmailStageView(ChallengeStageView):
return self.executor.stage_invalid()
# Check if we've already sent the initial e-mail
if PLAN_CONTEXT_EMAIL_SENT not in self.executor.plan.context:
self.send_email()
try:
self.send_email()
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
self.executor.plan.context[PLAN_CONTEXT_EMAIL_SENT] = True
return super().get(request, *args, **kwargs)

View File

@ -259,7 +259,7 @@ class TestEmailStage(FlowTestCase):
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
url += "?foo=bar"
url += "?query=" + urlencode({"foo": "bar"})
request = self.factory.get(url)
stage_view = EmailStageView(
FlowExecutorView(
@ -273,31 +273,3 @@ class TestEmailStage(FlowTestCase):
stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
f"http://testserver/if/flow/{self.flow.slug}/?foo=bar&flow_token={token}",
)
def test_url_existing_params_nested(self):
"""Test to ensure that URL params are preserved in the URL being sent (including nested)"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
url += "?foo=bar&"
url += "query=" + urlencode({"nested": "value"})
request = self.factory.get(url)
stage_view = EmailStageView(
FlowExecutorView(
request=request,
flow=self.flow,
),
request=request,
)
token = generate_id()
self.assertEqual(
stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
(
f"http://testserver/if/flow/{self.flow.slug}"
f"/?foo=bar&query=nested%3Dvalue&flow_token={token}"
),
)

View File

@ -4,11 +4,20 @@ from pathlib import Path
from shutil import rmtree
from tempfile import mkdtemp, mkstemp
from typing import Any
from unittest.mock import PropertyMock, patch
from django.conf import settings
from django.test import TestCase
from django.core.mail.backends.locmem import EmailBackend
from django.urls import reverse
from authentik.stages.email.models import get_template_choices
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.email.models import EmailStage, get_template_choices
def get_templates_setting(temp_dir: str) -> dict[str, Any]:
@ -18,11 +27,18 @@ def get_templates_setting(temp_dir: str) -> dict[str, Any]:
return templates_setting
class TestEmailStageTemplates(TestCase):
class TestEmailStageTemplates(FlowTestCase):
"""Email tests"""
def setUp(self) -> None:
self.dir = mkdtemp()
self.dir = Path(mkdtemp())
self.user = create_test_admin_user()
self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
self.stage = EmailStage.objects.create(
name="email",
)
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
def tearDown(self) -> None:
rmtree(self.dir)
@ -38,3 +54,37 @@ class TestEmailStageTemplates(TestCase):
self.assertEqual(len(choices), 3)
unlink(file)
unlink(file2)
def test_custom_template_invalid_syntax(self):
"""Test with custom template"""
with open(self.dir / Path("invalid.html"), "w+", encoding="utf-8") as _invalid:
_invalid.write("{% blocktranslate %}")
with self.settings(TEMPLATES=get_templates_setting(self.dir)):
self.stage.template = "invalid.html"
plan = FlowPlan(
flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]
)
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(
"authentik.stages.email.models.EmailStage.backend_class",
PropertyMock(return_value=EmailBackend),
):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
self.flow,
error_message="Unknown error",
)
events = Event.objects.filter(action=EventAction.CONFIGURATION_ERROR)
self.assertEqual(len(events), 1)
event = events.first()
self.assertEqual(
event.context["message"], "Exception occurred while rendering E-mail template"
)
self.assertEqual(event.context["template"], "invalid.html")

View File

@ -6,6 +6,7 @@ from django.urls import reverse
from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
@ -58,11 +59,33 @@ class TestUserWriteStage(FlowTestCase):
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"])
self.assertTrue(user_qs.exists())
self.assertTrue(user_qs.first().check_password(password))
self.assertEqual(
list(user_qs.first().ak_groups.order_by("name")), [self.other_group, self.group]
user = user_qs.first()
self.assertTrue(user.check_password(password))
self.assertEqual(list(user.ak_groups.order_by("name")), [self.other_group, self.group])
self.assertEqual(user.attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_CREATED,
context__model={
"app": "authentik_core",
"model_name": "user",
"pk": user.pk,
"name": "name",
},
)
)
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model={
"app": "authentik_core",
"model_name": "user",
"pk": user.pk,
"name": "name",
},
)
)
self.assertEqual(user_qs.first().attributes, {USER_ATTRIBUTE_SOURCES: [self.source.name]})
def test_user_update(self):
"""Test update of existing user"""

View File

@ -14,6 +14,7 @@ entries:
- attrs:
configure_flow: !KeyOf flow
token_count: 6
friendly_name: Static tokens
identifiers:
name: default-authenticator-static-setup
id: default-authenticator-static-setup

View File

@ -14,6 +14,7 @@ entries:
- attrs:
configure_flow: !KeyOf flow
digits: 6
friendly_name: TOTP Device
identifiers:
name: default-authenticator-totp-setup
id: default-authenticator-totp-setup

View File

@ -13,6 +13,7 @@ entries:
id: flow
- attrs:
configure_flow: !KeyOf flow
friendly_name: WebAuthn device
identifiers:
name: default-authenticator-webauthn-setup
id: default-authenticator-webauthn-setup

View File

@ -14,8 +14,11 @@ entries:
expression: |
# This mapping is used by the authentik proxy. It passes extra user attributes,
# which are used for example for the HTTP-Basic Authentication mapping.
session_id = None
if "token" in request.context:
session_id = request.context.get("token").session_id
return {
"sid": request.http_request.session.session_key,
"sid": session_id,
"ak_proxy": {
"user_attributes": request.user.group_attributes(request),
"is_superuser": request.user.is_superuser,

View File

@ -11,13 +11,15 @@ entries:
name: "authentik default SCIM Mapping: User"
expression: |
# Some implementations require givenName and familyName to be set
givenName, familyName = request.user.name, ""
givenName, familyName = request.user.name, " "
formatted = request.user.name + " "
# This default sets givenName to the name before the first space
# and the remainder as family name
# if the user's name has no space the givenName is the entire name
# (this might cause issues with some SCIM implementations)
if " " in request.user.name:
givenName, _, familyName = request.user.name.partition(" ")
formatted = request.user.name
# photos supports URLs to images, however authentik might return data URIs
avatar = request.user.avatar
@ -39,7 +41,7 @@ entries:
return {
"userName": request.user.username,
"name": {
"formatted": request.user.name,
"formatted": formatted,
"givenName": givenName,
"familyName": familyName,
},

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7}
restart: unless-stopped
command: server
environment:
@ -53,7 +53,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@ -4,7 +4,6 @@ go 1.21
require (
beryju.io/ldap v0.1.0
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/getsentry/sentry-go v0.25.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
@ -24,6 +23,7 @@ require (
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.2.1
github.com/sethvargo/go-envconfig v1.0.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4

8
go.sum
View File

@ -37,8 +37,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb h1:w9IDEB7P1VzNcBpOG7kMpFkZp2DkyJIUt0gDx5MBhRU=
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
@ -198,8 +196,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -303,6 +301,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sethvargo/go-envconfig v1.0.0 h1:1C66wzy4QrROf5ew4KdVw942CQDa55qmlYmw9FZxZdU=
github.com/sethvargo/go-envconfig v1.0.0/go.mod h1:Lzc75ghUn5ucmcRGIdGQ33DKJrcjk4kihFYgSTBmjIc=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=

View File

@ -1,6 +1,7 @@
package config
import (
"context"
_ "embed"
"errors"
"fmt"
@ -10,10 +11,11 @@ import (
"reflect"
"strings"
env "github.com/Netflix/go-env"
env "github.com/sethvargo/go-envconfig"
log "github.com/sirupsen/logrus"
"goauthentik.io/authentik/lib"
"gopkg.in/yaml.v2"
"goauthentik.io/authentik/lib"
)
var cfg *Config
@ -113,7 +115,8 @@ func (c *Config) LoadConfigFromFile(path string) error {
}
func (c *Config) fromEnv() error {
_, err := env.UnmarshalFromEnviron(c)
ctx := context.Background()
err := env.Process(ctx, c)
if err != nil {
return fmt.Errorf("failed to load environment variables: %w", err)
}

View File

@ -3,17 +3,17 @@ package config
type Config struct {
// Core specific config
Paths PathsConfig `yaml:"paths"`
LogLevel string `yaml:"log_level" env:"AUTHENTIK_LOG_LEVEL"`
ErrorReporting ErrorReportingConfig `yaml:"error_reporting"`
Redis RedisConfig `yaml:"redis"`
Outposts OutpostConfig `yaml:"outposts"`
LogLevel string `yaml:"log_level" env:"AUTHENTIK_LOG_LEVEL, overwrite"`
ErrorReporting ErrorReportingConfig `yaml:"error_reporting" env:", prefix=AUTHENTIK_ERROR_REPORTING__"`
Redis RedisConfig `yaml:"redis" env:", prefix=AUTHENTIK_REDIS__"`
Outposts OutpostConfig `yaml:"outposts" env:", prefix=AUTHENTIK_OUTPOSTS__"`
// Config for core and embedded outpost
SecretKey string `yaml:"secret_key" env:"AUTHENTIK_SECRET_KEY"`
SecretKey string `yaml:"secret_key" env:"AUTHENTIK_SECRET_KEY, overwrite"`
// Config for both core and outposts
Debug bool `yaml:"debug" env:"AUTHENTIK_DEBUG"`
Listen ListenConfig `yaml:"listen"`
Debug bool `yaml:"debug" env:"AUTHENTIK_DEBUG, overwrite"`
Listen ListenConfig `yaml:"listen" env:", prefix=AUTHENTIK_LISTEN__"`
// Outpost specific config
// These are only relevant for proxy/ldap outposts, and cannot be set via YAML
@ -25,27 +25,28 @@ type Config struct {
}
type RedisConfig struct {
Host string `yaml:"host" env:"AUTHENTIK_REDIS__HOST"`
Port int `yaml:"port" env:"AUTHENTIK_REDIS__PORT"`
Password string `yaml:"password" env:"AUTHENTIK_REDIS__PASSWORD"`
TLS bool `yaml:"tls" env:"AUTHENTIK_REDIS__TLS"`
TLSReqs string `yaml:"tls_reqs" env:"AUTHENTIK_REDIS__TLS_REQS"`
DB int `yaml:"cache_db" env:"AUTHENTIK_REDIS__DB"`
CacheTimeout int `yaml:"cache_timeout" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT"`
CacheTimeoutFlows int `yaml:"cache_timeout_flows" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_FLOWS"`
CacheTimeoutPolicies int `yaml:"cache_timeout_policies" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_POLICIES"`
CacheTimeoutReputation int `yaml:"cache_timeout_reputation" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_REPUTATION"`
Host string `yaml:"host" env:"HOST, overwrite"`
Port int `yaml:"port" env:"PORT, overwrite"`
DB int `yaml:"db" env:"DB, overwrite"`
Username string `yaml:"username" env:"USERNAME, overwrite"`
Password string `yaml:"password" env:"PASSWORD, overwrite"`
TLS bool `yaml:"tls" env:"TLS, overwrite"`
TLSReqs string `yaml:"tls_reqs" env:"TLS_REQS, overwrite"`
CacheTimeout int `yaml:"cache_timeout" env:"CACHE_TIMEOUT, overwrite"`
CacheTimeoutFlows int `yaml:"cache_timeout_flows" env:"CACHE_TIMEOUT_FLOWS, overwrite"`
CacheTimeoutPolicies int `yaml:"cache_timeout_policies" env:"CACHE_TIMEOUT_POLICIES, overwrite"`
CacheTimeoutReputation int `yaml:"cache_timeout_reputation" env:"CACHE_TIMEOUT_REPUTATION, overwrite"`
}
type ListenConfig struct {
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS"`
HTTP string `yaml:"listen_http" env:"HTTP, overwrite"`
HTTPS string `yaml:"listen_https" env:"HTTPS, overwrite"`
LDAP string `yaml:"listen_ldap" env:"LDAP, overwrite"`
LDAPS string `yaml:"listen_ldaps" env:"LDAPS, overwrite"`
Radius string `yaml:"listen_radius" env:"RADIUS, overwrite"`
Metrics string `yaml:"listen_metrics" env:"METRICS, overwrite"`
Debug string `yaml:"listen_debug" env:"DEBUG, overwrite"`
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"TRUSTED_PROXY_CIDRS, overwrite"`
}
type PathsConfig struct {
@ -53,15 +54,15 @@ type PathsConfig struct {
}
type ErrorReportingConfig struct {
Enabled bool `yaml:"enabled" env:"AUTHENTIK_ERROR_REPORTING__ENABLED"`
SentryDSN string `yaml:"sentry_dsn" env:"AUTHENTIK_ERROR_REPORTING__SENTRY_DSN"`
Environment string `yaml:"environment" env:"AUTHENTIK_ERROR_REPORTING__ENVIRONMENT"`
SendPII bool `yaml:"send_pii" env:"AUTHENTIK_ERROR_REPORTING__SEND_PII"`
SampleRate float64 `yaml:"sample_rate" env:"AUTHENTIK_ERROR_REPORTING__SAMPLE_RATE"`
Enabled bool `yaml:"enabled" env:"ENABLED, overwrite"`
SentryDSN string `yaml:"sentry_dsn" env:"SENTRY_DSN, overwrite"`
Environment string `yaml:"environment" env:"ENVIRONMENT, overwrite"`
SendPII bool `yaml:"send_pii" env:"SEND_PII, overwrite"`
SampleRate float64 `yaml:"sample_rate" env:"SAMPLE_RATE, overwrite"`
}
type OutpostConfig struct {
ContainerImageBase string `yaml:"container_image_base" env:"AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE"`
Discover bool `yaml:"discover" env:"AUTHENTIK_OUTPOSTS__DISCOVER"`
DisableEmbeddedOutpost bool `yaml:"disable_embedded_outpost" env:"AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST"`
ContainerImageBase string `yaml:"container_image_base" env:"CONTAINER_IMAGE_BASE, overwrite"`
Discover bool `yaml:"discover" env:"DISCOVER, overwrite"`
DisableEmbeddedOutpost bool `yaml:"disable_embedded_outpost" env:"DISABLE_EMBEDDED_OUTPOST, overwrite"`
}

View File

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

View File

@ -31,16 +31,11 @@ func (a *Application) redeemCallback(savedState string, u *url.URL, c context.Co
return nil, err
}
// Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("missing id_token")
}
a.log.WithField("id_token", rawIDToken).Trace("id_token")
jwt := oauth2Token.AccessToken
a.log.WithField("jwt", jwt).Trace("access_token")
// Parse and verify ID Token payload.
idToken, err := a.tokenVerifier.Verify(ctx, rawIDToken)
idToken, err := a.tokenVerifier.Verify(ctx, jwt)
if err != nil {
return nil, err
}
@ -53,6 +48,6 @@ func (a *Application) redeemCallback(savedState string, u *url.URL, c context.Co
if claims.Proxy == nil {
claims.Proxy = &ProxyClaims{}
}
claims.RawToken = rawIDToken
claims.RawToken = jwt
return claims, nil
}

View File

@ -62,7 +62,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
// when using OpenID Connect, since this can contain a large amount of extra information in the id_token
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
cs.MaxLength(math.MaxInt)
@ -71,7 +71,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
cs.Options.Domain = *p.CookieDomain
cs.Options.SameSite = http.SameSiteLaxMode
cs.Options.MaxAge = maxAge
cs.Options.Path = externalHost.Path
cs.Options.Path = "/"
a.log.WithField("dir", dir).Trace("using filesystem session backend")
return cs
}
@ -131,7 +131,6 @@ func (a *Application) Logout(ctx context.Context, filter func(c Claims) bool) er
}
if rs, ok := a.sessions.(*redisstore.RedisStore); ok {
client := rs.Client()
defer client.Close()
keys, err := client.Keys(ctx, fmt.Sprintf("%s*", RedisKeyPrefix)).Result()
if err != nil {
return err

View File

@ -36,6 +36,7 @@ func (ps *ProxyServer) handleWSMessage(ctx context.Context, args map[string]inte
switch msg.SubType {
case WSProviderSubTypeLogout:
for _, p := range ps.apps {
ps.log.WithField("provider", p.Host).Debug("Logging out")
err := p.Logout(ctx, func(c application.Claims) bool {
return c.Sid == msg.SessionID
})

View File

@ -1,5 +1,14 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM docker.io/golang:1.21.3-bookworm AS builder
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ARG GOOS=$TARGETOS
ARG GOARCH=$TARGETARCH
WORKDIR /go/src/goauthentik.io
@ -11,9 +20,9 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
ENV CGO_ENABLED=0
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /go/ldap ./cmd/ldap
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
GOARM="${TARGETVARIANT#v}" go build -o /go/ldap ./cmd/ldap
# Stage 2: Run
FROM gcr.io/distroless/static-debian11:debug

31
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
[[package]]
name = "aiohttp"
@ -2096,16 +2096,6 @@ files = [
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@ -2840,10 +2830,7 @@ files = [
[package.dependencies]
astroid = ">=3.0.1,<=3.1.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
{version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
]
dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""}
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
@ -3096,7 +3083,6 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@ -3104,15 +3090,8 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@ -3129,7 +3108,6 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@ -3137,7 +3115,6 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@ -4331,5 +4308,5 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "2fc746976187f4674f04575cffd6a367744723bf78c356b6951c2370bc47ceae"
python-versions = "~3.11"
content-hash = "5a57dede617d149e0f307fc42580dcfd0d4b76161009dc447d6f10b048426c98"

View File

@ -1,3 +1,5 @@
# syntax=docker/dockerfile:1
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/node:21 as web-builder
@ -15,7 +17,14 @@ COPY web .
RUN npm run build-proxy
# Stage 2: Build
FROM docker.io/golang:1.21.3-bookworm AS builder
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ARG GOOS=$TARGETOS
ARG GOARCH=$TARGETARCH
WORKDIR /go/src/goauthentik.io
@ -27,9 +36,9 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
ENV CGO_ENABLED=0
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /go/proxy ./cmd/proxy
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
GOARM="${TARGETVARIANT#v}" go build -o /go/proxy ./cmd/proxy
# Stage 3: Run
FROM gcr.io/distroless/static-debian11:debug

View File

@ -97,7 +97,7 @@ const-rgx = "[a-zA-Z0-9_]{1,40}$"
ignored-modules = ["binascii", "socket", "zlib"]
generated-members = ["xmlsec.constants.*", "xmlsec.tree.*", "xmlsec.template.*"]
ignore = "migrations"
ignore = ["migrations", "tests"]
max-attributes = 12
max-branches = 20
@ -113,7 +113,7 @@ filterwarnings = [
[tool.poetry]
name = "authentik"
version = "2023.10.2"
version = "2023.10.7"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]
@ -151,10 +151,10 @@ packaging = "*"
paramiko = "*"
psycopg = { extras = ["c"], version = "*" }
pycryptodome = "*"
pydantic = "<3.0.0"
pydantic-scim = "^0.0.8"
pydantic = "*"
pydantic-scim = "*"
pyjwt = "*"
python = "^3.11"
python = "~3.11"
pyyaml = "*"
requests-oauthlib = "*"
sentry-sdk = "*"

View File

@ -1,5 +1,14 @@
# syntax=docker/dockerfile:1
# Stage 1: Build
FROM docker.io/golang:1.21.3-bookworm AS builder
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21.4-bookworm AS builder
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ARG GOOS=$TARGETOS
ARG GOARCH=$TARGETARCH
WORKDIR /go/src/goauthentik.io
@ -11,9 +20,9 @@ RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
ENV CGO_ENABLED=0
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /go/radius ./cmd/radius
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
GOARM="${TARGETVARIANT#v}" go build -o /go/radius ./cmd/radius
# Stage 2: Run
FROM gcr.io/distroless/static-debian11:debug

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2023.10.2
version: 2023.10.7
description: Making authentication simple.
contact:
email: hello@goauthentik.io

View File

@ -36,8 +36,8 @@ class TestProviderOAuth2Github(SeleniumTestCase):
"auto_remove": True,
"healthcheck": Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:3000"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
interval=5 * 1_000 * 1_000_000,
start_period=1 * 1_000 * 1_000_000,
),
"environment": {
"GF_AUTH_GITHUB_ENABLED": "true",

View File

@ -42,8 +42,8 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
"auto_remove": True,
"healthcheck": Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:3000"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
interval=5 * 1_000 * 1_000_000,
start_period=1 * 1_000 * 1_000_000,
),
"environment": {
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",

View File

@ -113,8 +113,8 @@ class TestSourceOAuth2(SeleniumTestCase):
"command": "dex serve /config.yml",
"healthcheck": Healthcheck(
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
interval=5 * 1_000 * 1_000_000,
start_period=1 * 1_000 * 1_000_000,
),
"volumes": {str(Path(CONFIG_PATH).absolute()): {"bind": "/config.yml", "mode": "ro"}},
}

View File

@ -83,8 +83,8 @@ class TestSourceSAML(SeleniumTestCase):
"auto_remove": True,
"healthcheck": Healthcheck(
test=["CMD", "curl", "http://localhost:8080"],
interval=5 * 100 * 1000000,
start_period=1 * 100 * 1000000,
interval=5 * 1_000 * 1_000_000,
start_period=1 * 1_000 * 1_000_000,
),
"environment": {
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",

View File

@ -34,8 +34,8 @@ class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase):
privileged=True,
healthcheck=Healthcheck(
test=["CMD", "docker", "info"],
interval=5 * 100 * 1000000,
start_period=5 * 100 * 1000000,
interval=5 * 1_000 * 1_000_000,
start_period=5 * 1_000 * 1_000_000,
),
environment={"DOCKER_TLS_CERTDIR": "/ssl"},
volumes={

View File

@ -34,8 +34,8 @@ class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase):
privileged=True,
healthcheck=Healthcheck(
test=["CMD", "docker", "info"],
interval=5 * 100 * 1000000,
start_period=5 * 100 * 1000000,
interval=5 * 1_000 * 1_000_000,
start_period=5 * 1_000 * 1_000_000,
),
environment={"DOCKER_TLS_CERTDIR": "/ssl"},
volumes={

View File

@ -27,5 +27,8 @@
"precommit": "run-s lint:precommit lint:spelling prettier",
"prettier-check": "prettier --check .",
"prettier": "prettier --write ."
},
"engines": {
"node": ">=20"
}
}

3
web/package-lock.json generated
View File

@ -100,6 +100,9 @@
"typescript": "^5.2.2",
"vite-tsconfig-paths": "^4.2.1"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.19.5",
"@esbuild/linux-amd64": "^0.18.11",

View File

@ -125,5 +125,8 @@
"@esbuild/darwin-arm64": "^0.19.5",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.19.5"
},
"engines": {
"node": ">=20"
}
}

View File

@ -116,7 +116,7 @@ export class ApplicationForm extends ModelForm<Application, string> {
return app;
}
handleConfirmBackchannelProviders({ items }: { items: Provider[] }) {
handleConfirmBackchannelProviders(items: Provider[]) {
this.backchannelProviders = items;
this.requestUpdate();
return Promise.resolve();

View File

@ -63,7 +63,7 @@ export class AkBackchannelProvidersInput extends AKElement {
return html`
<ak-form-element-horizontal label=${this.label} name=${name}>
<div class="pf-c-input-group">
<ak-provider-select-table ?backchannelOnly=${true} .confirm=${confirm}>
<ak-provider-select-table ?backchannelOnly=${true} .confirm=${this.confirm}>
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
${this.tooltip ? this.tooltip : nothing}
<i class="fas fa-plus" aria-hidden="true"></i>

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