core: use custom inbuilt backend, set backend login information in flow plan for events
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
2655768f5a
commit
69a0153619
|
@ -0,0 +1,56 @@
|
||||||
|
"""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
|
||||||
|
# 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] = "password"
|
||||||
|
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class TokenBackend(ModelBackend):
|
||||||
|
"""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()
|
||||||
|
# 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] = "app_password"
|
||||||
|
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = {"token": token}
|
||||||
|
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||||
|
return token.user
|
|
@ -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.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
from authentik.policies.utils import delete_none_keys
|
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.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
|
||||||
|
@ -189,7 +189,7 @@ class SourceFlowManager:
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
# Since we authenticate the user by their token, they have no backend set
|
# 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_SSO: True,
|
||||||
PLAN_CONTEXT_SOURCE: self.source,
|
PLAN_CONTEXT_SOURCE: self.source,
|
||||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
"""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
|
|
||||||
|
|
||||||
|
|
||||||
class TokenBackend(ModelBackend):
|
|
||||||
"""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
|
|
||||||
return tokens.first().user
|
|
|
@ -15,6 +15,7 @@ from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
from authentik.flows.views import SESSION_KEY_PLAN
|
||||||
from authentik.stages.invitation.models import Invitation
|
from authentik.stages.invitation.models import Invitation
|
||||||
from authentik.stages.invitation.signals import invitation_used
|
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
|
from authentik.stages.user_write.signals import user_write
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,6 +48,10 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
||||||
if PLAN_CONTEXT_SOURCE in flow_plan.context:
|
if PLAN_CONTEXT_SOURCE in flow_plan.context:
|
||||||
# Login request came from an external source, save it in the 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["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
||||||
|
if PLAN_CONTEXT_METHOD in flow_plan.context:
|
||||||
|
thread.kwargs["method"] = flow_plan.context[PLAN_CONTEXT_METHOD]
|
||||||
|
# Save the login method used
|
||||||
|
thread.kwargs["method_args"] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {})
|
||||||
thread.user = user
|
thread.user = user
|
||||||
thread.run()
|
thread.run()
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.stages.identification.models import UserFields
|
from authentik.stages.identification.models import UserFields
|
||||||
from authentik.stages.password import BACKEND_APP_PASSWORD, 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):
|
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(
|
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
||||||
name="default-authentication-password",
|
name="default-authentication-password",
|
||||||
defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
|
defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
|
||||||
)
|
)
|
||||||
|
|
||||||
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from authentik.core.models import Token, TokenIntents
|
from authentik.core.models import Token, TokenIntents
|
||||||
from authentik.stages.password import BACKEND_DJANGO
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
|
|
||||||
|
|
||||||
class UseTokenView(View):
|
class UseTokenView(View):
|
||||||
|
@ -19,7 +19,7 @@ class UseTokenView(View):
|
||||||
if not tokens.exists():
|
if not tokens.exists():
|
||||||
raise Http404
|
raise Http404
|
||||||
token = tokens.first()
|
token = tokens.first()
|
||||||
login(request, token.user, backend=BACKEND_DJANGO)
|
login(request, token.user, backend=BACKEND_INBUILT)
|
||||||
token.delete()
|
token.delete()
|
||||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||||
return redirect("authentik_core:if-admin")
|
return redirect("authentik_core:if-admin")
|
||||||
|
|
|
@ -73,7 +73,8 @@ LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}"
|
||||||
SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}"
|
SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}"
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
"authentik.core.auth.InbuiltBackend",
|
||||||
|
"authentik.core.auth.TokenBackend",
|
||||||
"guardian.backends.ObjectPermissionBackend",
|
"guardian.backends.ObjectPermissionBackend",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,10 @@ from django.http import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.flows.planner import FlowPlan
|
||||||
|
from authentik.flows.views import SESSION_KEY_PLAN
|
||||||
from authentik.sources.ldap.models import LDAPSource
|
from authentik.sources.ldap.models import LDAPSource
|
||||||
|
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
||||||
|
@ -24,6 +27,13 @@ class LDAPBackend(ModelBackend):
|
||||||
LOGGER.debug("LDAP Auth attempt", source=source)
|
LOGGER.debug("LDAP Auth attempt", source=source)
|
||||||
user = self.auth_user(source, **kwargs)
|
user = self.auth_user(source, **kwargs)
|
||||||
if user:
|
if user:
|
||||||
|
# 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] = "ldap"
|
||||||
|
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = {"source": source}
|
||||||
|
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
"""LDAP Settings"""
|
"""LDAP Settings"""
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
|
||||||
"authentik.sources.ldap.auth.LDAPBackend",
|
|
||||||
]
|
|
||||||
|
|
||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
"sources_ldap_sync": {
|
"sources_ldap_sync": {
|
||||||
"task": "authentik.sources.ldap.tasks.ldap_sync_all",
|
"task": "authentik.sources.ldap.tasks.ldap_sync_all",
|
||||||
|
|
|
@ -39,7 +39,7 @@ from authentik.sources.saml.processors.constants import (
|
||||||
from authentik.sources.saml.processors.request import SESSION_REQUEST_ID
|
from authentik.sources.saml.processors.request import SESSION_REQUEST_ID
|
||||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
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()
|
LOGGER = get_logger()
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -136,7 +136,7 @@ class ResponseProcessor:
|
||||||
self._source.authentication_flow,
|
self._source.authentication_flow,
|
||||||
**{
|
**{
|
||||||
PLAN_CONTEXT_PENDING_USER: user,
|
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,
|
self._source.authentication_flow,
|
||||||
**{
|
**{
|
||||||
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
|
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
|
||||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from authentik.providers.oauth2.generators import generate_client_secret
|
from authentik.providers.oauth2.generators import generate_client_secret
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
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
|
from authentik.stages.password.models import PasswordStage
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class TestIdentificationStage(TestCase):
|
||||||
|
|
||||||
def test_valid_with_password(self):
|
def test_valid_with_password(self):
|
||||||
"""Test with valid email and password in single step"""
|
"""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.password_stage = pw_stage
|
||||||
self.stage.save()
|
self.stage.save()
|
||||||
form_data = {"uid_field": self.user.email, "password": self.password}
|
form_data = {"uid_field": self.user.email, "password": self.password}
|
||||||
|
@ -86,7 +86,7 @@ class TestIdentificationStage(TestCase):
|
||||||
|
|
||||||
def test_invalid_with_password(self):
|
def test_invalid_with_password(self):
|
||||||
"""Test with valid email and invalid password in single step"""
|
"""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.password_stage = pw_stage
|
||||||
self.stage.save()
|
self.stage.save()
|
||||||
form_data = {
|
form_data = {
|
||||||
|
|
|
@ -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.flows.views import SESSION_KEY_PLAN
|
||||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||||
from authentik.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT
|
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
|
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."""
|
"""Test without any invitation, continue_flow_without_invitation not set."""
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
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_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 = self.client.session
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
|
@ -74,7 +74,7 @@ class TestUserLoginStage(TestCase):
|
||||||
self.stage.save()
|
self.stage.save()
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
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_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 = self.client.session
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Backend paths"""
|
"""Backend paths"""
|
||||||
BACKEND_DJANGO = "django.contrib.auth.backends.ModelBackend"
|
BACKEND_INBUILT = "authentik.core.auth.InbuiltBackend"
|
||||||
BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend"
|
BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend"
|
||||||
BACKEND_APP_PASSWORD = "authentik.core.token_auth.TokenBackend" # nosec
|
BACKEND_APP_PASSWORD = "authentik.core.auth.TokenBackend" # nosec
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.apps.registry import Apps
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
from authentik.stages.password import BACKEND_APP_PASSWORD
|
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT
|
||||||
|
|
||||||
|
|
||||||
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
@ -19,6 +19,18 @@ def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor)
|
||||||
stage.save()
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@ -33,11 +45,8 @@ class Migration(migrations.Migration):
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
base_field=models.TextField(
|
base_field=models.TextField(
|
||||||
choices=[
|
choices=[
|
||||||
(
|
("authentik.core.auth.InbuiltBackend", "User database + standard password"),
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
("authentik.core.auth.TokenBackend", "User database + app passwords"),
|
||||||
"User database + standard password",
|
|
||||||
),
|
|
||||||
("authentik.core.token_auth.TokenBackend", "User database + app passwords"),
|
|
||||||
(
|
(
|
||||||
"authentik.sources.ldap.auth.LDAPBackend",
|
"authentik.sources.ldap.auth.LDAPBackend",
|
||||||
"User database + LDAP password",
|
"User database + LDAP password",
|
||||||
|
|
|
@ -9,14 +9,14 @@ from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
from authentik.core.types import UserSettingSerializer
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.flows.models import ConfigurableStage, Stage
|
from authentik.flows.models import ConfigurableStage, Stage
|
||||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_DJANGO, BACKEND_LDAP
|
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||||
|
|
||||||
|
|
||||||
def get_authentication_backends():
|
def get_authentication_backends():
|
||||||
"""Return all available authentication backends as tuple set"""
|
"""Return all available authentication backends as tuple set"""
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
BACKEND_DJANGO,
|
BACKEND_INBUILT,
|
||||||
_("User database + standard password"),
|
_("User database + standard password"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
|
@ -27,6 +27,8 @@ from authentik.stages.password.models import PasswordStage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
|
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
|
||||||
|
PLAN_CONTEXT_METHOD = "method"
|
||||||
|
PLAN_CONTEXT_METHOD_ARGS = "method_args"
|
||||||
SESSION_INVALID_TRIES = "user_invalid_tries"
|
SESSION_INVALID_TRIES = "user_invalid_tries"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
from authentik.flows.views import SESSION_KEY_PLAN
|
||||||
from authentik.providers.oauth2.generators import generate_client_secret
|
from authentik.providers.oauth2.generators import generate_client_secret
|
||||||
from authentik.stages.password import BACKEND_DJANGO
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
from authentik.stages.password.models import PasswordStage
|
from authentik.stages.password.models import PasswordStage
|
||||||
|
|
||||||
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
|
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
|
||||||
|
@ -36,7 +36,7 @@ class TestPasswordStage(TestCase):
|
||||||
slug="test-password",
|
slug="test-password",
|
||||||
designation=FlowDesignation.AUTHENTICATION,
|
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)
|
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
|
|
|
@ -8,7 +8,7 @@ from structlog.stdlib import get_logger
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
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
|
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
@ -26,7 +26,7 @@ class UserLoginStageView(StageView):
|
||||||
LOGGER.debug(message)
|
LOGGER.debug(message)
|
||||||
return self.executor.stage_invalid()
|
return self.executor.stage_invalid()
|
||||||
backend = self.executor.plan.context.get(
|
backend = self.executor.plan.context.get(
|
||||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_DJANGO
|
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT
|
||||||
)
|
)
|
||||||
login(
|
login(
|
||||||
self.request,
|
self.request,
|
||||||
|
|
|
@ -9,7 +9,7 @@ from authentik.flows.markers import StageMarker
|
||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
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.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
from authentik.stages.user_logout.models import UserLogoutStage
|
from authentik.stages.user_logout.models import UserLogoutStage
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class TestUserLogoutStage(TestCase):
|
||||||
"""Test with a valid pending user and backend"""
|
"""Test with a valid pending user and backend"""
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
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_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 = self.client.session
|
||||||
session[SESSION_KEY_PLAN] = plan
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session.save()
|
session.save()
|
||||||
|
|
Reference in New Issue