stages/authentiactor_validate: improve error handling for duo

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-07-28 20:57:29 +02:00
parent 1f90359310
commit 0248755cda
14 changed files with 180 additions and 87 deletions

View File

@ -1,10 +1,12 @@
"""Validation stage challenge checking"""
from json import dumps, loads
from typing import Optional
from urllib.parse import urlencode
from django.http import HttpRequest
from django.http.response import Http404
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as __
from django.utils.translation import gettext_lazy as _
from django_otp import match_token
from django_otp.models import Device
@ -17,9 +19,11 @@ from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from webauthn.helpers.structs import AuthenticationCredential
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.core.models import Application, User
from authentik.core.signals import login_failed
from authentik.events.models import Event, EventAction
from authentik.flows.stage import StageView
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
from authentik.lib.utils.http import get_client_ip
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_sms.models import SMSDevice
@ -27,6 +31,7 @@ from authentik.stages.authenticator_validate.models import DeviceClasses
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TITLE
LOGGER = get_logger()
@ -155,13 +160,32 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
LOGGER.warning("device mismatch")
raise Http404
stage: AuthenticatorDuoStage = device.stage
# Get additional context for push
pushinfo = {
__("Domain"): stage_view.request.get_host(),
}
if PLAN_CONTEXT_CONSENT_TITLE in stage_view.executor.plan.context:
pushinfo[__("Title")] = stage_view.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE]
if SESSION_KEY_APPLICATION_PRE in stage_view.request.session:
pushinfo[__("Application")] = stage_view.request.session.get(
SESSION_KEY_APPLICATION_PRE, Application()
).name
try:
response = stage.client.auth(
"auto",
user_id=device.duo_user_id,
ipaddr=get_client_ip(stage_view.request),
type="authentik Login request",
type=__(
"%(brand_name)s Login request"
% {
"brand_name": stage_view.request.tenant.branding_title,
}
),
display_username=user.username,
device="auto",
pushinfo=urlencode(pushinfo),
)
# {'result': 'allow', 'status': 'allow', 'status_msg': 'Success. Logging you in...'}
if response["result"] == "deny":
@ -175,3 +199,10 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
raise ValidationError("Duo denied access")
device.save()
return device
except RuntimeError as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to DUO authenticate user: {str(exc)}",
user=user,
).from_http(stage_view.request, user)
raise ValidationError("Duo denied access")

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-07-02 15:10+0000\n"
"POT-Creation-Date: 2022-07-28 19:11+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -39,11 +39,11 @@ msgstr ""
msgid "Create a SAML Provider by importing its Metadata."
msgstr ""
#: authentik/core/api/users.py:90
#: authentik/core/api/users.py:93
msgid "No leading or trailing slashes allowed."
msgstr ""
#: authentik/core/api/users.py:93
#: authentik/core/api/users.py:96
msgid "No empty segments in user path allowed."
msgstr ""
@ -59,105 +59,105 @@ msgstr ""
msgid "User's display name."
msgstr ""
#: authentik/core/models.py:237 authentik/providers/oauth2/models.py:318
#: authentik/core/models.py:239 authentik/providers/oauth2/models.py:321
msgid "User"
msgstr ""
#: authentik/core/models.py:238
#: authentik/core/models.py:240
msgid "Users"
msgstr ""
#: authentik/core/models.py:249
#: authentik/core/models.py:251
msgid "Flow used when authorizing this provider."
msgstr ""
#: authentik/core/models.py:282
#: authentik/core/models.py:284
msgid "Application's display Name."
msgstr ""
#: authentik/core/models.py:283
#: authentik/core/models.py:285
msgid "Internal application name, used in URLs."
msgstr ""
#: authentik/core/models.py:295
#: authentik/core/models.py:297
msgid "Open launch URL in a new browser tab or window."
msgstr ""
#: authentik/core/models.py:354
#: authentik/core/models.py:356
msgid "Application"
msgstr ""
#: authentik/core/models.py:355
#: authentik/core/models.py:357
msgid "Applications"
msgstr ""
#: authentik/core/models.py:361
#: authentik/core/models.py:363
msgid "Use the source-specific identifier"
msgstr ""
#: authentik/core/models.py:369
#: authentik/core/models.py:371
msgid ""
"Use the user's email address, but deny enrollment when the email address "
"already exists."
msgstr ""
#: authentik/core/models.py:378
#: authentik/core/models.py:380
msgid ""
"Use the user's username, but deny enrollment when the username already "
"exists."
msgstr ""
#: authentik/core/models.py:385
#: authentik/core/models.py:387
msgid "Source's display Name."
msgstr ""
#: authentik/core/models.py:386
#: authentik/core/models.py:388
msgid "Internal source name, used in URLs."
msgstr ""
#: authentik/core/models.py:399
#: authentik/core/models.py:401
msgid "Flow to use when authenticating existing users."
msgstr ""
#: authentik/core/models.py:408
#: authentik/core/models.py:410
msgid "Flow to use when enrolling new users."
msgstr ""
#: authentik/core/models.py:557
#: authentik/core/models.py:560
msgid "Token"
msgstr ""
#: authentik/core/models.py:558
#: authentik/core/models.py:561
msgid "Tokens"
msgstr ""
#: authentik/core/models.py:601
#: authentik/core/models.py:604
msgid "Property Mapping"
msgstr ""
#: authentik/core/models.py:602
#: authentik/core/models.py:605
msgid "Property Mappings"
msgstr ""
#: authentik/core/models.py:638
#: authentik/core/models.py:641
msgid "Authenticated Session"
msgstr ""
#: authentik/core/models.py:639
#: authentik/core/models.py:642
msgid "Authenticated Sessions"
msgstr ""
#: authentik/core/sources/flow_manager.py:177
#: authentik/core/sources/flow_manager.py:176
msgid "source"
msgstr ""
#: authentik/core/sources/flow_manager.py:245
#: authentik/core/sources/flow_manager.py:283
#: authentik/core/sources/flow_manager.py:243
#: authentik/core/sources/flow_manager.py:281
#, python-format
msgid "Successfully authenticated with %(source)s!"
msgstr ""
#: authentik/core/sources/flow_manager.py:264
#: authentik/core/sources/flow_manager.py:262
#, python-format
msgid "Successfully linked %(source)s!"
msgstr ""
@ -168,8 +168,8 @@ msgstr ""
#: authentik/core/templates/if/admin.html:18
#: authentik/core/templates/if/admin.html:24
#: authentik/core/templates/if/flow.html:35
#: authentik/core/templates/if/flow.html:41
#: authentik/core/templates/if/flow.html:37
#: authentik/core/templates/if/flow.html:43
#: authentik/core/templates/if/user.html:18
#: authentik/core/templates/if/user.html:24
msgid "Loading..."
@ -355,6 +355,10 @@ msgstr ""
msgid "Flow not applicable to current user/request: %(messages)s"
msgstr ""
#: authentik/flows/exceptions.py:17
msgid "Flow does not apply to current user (denied by policy)."
msgstr ""
#: authentik/flows/models.py:117
msgid "Visible in the URL."
msgstr ""
@ -742,98 +746,104 @@ msgstr ""
msgid "Client Type"
msgstr ""
#: authentik/providers/oauth2/models.py:151
#: authentik/providers/oauth2/models.py:147
msgid ""
"Confidential clients are capable of maintaining the confidentiality of their "
"credentials. Public clients are incapable"
msgstr ""
#: authentik/providers/oauth2/models.py:154
msgid "Client ID"
msgstr ""
#: authentik/providers/oauth2/models.py:157
#: authentik/providers/oauth2/models.py:160
msgid "Client Secret"
msgstr ""
#: authentik/providers/oauth2/models.py:163
#: authentik/providers/oauth2/models.py:166
msgid "Redirect URIs"
msgstr ""
#: authentik/providers/oauth2/models.py:164
#: authentik/providers/oauth2/models.py:167
msgid "Enter each URI on a new line."
msgstr ""
#: authentik/providers/oauth2/models.py:169
#: authentik/providers/oauth2/models.py:172
msgid "Include claims in id_token"
msgstr ""
#: authentik/providers/oauth2/models.py:217
#: authentik/providers/oauth2/models.py:220
msgid "Signing Key"
msgstr ""
#: authentik/providers/oauth2/models.py:221
#: authentik/providers/oauth2/models.py:224
msgid ""
"Key used to sign the tokens. Only required when JWT Algorithm is set to "
"RS256."
msgstr ""
#: authentik/providers/oauth2/models.py:228
#: authentik/providers/oauth2/models.py:231
msgid ""
"Any JWT signed by the JWK of the selected source can be used to authenticate."
msgstr ""
#: authentik/providers/oauth2/models.py:310
#: authentik/providers/oauth2/models.py:313
msgid "OAuth2/OpenID Provider"
msgstr ""
#: authentik/providers/oauth2/models.py:311
#: authentik/providers/oauth2/models.py:314
msgid "OAuth2/OpenID Providers"
msgstr ""
#: authentik/providers/oauth2/models.py:319
#: authentik/providers/oauth2/models.py:322
msgid "Scopes"
msgstr ""
#: authentik/providers/oauth2/models.py:338
#: authentik/providers/oauth2/models.py:341
msgid "Code"
msgstr ""
#: authentik/providers/oauth2/models.py:339
#: authentik/providers/oauth2/models.py:342
msgid "Nonce"
msgstr ""
#: authentik/providers/oauth2/models.py:340
#: authentik/providers/oauth2/models.py:343
msgid "Is Authentication?"
msgstr ""
#: authentik/providers/oauth2/models.py:341
#: authentik/providers/oauth2/models.py:344
msgid "Code Challenge"
msgstr ""
#: authentik/providers/oauth2/models.py:343
#: authentik/providers/oauth2/models.py:346
msgid "Code Challenge Method"
msgstr ""
#: authentik/providers/oauth2/models.py:357
#: authentik/providers/oauth2/models.py:360
msgid "Authorization Code"
msgstr ""
#: authentik/providers/oauth2/models.py:358
#: authentik/providers/oauth2/models.py:361
msgid "Authorization Codes"
msgstr ""
#: authentik/providers/oauth2/models.py:401
#: authentik/providers/oauth2/models.py:404
msgid "Access Token"
msgstr ""
#: authentik/providers/oauth2/models.py:402
#: authentik/providers/oauth2/models.py:405
msgid "Refresh Token"
msgstr ""
#: authentik/providers/oauth2/models.py:403
#: authentik/providers/oauth2/models.py:406
msgid "ID Token"
msgstr ""
#: authentik/providers/oauth2/models.py:406
#: authentik/providers/oauth2/models.py:409
msgid "OAuth2 Token"
msgstr ""
#: authentik/providers/oauth2/models.py:407
#: authentik/providers/oauth2/models.py:410
msgid "OAuth2 Tokens"
msgstr ""
@ -1385,7 +1395,7 @@ msgstr ""
msgid "TOTP Authenticator Setup Stages"
msgstr ""
#: authentik/stages/authenticator_validate/challenge.py:110
#: authentik/stages/authenticator_validate/challenge.py:115
msgid "Invalid Token"
msgstr ""
@ -1687,51 +1697,57 @@ msgstr ""
msgid "Invalid password"
msgstr ""
#: authentik/stages/prompt/models.py:38
#: authentik/stages/prompt/models.py:40
msgid "Text: Simple Text input"
msgstr ""
#: authentik/stages/prompt/models.py:41
#: authentik/stages/prompt/models.py:43
msgid "Text (read-only): Simple Text input, but cannot be edited."
msgstr ""
#: authentik/stages/prompt/models.py:48
#: authentik/stages/prompt/models.py:50
msgid "Email: Text field with Email type."
msgstr ""
#: authentik/stages/prompt/models.py:64
#: authentik/stages/prompt/models.py:69
msgid ""
"File: File upload for arbitrary files. File content will be available in "
"flow context as data-URI"
msgstr ""
#: authentik/stages/prompt/models.py:74
msgid "Separator: Static Separator Line"
msgstr ""
#: authentik/stages/prompt/models.py:65
#: authentik/stages/prompt/models.py:75
msgid "Hidden: Hidden field, can be used to insert data into form."
msgstr ""
#: authentik/stages/prompt/models.py:66
#: authentik/stages/prompt/models.py:76
msgid "Static: Static value, displayed as-is."
msgstr ""
#: authentik/stages/prompt/models.py:68
#: authentik/stages/prompt/models.py:78
msgid "authentik: Selection of locales authentik supports"
msgstr ""
#: authentik/stages/prompt/models.py:77
#: authentik/stages/prompt/models.py:101
msgid "Name of the form field, also used to store the value"
msgstr ""
#: authentik/stages/prompt/models.py:170
#: authentik/stages/prompt/models.py:198
msgid "Prompt"
msgstr ""
#: authentik/stages/prompt/models.py:171
#: authentik/stages/prompt/models.py:199
msgid "Prompts"
msgstr ""
#: authentik/stages/prompt/models.py:199
#: authentik/stages/prompt/models.py:227
msgid "Prompt Stage"
msgstr ""
#: authentik/stages/prompt/models.py:200
#: authentik/stages/prompt/models.py:228
msgid "Prompt Stages"
msgstr ""
@ -1807,10 +1823,10 @@ msgid ""
"and `ba.b`"
msgstr ""
#: authentik/tenants/models.py:75
#: authentik/tenants/models.py:80
msgid "Tenant"
msgstr ""
#: authentik/tenants/models.py:76
#: authentik/tenants/models.py:81
msgid "Tenants"
msgstr ""

View File

@ -55,6 +55,7 @@ export class AuthenticatorValidateStage
set selectedDeviceChallenge(value: DeviceChallenge | undefined) {
this._selectedDeviceChallenge = value;
if (!value) return;
if (value === this._selectedDeviceChallenge) return;
// We don't use this.submit here, as we don't want to advance the flow.
// We just want to notify the backend which challenge has been selected.
new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({

View File

@ -50,6 +50,7 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<
if (!this.challenge) {
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
const errors = this.challenge.responseErrors?.duo || [];
return html`<div class="pf-c-login__main-body">
<form
class="pf-c-form"
@ -69,6 +70,10 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<
</div>
</ak-form-static>
${errors.map((err) => {
return html`<p>${err.string}</p>`;
})}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}

View File

@ -2259,6 +2259,10 @@ msgstr "Felder"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "Felder, mit denen sich ein Benutzer identifizieren kann. Wenn keine Felder ausgewählt sind, kann der Benutzer nur Quellen verwenden."
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr ""

View File

@ -2295,6 +2295,10 @@ msgstr "Fields"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr "File"
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr "Finish"

View File

@ -2250,6 +2250,10 @@ msgstr "Campos"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "Campos con los que un usuario puede identificarse. Si no se seleccionan campos, el usuario solo podrá usar fuentes."
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr ""

View File

@ -2275,6 +2275,10 @@ msgstr "Champs"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "Champs avec lesquels un utilisateur peut s'identifier. Si aucun champ n'est sélectionné, l'utilisateur ne pourra utiliser que des sources."
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr ""

View File

@ -2247,6 +2247,10 @@ msgstr "Pola"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "Pola, z którymi użytkownik może się identyfikować. Jeśli żadne pola nie zostaną wybrane, użytkownik będzie mógł korzystać tylko ze źródeł."
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr ""

View File

@ -2281,6 +2281,10 @@ msgstr ""
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr ""
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr ""

View File

@ -2250,6 +2250,10 @@ msgstr "Alanlar"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "Kullanıcının kendilerini tanımlayabileceği alanlar. Herhangi bir alan seçilmezse, kullanıcı yalnızca kaynakları kullanabilir."
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr ""

View File

@ -2238,6 +2238,10 @@ msgstr "字段"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "用户可以用来标识自己的字段。如果未选择任何字段,则用户将只能使用源。"
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr "完成"

View File

@ -2241,6 +2241,10 @@ msgstr "字段"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "用户可以用来标识自己的字段。如果未选择任何字段,则用户将只能使用源。"
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr "完成"

View File

@ -2241,6 +2241,10 @@ msgstr "字段"
msgid "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources."
msgstr "用户可以用来标识自己的字段。如果未选择任何字段,则用户将只能使用源。"
#: src/pages/stages/prompt/PromptForm.ts
msgid "File"
msgstr ""
#: src/elements/wizard/Wizard.ts
msgid "Finish"
msgstr "完成"