diff --git a/authentik/policies/event_matcher/migrations/0010_auto_20210222_1821.py b/authentik/policies/event_matcher/migrations/0010_auto_20210222_1821.py new file mode 100644 index 000000000..0701aaf20 --- /dev/null +++ b/authentik/policies/event_matcher/migrations/0010_auto_20210222_1821.py @@ -0,0 +1,86 @@ +# Generated by Django 3.1.6 on 2021-02-22 18:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_policies_event_matcher", "0009_auto_20210215_2159"), + ] + + 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.authenticator_static", + "authentik Stages.Authenticator.Static", + ), + ( + "authentik.stages.authenticator_totp", + "authentik Stages.Authenticator.TOTP", + ), + ( + "authentik.stages.authenticator_validate", + "authentik Stages.Authenticator.Validate", + ), + ( + "authentik.stages.authenticator_webauthn", + "authentik Stages.Authenticator.WebAuthn", + ), + ("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.identification", + "authentik Stages.Identification", + ), + ("authentik.stages.invitation", "authentik Stages.User Invitation"), + ("authentik.stages.password", "authentik Stages.Password"), + ("authentik.stages.prompt", "authentik Stages.Prompt"), + ("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.managed", "authentik Managed"), + ("authentik.core", "authentik Core"), + ], + default="", + help_text="Match events created by selected application. When left empty, all applications are matched.", + ), + ), + ] diff --git a/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py b/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py index 6be74a728..bdb88d01b 100644 --- a/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py +++ b/authentik/stages/authenticator_webauthn/migrations/0002_default_setup_flow.py @@ -22,7 +22,7 @@ def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEdito designation=FlowDesignation.STAGE_CONFIGURATION, defaults={ "name": "default-authenticator-webuahtn-setup", - "title": "Setup Static OTP Tokens", + "title": "Setup WebAuthn", }, ) diff --git a/authentik/stages/authenticator_webauthn/migrations/0003_webauthndevice_confirmed.py b/authentik/stages/authenticator_webauthn/migrations/0003_webauthndevice_confirmed.py new file mode 100644 index 000000000..be8e9035d --- /dev/null +++ b/authentik/stages/authenticator_webauthn/migrations/0003_webauthndevice_confirmed.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.6 on 2021-02-22 18:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_webauthn", "0002_default_setup_flow"), + ] + + operations = [ + migrations.AddField( + model_name="webauthndevice", + name="confirmed", + field=models.BooleanField( + default=True, help_text="Is this device ready for use?" + ), + ), + ] diff --git a/authentik/stages/authenticator_webauthn/models.py b/authentik/stages/authenticator_webauthn/models.py index 2fb6c89af..f84c0b799 100644 --- a/authentik/stages/authenticator_webauthn/models.py +++ b/authentik/stages/authenticator_webauthn/models.py @@ -8,6 +8,7 @@ from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django.views import View +from django_otp.models import Device from rest_framework.serializers import BaseSerializer from authentik.flows.models import ConfigurableStage, Stage @@ -56,7 +57,7 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage): verbose_name_plural = _("WebAuthn Authenticator Setup Stages") -class WebAuthnDevice(models.Model): +class WebAuthnDevice(Device): """WebAuthn Device for a single user""" user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) diff --git a/authentik/stages/authenticator_webauthn/stage.py b/authentik/stages/authenticator_webauthn/stage.py index 581cac6da..bf25a4628 100644 --- a/authentik/stages/authenticator_webauthn/stage.py +++ b/authentik/stages/authenticator_webauthn/stage.py @@ -7,6 +7,7 @@ from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger from webauthn.webauthn import ( RegistrationRejectedException, + WebAuthnCredential, WebAuthnMakeCredentialOptions, WebAuthnRegistrationResponse, ) @@ -87,7 +88,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse): ) webauthn_credential.public_key = str(webauthn_credential.public_key, "utf-8") - return webauthn_registration_response + return webauthn_credential class AuthenticatorWebAuthnStageView(ChallengeStageView): @@ -145,7 +146,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # Webauthn Challenge has already been validated - webauthn_credential = response.validated_data["response"] + webauthn_credential: WebAuthnCredential = response.validated_data["response"] existing_device = WebAuthnDevice.objects.filter( credential_id=webauthn_credential.credential_id ).first() diff --git a/authentik/stages/prompt/migrations/0003_auto_20210222_1821.py b/authentik/stages/prompt/migrations/0003_auto_20210222_1821.py new file mode 100644 index 000000000..c6f97e91c --- /dev/null +++ b/authentik/stages/prompt/migrations/0003_auto_20210222_1821.py @@ -0,0 +1,42 @@ +# Generated by Django 3.1.6 on 2021-02-22 18:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_prompt", "0002_auto_20200920_1859"), + ] + + operations = [ + migrations.AlterField( + model_name="prompt", + name="type", + field=models.CharField( + choices=[ + ("text", "Text: Simple Text input"), + ( + "username", + "Username: Same as Text input, but checks for and prevents duplicate usernames.", + ), + ("email", "Email: Text field with Email type."), + ( + "password", + "Password: Masked input, password is validated against sources. Policies still have to be applied to this Stage. If two of these are used in the same stage, they are ensured to be identical.", + ), + ("number", "Number"), + ("checkbox", "Checkbox"), + ("date", "Date"), + ("date-time", "Date Time"), + ("separator", "Separator: Static Separator Line"), + ( + "hidden", + "Hidden: Hidden field, can be used to insert data into form.", + ), + ("static", "Static: Static value, displayed as-is."), + ], + max_length=100, + ), + ), + ] diff --git a/web/src/elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts b/web/src/elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts index 9bbe2c3b4..b9981db2b 100644 --- a/web/src/elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts +++ b/web/src/elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts @@ -57,10 +57,9 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage { // post the transformed credential data to the server for validation // and storing the public key try { - const response = { - response: newAssertionForServer - }; - await this.host?.submit(JSON.stringify(response)); + const formData = new FormData(); + formData.set("response", JSON.stringify(newAssertionForServer)) + await this.host?.submit(formData); } catch (err) { throw new Error(gettext(`Server validation of credential failed: ${err}`)); } @@ -100,6 +99,9 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage { `: html`
+ ${this.challenge?.response_errors ? + html`

${this.challenge.response_errors["response"][0].string}

`: + html``}

${this.registerMessage}