def test_is_two_factor_backup_code_valid_matches(user_id: UserId) -> None: codes = userdb.make_two_factor_backup_codes(rounds=5) credentials = userdb.load_two_factor_credentials(user_id) credentials["backup_codes"] = [pwhashed for _password, pwhashed in codes] userdb.save_two_factor_credentials(user_id, credentials) assert len(credentials["backup_codes"]) == 10 valid = userdb.is_two_factor_backup_code_valid(user_id, codes[3][0]) assert valid credentials = userdb.load_two_factor_credentials(user_id) assert len(credentials["backup_codes"]) == 9
def test_is_two_factor_backup_code_valid_matches(user_id) -> None: display_codes, store_codes = userdb.make_two_factor_backup_codes() credentials = userdb.load_two_factor_credentials(user_id) credentials["backup_codes"] = store_codes assert len(credentials["backup_codes"]) == 10 userdb.save_two_factor_credentials(user_id, credentials) assert userdb.is_two_factor_backup_code_valid(user_id, display_codes[3]) is True credentials = userdb.load_two_factor_credentials(user_id) assert len(credentials["backup_codes"]) == 9
def page(self) -> CBORPageResult: assert user.id is not None if not is_two_factor_login_enabled(user.id): raise MKGeneralException( _("Two-factor authentication not enabled")) data: dict[str, object] = cbor.decode(request.get_data()) credential_id = data["credentialId"] client_data = ClientData(data["clientDataJSON"]) auth_data = AuthenticatorData(data["authenticatorData"]) signature = data["signature"] logger.debug("ClientData: %r", client_data) logger.debug("AuthenticatorData: %r", auth_data) make_fido2_server().authenticate_complete( session.session_info.webauthn_action_state, [ AttestedCredentialData.unpack_from(v["credential_data"])[0] for v in load_two_factor_credentials(user.id) ["webauthn_credentials"].values() ], credential_id, client_data, auth_data, signature, ) session.session_info.webauthn_action_state = None set_two_factor_completed() return {"status": "OK"}
def _show_form(self) -> None: assert user.id is not None credentials = load_two_factor_credentials(user.id) credential_id = request.get_ascii_input_mandatory("_edit") credential = credentials["webauthn_credentials"].get(credential_id) if credential is None: raise MKUserError("_edit", _("The credential does not exist")) html.begin_form("profile", method="POST") html.prevent_password_auto_completion() html.open_div(class_="wato") self._valuespec(credential).render_input( "profile", { "registered_at": credential["registered_at"], "alias": credential["alias"], }, ) forms.end() html.close_div() html.hidden_field("_edit", credential_id) html.hidden_fields() html.end_form() html.footer()
def _action(self) -> None: assert user.id is not None credentials = load_two_factor_credentials(user.id, lock=True) credential_id = request.get_ascii_input_mandatory("_edit") credential = credentials["webauthn_credentials"].get(credential_id) if credential is None: raise MKUserError("_edit", _("The credential does not exist")) vs = self._valuespec(credential) settings = vs.from_html_vars("profile") vs.validate_value(settings, "profile") credential["alias"] = settings["alias"] save_two_factor_credentials(user.id, credentials) flash(_("Successfully changed the credential.")) # In distributed setups with remote sites where the user can login, start the # user profile replication now which will redirect the user to the destination # page after completion. Otherwise directly open up the destination page. origtarget = "user_two_factor_overview.py" if user.authorized_login_sites(): raise redirect( makeuri_contextless(request, [("back", origtarget)], filename="user_profile_replicate.py")) raise redirect(origtarget)
def _show_form(self) -> None: assert user.id is not None credentials = load_two_factor_credentials(user.id) webauthn_credentials = credentials["webauthn_credentials"] backup_codes = credentials["backup_codes"] html.begin_form("two_factor", method="POST") html.div("", id_="webauthn_message") forms.header(_("Credentials")) forms.section(_("Registered credentials"), simple=True) if webauthn_credentials: self._show_credentials(webauthn_credentials) else: html.i(_("No credentials registered")) forms.section(_("Backup codes"), simple=True) if backup_codes: html.p( _("You have %d unused backup codes left. You can use them as one-time password " "if your key is not available.") % len(backup_codes)) html.i( _("If you regenerate backup codes, you automatically invalidate the existing codes." )) else: html.i(_("No backup codes created yet.")) forms.end() html.hidden_fields() html.end_form() html.footer()
def page(self) -> CBORPageResult: assert user.id is not None user.need_permission("general.manage_2fa") raw_data = request.get_data() logger.debug("Raw request: %r", raw_data) data: dict[str, object] = cbor.decode(raw_data) client_data = ClientData(data["clientDataJSON"]) att_obj = AttestationObject(data["attestationObject"]) logger.debug("Client data: %r", client_data) logger.debug("Attestation object: %r", att_obj) auth_data = make_fido2_server().register_complete( session.session_info.webauthn_action_state, client_data, att_obj ) ident = auth_data.credential_data.credential_id.hex() credentials = load_two_factor_credentials(user.id, lock=True) if ident in credentials["webauthn_credentials"]: raise MKGeneralException(_("Your WebAuthn credetial is already in use")) credentials["webauthn_credentials"][ident] = WebAuthnCredential( { "credential_id": ident, "registered_at": int(time.time()), "alias": "", "credential_data": bytes(auth_data.credential_data), } ) save_two_factor_credentials(user.id, credentials) flash(_("Registration successful")) return {"status": "OK"}
def _action(self) -> None: assert user.id is not None credentials = load_two_factor_credentials(user.id) if credential_id := request.get_ascii_input("_delete"): if credential_id not in credentials["webauthn_credentials"]: return del credentials["webauthn_credentials"][credential_id] save_two_factor_credentials(user.id, credentials) flash(_("Credential has been deleted"))
def _show_form(self) -> None: assert user.id is not None credentials = load_two_factor_credentials(user.id) html.begin_form("two_factor", method="POST") self._show_webauthn_credentials(credentials["webauthn_credentials"]) self._show_backup_codes(credentials["backup_codes"]) html.hidden_fields() html.end_form() html.footer()
def page(self) -> CBORPageResult: assert user.id is not None if not is_two_factor_login_enabled(user.id): raise MKGeneralException(_("Two-factor authentication not enabled")) auth_data, state = make_fido2_server().authenticate_begin( [ AttestedCredentialData.unpack_from(v["credential_data"])[0] for v in load_two_factor_credentials(user.id)["webauthn_credentials"].values() ], user_verification="discouraged", ) session.session_info.webauthn_action_state = state logger.debug("Authentication data: %r", auth_data) return auth_data
def page(self) -> CBORPageResult: assert user.id is not None user.need_permission("general.manage_2fa") raw_data = request.get_data() logger.debug("Raw request: %r", raw_data) data: dict[str, object] = cbor.decode(raw_data) client_data = ClientData(data["clientDataJSON"]) att_obj = AttestationObject(data["attestationObject"]) logger.debug("Client data: %r", client_data) logger.debug("Attestation object: %r", att_obj) try: auth_data = make_fido2_server().register_complete( session.session_info.webauthn_action_state, client_data, att_obj) except ValueError as e: if "Invalid origin in ClientData" in str(e): raise MKGeneralException( "The origin %r is not valid. You need to access the UI via HTTPS " "and you need to use a valid host or domain name. See werk #13325 for " "further information" % client_data.get("origin")) from e raise ident = auth_data.credential_data.credential_id.hex() credentials = load_two_factor_credentials(user.id, lock=True) if ident in credentials["webauthn_credentials"]: raise MKGeneralException( _("Your WebAuthn credential is already in use")) credentials["webauthn_credentials"][ident] = WebAuthnCredential({ "credential_id": ident, "registered_at": int(time.time()), "alias": "", "credential_data": bytes(auth_data.credential_data), }) save_two_factor_credentials(user.id, credentials) flash(_("Registration successful")) return {"status": "OK"}
def test_save_two_factor_credentials(user_id: UserId) -> None: credentials = userdb.TwoFactorCredentials({ "webauthn_credentials": { "id": userdb.WebAuthnCredential({ "credential_id": "id", "registered_at": 1337, "alias": "Steckding", "credential_data": b"whatever", }), }, "backup_codes": [ "asdr2ar2a2ra2rara2", "dddddddddddddddddd", ], }) userdb.save_two_factor_credentials(user_id, credentials) assert userdb.load_two_factor_credentials(user_id) == credentials
def page(self) -> CBORPageResult: assert user.id is not None user.need_permission("general.manage_2fa") registration_data, state = make_fido2_server().register_begin( { "id": user.id.encode("utf-8"), "name": user.id, "displayName": user.alias, "icon": "", }, [ AttestedCredentialData.unpack_from(v["credential_data"])[0] for v in load_two_factor_credentials(user.id)["webauthn_credentials"].values() ], user_verification="discouraged", authenticator_attachment="cross-platform", ) session.session_info.webauthn_action_state = state logger.debug("Registration data: %r", registration_data) return registration_data
def test_load_two_factor_credentials_unset(user_id: UserId) -> None: assert userdb.load_two_factor_credentials(user_id) == { "webauthn_credentials": {}, "backup_codes": [], }