Skip to content

Commit

Permalink
stages: add WebAuthn stage (#550)
Browse files Browse the repository at this point in the history
* core: add User.uid for globally unique user ID

* admin: fix ?next for Flow list

* stages: add initial webauthn implementation

* web: add ak-flow-submit event to submit flow stage

* web: show error message for webauthn registration

* admin: fix next param not redirecting correctly

* stages/webauthn: remove form

* stages/webauthn: add API

* web: update flow diagram on ak-refresh

* stages/webauthn: add initial authentication

* stages/webauthn: initial authentication implementation

* web: cleanup webauthn utils

* stages: rename otp_* to authenticator and move webauthn to authenticator

* docs: fix broken links

* stages/authenticator_*: fix template paths

* stages/authenticator_validate: add device classes

* stages/authenticator_webauthn: implement django_otp.devices

* stages/authenticator_*: update default stage names

* web: add button to create stage on flow page

* web: don't minify HTML, remove nbsp

* admin: fix typo in stage list

* stages/*: use common base class for stage serializer

* stages/authenticator_*: create default objects after rename

* tests/e2e: adjust stage order
  • Loading branch information
BeryJu committed Feb 17, 2021
1 parent e020b8b commit 8708e48
Show file tree
Hide file tree
Showing 128 changed files with 2,945 additions and 870 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ kubernetes = "*"
docker = "*"
xmlsec = "*"
geoip2 = "*"
webauthn = "*"

[requires]
python_version = "3.9"
Expand Down
114 changes: 108 additions & 6 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion authentik/admin/templates/administration/stage/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ <h1>
<td role="cell">
<ul>
{% for flow in stage.flow_set.all %}
<li>{{ flow.slug }}<</li>
<li>{{ flow.slug }}</li>
{% empty %}
<li>-</li>
{% endfor %}
Expand Down
16 changes: 10 additions & 6 deletions authentik/api/v2/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,18 @@
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
from authentik.sources.oauth.api import OAuthSourceViewSet
from authentik.sources.saml.api import SAMLSourceViewSet
from authentik.stages.authenticator_static.api import AuthenticatorStaticStageViewSet
from authentik.stages.authenticator_totp.api import AuthenticatorTOTPStageViewSet
from authentik.stages.authenticator_validate.api import (
AuthenticatorValidateStageViewSet,
)
from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageViewSet
from authentik.stages.captcha.api import CaptchaStageViewSet
from authentik.stages.consent.api import ConsentStageViewSet
from authentik.stages.dummy.api import DummyStageViewSet
from authentik.stages.email.api import EmailStageViewSet
from authentik.stages.identification.api import IdentificationStageViewSet
from authentik.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
from authentik.stages.otp_static.api import OTPStaticStageViewSet
from authentik.stages.otp_time.api import OTPTimeStageViewSet
from authentik.stages.otp_validate.api import OTPValidateStageViewSet
from authentik.stages.password.api import PasswordStageViewSet
from authentik.stages.prompt.api import PromptStageViewSet, PromptViewSet
from authentik.stages.user_delete.api import UserDeleteStageViewSet
Expand Down Expand Up @@ -134,15 +137,16 @@
router.register("propertymappings/scope", ScopeMappingViewSet)

router.register("stages/all", StageViewSet)
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
router.register("stages/authenticator/webauthn", AuthenticateWebAuthnStageViewSet)
router.register("stages/captcha", CaptchaStageViewSet)
router.register("stages/consent", ConsentStageViewSet)
router.register("stages/email", EmailStageViewSet)
router.register("stages/identification", IdentificationStageViewSet)
router.register("stages/invitation", InvitationStageViewSet)
router.register("stages/invitation/invitations", InvitationViewSet)
router.register("stages/otp_static", OTPStaticStageViewSet)
router.register("stages/otp_time", OTPTimeStageViewSet)
router.register("stages/otp_validate", OTPValidateStageViewSet)
router.register("stages/password", PasswordStageViewSet)
router.register("stages/prompt/prompts", PromptViewSet)
router.register("stages/prompt/stages", PromptStageViewSet)
Expand Down
35 changes: 25 additions & 10 deletions authentik/flows/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.core.cache import cache
from django.db.models import Model
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, reverse
from drf_yasg2.utils import swagger_auto_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
Expand All @@ -18,8 +18,11 @@
)
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet

from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.flows.planner import cache_key
from authentik.lib.templatetags.authentik_utils import verbose_name
from authentik.lib.utils.reflection import all_subclasses


class FlowSerializer(ModelSerializer):
Expand Down Expand Up @@ -154,24 +157,19 @@ def diagram(self, request: Request, slug: str) -> Response:
return Response({"diagram": diagram})


class StageSerializer(ModelSerializer):
class StageSerializer(ModelSerializer, MetaNameSerializer):
"""Stage Serializer"""

__type__ = SerializerMethodField(method_name="get_type")
verbose_name = SerializerMethodField(method_name="get_verbose_name")
object_type = SerializerMethodField()

def get_type(self, obj: Stage) -> str:
def get_object_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace("stage", "")

def get_verbose_name(self, obj: Stage) -> str:
"""Get verbose name for UI"""
return obj._meta.verbose_name

class Meta:

model = Stage
fields = ["pk", "name", "__type__", "verbose_name"]
fields = ["pk", "name", "object_type", "verbose_name", "verbose_name_plural"]


class StageViewSet(ReadOnlyModelViewSet):
Expand All @@ -183,6 +181,23 @@ class StageViewSet(ReadOnlyModelViewSet):
def get_queryset(self):
return Stage.objects.select_subclasses()

@swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False)
def types(self, request: Request) -> Response:
"""Get all creatable stage types"""
data = []
for subclass in all_subclasses(self.queryset.model, False):
data.append(
{
"name": verbose_name(subclass),
"description": subclass.__doc__,
"link": reverse("authentik_admin:stage-create")
+ f"?type={subclass.__name__}",
}
)
data = sorted(data, key=lambda x: x["name"])
return Response(TypeCreateSerializer(data, many=True).data)


class FlowStageBindingSerializer(ModelSerializer):
"""FlowStageBinding Serializer"""
Expand Down
6 changes: 3 additions & 3 deletions authentik/flows/migrations/0008_default_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,21 @@ def create_default_authentication_flow(
target=flow,
stage=identification_stage,
defaults={
"order": 0,
"order": 10,
},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=password_stage,
defaults={
"order": 1,
"order": 20,
},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
target=flow,
stage=login_stage,
defaults={
"order": 2,
"order": 100,
},
)

Expand Down
2 changes: 1 addition & 1 deletion authentik/flows/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_models(self):
def test_api_serializer(self):
"""Test that stage serializer returns the correct type"""
obj = DummyStage()
self.assertEqual(StageSerializer().get_type(obj), "dummy")
self.assertEqual(StageSerializer().get_object_type(obj), "dummy")
self.assertEqual(StageSerializer().get_verbose_name(obj), "Dummy Stage")

def test_api_viewset(self):
Expand Down
17 changes: 14 additions & 3 deletions authentik/flows/transfer/common.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
"""transfer common classes"""
from dataclasses import asdict, dataclass, field, is_dataclass
from json.encoder import JSONEncoder
from typing import Any, Dict, List
from uuid import UUID

from django.core.serializers.json import DjangoJSONEncoder

from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException


def get_attrs(obj: SerializerModel) -> Dict[str, Any]:
"""Get object's attributes via their serializer, and covert it to a normal dict"""
data = dict(obj.serializer(obj).data)
to_remove = ("policies", "stages", "pk", "background", "group", "user")
to_remove = (
"policies",
"stages",
"pk",
"background",
"group",
"user",
"verbose_name",
"verbose_name_plural",
"object_type",
)
for to_remove_name in to_remove:
if to_remove_name in data:
data.pop(to_remove_name)
Expand Down Expand Up @@ -53,7 +64,7 @@ class FlowBundle:
entries: List[FlowBundleEntry] = field(default_factory=list)


class DataclassEncoder(JSONEncoder):
class DataclassEncoder(DjangoJSONEncoder):
"""Convert FlowBundleEntry to json"""

def default(self, o):
Expand Down
3 changes: 2 additions & 1 deletion authentik/flows/transfer/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ def export(self) -> FlowBundle:

def export_to_string(self) -> str:
"""Call export and convert it to json"""
return dumps(self.export(), cls=DataclassEncoder)
bundle = self.export()
return dumps(bundle, cls=DataclassEncoder)
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ class Migration(migrations.Migration):
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.stages.otp_static", "authentik OTP.Static"),
("authentik.stages.otp_time", "authentik OTP.Time"),
("authentik.stages.otp_validate", "authentik OTP.Validate"),
("authentik.stages.authenticator_totp", "authentik OTP.Time"),
(
"authentik.stages.authenticator_validate",
"authentik OTP.Validate",
),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.core", "authentik Core"),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,14 @@ class Migration(migrations.Migration):
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
(
"authentik.stages.authenticator_totp",
"authentik Stages.OTP.Time",
),
(
"authentik.stages.authenticator_validate",
"authentik Stages.OTP.Validate",
),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.core", "authentik Core"),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,14 @@ class Migration(migrations.Migration):
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
("authentik.stages.otp_time", "authentik Stages.OTP.Time"),
("authentik.stages.otp_validate", "authentik Stages.OTP.Validate"),
(
"authentik.stages.authenticator_totp",
"authentik Stages.OTP.Time",
),
(
"authentik.stages.authenticator_validate",
"authentik Stages.OTP.Validate",
),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.managed", "authentik Managed"),
("authentik.core", "authentik Core"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Generated by Django 3.1.6 on 2021-02-13 16:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("authentik_policies_event_matcher", "0007_auto_20210209_1657"),
]

operations = [
migrations.AlterField(
model_name="eventmatcherpolicy",
name="app",
field=models.TextField(
blank=True,
choices=[
("authentik.admin", "authentik Admin"),
("authentik.api", "authentik API"),
("authentik.events", "authentik Events"),
("authentik.crypto", "authentik Crypto"),
("authentik.flows", "authentik Flows"),
("authentik.outposts", "authentik Outpost"),
("authentik.lib", "authentik lib"),
("authentik.policies", "authentik Policies"),
("authentik.policies.dummy", "authentik Policies.Dummy"),
(
"authentik.policies.event_matcher",
"authentik Policies.Event Matcher",
),
("authentik.policies.expiry", "authentik Policies.Expiry"),
("authentik.policies.expression", "authentik Policies.Expression"),
(
"authentik.policies.group_membership",
"authentik Policies.Group Membership",
),
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
("authentik.policies.password", "authentik Policies.Password"),
("authentik.policies.reputation", "authentik Policies.Reputation"),
("authentik.providers.proxy", "authentik Providers.Proxy"),
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
("authentik.providers.saml", "authentik Providers.SAML"),
("authentik.recovery", "authentik Recovery"),
("authentik.sources.ldap", "authentik Sources.LDAP"),
("authentik.sources.oauth", "authentik Sources.OAuth"),
("authentik.sources.saml", "authentik Sources.SAML"),
("authentik.stages.captcha", "authentik Stages.Captcha"),
("authentik.stages.consent", "authentik Stages.Consent"),
("authentik.stages.dummy", "authentik Stages.Dummy"),
("authentik.stages.email", "authentik Stages.Email"),
("authentik.stages.prompt", "authentik Stages.Prompt"),
(
"authentik.stages.identification",
"authentik Stages.Identification",
),
("authentik.stages.invitation", "authentik Stages.User Invitation"),
("authentik.stages.user_delete", "authentik Stages.User Delete"),
("authentik.stages.user_login", "authentik Stages.User Login"),
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.stages.otp_static", "authentik Stages.OTP.Static"),
(
"authentik.stages.authenticator_totp",
"authentik Stages.OTP.Time",
),
(
"authentik.stages.authenticator_validate",
"authentik Stages.OTP.Validate",
),
("authentik.stages.password", "authentik Stages.Password"),
(
"authentik.stages.authenticator_webauthn",
"authentik Stages.WebAuthn",
),
("authentik.managed", "authentik Managed"),
("authentik.core", "authentik Core"),
],
default="",
help_text="Match events created by selected application. When left empty, all applications are matched.",
),
),
]
Loading

0 comments on commit 8708e48

Please sign in to comment.