core: add new token intent and auth backend (#1284)

* core: add new token intent and auth backend

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* root: update schema

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/admin: allow users to create app password tokens

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/admin: display token's intents

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/password: auto-enable app password backend

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/admin: fix missing app passwords backend

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: use custom inbuilt backend, set backend login information in flow plan for events

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* website/docs: add docs for `auth_method` and `auth_method_args` fields

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* website: fix example flows using incorrect backend

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* root: add alias for akflow files

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: fix token intent not defaulting correctly

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* website: update akflows orders

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/admin: improve delete modal for stage bindings and policy bindings

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* events: fix linting

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* website: make default login-2fa flow ignore 2fa with app passwords

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web/admin: select all password stage backends by default

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* root: fix mis-matched postgres version for CI

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: fix lint error

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: fix authentication error when no request is given

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* ci: set debug log level

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* stages/user_write: fix wrong fallback authentication backend

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: add token tests for invalid intent and token auth

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2021-08-23 21:21:39 +02:00 committed by GitHub
commit 6b7a8b6ac7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 623 additions and 194 deletions

22
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"cSpell.words": [
"asgi",
"authentik",
"authn",
"goauthentik",
"jwks",
"oidc",
"openid",
"plex",
"saml",
"totp",
"webauthn"
],
"python.linting.pylintEnabled": true,
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"python.formatting.provider": "black",
"files.associations": {
"*.akflow": "json"
}
}

View file

@ -1,7 +1,10 @@
"""Tokens API Viewset"""
from typing import Any
from django.http.response import Http404
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField
from rest_framework.request import Request
from rest_framework.response import Response
@ -22,6 +25,13 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
user = UserSerializer(required=False)
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""
attrs.setdefault("intent", TokenIntents.INTENT_API)
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
return attrs
class Meta:
model = Token
@ -69,7 +79,6 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
def perform_create(self, serializer: TokenSerializer):
serializer.save(
user=self.request.user,
intent=TokenIntents.INTENT_API,
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
)

58
authentik/core/auth.py Normal file
View file

@ -0,0 +1,58 @@
"""Authenticate with tokens"""
from typing import Any, Optional
from django.contrib.auth.backends import ModelBackend
from django.http.request import HttpRequest
from authentik.core.models import Token, TokenIntents, User
from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
class InbuiltBackend(ModelBackend):
"""Inbuilt backend"""
def authenticate(
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
) -> Optional[User]:
user = super().authenticate(request, username=username, password=password, **kwargs)
if not user:
return None
self.set_method("password", request)
return user
def set_method(self, method: str, request: Optional[HttpRequest], **kwargs):
"""Set method data on current flow, if possbiel"""
if not request:
return
# Since we can't directly pass other variables to signals, and we want to log the method
# and the token used, we assume we're running in a flow and set a variable in the context
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
flow_plan.context[PLAN_CONTEXT_METHOD] = method
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = kwargs
request.session[SESSION_KEY_PLAN] = flow_plan
class TokenBackend(InbuiltBackend):
"""Authenticate with token"""
def authenticate(
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
) -> Optional[User]:
try:
user = User._default_manager.get_by_natural_key(username)
except User.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
User().set_password(password)
return None
tokens = Token.filter_not_expired(
user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD
)
if not tokens.exists():
return None
token = tokens.first()
self.set_method("password", request, token=token)
return token.user

View file

@ -0,0 +1,26 @@
# Generated by Django 3.2.6 on 2021-08-23 14:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0027_bootstrap_token"),
]
operations = [
migrations.AlterField(
model_name="token",
name="intent",
field=models.TextField(
choices=[
("verification", "Intent Verification"),
("api", "Intent Api"),
("recovery", "Intent Recovery"),
("app_password", "Intent App Password"),
],
default="verification",
),
),
]

View file

@ -411,6 +411,9 @@ class TokenIntents(models.TextChoices):
# Recovery use for the recovery app
INTENT_RECOVERY = "recovery"
# App-specific passwords
INTENT_APP_PASSWORD = "app_password" # nosec
class Token(ManagedModel, ExpiringModel):
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""

View file

@ -25,7 +25,7 @@ from authentik.flows.planner import (
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys
from authentik.stages.password import BACKEND_DJANGO
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
@ -189,7 +189,7 @@ class SourceFlowManager:
kwargs.update(
{
# Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_REDIRECT: final_redirect,

View file

@ -1,16 +1,13 @@
"""Test Source flow_manager"""
from django.contrib.auth.models import AnonymousUser
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.http.request import HttpRequest
from django.test import TestCase
from django.test.client import RequestFactory
from guardian.utils import get_anonymous_user
from authentik.core.models import SourceUserMatchingModes, User
from authentik.core.sources.flow_manager import Action
from authentik.flows.tests.test_planner import dummy_get_response
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.views.callback import OAuthSourceFlowManager
@ -24,22 +21,10 @@ class TestSourceFlowManager(TestCase):
self.factory = RequestFactory()
self.identifier = generate_id()
def get_request(self, user: User) -> HttpRequest:
"""Helper to create a get request with session and message middleware"""
request = self.factory.get("/")
request.user = user
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
middleware = MessageMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
return request
def test_unauthenticated_enroll(self):
"""Test un-authenticated user enrolling"""
flow_manager = OAuthSourceFlowManager(
self.source, self.get_request(AnonymousUser()), self.identifier, {}
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.ENROLL)
@ -52,7 +37,7 @@ class TestSourceFlowManager(TestCase):
)
flow_manager = OAuthSourceFlowManager(
self.source, self.get_request(AnonymousUser()), self.identifier, {}
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.AUTH)
@ -65,7 +50,7 @@ class TestSourceFlowManager(TestCase):
)
user = User.objects.create(username="foo", email="foo@bar.baz")
flow_manager = OAuthSourceFlowManager(
self.source, self.get_request(user), self.identifier, {}
self.source, get_request("/", user=user), self.identifier, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
@ -78,7 +63,7 @@ class TestSourceFlowManager(TestCase):
# Without email, deny
flow_manager = OAuthSourceFlowManager(
self.source, self.get_request(AnonymousUser()), self.identifier, {}
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.DENY)
@ -86,7 +71,7 @@ class TestSourceFlowManager(TestCase):
# With email
flow_manager = OAuthSourceFlowManager(
self.source,
self.get_request(AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{"email": "foo@bar.baz"},
)
@ -101,7 +86,7 @@ class TestSourceFlowManager(TestCase):
# Without username, deny
flow_manager = OAuthSourceFlowManager(
self.source, self.get_request(AnonymousUser()), self.identifier, {}
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.DENY)
@ -109,7 +94,7 @@ class TestSourceFlowManager(TestCase):
# With username
flow_manager = OAuthSourceFlowManager(
self.source,
self.get_request(AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{"username": "foo"},
)
@ -125,7 +110,7 @@ class TestSourceFlowManager(TestCase):
# With non-existent username, enroll
flow_manager = OAuthSourceFlowManager(
self.source,
self.get_request(AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{
"username": "bar",
@ -137,7 +122,7 @@ class TestSourceFlowManager(TestCase):
# With username
flow_manager = OAuthSourceFlowManager(
self.source,
self.get_request(AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{"username": "foo"},
)
@ -151,7 +136,7 @@ class TestSourceFlowManager(TestCase):
flow_manager = OAuthSourceFlowManager(
self.source,
self.get_request(AnonymousUser()),
get_request("/", user=AnonymousUser()),
self.identifier,
{"username": "foo"},
)

View file

@ -27,6 +27,14 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
def test_token_create_invalid(self):
"""Test token creation endpoint (invalid data)"""
response = self.client.post(
reverse("authentik_api:token-list"),
{"identifier": "test-token", "intent": TokenIntents.INTENT_RECOVERY},
)
self.assertEqual(response.status_code, 400)
def test_token_create_non_expiring(self):
"""Test token creation endpoint"""
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = False

View file

@ -0,0 +1,40 @@
"""Test token auth"""
from django.test import TestCase
from authentik.core.auth import TokenBackend
from authentik.core.models import Token, TokenIntents, User
from authentik.flows.planner import FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.lib.tests.utils import get_request
class TestTokenAuth(TestCase):
"""Test token auth"""
def setUp(self) -> None:
self.user = User.objects.create(username="test-user")
self.token = Token.objects.create(
expiring=False, user=self.user, intent=TokenIntents.INTENT_APP_PASSWORD
)
# To test with session we need to create a request and pass it through all middlewares
self.request = get_request("/")
self.request.session[SESSION_KEY_PLAN] = FlowPlan("test")
def test_token_auth(self):
"""Test auth with token"""
self.assertEqual(
TokenBackend().authenticate(self.request, "test-user", self.token.key), self.user
)
def test_token_auth_none(self):
"""Test auth with token (non-existent user)"""
self.assertIsNone(
TokenBackend().authenticate(self.request, "test-user-foo", self.token.key), self.user
)
def test_token_auth_invalid(self):
"""Test auth with token (invalid token)"""
self.assertIsNone(
TokenBackend().authenticate(self.request, "test-user", self.token.key + "foo"),
self.user,
)

View file

@ -15,6 +15,7 @@ from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
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
from authentik.stages.user_write.signals import user_write
@ -46,7 +47,13 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
if PLAN_CONTEXT_SOURCE in flow_plan.context:
# Login request came from an external source, save it in the context
thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
thread.kwargs[PLAN_CONTEXT_SOURCE] = flow_plan.context[PLAN_CONTEXT_SOURCE]
if PLAN_CONTEXT_METHOD in flow_plan.context:
thread.kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
# Save the login method used
thread.kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(
PLAN_CONTEXT_METHOD_ARGS, {}
)
thread.user = user
thread.run()

View file

@ -6,7 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.flows.models import FlowDesignation
from authentik.stages.identification.models import UserFields
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
@ -26,7 +26,7 @@ def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSc
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
name="default-authentication-password",
defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP]},
defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
)
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(

View file

@ -3,7 +3,6 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.cache import cache
from django.http import HttpRequest
from django.test import RequestFactory, TestCase
from django.urls import reverse
from guardian.shortcuts import get_anonymous_user
@ -13,6 +12,7 @@ from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableExce
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.lib.tests.utils import dummy_get_response
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.policies.types import PolicyResult
@ -24,11 +24,6 @@ CACHE_MOCK = Mock(wraps=cache)
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
def dummy_get_response(request: HttpRequest): # pragma: no cover
"""Dummy get_response for SessionMiddleware"""
return None
class TestFlowPlanner(TestCase):
"""Test planner logic"""

View file

@ -25,6 +25,8 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]:
"component",
"flow_set",
"promptstage_set",
"policybindingmodel_ptr_id",
"export_url",
)
for to_remove_name in to_remove:
if to_remove_name in data:

View file

@ -0,0 +1,27 @@
"""Test utils"""
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpRequest
from django.test.client import RequestFactory
from guardian.utils import get_anonymous_user
def dummy_get_response(request: HttpRequest): # pragma: no cover
"""Dummy get_response for SessionMiddleware"""
return None
def get_request(*args, user=None, **kwargs):
"""Get a request with usable session"""
request = RequestFactory().get(*args, **kwargs)
if user:
request.user = user
else:
request.user = get_anonymous_user()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
middleware = MessageMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
return request

View file

@ -1,16 +1,14 @@
"""Test AuthN Request generator and parser"""
from base64 import b64encode
from django.contrib.sessions.middleware import SessionMiddleware
from django.http.request import QueryDict
from django.test import RequestFactory, TestCase
from guardian.utils import get_anonymous_user
from authentik.core.models import User
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow
from authentik.flows.tests.test_planner import dummy_get_response
from authentik.lib.tests.utils import get_request
from authentik.managed.manager import ObjectManager
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor
@ -99,11 +97,7 @@ class TestAuthNRequest(TestCase):
def test_signed_valid(self):
"""Test generated AuthNRequest with valid signature"""
http_request = self.factory.get("/")
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
http_request = get_request("/")
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
@ -117,12 +111,7 @@ class TestAuthNRequest(TestCase):
def test_request_full_signed(self):
"""Test full SAML Request/Response flow, fully signed"""
http_request = self.factory.get("/")
http_request.user = get_anonymous_user()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
http_request = get_request("/")
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
@ -145,12 +134,7 @@ class TestAuthNRequest(TestCase):
def test_request_id_invalid(self):
"""Test generated AuthNRequest with invalid request ID"""
http_request = self.factory.get("/")
http_request.user = get_anonymous_user()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
http_request = get_request("/")
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
@ -179,11 +163,7 @@ class TestAuthNRequest(TestCase):
def test_signed_valid_detached(self):
"""Test generated AuthNRequest with valid signature (detached)"""
http_request = self.factory.get("/")
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
http_request = get_request("/")
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
@ -243,12 +223,7 @@ class TestAuthNRequest(TestCase):
def test_request_attributes(self):
"""Test full SAML Request/Response flow, fully signed"""
http_request = self.factory.get("/")
http_request.user = User.objects.get(username="akadmin")
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
http_request = get_request("/", user=User.objects.get(username="akadmin"))
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
@ -264,12 +239,7 @@ class TestAuthNRequest(TestCase):
def test_request_attributes_invalid(self):
"""Test full SAML Request/Response flow, fully signed"""
http_request = self.factory.get("/")
http_request.user = User.objects.get(username="akadmin")
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
http_request = get_request("/", user=User.objects.get(username="akadmin"))
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")

View file

@ -1,18 +1,16 @@
"""Test Requests and Responses against schema"""
from base64 import b64encode
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import RequestFactory, TestCase
from guardian.utils import get_anonymous_user
from lxml import etree # nosec
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.lib.tests.utils import get_request
from authentik.managed.manager import ObjectManager
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
from authentik.providers.saml.tests.test_auth_n_request import dummy_get_response
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.request import RequestProcessor
@ -43,11 +41,7 @@ class TestSchema(TestCase):
def test_request_schema(self):
"""Test generated AuthNRequest against Schema"""
http_request = self.factory.get("/")
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
http_request = get_request("/")
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")
@ -60,12 +54,7 @@ class TestSchema(TestCase):
def test_response_schema(self):
"""Test generated AuthNRequest against Schema"""
http_request = self.factory.get("/")
http_request.user = get_anonymous_user()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(http_request)
http_request.session.save()
http_request = get_request("/")
# First create an AuthNRequest
request_proc = RequestProcessor(self.source, http_request, "test_state")

View file

@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
from django.views import View
from authentik.core.models import Token, TokenIntents
from authentik.stages.password import BACKEND_DJANGO
from authentik.stages.password import BACKEND_INBUILT
class UseTokenView(View):
@ -19,7 +19,7 @@ class UseTokenView(View):
if not tokens.exists():
raise Http404
token = tokens.first()
login(request, token.user, backend=BACKEND_DJANGO)
login(request, token.user, backend=BACKEND_INBUILT)
token.delete()
messages.warning(request, _("Used recovery-link to authenticate."))
return redirect("authentik_core:if-admin")

View file

@ -73,7 +73,8 @@ LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}"
SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}"
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"authentik.core.auth.InbuiltBackend",
"authentik.core.auth.TokenBackend",
"guardian.backends.ObjectPermissionBackend",
]

View file

@ -2,10 +2,10 @@
from typing import Optional
import ldap3
from django.contrib.auth.backends import ModelBackend
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.auth import InbuiltBackend
from authentik.core.models import User
from authentik.sources.ldap.models import LDAPSource
@ -13,7 +13,7 @@ LOGGER = get_logger()
LDAP_DISTINGUISHED_NAME = "distinguishedName"
class LDAPBackend(ModelBackend):
class LDAPBackend(InbuiltBackend):
"""Authenticate users against LDAP Server"""
def authenticate(self, request: HttpRequest, **kwargs):
@ -24,6 +24,7 @@ class LDAPBackend(ModelBackend):
LOGGER.debug("LDAP Auth attempt", source=source)
user = self.auth_user(source, **kwargs)
if user:
self.set_method("ldap", request, source=source)
return user
return None

View file

@ -1,10 +1,6 @@
"""LDAP Settings"""
from celery.schedules import crontab
AUTHENTICATION_BACKENDS = [
"authentik.sources.ldap.auth.LDAPBackend",
]
CELERY_BEAT_SCHEDULE = {
"sources_ldap_sync": {
"task": "authentik.sources.ldap.tasks.ldap_sync_all",

View file

@ -39,7 +39,7 @@ from authentik.sources.saml.processors.constants import (
from authentik.sources.saml.processors.request import SESSION_REQUEST_ID
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.stages.user_login.stage import BACKEND_DJANGO
from authentik.stages.user_login.stage import BACKEND_INBUILT
LOGGER = get_logger()
if TYPE_CHECKING:
@ -136,7 +136,7 @@ class ResponseProcessor:
self._source.authentication_flow,
**{
PLAN_CONTEXT_PENDING_USER: user,
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
},
)
@ -199,7 +199,7 @@ class ResponseProcessor:
self._source.authentication_flow,
**{
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
PLAN_CONTEXT_REDIRECT: final_redirect,
},
)

View file

@ -1,7 +1,6 @@
"""Test validator stage"""
from unittest.mock import MagicMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase
from django.test.client import RequestFactory
from django.urls.base import reverse
@ -12,8 +11,8 @@ from rest_framework.exceptions import ValidationError
from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
from authentik.flows.tests.test_planner import dummy_get_response
from authentik.lib.generators import generate_id, generate_key
from authentik.lib.tests.utils import get_request
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
from authentik.stages.authenticator_validate.challenge import (
@ -97,11 +96,8 @@ class AuthenticatorValidateStageTests(TestCase):
def test_device_challenge_webauthn(self):
"""Test webauthn"""
request = self.request_factory.get("/")
request = get_request("/")
request.user = self.user
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
webauthn_device = WebAuthnDevice.objects.create(
user=self.user,

View file

@ -9,7 +9,7 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.lib.generators import generate_key
from authentik.sources.oauth.models import OAuthSource
from authentik.stages.identification.models import IdentificationStage, UserFields
from authentik.stages.password import BACKEND_DJANGO
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.models import PasswordStage
@ -68,7 +68,7 @@ class TestIdentificationStage(TestCase):
def test_valid_with_password(self):
"""Test with valid email and password in single step"""
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
self.stage.password_stage = pw_stage
self.stage.save()
form_data = {"uid_field": self.user.email, "password": self.password}
@ -86,7 +86,7 @@ class TestIdentificationStage(TestCase):
def test_invalid_with_password(self):
"""Test with valid email and invalid password in single step"""
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
self.stage.password_stage = pw_stage
self.stage.save()
form_data = {

View file

@ -17,7 +17,7 @@ from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.invitation.models import Invitation, InvitationStage
from authentik.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT
from authentik.stages.password import BACKEND_DJANGO
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
@ -45,7 +45,7 @@ class TestUserLoginStage(TestCase):
"""Test without any invitation, continue_flow_without_invitation not set."""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
@ -74,7 +74,7 @@ class TestUserLoginStage(TestCase):
self.stage.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

View file

@ -1,3 +1,4 @@
"""Backend paths"""
BACKEND_DJANGO = "django.contrib.auth.backends.ModelBackend"
BACKEND_INBUILT = "authentik.core.auth.InbuiltBackend"
BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend"
BACKEND_APP_PASSWORD = "authentik.core.auth.TokenBackend" # nosec

View file

@ -0,0 +1,60 @@
# Generated by Django 3.2.6 on 2021-08-23 14:34
import django.contrib.postgres.fields
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
PasswordStage = apps.get_model("authentik_stages_password", "passwordstage")
db_alias = schema_editor.connection.alias
stages = PasswordStage.objects.using(db_alias).filter(name="default-authentication-password")
if not stages.exists():
return
stage = stages.first()
stage.backends.append(BACKEND_APP_PASSWORD)
stage.save()
def replace_inbuilt(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
PasswordStage = apps.get_model("authentik_stages_password", "passwordstage")
db_alias = schema_editor.connection.alias
for stage in PasswordStage.objects.using(db_alias).all():
if "django.contrib.auth.backends.ModelBackend" not in stage.backends:
continue
stage.backends.remove("django.contrib.auth.backends.ModelBackend")
stage.backends.append(BACKEND_INBUILT)
stage.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_password", "0006_passwordchange_rename"),
]
operations = [
migrations.AlterField(
model_name="passwordstage",
name="backends",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(
choices=[
("authentik.core.auth.InbuiltBackend", "User database + standard password"),
("authentik.core.auth.TokenBackend", "User database + app passwords"),
(
"authentik.sources.ldap.auth.LDAPBackend",
"User database + LDAP password",
),
]
),
help_text="Selection of backends to test the password against.",
size=None,
),
),
migrations.RunPython(update_default_backends),
]

View file

@ -9,19 +9,23 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, Stage
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
def get_authentication_backends():
"""Return all available authentication backends as tuple set"""
return [
(
BACKEND_DJANGO,
_("authentik-internal Userdatabase"),
BACKEND_INBUILT,
_("User database + standard password"),
),
(
BACKEND_APP_PASSWORD,
_("User database + app passwords"),
),
(
BACKEND_LDAP,
_("authentik LDAP"),
_("User database + LDAP password"),
),
]

View file

@ -27,6 +27,8 @@ from authentik.stages.password.models import PasswordStage
LOGGER = get_logger()
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
PLAN_CONTEXT_METHOD = "auth_method"
PLAN_CONTEXT_METHOD_ARGS = "auth_method_args"
SESSION_INVALID_TRIES = "user_invalid_tries"

View file

@ -14,7 +14,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.lib.generators import generate_key
from authentik.stages.password import BACKEND_DJANGO
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.models import PasswordStage
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
@ -36,7 +36,7 @@ class TestPasswordStage(TestCase):
slug="test-password",
designation=FlowDesignation.AUTHENTICATION,
)
self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
@patch(
@ -158,7 +158,7 @@ class TestPasswordStage(TestCase):
TO_STAGE_RESPONSE_MOCK,
)
@patch(
"django.contrib.auth.backends.ModelBackend.authenticate",
"authentik.core.auth.InbuiltBackend.authenticate",
MOCK_BACKEND_AUTHENTICATE,
)
def test_permission_denied(self):

View file

@ -8,7 +8,7 @@ from structlog.stdlib import get_logger
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
from authentik.lib.utils.time import timedelta_from_string
from authentik.stages.password import BACKEND_DJANGO
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
LOGGER = get_logger()
@ -26,7 +26,7 @@ class UserLoginStageView(StageView):
LOGGER.debug(message)
return self.executor.stage_invalid()
backend = self.executor.plan.context.get(
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_DJANGO
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT
)
login(
self.request,

View file

@ -9,7 +9,7 @@ from authentik.flows.markers import StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.stages.password import BACKEND_DJANGO
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.user_logout.models import UserLogoutStage
@ -34,7 +34,7 @@ class TestUserLogoutStage(TestCase):
"""Test with a valid pending user and backend"""
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()

View file

@ -1,7 +1,6 @@
"""Write stage logic"""
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.backends import ModelBackend
from django.db import transaction
from django.db.utils import IntegrityError
from django.http import HttpRequest, HttpResponse
@ -13,7 +12,7 @@ from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnec
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
from authentik.lib.utils.reflection import class_to_path
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.stages.user_write.signals import user_write
@ -42,9 +41,7 @@ class UserWriteStageView(StageView):
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
is_active=not self.executor.current_stage.create_users_as_inactive
)
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = class_to_path(
ModelBackend
)
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
LOGGER.debug(
"Created new user",
flow_slug=self.executor.flow.slug,

View file

@ -2515,6 +2515,7 @@ paths:
type: string
enum:
- api
- app_password
- recovery
- verification
- name: ordering
@ -20381,7 +20382,8 @@ components:
- url
BackendsEnum:
enum:
- django.contrib.auth.backends.ModelBackend
- authentik.core.auth.InbuiltBackend
- authentik.core.auth.TokenBackend
- authentik.sources.ldap.auth.LDAPBackend
type: string
BindingTypeEnum:
@ -22211,6 +22213,7 @@ components:
- verification
- api
- recovery
- app_password
type: string
InvalidResponseActionEnum:
enum:

View file

@ -3,7 +3,7 @@ version: '3.7'
services:
postgresql:
container_name: postgres
image: library/postgres:11
image: library/postgres:12
volumes:
- db-data:/var/lib/postgresql/data
environment:

View file

@ -3,7 +3,7 @@ version: '3.7'
services:
postgresql:
container_name: postgres
image: library/postgres:11
image: library/postgres:12
volumes:
- db-data:/var/lib/postgresql/data
environment:

View file

@ -4,5 +4,6 @@ from yaml import safe_dump
with open("local.env.yml", "w") as _config:
safe_dump({
"secret_key": generate_id()
"log_level": "debug",
"secret_key": generate_id(),
}, _config, default_flow_style=False)

View file

@ -90,6 +90,7 @@ export default [
// Main Application
{
input: "./src/interfaces/AdminInterface.ts",
context: "window",
output: [
{
format: "es",
@ -122,6 +123,7 @@ export default [
// Flow executor
{
input: "./src/interfaces/FlowInterface.ts",
context: "window",
output: [
{
format: "es",

View file

@ -63,6 +63,10 @@ msgstr "ANY, any policy must match to grant access."
msgid "ANY, any policy must match to include this stage access."
msgstr "ANY, any policy must match to include this stage access."
#: src/pages/tokens/TokenListPage.ts
msgid "API Access"
msgstr "API Access"
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
msgid "API Hostname"
msgstr "API Hostname"
@ -231,6 +235,10 @@ msgstr "Always require consent"
msgid "App"
msgstr "App"
#: src/pages/tokens/TokenListPage.ts
msgid "App password"
msgstr "App password"
#: src/elements/user/UserConsentList.ts
#: src/pages/admin-overview/TopApplicationsTable.ts
#: src/pages/providers/ProviderListPage.ts
@ -952,6 +960,11 @@ msgstr "Copy recovery link"
msgid "Create"
msgstr "Create"
#: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/user-settings/tokens/UserTokenList.ts
msgid "Create App password"
msgstr "Create App password"
#: src/pages/applications/ApplicationListPage.ts
#: src/pages/providers/RelatedApplicationButton.ts
msgid "Create Application"
@ -1018,6 +1031,7 @@ msgstr "Create Stage binding"
msgid "Create Tenant"
msgstr "Create Tenant"
#: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/user-settings/tokens/UserTokenList.ts
msgid "Create Token"
msgstr "Create Token"
@ -2047,6 +2061,11 @@ msgstr "Integration key"
msgid "Integrations"
msgstr "Integrations"
#: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/tokens/UserTokenList.ts
msgid "Intent"
msgstr "Intent"
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "Internal Host"
msgstr "Internal Host"
@ -2807,6 +2826,7 @@ msgstr "Optionally set this to your parent domain, if you want authentication an
#: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts
#: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/PolicyBindingForm.ts
#: src/pages/stages/prompt/PromptForm.ts
#: src/pages/stages/prompt/PromptListPage.ts
@ -2954,6 +2974,7 @@ msgstr "Policy / Group / User Bindings"
msgid "Policy / Policies"
msgstr "Policy / Policies"
#: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/BoundPoliciesList.ts
msgid "Policy / User / Group"
msgstr "Policy / User / Group"
@ -3203,6 +3224,7 @@ msgid "Receive a push notification on your phone to prove your identity."
msgstr "Receive a push notification on your phone to prove your identity."
#: src/pages/flows/FlowForm.ts
#: src/pages/tokens/TokenListPage.ts
#: src/pages/users/UserListPage.ts
msgid "Recovery"
msgstr "Recovery"
@ -3696,6 +3718,7 @@ msgstr "Sources"
msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
msgstr "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
#: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts
msgid "Stage"
msgstr "Stage"
@ -3716,6 +3739,10 @@ msgstr "Stage Configuration"
msgid "Stage binding(s)"
msgstr "Stage binding(s)"
#: src/pages/flows/BoundStagesList.ts
msgid "Stage type"
msgstr "Stage type"
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
msgstr "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
@ -4388,10 +4415,13 @@ msgstr "Token(s)"
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/UserSettingsPage.ts
msgid "Tokens"
msgstr "Tokens"
#: src/pages/user-settings/UserSettingsPage.ts
msgid "Tokens and App passwords"
msgstr "Tokens and App passwords"
#: src/pages/tokens/TokenListPage.ts
msgid "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access."
msgstr "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access."
@ -4740,6 +4770,18 @@ msgstr "User Reputation"
msgid "User Settings"
msgstr "User Settings"
#: src/pages/stages/password/PasswordStageForm.ts
msgid "User database + LDAP password"
msgstr "User database + LDAP password"
#: src/pages/stages/password/PasswordStageForm.ts
msgid "User database + app passwords"
msgstr "User database + app passwords"
#: src/pages/stages/password/PasswordStageForm.ts
msgid "User database + standard password"
msgstr "User database + standard password"
#: src/pages/user-settings/UserSettingsPage.ts
msgid "User details"
msgstr "User details"
@ -4859,6 +4901,10 @@ msgstr "Validation Policies"
msgid "Validity days"
msgstr "Validity days"
#: src/pages/tokens/TokenListPage.ts
msgid "Verification"
msgstr "Verification"
#: src/pages/providers/saml/SAMLProviderForm.ts
msgid "Verification Certificate"
msgstr "Verification Certificate"
@ -5022,13 +5068,13 @@ msgstr "You can only select providers that match the type of the outpost."
msgid "You're currently impersonating {0}. Click to stop."
msgstr "You're currently impersonating {0}. Click to stop."
#: src/pages/stages/password/PasswordStageForm.ts
msgid "authentik Builtin Database"
msgstr "authentik Builtin Database"
#:
#~ msgid "authentik Builtin Database"
#~ msgstr "authentik Builtin Database"
#: src/pages/stages/password/PasswordStageForm.ts
msgid "authentik LDAP Backend"
msgstr "authentik LDAP Backend"
#:
#~ msgid "authentik LDAP Backend"
#~ msgstr "authentik LDAP Backend"
#: src/elements/forms/DeleteForm.ts
msgid "connecting object will be deleted"

View file

@ -63,6 +63,10 @@ msgstr ""
msgid "ANY, any policy must match to include this stage access."
msgstr ""
#: src/pages/tokens/TokenListPage.ts
msgid "API Access"
msgstr ""
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
msgid "API Hostname"
msgstr ""
@ -231,6 +235,10 @@ msgstr ""
msgid "App"
msgstr ""
#: src/pages/tokens/TokenListPage.ts
msgid "App password"
msgstr ""
#: src/elements/user/UserConsentList.ts
#: src/pages/admin-overview/TopApplicationsTable.ts
#: src/pages/providers/ProviderListPage.ts
@ -946,6 +954,11 @@ msgstr ""
msgid "Create"
msgstr ""
#: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/user-settings/tokens/UserTokenList.ts
msgid "Create App password"
msgstr ""
#: src/pages/applications/ApplicationListPage.ts
#: src/pages/providers/RelatedApplicationButton.ts
msgid "Create Application"
@ -1012,6 +1025,7 @@ msgstr ""
msgid "Create Tenant"
msgstr ""
#: src/pages/user-settings/tokens/UserTokenList.ts
#: src/pages/user-settings/tokens/UserTokenList.ts
msgid "Create Token"
msgstr ""
@ -2039,6 +2053,11 @@ msgstr ""
msgid "Integrations"
msgstr ""
#: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/tokens/UserTokenList.ts
msgid "Intent"
msgstr ""
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "Internal Host"
msgstr ""
@ -2799,6 +2818,7 @@ msgstr ""
#: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts
#: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/PolicyBindingForm.ts
#: src/pages/stages/prompt/PromptForm.ts
#: src/pages/stages/prompt/PromptListPage.ts
@ -2946,6 +2966,7 @@ msgstr ""
msgid "Policy / Policies"
msgstr ""
#: src/pages/policies/BoundPoliciesList.ts
#: src/pages/policies/BoundPoliciesList.ts
msgid "Policy / User / Group"
msgstr ""
@ -3195,6 +3216,7 @@ msgid "Receive a push notification on your phone to prove your identity."
msgstr ""
#: src/pages/flows/FlowForm.ts
#: src/pages/tokens/TokenListPage.ts
#: src/pages/users/UserListPage.ts
msgid "Recovery"
msgstr ""
@ -3688,6 +3710,7 @@ msgstr ""
msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
msgstr ""
#: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts
msgid "Stage"
msgstr ""
@ -3708,6 +3731,10 @@ msgstr ""
msgid "Stage binding(s)"
msgstr ""
#: src/pages/flows/BoundStagesList.ts
msgid "Stage type"
msgstr ""
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
msgstr ""
@ -4373,10 +4400,13 @@ msgstr ""
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/tokens/TokenListPage.ts
#: src/pages/user-settings/UserSettingsPage.ts
msgid "Tokens"
msgstr ""
#: src/pages/user-settings/UserSettingsPage.ts
msgid "Tokens and App passwords"
msgstr ""
#: src/pages/tokens/TokenListPage.ts
msgid "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access."
msgstr ""
@ -4725,6 +4755,18 @@ msgstr ""
msgid "User Settings"
msgstr ""
#: src/pages/stages/password/PasswordStageForm.ts
msgid "User database + LDAP password"
msgstr ""
#: src/pages/stages/password/PasswordStageForm.ts
msgid "User database + app passwords"
msgstr ""
#: src/pages/stages/password/PasswordStageForm.ts
msgid "User database + standard password"
msgstr ""
#: src/pages/user-settings/UserSettingsPage.ts
msgid "User details"
msgstr ""
@ -4844,6 +4886,10 @@ msgstr ""
msgid "Validity days"
msgstr ""
#: src/pages/tokens/TokenListPage.ts
msgid "Verification"
msgstr ""
#: src/pages/providers/saml/SAMLProviderForm.ts
msgid "Verification Certificate"
msgstr ""
@ -5005,13 +5051,13 @@ msgstr ""
msgid "You're currently impersonating {0}. Click to stop."
msgstr ""
#: src/pages/stages/password/PasswordStageForm.ts
msgid "authentik Builtin Database"
msgstr ""
#:
#~ msgid "authentik Builtin Database"
#~ msgstr ""
#: src/pages/stages/password/PasswordStageForm.ts
msgid "authentik LDAP Backend"
msgstr ""
#:
#~ msgid "authentik LDAP Backend"
#~ msgstr ""
#: src/elements/forms/DeleteForm.ts
msgid "connecting object will be deleted"

View file

@ -48,6 +48,12 @@ export class BoundStagesList extends Table<FlowStageBinding> {
return html`<ak-forms-delete-bulk
objectLabel=${t`Stage binding(s)`}
.objects=${this.selectedElements}
.metadata=${(item: FlowStageBinding) => {
return [
{ key: t`Stage`, value: item.stageObj?.name },
{ key: t`Stage type`, value: item.stageObj?.verboseName },
];
}}
.usedBy=${(item: FlowStageBinding) => {
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUsedByList({
fsbUuid: item.pk,

View file

@ -100,6 +100,12 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
return html`<ak-forms-delete-bulk
objectLabel=${t`Policy binding(s)`}
.objects=${this.selectedElements}
.metadata=${(item: PolicyBinding) => {
return [
{ key: t`Order`, value: item.order.toString() },
{ key: t`Policy / User / Group`, value: this.getPolicyUserGroupRow(item) },
];
}}
.usedBy=${(item: PolicyBinding) => {
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsUsedByList({
policyBindingUuid: item.pk,

View file

@ -156,11 +156,20 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
.sourcesAllList({})
.then((sources) => {
return sources.results.map((source) => {
const selected = Array.from(
let selected = Array.from(
this.instance?.sources || [],
).some((su) => {
return su == source.pk;
});
// Creating a new instance, auto-select built-in source
// Only when no other sources exist
if (
!this.instance &&
source.component === "" &&
sources.results.length < 2
) {
selected = true;
}
return html`<option
value=${ifDefined(source.pk)}
?selected=${selected}

View file

@ -46,8 +46,11 @@ export class PasswordStageForm extends ModelForm<PasswordStage, string> {
};
isBackendSelected(field: BackendsEnum): boolean {
if (!this.instance) {
return true;
}
return (
(this.instance?.backends || []).filter((isField) => {
this.instance.backends.filter((isField) => {
return field === isField;
}).length > 0
);
@ -76,20 +79,28 @@ export class PasswordStageForm extends ModelForm<PasswordStage, string> {
>
<select name="users" class="pf-c-form-control" multiple>
<option
value=${BackendsEnum.DjangoContribAuthBackendsModelBackend}
value=${BackendsEnum.CoreAuthInbuiltBackend}
?selected=${this.isBackendSelected(
BackendsEnum.DjangoContribAuthBackendsModelBackend,
BackendsEnum.CoreAuthInbuiltBackend,
)}
>
${t`authentik Builtin Database`}
${t`User database + standard password`}
</option>
<option
value=${BackendsEnum.AuthentikSourcesLdapAuthLdapBackend}
value=${BackendsEnum.CoreAuthTokenBackend}
?selected=${this.isBackendSelected(
BackendsEnum.AuthentikSourcesLdapAuthLdapBackend,
BackendsEnum.CoreAuthTokenBackend,
)}
>
${t`authentik LDAP Backend`}
${t`User database + app passwords`}
</option>
<option
value=${BackendsEnum.SourcesLdapAuthLdapBackend}
?selected=${this.isBackendSelected(
BackendsEnum.SourcesLdapAuthLdapBackend,
)}
>
${t`User database + LDAP password`}
</option>
</select>
<p class="pf-c-form__helper-text">

View file

@ -8,9 +8,22 @@ import "../../elements/buttons/TokenCopyButton";
import "../../elements/forms/DeleteBulkForm";
import { TableColumn } from "../../elements/table/Table";
import { PAGE_SIZE } from "../../constants";
import { CoreApi, Token } from "@goauthentik/api";
import { CoreApi, IntentEnum, Token } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../api/Config";
export function IntentToLabel(intent: IntentEnum): string {
switch (intent) {
case IntentEnum.Api:
return t`API Access`;
case IntentEnum.AppPassword:
return t`App password`;
case IntentEnum.Recovery:
return t`Recovery`;
case IntentEnum.Verification:
return t`Verification`;
}
}
@customElement("ak-token-list")
export class TokenListPage extends TablePage<Token> {
searchEnabled(): boolean {
@ -46,6 +59,7 @@ export class TokenListPage extends TablePage<Token> {
new TableColumn(t`User`, "user"),
new TableColumn(t`Expires?`, "expiring"),
new TableColumn(t`Expiry date`, "expires"),
new TableColumn(t`Intent`, "intent"),
new TableColumn(t`Actions`),
];
}
@ -78,6 +92,7 @@ export class TokenListPage extends TablePage<Token> {
html`${item.user?.username}`,
html`${item.expiring ? t`Yes` : t`No`}`,
html`${item.expiring ? item.expires?.toLocaleString() : "-"}`,
html`${IntentToLabel(item.intent || IntentEnum.Api)}`,
html`
<ak-token-copy-button identifier="${item.identifier}">
${t`Copy Key`}

View file

@ -148,7 +148,7 @@ export class UserSettingsPage extends LitElement {
</section>
<section
slot="page-tokens"
data-tab-title="${t`Tokens`}"
data-tab-title="${t`Tokens and App passwords`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<ak-user-token-list></ak-user-token-list>

View file

@ -1,6 +1,6 @@
import { CoreApi, Token } from "@goauthentik/api";
import { CoreApi, IntentEnum, Token } from "@goauthentik/api";
import { t } from "@lingui/macro";
import { customElement } from "lit-element";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
@ -9,6 +9,9 @@ import { ModelForm } from "../../../elements/forms/ModelForm";
@customElement("ak-user-token-form")
export class UserTokenForm extends ModelForm<Token, string> {
@property()
intent: IntentEnum = IntentEnum.Api;
loadInstance(pk: string): Promise<Token> {
return new CoreApi(DEFAULT_CONFIG).coreTokensRetrieve({
identifier: pk,
@ -30,6 +33,7 @@ export class UserTokenForm extends ModelForm<Token, string> {
tokenRequest: data,
});
} else {
data.intent = this.intent;
return new CoreApi(DEFAULT_CONFIG).coreTokensCreate({
tokenRequest: data,
});

View file

@ -10,9 +10,10 @@ import "../../../elements/buttons/Dropdown";
import "../../../elements/buttons/TokenCopyButton";
import { Table, TableColumn } from "../../../elements/table/Table";
import { PAGE_SIZE } from "../../../constants";
import { CoreApi, Token } from "@goauthentik/api";
import { CoreApi, IntentEnum, Token } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import "./UserTokenForm";
import { IntentToLabel } from "../../tokens/TokenListPage";
@customElement("ak-user-token-list")
export class UserTokenList extends Table<Token> {
@ -48,8 +49,19 @@ export class UserTokenList extends Table<Token> {
<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create Token`} </span>
<ak-user-token-form slot="form"> </ak-user-token-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
<ak-user-token-form intent=${IntentEnum.Api} slot="form"> </ak-user-token-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${t`Create Token`}
</button>
</ak-forms-modal>
<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create App password`} </span>
<ak-user-token-form intent=${IntentEnum.AppPassword} slot="form">
</ak-user-token-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${t`Create App password`}
</button>
</ak-forms-modal>
${super.renderToolbar()}
`;
@ -89,6 +101,16 @@ export class UserTokenList extends Table<Token> {
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Intent`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${IntentToLabel(item.intent || IntentEnum.Api)}
</div>
</dd>
</div>
</dl>
</div>
</td>

View file

@ -59,3 +59,31 @@ This includes the following:
- `prompt_data`: Data which has been saved from a prompt stage or an external source.
- `application`: The application the user is in the process of authorizing.
- `pending_user`: The currently pending user, see [User](/docs/expressions/reference/user-object)
- `auth_method`: Authentication method set (this value is set by password stages)
Depending on method, `auth_method_args` is also set.
Can be any of:
- `password`: Standard password login
- `app_password`: App passowrd (token)
Sets `auth_method_args` to
```json
{
"token": {
"pk": "f6d639aac81940f38dcfdc6e0fe2a786",
"app": "authentik_core",
"name": "test (expires=2021-08-23 15:45:54.725880+00:00)",
"model_name": "token"
}
}
```
- `ldap`: LDAP bind authentication
Sets `auth_method_args` to
```json
{
"source": {} // Information about the source used
}
```

View file

@ -133,7 +133,7 @@
"pk": "34e1e7d5-8eed-4549-bc7a-305069ff7df0",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "20375f30-7fa7-4562-8f6e-0f61889f2963",
"order": 0
"order": 10
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -145,7 +145,7 @@
"pk": "e40467a6-3052-488c-a1b5-1ad7a80fe7b3",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "6c342b94-790d-425a-ae31-6196b6570722",
"order": 1
"order": 11
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -157,7 +157,7 @@
"pk": "76bc594e-2715-49ab-bd40-994abd9a7b70",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "a4090add-f483-4ac6-8917-10b493ef843e",
"order": 2
"order": 20
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -169,7 +169,7 @@
"pk": "2f324f6d-7646-4108-a6e2-e7f90985477f",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "77090897-eb3f-40db-81e6-b4074b1998c4",
"order": 3
"order": 100
},
"model": "authentik_flows.flowstagebinding",
"attrs": {

View file

@ -154,7 +154,7 @@
"pk": "34e1e7d5-8eed-4549-bc7a-305069ff7df0",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "20375f30-7fa7-4562-8f6e-0f61889f2963",
"order": 0
"order": 10
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -166,7 +166,7 @@
"pk": "e40467a6-3052-488c-a1b5-1ad7a80fe7b3",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "6c342b94-790d-425a-ae31-6196b6570722",
"order": 1
"order": 11
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -178,7 +178,7 @@
"pk": "76bc594e-2715-49ab-bd40-994abd9a7b70",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "a4090add-f483-4ac6-8917-10b493ef843e",
"order": 2
"order": 20
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -190,7 +190,7 @@
"pk": "1db34a14-8985-4184-b5c9-254cd585d94f",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "096e6282-6b30-4695-bd03-3b143eab5580",
"order": 3
"order": 30
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -202,7 +202,7 @@
"pk": "2f324f6d-7646-4108-a6e2-e7f90985477f",
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
"stage": "77090897-eb3f-40db-81e6-b4074b1998c4",
"order": 4
"order": 40
},
"model": "authentik_flows.flowstagebinding",
"attrs": {

View file

@ -13,6 +13,18 @@
"designation": "authentication"
}
},
{
"identifiers": {
"pk": "7db93f1e-788b-4af6-8dc6-5cdeb59d8be7"
},
"model": "authentik_policies_expression.expressionpolicy",
"attrs": {
"name": "test-not-app-password",
"execution_logging": false,
"bound_to": 1,
"expression": "return auth_method != \"app_password\""
}
},
{
"identifiers": {
"pk": "69d41125-3987-499b-8d74-ef27b54b88c8",
@ -39,7 +51,7 @@
{
"identifiers": {
"pk": "37f709c3-8817-45e8-9a93-80a925d293c2",
"name": "default-authentication-flow-totp"
"name": "default-authentication-flow-mfa"
},
"model": "authentik_stages_authenticator_validate.AuthenticatorValidateStage",
"attrs": {}
@ -52,7 +64,8 @@
"model": "authentik_stages_password.passwordstage",
"attrs": {
"backends": [
"django.contrib.auth.backends.ModelBackend",
"authentik.core.auth.InbuiltBackend",
"authentik.core.auth.TokenBackend",
"authentik.sources.ldap.auth.LDAPBackend"
]
}
@ -62,7 +75,7 @@
"pk": "a3056482-b692-4e3a-93f1-7351c6a351c7",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "5f594f27-0def-488d-9855-fe604eb13de5",
"order": 0
"order": 10
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -74,7 +87,7 @@
"pk": "4e8538cf-3e18-4a68-82ae-6df6725fa2e6",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "d8affa62-500c-4c5c-a01f-5835e1ffdf40",
"order": 1
"order": 20
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -86,7 +99,22 @@
"pk": "688aec6f-5622-42c6-83a5-d22072d7e798",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "37f709c3-8817-45e8-9a93-80a925d293c2",
"order": 2
"order": 30
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
"evaluate_on_plan": false,
"re_evaluate_policies": true,
"policy_engine_mode": "any",
"invalid_response_action": "retry"
}
},
{
"identifiers": {
"pk": "f3fede3a-a9b5-4232-9ec7-be7ff4194b27",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "69d41125-3987-499b-8d74-ef27b54b88c8",
"order": 100
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -95,14 +123,16 @@
},
{
"identifiers": {
"pk": "f3fede3a-a9b5-4232-9ec7-be7ff4194b27",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "69d41125-3987-499b-8d74-ef27b54b88c8",
"order": 3
"pk": "6e40ae4d-a4ed-4bd7-a784-27b1fe5859d2",
"policy": "7db93f1e-788b-4af6-8dc6-5cdeb59d8be7",
"target": "688aec6f-5622-42c6-83a5-d22072d7e798",
"order": 0
},
"model": "authentik_flows.flowstagebinding",
"model": "authentik_policies.policybinding",
"attrs": {
"re_evaluate_policies": false
"negate": false,
"enabled": true,
"timeout": 30
}
}
]

View file

@ -55,7 +55,8 @@
"model": "authentik_stages_password.passwordstage",
"attrs": {
"backends": [
"django.contrib.auth.backends.ModelBackend",
"authentik.core.auth.InbuiltBackend",
"authentik.core.auth.TokenBackend",
"authentik.sources.ldap.auth.LDAPBackend"
]
}
@ -65,7 +66,7 @@
"pk": "a3056482-b692-4e3a-93f1-7351c6a351c7",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "5f594f27-0def-488d-9855-fe604eb13de5",
"order": 0
"order": 10
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -77,7 +78,7 @@
"pk": "4e8538cf-3e18-4a68-82ae-6df6725fa2e6",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "d8affa62-500c-4c5c-a01f-5835e1ffdf40",
"order": 1
"order": 20
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -89,7 +90,7 @@
"pk": "3bcd6af0-48a6-4e18-87f3-d251a1a58226",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "a368cafc-1494-45e9-b75b-b5e7ac2bd3e4",
"order": 2
"order": 30
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -102,7 +103,7 @@
"pk": "f3fede3a-a9b5-4232-9ec7-be7ff4194b27",
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
"stage": "69d41125-3987-499b-8d74-ef27b54b88c8",
"order": 3
"order": 100
},
"model": "authentik_flows.flowstagebinding",
"attrs": {

View file

@ -123,7 +123,7 @@
"pk": "7af7558e-2196-4b9f-a08e-d38420b7cfbb",
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
"stage": "e54045a7-6ecb-4ad9-ad37-28e72d8e565e",
"order": 0
"order": 10
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -135,7 +135,7 @@
"pk": "29446fd6-dd93-4e92-9830-2d81debad5ae",
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
"stage": "66f948dc-3f74-42b2-b26b-b8b9df109efb",
"order": 1
"order": 20
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -147,7 +147,7 @@
"pk": "1219d06e-2c06-4c5b-a162-78e3959c6cf0",
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
"stage": "975d5502-1e22-4d10-b560-fbc5bd70ff4d",
"order": 2
"order": 30
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -159,7 +159,7 @@
"pk": "66de86ba-0707-46a0-8475-ff2e260d6935",
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
"stage": "3909fd60-b013-4668-8806-12e9507dab97",
"order": 3
"order": 40
},
"model": "authentik_flows.flowstagebinding",
"attrs": {
@ -171,7 +171,7 @@
"pk": "9cec2334-d4a2-4895-a2b2-bc5ae4e9639a",
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
"stage": "fcdd4206-0d35-4ad2-a59f-5a72422936bb",
"order": 4
"order": 100
},
"model": "authentik_flows.flowstagebinding",
"attrs": {

View file

@ -26,7 +26,7 @@
"pk": "eb9aff2b-b95d-40b3-ad08-233aa77bbcf3",
"target": "59a576ce-2f23-4a63-b63a-d18dc7e550f5",
"stage": "c62ac2a4-2735-4a0f-abd0-8523d68c1209",
"order": 0
"order": 10
},
"model": "authentik_flows.flowstagebinding",
"attrs": {