You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by lo...@apache.org on 2021/07/08 13:41:18 UTC

[syncope] branch master updated: [SYNCOPE-1435] New wicket enduser (#275)

This is an automated email from the ASF dual-hosted git repository.

loredicola pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new a2ac11d  [SYNCOPE-1435] New wicket enduser (#275)
a2ac11d is described below

commit a2ac11d426a504581c7c714f4016dcede910f8ac
Author: Lorenzo <lo...@tirasa.net>
AuthorDate: Thu Jul 8 15:41:10 2021 +0200

    [SYNCOPE-1435] New wicket enduser (#275)
---
 .../syncope/client/ui/commons/BaseLogin.java       |   10 +-
 .../syncope/client/ui/commons/Constants.java       |    2 +
 .../commons/markup/html/form/AjaxPalettePanel.java |   30 +
 .../markup/html/form/BinaryFieldDownload.java      |    2 +-
 .../client/ui/commons/panels/CardPanel.java        |   66 +
 .../client/ui/commons}/themes/AdminLTE.java        |    2 +-
 .../themes/AdminLTECssResourceReference.java       |    2 +-
 .../ui/commons/wizards/any/PasswordPanel.java      |   20 +-
 .../client/ui/commons/panels/CardPanel.html}       |   12 +-
 .../client/ui/commons}/themes/css/AdminLTE.css     |    0
 .../ui/commons}/themes/js/AdminLTE-app.min.js      |    0
 client/idrepo/console/pom.xml                      |    1 -
 .../client/console/SyncopeWebApplication.java      |   10 +-
 .../syncope/client/console/pages/BasePage.java     |   18 +-
 .../apache/syncope/client/console/pages/Login.java |    6 -
 .../console/panels/SchemaTypeWizardBuilder.java    |    3 +-
 .../client/console/status/ChangePasswordModal.java |    2 +-
 .../markup/html/bootstrap/dialog/BaseModal.java    |    2 -
 .../wicket/markup/html/form/BinaryFieldPanel.java  |    2 +
 .../wizards/any/AbstractAttrsWizardStep.java       |    3 +-
 .../client/console/wizards/any/UserDetails.java    |    2 +-
 client/idrepo/enduser/pom.xml                      |   13 +-
 ...tants.java => BookmarkablePageLinkBuilder.java} |   24 +-
 .../syncope/client/enduser/PreferenceManager.java  |  172 +++
 .../SyncopeEnduserRequestCycleListener.java        |  104 ++
 .../client/enduser/SyncopeEnduserSession.java      |  264 ++--
 .../client/enduser/SyncopeWebApplication.java      |  188 +--
 .../client/enduser/assets/SyncopeEnduserCss.java   |   49 -
 .../EnduserConstants.java}                         |   21 +-
 .../init/ClassPathScanImplementationLookup.java    |   27 +-
 .../client/enduser/layout/AnyLayoutUtils.java      |  116 --
 .../client/enduser/layout/CustomizationOption.java |   14 -
 .../client/enduser/layout/UserFormLayoutInfo.java  |   16 +-
 .../enduser/markup/html/form/AjaxDownload.java     |   88 --
 .../enduser/markup/html/form/BinaryFieldPanel.java |   29 +-
 .../syncope/client/enduser/navigation/Navbar.java  |  153 ---
 .../enduser/pages/AbstractChangePassword.java      |   89 ++
 .../syncope/client/enduser/pages/BasePage.java     |  161 +++
 .../{BaseEnduserWebPage.java => Dashboard.java}    |   31 +-
 .../client/enduser/pages/EditChangePassword.java   |   80 ++
 .../client/enduser/pages/EditSecurityQuestion.java |  210 +++
 .../syncope/client/enduser/pages/EditUser.java     |   62 +
 .../apache/syncope/client/enduser/pages/Login.java |   54 +-
 .../client/enduser/pages/MustChangePassword.java   |   35 +-
 .../apache/syncope/client/enduser/pages/Self.java  |  137 --
 .../enduser/pages/SelfConfirmPasswordReset.java    |  137 +-
 .../client/enduser/pages/SelfPasswordReset.java    |  202 ++-
 .../client/enduser/pages/SelfRegistration.java     |   86 ++
 .../syncope/client/enduser/pages/SelfResult.java   |   67 +
 .../enduser/panels/AbstractAnyFormPanel.java       |   95 ++
 .../client/enduser/panels/AbstractFormPanel.java   |   63 +
 .../client/enduser/panels/AnyFormPanel.java        |  184 +++
 .../client/enduser/panels/ChangePasswordPanel.java |  199 +++
 .../client/enduser/panels/SelfPwdResetPanel.java   |  217 ---
 .../syncope/client/enduser/panels/Sidebar.java     |  200 +++
 .../client/enduser/panels/UserFormPanel.java       |  161 +++
 .../client/enduser/panels/UserSelfFormPanel.java   |  135 ++
 .../{wizards => panels}/any/AbstractAttrs.java     |   84 +-
 .../enduser/{wizards => panels}/any/DerAttrs.java  |   32 +-
 .../any/Resources.java => panels/any/Details.java} |   31 +-
 .../enduser/{wizards => panels}/any/Groups.java    |   91 +-
 .../{wizards => panels}/any/PlainAttrs.java        |  197 ++-
 .../client/enduser/panels/any/Resources.java       |   87 ++
 .../client/enduser/panels/any/SelfUserDetails.java |   46 +
 .../client/enduser/panels/any/UserDetails.java     |  113 ++
 .../enduser/{wizards => panels}/any/VirAttrs.java  |   63 +-
 .../any => panels/captcha}/CaptchaPanel.java       |   23 +-
 .../client/enduser/resources/CaptchaResource.java  |   56 -
 .../client/enduser/rest/AnyTypeRestClient.java     |    3 +-
 .../client/enduser/rest/BaseRestClient.java        |   27 +-
 .../client/enduser/rest/GroupRestClient.java       |   19 +-
 .../client/enduser/rest/RoleRestClient.java        |   80 --
 .../client/enduser/rest/SchemaRestClient.java      |    3 +-
 .../client/enduser/rest/SyncopeRestClient.java     |    8 +-
 .../client/enduser/rest/UserSelfRestClient.java    |   42 +-
 .../markup/head/MetaHeaderItem.java}               |   36 +-
 .../SelfUpdate.java => widgets/BaseWidget.java}    |   12 +-
 .../client/enduser/widgets/UserProfileWidget.java  |   65 +
 .../enduser/wizards/any/AnyWizardBuilder.java      |  293 -----
 .../enduser/wizards/any/EnduserAuxClasses.java     |   45 -
 .../client/enduser/wizards/any/UserDetails.java    |  235 ----
 .../enduser/wizards/any/UserWizardBuilder.java     |  133 --
 .../AdminLTE_plugins/dataTables.bootstrap4.min.css |    1 +
 .../META-INF/resources/css/accessibility.css}      |   30 +-
 .../css/accessibility/accessibilityFont.css        |  101 ++
 .../css/accessibility/accessibilityHC.css          |  296 +++++
 .../resources/META-INF/resources/css/fonts.css     |   89 ++
 ...-latin-ext_cyrillic_latin_cyrillic-ext-300.woff |  Bin 0 -> 113012 bytes
 ...latin-ext_cyrillic_latin_cyrillic-ext-300.woff2 |  Bin 0 -> 86284 bytes
 ...-ext_cyrillic_latin_cyrillic-ext-300italic.woff |  Bin 0 -> 46356 bytes
 ...ext_cyrillic_latin_cyrillic-ext-300italic.woff2 |  Bin 0 -> 35780 bytes
 ...-latin-ext_cyrillic_latin_cyrillic-ext-600.woff |  Bin 0 -> 113196 bytes
 ...latin-ext_cyrillic_latin_cyrillic-ext-600.woff2 |  Bin 0 -> 86120 bytes
 ...-ext_cyrillic_latin_cyrillic-ext-600italic.woff |  Bin 0 -> 46280 bytes
 ...ext_cyrillic_latin_cyrillic-ext-600italic.woff2 |  Bin 0 -> 35928 bytes
 ...-latin-ext_cyrillic_latin_cyrillic-ext-700.woff |  Bin 0 -> 112628 bytes
 ...latin-ext_cyrillic_latin_cyrillic-ext-700.woff2 |  Bin 0 -> 85436 bytes
 ...tin-ext_cyrillic_latin_cyrillic-ext-italic.woff |  Bin 0 -> 46364 bytes
 ...in-ext_cyrillic_latin_cyrillic-ext-italic.woff2 |  Bin 0 -> 35924 bytes
 ...in-ext_cyrillic_latin_cyrillic-ext-regular.woff |  Bin 0 -> 113924 bytes
 ...n-ext_cyrillic_latin_cyrillic-ext-regular.woff2 |  Bin 0 -> 86732 bytes
 .../resources/META-INF/resources/css/login.css     |  141 ++
 .../META-INF/resources/css/search.css}             |   63 +-
 .../META-INF/resources/css/syncopeEnduser.css      | 1377 ++++++++++++++++++++
 .../META-INF/resources/js/accessibility.js         |  228 ++++
 .../META-INF/resources/js/copyToClipboard.js       |   79 ++
 .../META-INF/resources/js/syncopeEnduser.js}       |   21 +-
 .../src/main/resources/customFormAttributes.json   |    1 -
 .../src/main/resources/customFormLayout.json       |   14 +
 .../enduser/src/main/resources/enduser.properties  |    9 +
 ...s_ru.properties => oidcclient-agent.properties} |   11 +-
 ...erties => SyncopeEnduserApplication.properties} |   25 +-
 .../SyncopeEnduserApplication_fr_CA.properties     |  105 ++
 ...ies => SyncopeEnduserApplication_it.properties} |   28 +-
 ...ies => SyncopeEnduserApplication_ja.properties} |   28 +-
 ... => SyncopeEnduserApplication_pt_BR.properties} |   28 +-
 ...ies => SyncopeEnduserApplication_ru.properties} |   28 +-
 .../enduser/markup/html/form/BinaryFieldPanel.html |    5 +-
 .../enduser/markup/html/form/MultiFieldPanel.html  |    1 +
 .../markup/html/form/MultiFieldPanel_ru.properties |   29 -
 .../syncope/client/enduser/navigation/Navbar.html  |    2 +-
 .../client/enduser/pages/BaseEnduserWebPage.html   |   57 -
 .../syncope/client/enduser/pages/BasePage.html     |   90 ++
 .../BasePage.properties}                           |   11 +-
 ..._pt_BR.properties => BasePage_fr_CA.properties} |   10 +-
 .../BasePage_it.properties}                        |   11 +-
 .../BasePage_ja.properties}                        |   11 +-
 .../BasePage_pt_BR.properties}                     |   11 +-
 .../client/enduser/pages/BasePage_ru.properties}   |   26 +-
 .../syncope/client/enduser/pages/Dashboard.html    |    5 +-
 ...Reset_pt_BR.properties => Dashboard.properties} |    4 +-
 ...pt_BR.properties => Dashboard_fr_CA.properties} |    4 +-
 ...et_pt_BR.properties => Dashboard_it.properties} |    4 +-
 ...et_pt_BR.properties => Dashboard_ja.properties} |    4 +-
 ...pt_BR.properties => Dashboard_pt_BR.properties} |    4 +-
 ...rd_pt_BR.properties => Dashboard_ru.properties} |   12 +-
 .../client/enduser/pages/EditChangePassword.html   |    7 +-
 ...rd.properties => EditChangePassword.properties} |    6 +-
 .../pages/EditChangePassword_fr_CA.properties      |   24 +
 ...properties => EditChangePassword_it.properties} |    7 +-
 ...properties => EditChangePassword_ja.properties} |    7 +-
 ...perties => EditChangePassword_pt_BR.properties} |    8 +-
 ...properties => EditChangePassword_ru.properties} |   10 +-
 .../client/enduser/pages/EditSecurityQuestion.html |   64 +
 ....properties => EditSecurityQuestion.properties} |    8 +-
 ...operties => EditSecurityQuestion_it.properties} |    9 +-
 ...operties => EditSecurityQuestion_ja.properties} |    9 +-
 ...rties => EditSecurityQuestion_pt_BR.properties} |    9 +-
 .../pages/EditSecurityQuestion_ru.properties       |   24 +
 .../syncope/client/enduser/pages/EditUser.html     |   19 +-
 ...wordReset_ja.properties => EditUser.properties} |    8 +-
 ..._pt_BR.properties => EditUser_fr_CA.properties} |    4 +-
 ...set_pt_BR.properties => EditUser_it.properties} |    4 +-
 ...set_pt_BR.properties => EditUser_ja.properties} |    4 +-
 ..._pt_BR.properties => EditUser_pt_BR.properties} |    4 +-
 ...ord_pt_BR.properties => EditUser_ru.properties} |   12 +-
 .../apache/syncope/client/enduser/pages/Login.html |   13 +-
 .../client/enduser/pages/Login_ru.properties       |    1 -
 .../client/enduser/pages/MustChangePassword.html   |   93 +-
 .../enduser/pages/MustChangePassword.properties    |    3 +
 .../enduser/pages/MustChangePassword_it.properties |    3 +
 .../enduser/pages/MustChangePassword_ja.properties |    3 +
 .../pages/MustChangePassword_pt_BR.properties      |    2 +
 .../enduser/pages/MustChangePassword_ru.properties |    5 +-
 .../apache/syncope/client/enduser/pages/Self.html  |    1 +
 .../enduser/pages/SelfConfirmPasswordReset.html    |   48 +-
 .../pages/SelfConfirmPasswordReset.properties      |    6 +-
 .../pages/SelfConfirmPasswordReset_it.properties   |    6 +-
 .../pages/SelfConfirmPasswordReset_ja.properties   |    3 +
 .../SelfConfirmPasswordReset_pt_BR.properties      |    3 +
 .../pages/SelfConfirmPasswordReset_ru.properties   |    6 +-
 .../SelfPasswordReset$SelfPwdResetPanel.html}      |   23 +-
 ...SelfPasswordReset$SelfPwdResetPanel.properties} |    2 -
 ...sswordReset$SelfPwdResetPanel_fr_CA.properties} |   12 +-
 ...fPasswordReset$SelfPwdResetPanel_it.properties} |    2 -
 ...fPasswordReset$SelfPwdResetPanel_ja.properties} |    2 -
 ...sswordReset$SelfPwdResetPanel_pt_BR.properties} |    2 -
 ...fPasswordReset$SelfPwdResetPanel_ru.properties} |    2 -
 .../client/enduser/pages/SelfPasswordReset.html    |   33 +-
 .../enduser/pages/SelfPasswordReset.properties     |    4 +
 ...operties => SelfPasswordReset_fr_CA.properties} |    4 +
 .../enduser/pages/SelfPasswordReset_it.properties  |    4 +
 .../enduser/pages/SelfPasswordReset_ja.properties  |    4 +
 .../pages/SelfPasswordReset_pt_BR.properties       |    4 +
 .../enduser/pages/SelfPasswordReset_ru.properties  |    4 +
 ...elfPasswordReset.html => SelfRegistration.html} |   36 +-
 .../syncope/client/enduser/pages/SelfResult.html   |   74 ++
 ...swordReset.properties => SelfResult.properties} |    2 +-
 ...eset.properties => SelfResult_fr_CA.properties} |    2 +-
 ...rdReset.properties => SelfResult_it.properties} |    2 +-
 ...rdReset.properties => SelfResult_ja.properties} |    2 +-
 ...eset.properties => SelfResult_pt_BR.properties} |    2 +-
 ...rdReset.properties => SelfResult_ru.properties} |    2 +-
 .../syncope/client/enduser/pages/SelfUpdate.html   |    1 +
 .../client/enduser/pages/WizardMgtPanel.html       |   60 -
 .../client/enduser/panels/ChangePasswordPanel.html |   61 +
 .../ChangePasswordPanel.properties}                |    1 +
 .../ChangePasswordPanel_it.properties}             |    0
 .../ChangePasswordPanel_ja.properties}             |    0
 .../ChangePasswordPanel_pt_BR.properties}          |    0
 .../ChangePasswordPanel_ru.properties}             |    2 -
 .../syncope/client/enduser/panels/Sidebar.html     |   58 +
 .../client/enduser/panels/UserFormPanel.html       |   45 +
 .../UserFormPanel.properties}                      |    6 +-
 .../UserFormPanel_it.properties}                   |   12 +-
 .../UserFormPanel_ja.properties}                   |   12 +-
 .../UserFormPanel_pt_BR.properties}                |   12 +-
 .../enduser/panels/UserFormPanel_ru.properties}    |   21 +-
 .../any/AbstractAttrs$Schemas.html                 |    1 +
 .../{wizards => panels}/any/AbstractAttrs.html     |    1 +
 .../any/AbstractAttrs.properties                   |    0
 .../any/AbstractAttrs_it.properties                |    0
 .../any/AbstractAttrs_ja.properties                |    0
 .../any/AbstractAttrs_pt_BR.properties             |    0
 .../any/AbstractAttrs_ru.properties                |    1 -
 .../enduser/{wizards => panels}/any/DerAttrs.html  |    1 +
 .../{wizards => panels}/any/DerAttrs.properties    |    0
 .../{wizards => panels}/any/DerAttrs_it.properties |    0
 .../{wizards => panels}/any/DerAttrs_ja.properties |    0
 .../any/DerAttrs_pt_BR.properties                  |    0
 .../{wizards => panels}/any/DerAttrs_ru.properties |    1 -
 .../enduser/{wizards => panels}/any/Groups.html    |    6 +-
 .../{wizards => panels}/any/Groups.properties      |    0
 .../{wizards => panels}/any/Groups_it.properties   |    0
 .../{wizards => panels}/any/Groups_ja.properties   |    0
 .../any/Groups_pt_BR.properties                    |    0
 .../{wizards => panels}/any/Groups_ru.properties   |    1 -
 .../{wizards => panels}/any/PlainAttrs.html        |    3 +-
 .../{wizards => panels}/any/PlainAttrs.properties  |    0
 .../any/PlainAttrs_it.properties                   |    0
 .../any/PlainAttrs_ja.properties                   |    0
 .../any/PlainAttrs_pt_BR.properties                |    0
 .../any/PlainAttrs_ru.properties                   |    1 -
 .../enduser/{wizards => panels}/any/Resources.html |    6 +-
 .../{wizards => panels}/any/Resources.properties   |    0
 .../any/Resources_it.properties                    |    0
 .../any/Resources_ja.properties                    |    0
 .../any/Resources_pt_BR.properties                 |    0
 .../any/Resources_ru.properties                    |    2 -
 .../client/enduser/panels/any/SelfUserDetails.html |    3 +-
 .../any/UserDetails$EditUserPasswordPanel.html     |    3 +-
 .../{wizards => panels}/any/UserDetails.html       |    9 +-
 .../{wizards => panels}/any/UserDetails.properties |    0
 .../any/UserDetails_it.properties                  |    0
 .../any/UserDetails_ja.properties                  |    0
 .../any/UserDetails_pt_BR.properties               |    0
 .../any/UserDetails_ru.properties                  |    0
 .../enduser/{wizards => panels}/any/VirAttrs.html  |    1 +
 .../{wizards => panels}/any/VirAttrs.properties    |    0
 .../{wizards => panels}/any/VirAttrs_it.properties |    0
 .../{wizards => panels}/any/VirAttrs_ja.properties |    0
 .../any/VirAttrs_pt_BR.properties                  |    0
 .../{wizards => panels}/any/VirAttrs_ru.properties |    1 -
 .../any => panels/captcha}/CaptchaPanel.html       |    8 +-
 .../any => panels/captcha}/CaptchaPanel.properties |    0
 .../captcha}/CaptchaPanel_it.properties            |    0
 .../captcha}/CaptchaPanel_ja.properties            |    0
 .../client/enduser/widgets/UserProfileWidget.html  |   50 +
 .../UserProfileWidget.properties}                  |    5 +-
 .../UserProfileWidget_fr_CA.properties}            |    5 +-
 .../UserProfileWidget_it.properties}               |    6 +-
 .../UserProfileWidget_ja.properties}               |    6 +-
 .../UserProfileWidget_pt_BR.properties}            |    6 +-
 .../UserProfileWidget_ru.properties}               |    6 +-
 .../wizards/any/CaptchaPanel_pt_BR.properties      |   17 -
 .../enduser/wizards/any/CaptchaPanel_ru.properties |   17 -
 .../enduser/wizards/any/EnduserAuxClasses.html     |   35 -
 .../wizards/any/EnduserAuxClasses_it.properties    |   17 -
 .../wizards/any/EnduserAuxClasses_ja.properties    |   17 -
 .../wizards/any/EnduserAuxClasses_pt_BR.properties |   17 -
 .../wizards/any/EnduserAuxClasses_ru.properties    |   18 -
 .../client/ui/commons/wizards/AjaxWizard.html      |   46 -
 .../ui/commons/wizards/AjaxWizardMgtButtonBar.html |   38 -
 .../ui/commons/wizards/AjaxWizard_it.properties    |   23 -
 .../ui/commons/wizards/AjaxWizard_ja.properties    |   23 -
 .../ui/commons/wizards/AjaxWizard_pt_BR.properties |   23 -
 .../ui/commons/wizards/AjaxWizard_ru.properties    |   30 -
 .../syncope/client/enduser/AbstractTest.java       |  229 ++++
 .../enduser/SyncopeEnduserApplicationTest.java     |  138 ++
 .../idrepo/enduser/src/test/resources/log4j2.xml   |   54 +
 .../syncope/client/enduser/pages/Flowable.java     |  131 +-
 .../client/enduser/panels/UserRequestDetails.java  |  126 ++
 .../client/enduser/rest/BaseRestClient.java        |    2 +-
 .../client/enduser/rest/UserRequestRestClient.java |   16 +-
 .../common/ui/panels/UserRequestFormPanel.java     |  220 ++++
 .../enduser/pages/Flowable$UserRequestDetails.html |   36 -
 .../syncope/client/enduser/pages/Flowable.html     |   85 +-
 .../client/enduser/pages/Flowable.properties       |    1 +
 .../client/enduser/pages/Flowable_it.properties    |    1 +
 .../client/enduser/pages/Flowable_ja.properties    |    1 +
 .../client/enduser/pages/Flowable_pt_BR.properties |    1 +
 .../client/enduser/pages/Flowable_ru.properties    |    1 +
 .../client/enduser/pages/UserRequestForms.html     |    1 +
 .../client/enduser/panels/UserRequestDetails.html  |   29 +-
 .../common/ui/panels/UserRequestFormPanel.html     |   13 +-
 .../ui/panels/UserRequestFormPanel.properties      |    3 +-
 .../ui/panels/UserRequestFormPanel_it.properties   |    3 +-
 .../ui/panels/UserRequestFormPanel_ja.properties   |    3 +-
 .../panels/UserRequestFormPanel_pt_BR.properties   |    3 +-
 .../ui/panels/UserRequestFormPanel_ru.properties   |   10 +-
 .../enduser/panels/OIDCSSOLoginFormPanel.java      |    4 +-
 .../resources/EnduserCodeConsumerResource.java     |    6 +-
 .../enduser/panels/OIDCSSOLoginFormPanel.html      |    9 +-
 .../enduser/panels/SAMLSSOLoginFormPanel.java      |    4 +-
 .../EnduserAssertionConsumerResource.java          |    6 +-
 .../enduser/panels/SAMLSSOLoginFormPanel.html      |    9 +-
 .../syncope/fit/{ui => }/AbstractUITCase.java      |    2 +-
 .../syncope/fit/console/AbstractConsoleITCase.java |    2 +-
 .../syncope/fit/console/AbstractTypesITCase.java   |   30 +-
 .../syncope/fit/console/AjaxBrowseITCase.java      |   16 +-
 .../syncope/fit/console/AnyObjectsITCase.java      |    8 +-
 .../syncope/fit/console/AnyTypeClassesITCase.java  |    4 +-
 .../apache/syncope/fit/console/AnyTypesITCase.java |    2 +-
 .../apache/syncope/fit/console/BatchesITCase.java  |   14 +-
 .../fit/console/DisplayAttributesITCase.java       |    2 +-
 .../apache/syncope/fit/console/GroupsITCase.java   |    8 +-
 .../syncope/fit/console/LinkedAccountsITCase.java  |   10 +-
 .../org/apache/syncope/fit/console/LogsITCase.java |    2 +-
 .../syncope/fit/console/NotificationsITCase.java   |    8 +-
 .../syncope/fit/console/ParametersITCase.java      |    2 +-
 .../apache/syncope/fit/console/PoliciesITCase.java |    6 +-
 .../apache/syncope/fit/console/RealmsITCase.java   |    4 +-
 .../apache/syncope/fit/console/ReportsITCase.java  |    6 +-
 .../apache/syncope/fit/console/RolesITCase.java    |    4 +-
 .../fit/console/SecurityQuestionsITCase.java       |    4 +-
 .../apache/syncope/fit/console/TopologyITCase.java |    8 +-
 .../apache/syncope/fit/console/UsersITCase.java    |   22 +-
 .../apache/syncope/fit/core/PlainSchemaITCase.java |   95 +-
 .../syncope/fit/core/PropagationTaskITCase.java    |    9 +-
 .../syncope/fit/enduser/AbstractEnduserITCase.java |   48 +-
 .../syncope/fit/enduser/AnonymousITCase.java       |  154 +++
 .../syncope/fit/enduser/AuthenticatedITCase.java   |  122 ++
 .../apache/syncope/fit/enduser/EnduserITCase.java  |  305 -----
 .../src/test/resources/enduser.properties          |    9 +
 .../src/main/resources/enduser.properties          |    9 +
 335 files changed, 8449 insertions(+), 3929 deletions(-)

diff --git a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/BaseLogin.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/BaseLogin.java
index f03f20a..44c4e23 100644
--- a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/BaseLogin.java
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/BaseLogin.java
@@ -63,6 +63,10 @@ public abstract class BaseLogin extends WebPage {
 
     protected static final Logger LOG = LoggerFactory.getLogger(BaseLogin.class);
 
+    public static final List<Locale> SUPPORTED_LOCALES = List.of(
+            Locale.ENGLISH, Locale.CANADA_FRENCH, Locale.ITALIAN, Locale.JAPANESE, new Locale("pt", "BR"),
+            new Locale("ru"));
+
     @SpringBean
     private DomainOps domainOps;
 
@@ -219,8 +223,6 @@ public abstract class BaseLogin extends WebPage {
 
     protected abstract String getAnonymousUser();
 
-    protected abstract List<Locale> getSupportedLocales();
-
     protected abstract void authenticate(
             String username,
             String password,
@@ -245,7 +247,7 @@ public abstract class BaseLogin extends WebPage {
         }
 
         LocaleDropDown(final String id) {
-            super(id, getSupportedLocales());
+            super(id, SUPPORTED_LOCALES);
 
             setChoiceRenderer(new LocaleRenderer());
             setModel(new IModel<Locale>() {
@@ -275,7 +277,7 @@ public abstract class BaseLogin extends WebPage {
                     getHeader(HttpHeaders.ACCEPT_LANGUAGE);
             if (StringUtils.isNotBlank(acceptLanguage)) {
                 try {
-                    filtered = Locale.filter(Locale.LanguageRange.parse(acceptLanguage), getSupportedLocales());
+                    filtered = Locale.filter(Locale.LanguageRange.parse(acceptLanguage), SUPPORTED_LOCALES);
                 } catch (Exception e) {
                     LOG.debug("Could not parse {} HTTP header value '{}'",
                             HttpHeaders.ACCEPT_LANGUAGE, acceptLanguage, e);
diff --git a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/Constants.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/Constants.java
index ee8976a..2efdb0b 100644
--- a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/Constants.java
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/Constants.java
@@ -113,6 +113,8 @@ public final class Constants {
 
     public static final int MAX_ROLE_LIST_SIZE = 30;
 
+    public static final String NOTIFICATION_TITLE_PARAM = "notificationTitle";
+
     public static final String NOTIFICATION_MSG_PARAM = "notificationMessage";
 
     public static final String NOTIFICATION_LEVEL_PARAM = "notificationLevel";
diff --git a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AjaxPalettePanel.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AjaxPalettePanel.java
index 339bea7..c6c2250 100644
--- a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AjaxPalettePanel.java
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/AjaxPalettePanel.java
@@ -24,6 +24,7 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.function.Function;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -33,6 +34,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
 import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
+import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.wicket.Component;
 import org.apache.wicket.Session;
 import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -193,6 +195,7 @@ public class AjaxPalettePanel<T extends Serializable> extends AbstractFieldPanel
                     @Override
                     protected void onUpdate(final AjaxRequestTarget target) {
                         processInput();
+                        Optional.ofNullable(builder.event).ifPresent(e -> e.apply(target));
                     }
                 });
 
@@ -267,6 +270,8 @@ public class AjaxPalettePanel<T extends Serializable> extends AbstractFieldPanel
         protected Function<String, Stream<String>> idExtractor =
                 (Function<String, Stream<String>> & Serializable) input -> Stream.of(Strings.split(input, ','));
 
+        protected Function<AjaxRequestTarget, Boolean> event;
+
         protected Function<Object, Map<String, String>> additionalAttributes;
 
         public Builder<T> setName(final String name) {
@@ -320,6 +325,11 @@ public class AjaxPalettePanel<T extends Serializable> extends AbstractFieldPanel
             return this;
         }
 
+        public Builder<T> event(final Function<AjaxRequestTarget, Boolean> event) {
+            this.event = event;
+            return this;
+        }
+
         public Builder<T> additionalAttributes(final Function<Object, Map<String, String>> additionalAttributes) {
             this.additionalAttributes = additionalAttributes;
             return this;
@@ -404,4 +414,24 @@ public class AjaxPalettePanel<T extends Serializable> extends AbstractFieldPanel
             return filtered;
         }
     }
+
+    public static class UpdateActionEvent {
+
+        private final UserTO item;
+
+        private final AjaxRequestTarget target;
+
+        public UpdateActionEvent(final UserTO item, final AjaxRequestTarget target) {
+            this.item = item;
+            this.target = target;
+        }
+
+        public UserTO getItem() {
+            return item;
+        }
+
+        public AjaxRequestTarget getTarget() {
+            return target;
+        }
+    }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/BinaryFieldDownload.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/BinaryFieldDownload.java
similarity index 98%
rename from client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/BinaryFieldDownload.java
rename to client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/BinaryFieldDownload.java
index 65b0c03..d64f997 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/BinaryFieldDownload.java
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/markup/html/form/BinaryFieldDownload.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.console.wicket.markup.html.form;
+package org.apache.syncope.client.ui.commons.markup.html.form;
 
 import java.time.Duration;
 import org.apache.commons.lang3.StringUtils;
diff --git a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/panels/CardPanel.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/panels/CardPanel.java
new file mode 100644
index 0000000..face722
--- /dev/null
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/panels/CardPanel.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.ui.commons.panels;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.ResourceModel;
+
+public final class CardPanel<T extends Component> extends Panel {
+
+    private static final long serialVersionUID = -7906010415162945394L;
+
+    private CardPanel(final String id, final Builder<T> builder) {
+        super(id);
+        this.setOutputMarkupId(true);
+        this.setVisible(builder.visible);
+
+        this.add(new Label("cardLabel", new ResourceModel(builder.name, builder.name)).setOutputMarkupId(true));
+        this.add(builder.component);
+    }
+
+    public static class Builder<T extends Component> {
+
+        protected String name;
+
+        protected T component;
+
+        protected boolean visible;
+
+        public Builder<T> setName(final String name) {
+            this.name = name;
+            return this;
+        }
+
+        public Builder<T> setComponent(final T component) {
+            this.component = component;
+            return this;
+        }
+
+        public Builder<T> isVisible(final boolean visible) {
+            this.visible = visible;
+            return this;
+        }
+
+        public CardPanel<T> build(final String id) {
+            return new CardPanel<>(id, this);
+        }
+    }
+}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/themes/AdminLTE.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/themes/AdminLTE.java
similarity index 96%
rename from client/idrepo/console/src/main/java/org/apache/syncope/client/console/themes/AdminLTE.java
rename to client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/themes/AdminLTE.java
index d270149..1dccc49 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/themes/AdminLTE.java
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/themes/AdminLTE.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.console.themes;
+package org.apache.syncope.client.ui.commons.themes;
 
 import de.agilecoders.wicket.core.settings.Theme;
 import java.util.ArrayList;
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/themes/AdminLTECssResourceReference.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/themes/AdminLTECssResourceReference.java
similarity index 97%
rename from client/idrepo/console/src/main/java/org/apache/syncope/client/console/themes/AdminLTECssResourceReference.java
rename to client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/themes/AdminLTECssResourceReference.java
index 397ef5e..f9e3617 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/themes/AdminLTECssResourceReference.java
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/themes/AdminLTECssResourceReference.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.console.themes;
+package org.apache.syncope.client.ui.commons.themes;
 
 import de.agilecoders.wicket.core.Bootstrap;
 import java.util.ArrayList;
diff --git a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/wizards/any/PasswordPanel.java b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/wizards/any/PasswordPanel.java
index 6cf3087..d34ef77 100644
--- a/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/wizards/any/PasswordPanel.java
+++ b/client/idrepo/common-ui/src/main/java/org/apache/syncope/client/ui/commons/wizards/any/PasswordPanel.java
@@ -38,14 +38,16 @@ public class PasswordPanel extends Panel {
     public PasswordPanel(
             final String id,
             final UserWrapper wrapper,
+            final Boolean storePasswordInSyncope,
             final boolean templateMode) {
-        this(id, wrapper, templateMode, null);
+        this(id, wrapper, templateMode, storePasswordInSyncope, null);
     }
 
     public PasswordPanel(
             final String id,
             final UserWrapper wrapper,
             final boolean templateMode,
+            final Boolean storePasswordInSyncope,
             final PasswordStrengthBehavior passwordStrengthBehavior) {
 
         super(id);
@@ -88,16 +90,16 @@ public class PasswordPanel extends Panel {
             form.add(new EqualPasswordInputValidator(passwordField.getField(), confirmPasswordField.getField()));
         }
 
-        AjaxCheckBoxPanel storePasswordInSyncope = new AjaxCheckBoxPanel("storePasswordInSyncope",
+        AjaxCheckBoxPanel storePasswordInSyncopePanel = new AjaxCheckBoxPanel("storePasswordInSyncope",
                 "storePasswordInSyncope", new PropertyModel<>(wrapper, "storePasswordInSyncope"));
-        storePasswordInSyncope.getField().setLabel(new ResourceModel("storePasswordInSyncope"));
-        storePasswordInSyncope.setOutputMarkupId(true);
-        storePasswordInSyncope.setOutputMarkupPlaceholderTag(true);
-        if (wrapper.getInnerObject().getKey() == null) {
-            storePasswordInSyncope.getField().setDefaultModelObject(Boolean.TRUE);
+        storePasswordInSyncopePanel.getField().setLabel(new ResourceModel("storePasswordInSyncope"));
+        storePasswordInSyncopePanel.setOutputMarkupId(true);
+        storePasswordInSyncopePanel.setOutputMarkupPlaceholderTag(true);
+        if (storePasswordInSyncope) {
+            storePasswordInSyncopePanel.getField().setDefaultModelObject(Boolean.TRUE);
         } else {
-            storePasswordInSyncope.setVisible(false);
+            storePasswordInSyncopePanel.setVisible(false);
         }
-        form.add(storePasswordInSyncope);
+        form.add(storePasswordInSyncopePanel);
     }
 }
diff --git a/client/idrepo/enduser/src/main/resources/org/apache/syncope/client/enduser/wizards/any/UserDetails$EditUserPasswordPanel.html b/client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/panels/CardPanel.html
similarity index 73%
copy from client/idrepo/enduser/src/main/resources/org/apache/syncope/client/enduser/wizards/any/UserDetails$EditUserPasswordPanel.html
copy to client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/panels/CardPanel.html
index e77ed5d..1ef8d64 100644
--- a/client/idrepo/enduser/src/main/resources/org/apache/syncope/client/enduser/wizards/any/UserDetails$EditUserPasswordPanel.html
+++ b/client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/panels/CardPanel.html
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" ?>
 <!--
 Licensed to the Apache Software Foundation (ASF) under one
 or more contributor license agreements.  See the NOTICE file
@@ -18,12 +19,13 @@ under the License.
 -->
 <html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
   <wicket:panel>
-    <div id="editUserChangePassword">
-      <div class="alert alert-warning">
-        <i class="fas fa-exclamation-triangle"></i> <label wicket:id="warning">[warning]</label>
+    <div class="box-header formcard">
+      <header class="card-container card-red">
+        <label class="card-header-style" wicket:id="cardLabel">[CARD LABEL]</label>
+      </header>
+      <div class="card-container-body">
+        <span wicket:id="contentPanel">[CONTENT PANEL]</span>
       </div>
-
-      <div wicket:id="passwordPanel">[change password panel]</div>
     </div>
   </wicket:panel>
 </html>
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/themes/css/AdminLTE.css b/client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/themes/css/AdminLTE.css
similarity index 100%
rename from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/themes/css/AdminLTE.css
rename to client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/themes/css/AdminLTE.css
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/themes/js/AdminLTE-app.min.js b/client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/themes/js/AdminLTE-app.min.js
similarity index 100%
rename from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/themes/js/AdminLTE-app.min.js
rename to client/idrepo/common-ui/src/main/resources/org/apache/syncope/client/ui/commons/themes/js/AdminLTE-app.min.js
diff --git a/client/idrepo/console/pom.xml b/client/idrepo/console/pom.xml
index db78158..dc4ca1a 100644
--- a/client/idrepo/console/pom.xml
+++ b/client/idrepo/console/pom.xml
@@ -123,7 +123,6 @@ under the License.
       <artifactId>disruptor</artifactId>
     </dependency>
 
-    <!-- required by wicket tester -->
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
index 76ff69c..a9e48da 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
@@ -28,8 +28,6 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import org.apache.commons.lang3.BooleanUtils;
@@ -43,7 +41,6 @@ import org.apache.syncope.client.console.init.ClassPathScanImplementationLookup;
 import org.apache.syncope.client.console.pages.BasePage;
 import org.apache.syncope.client.console.pages.Dashboard;
 import org.apache.syncope.client.console.pages.Login;
-import org.apache.syncope.client.console.themes.AdminLTE;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
 import org.apache.syncope.common.lib.PropertyUtils;
 import org.apache.wicket.Page;
@@ -69,6 +66,7 @@ import org.apache.syncope.client.console.commons.StatusProvider;
 import org.apache.syncope.client.console.commons.VirSchemaDetailsPanelProvider;
 import org.apache.syncope.client.console.pages.MustChangePassword;
 import org.apache.syncope.client.console.panels.AnyPanel;
+import org.apache.syncope.client.ui.commons.themes.AdminLTE;
 import org.apache.syncope.client.ui.commons.SyncopeUIRequestCycleListener;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
@@ -88,10 +86,6 @@ public class SyncopeWebApplication extends WicketBootSecuredWebApplication {
 
     private static final String CONSOLE_PROPERTIES = "console.properties";
 
-    public static final List<Locale> SUPPORTED_LOCALES = List.of(
-            Locale.ENGLISH, Locale.CANADA_FRENCH, Locale.ITALIAN, Locale.JAPANESE, new Locale("pt", "BR"),
-            new Locale("ru"));
-
     public static SyncopeWebApplication get() {
         return (SyncopeWebApplication) WebApplication.get();
     }
@@ -169,7 +163,7 @@ public class SyncopeWebApplication extends WicketBootSecuredWebApplication {
         }
     }
 
-    protected void setSecurityHeaders(final Properties props, final WebResponse response) {
+    protected static void setSecurityHeaders(final Properties props, final WebResponse response) {
         @SuppressWarnings("unchecked")
         Enumeration<String> propNames = (Enumeration<String>) props.propertyNames();
         while (propNames.hasMoreElements()) {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
index 11a1cad..a6ff75f 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
@@ -22,6 +22,7 @@ import java.io.Serializable;
 import java.lang.reflect.Constructor;
 import java.time.Duration;
 import java.util.List;
+import java.util.stream.StreamSupport;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.BookmarkablePageLinkBuilder;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
@@ -394,19 +395,16 @@ public class BasePage extends BaseWebPage {
         }
         // 4. when found, set CSS coordinates for menu
         if (containingLI != null) {
-            for (Component child : containingLI) {
-                if (child instanceof Link) {
-                    child.add(new Behavior() {
+            StreamSupport.stream(containingLI.spliterator(), false).filter(Link.class::isInstance).
+                    forEach(child -> child.add(new Behavior() {
 
-                        private static final long serialVersionUID = -5775607340182293596L;
+                private static final long serialVersionUID = -5775607340182293596L;
 
-                        @Override
-                        public void onComponentTag(final Component component, final ComponentTag tag) {
-                            tag.append("class", "active", " ");
-                        }
-                    });
+                @Override
+                public void onComponentTag(final Component component, final ComponentTag tag) {
+                    tag.append("class", "active", " ");
                 }
-            }
+            }));
 
             if (keymasterULContainer.getId().equals(containingLI.getParent().getId())) {
                 keymasterULContainer.add(new Behavior() {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Login.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Login.java
index 37944e8..a1244fe 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Login.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Login.java
@@ -21,7 +21,6 @@ package org.apache.syncope.client.console.pages;
 import java.security.AccessControlException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Locale;
 import org.apache.syncope.client.console.SyncopeWebApplication;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.client.ui.commons.BaseLogin;
@@ -71,11 +70,6 @@ public class Login extends BaseLogin {
     }
 
     @Override
-    protected List<Locale> getSupportedLocales() {
-        return SyncopeWebApplication.SUPPORTED_LOCALES;
-    }
-
-    @Override
     protected void authenticate(final String username, final String password, final AjaxRequestTarget target)
             throws AccessControlException {
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemaTypeWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemaTypeWizardBuilder.java
index b646d0a..77366e6 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemaTypeWizardBuilder.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemaTypeWizardBuilder.java
@@ -32,6 +32,7 @@ import org.apache.syncope.client.console.rest.SchemaRestClient;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
 import org.apache.syncope.client.console.wizards.BaseAjaxWizardBuilder;
+import org.apache.syncope.client.ui.commons.BaseLogin;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.common.lib.to.DerSchemaTO;
@@ -163,7 +164,7 @@ public class SchemaTypeWizardBuilder extends BaseAjaxWizardBuilder<SchemaTO> {
                         }
                     });
                     locale.setRequired(true).hideLabel();
-                    locale.setChoices(SyncopeWebApplication.SUPPORTED_LOCALES.stream().
+                    locale.setChoices(BaseLogin.SUPPORTED_LOCALES.stream().
                             map(Objects::toString).collect(Collectors.toList()));
                     locale.addValidator(validatable -> {
                         try {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/status/ChangePasswordModal.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/status/ChangePasswordModal.java
index 5d56dba..0e865ec 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/status/ChangePasswordModal.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/status/ChangePasswordModal.java
@@ -58,7 +58,7 @@ public class ChangePasswordModal extends AbstractModalPanel<AnyWrapper<UserTO>>
 
         this.wrapper = wrapper;
 
-        final PasswordPanel passwordPanel = new PasswordPanel("passwordPanel", wrapper, false);
+        final PasswordPanel passwordPanel = new PasswordPanel("passwordPanel", wrapper, false, false);
         passwordPanel.setOutputMarkupId(true);
         add(passwordPanel);
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/bootstrap/dialog/BaseModal.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/bootstrap/dialog/BaseModal.java
index 861394e..2bdd5b4 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/bootstrap/dialog/BaseModal.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/bootstrap/dialog/BaseModal.java
@@ -102,8 +102,6 @@ public class BaseModal<T extends Serializable> extends Modal<T> {
         this.windowClosedCallback = null;
         components = new ArrayList<>();
 
-        // Note: not adding this would imply adding WebjarsJavaScriptResourceReference about JQuery resizable and mouse
-        // add(new Resizable().withChildSelector(".modal-content"));
         // Note: not adding this would imply adding of WebjarsJavaScriptResourceReference about JQuery draggable
         add(new Draggable(new DraggableConfig().withHandle(".modal-header").withCursor("move")));
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/BinaryFieldPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/BinaryFieldPanel.java
index bc48ae0..bc46754 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/BinaryFieldPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/BinaryFieldPanel.java
@@ -18,6 +18,8 @@
  */
 package org.apache.syncope.client.console.wicket.markup.html.form;
 
+import org.apache.syncope.client.ui.commons.markup.html.form.BinaryFieldDownload;
+
 import static de.agilecoders.wicket.jquery.JQuery.$;
 
 import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.fileinput.BootstrapFileInputField;
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/AbstractAttrsWizardStep.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/AbstractAttrsWizardStep.java
index f4ae2c3..600fc1d 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/AbstractAttrsWizardStep.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/AbstractAttrsWizardStep.java
@@ -285,7 +285,7 @@ public abstract class AbstractAttrsWizardStep<S extends SchemaTO> extends Wizard
                 break;
 
             case Binary:
-                final PageReference pageRef = getPageReference();
+                PageReference pageRef = getPageReference();
                 panel = new BinaryFieldPanel(
                         "panel",
                         plainSchema.getLabel(SyncopeConsoleSession.get().getLocale()),
@@ -299,7 +299,6 @@ public abstract class AbstractAttrsWizardStep<S extends SchemaTO> extends Wizard
                     protected PageReference getPageReference() {
                         return pageRef;
                     }
-
                 };
                 if (required) {
                     panel.addRequiredLabel();
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/UserDetails.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/UserDetails.java
index d8c49e3..e067ef1 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/UserDetails.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/UserDetails.java
@@ -137,7 +137,7 @@ public class UserDetails extends Details<UserTO> {
             super(id);
             setOutputMarkupId(true);
             add(new Label("warning", new ResourceModel("password.change.warning")));
-            add(new PasswordPanel("passwordPanel", wrapper, templateMode));
+            add(new PasswordPanel("passwordPanel", wrapper, false, templateMode));
         }
     }
 }
diff --git a/client/idrepo/enduser/pom.xml b/client/idrepo/enduser/pom.xml
index f7ed9fa..9bf1786 100644
--- a/client/idrepo/enduser/pom.xml
+++ b/client/idrepo/enduser/pom.xml
@@ -110,14 +110,16 @@ under the License.
       <groupId>com.lmax</groupId>
       <artifactId>disruptor</artifactId>
     </dependency>
+
     <dependency>
-      <groupId>commons-logging</groupId>
-      <artifactId>commons-logging</artifactId>
-      <scope>provided</scope>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>jcl-over-slf4j</artifactId>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
     </dependency>
   </dependencies>
   
@@ -240,6 +242,7 @@ under the License.
             <configuration>
               <jvmArguments>
                 -Dwicket.core.settings.general.configuration-type=development
+                -XX:HotswapAgent=fatjar
                 -Xdebug -Xrunjdwp:transport=dt_socket,address=8004,server=y,suspend=n
               </jvmArguments>
               <profiles>
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/BookmarkablePageLinkBuilder.java
similarity index 50%
copy from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
copy to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/BookmarkablePageLinkBuilder.java
index 0a683a9..7f5c806 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/BookmarkablePageLinkBuilder.java
@@ -18,18 +18,28 @@
  */
 package org.apache.syncope.client.enduser;
 
-public final class SyncopeEnduserConstants {
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 
-    public static final String CAPTCHA_SESSION_KEY = "captcha";
+public final class BookmarkablePageLinkBuilder {
 
-    public static final String XSRF_COOKIE = "XSRF-TOKEN";
+    public static <T extends WebPage> BookmarkablePageLink<T> build(
+            final String key, final Class<T> defaultPageClass) {
 
-    public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
+        return build(key, key, defaultPageClass);
+    }
 
-    public static final String MEMBERSHIP_ATTR_SEPARATOR = "#";
+    public static <T extends WebPage> BookmarkablePageLink<T> build(
+            final String key, final String id, final Class<T> defaultPageClass) {
 
-    private SyncopeEnduserConstants() {
-        // private constructor for utility class
+        @SuppressWarnings("unchecked")
+        Class<T> pageClass = (Class<T>) SyncopeWebApplication.get().getPageClass(key);
+        return new BookmarkablePageLink<>(
+                id,
+                pageClass == null ? defaultPageClass : pageClass);
     }
 
+    private BookmarkablePageLinkBuilder() {
+        // private constructor for static utility class
+    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/PreferenceManager.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/PreferenceManager.java
new file mode 100644
index 0000000..845e7a6
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/PreferenceManager.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.wicket.util.cookies.CookieDefaults;
+import org.apache.wicket.util.cookies.CookieUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PreferenceManager implements Serializable {
+
+    private static final long serialVersionUID = 3581434664555284193L;
+
+    private static final Logger LOG = LoggerFactory.getLogger(PreferenceManager.class);
+
+    private static final String COOKIE_NAME = "syncope2EnduserPrefs";
+
+    private static final int ONE_YEAR_TIME = 60 * 60 * 24 * 365;
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private static final TypeReference<Map<String, String>> MAP_TYPE_REF = new TypeReference<Map<String, String>>() {
+    };
+
+    private static final List<Integer> PAGINATOR_CHOICES = Arrays.asList(new Integer[] { 10, 25, 50 });
+
+    private static final CookieUtils COOKIE_UTILS;
+
+    static {
+        CookieDefaults cookieDefaults = new CookieDefaults();
+        cookieDefaults.setMaxAge(ONE_YEAR_TIME);
+        COOKIE_UTILS = new CookieUtils(cookieDefaults);
+    }
+
+    public List<Integer> getPaginatorChoices() {
+        return PAGINATOR_CHOICES;
+    }
+
+    private Map<String, String> getPrefs(final String value) {
+        Map<String, String> prefs;
+        try {
+            if (StringUtils.isNotBlank(value)) {
+                prefs = MAPPER.readValue(value, MAP_TYPE_REF);
+            } else {
+                throw new Exception("Invalid cookie value '" + value + "'");
+            }
+        } catch (Exception e) {
+            LOG.debug("No preferences found", e);
+            prefs = new HashMap<>();
+        }
+
+        return prefs;
+    }
+
+    private String setPrefs(final Map<String, String> prefs) throws IOException {
+        StringWriter writer = new StringWriter();
+        MAPPER.writeValue(writer, prefs);
+
+        return writer.toString();
+    }
+
+    public String get(final String key) {
+        String result = null;
+
+        String prefString = COOKIE_UTILS.load(COOKIE_NAME);
+        if (prefString != null) {
+            Map<String, String> prefs = getPrefs(new String(Base64.getDecoder().decode(prefString.getBytes())));
+            result = prefs.get(key);
+        }
+
+        return result;
+    }
+
+    public Integer getPaginatorRows(final String key) {
+        Integer result = getPaginatorChoices().get(0);
+
+        String value = get(key);
+        if (value != null) {
+            result = NumberUtils.toInt(value, 10);
+        }
+
+        return result;
+    }
+
+    public List<String> getList(final String key) {
+        List<String> result = new ArrayList<>();
+
+        String compound = get(key);
+
+        if (StringUtils.isNotBlank(compound)) {
+            String[] items = compound.split(";");
+            result.addAll(Arrays.asList(items));
+        }
+
+        return result;
+    }
+
+    public void set(final Map<String, List<String>> prefs) {
+        Map<String, String> current = new HashMap<>();
+
+        String prefString = COOKIE_UTILS.load(COOKIE_NAME);
+        if (prefString != null) {
+            current.putAll(getPrefs(new String(Base64.getDecoder().decode(prefString.getBytes()))));
+        }
+
+        // after retrieved previous setting in order to overwrite the key ...
+        prefs.forEach((key, values) -> {
+            current.put(key, StringUtils.join(values, ";"));
+        });
+
+        try {
+            COOKIE_UTILS.save(COOKIE_NAME, Base64.getEncoder().encodeToString(setPrefs(current).getBytes()));
+        } catch (IOException e) {
+            LOG.error("Could not save {} info: {}", getClass().getSimpleName(), current, e);
+        }
+    }
+
+    public void set(final String key, final String value) {
+        String prefString = COOKIE_UTILS.load(COOKIE_NAME);
+
+        final Map<String, String> current = new HashMap<>();
+        if (prefString != null) {
+            current.putAll(getPrefs(new String(Base64.getDecoder().decode(prefString.getBytes()))));
+        }
+
+        // after retrieved previous setting in order to overwrite the key ...
+        current.put(key, value);
+
+        try {
+            COOKIE_UTILS.save(COOKIE_NAME, Base64.getEncoder().encodeToString(setPrefs(current).getBytes()));
+        } catch (IOException e) {
+            LOG.error("Could not save {} info: {}", getClass().getSimpleName(), current, e);
+        }
+    }
+
+    public void setList(final String key, final List<String> values) {
+        set(key, StringUtils.join(values, ";"));
+    }
+
+    public void setList(final Map<String, List<String>> prefs) {
+        set(prefs);
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserRequestCycleListener.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserRequestCycleListener.java
new file mode 100644
index 0000000..a0e2192
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserRequestCycleListener.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser;
+
+import java.security.AccessControlException;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.ForbiddenException;
+import javax.xml.ws.WebServiceException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.enduser.pages.Login;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.wicket.authorization.UnauthorizedInstantiationException;
+import org.apache.wicket.core.request.handler.PageProvider;
+import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
+import org.apache.wicket.markup.html.pages.ExceptionErrorPage;
+import org.apache.wicket.protocol.http.PageExpiredException;
+import org.apache.wicket.request.IRequestHandler;
+import org.apache.wicket.request.component.IRequestablePage;
+import org.apache.wicket.request.cycle.IRequestCycleListener;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SyncopeEnduserRequestCycleListener implements IRequestCycleListener {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SyncopeEnduserRequestCycleListener.class);
+
+    private Throwable instanceOf(final Exception e, final Class<? extends Exception> clazz) {
+        return clazz.isAssignableFrom(e.getClass())
+                ? e
+                : e.getCause() != null && clazz.isAssignableFrom(e.getCause().getClass())
+                ? e.getCause()
+                : e.getCause() != null && e.getCause().getCause() != null
+                && clazz.isAssignableFrom(e.getCause().getCause().getClass())
+                ? e.getCause().getCause()
+                : null;
+    }
+
+    @Override
+    public IRequestHandler onException(final RequestCycle cycle, final Exception e) {
+        LOG.error("Exception found", e);
+
+        PageParameters errorParameters = new PageParameters();
+
+        IRequestablePage errorPage;
+        if (instanceOf(e, UnauthorizedInstantiationException.class) != null) {
+            errorParameters.add("errorMessage", SyncopeEnduserSession.Error.AUTHORIZATION.fallback());
+            errorPage = new Login(errorParameters);
+        } else if (instanceOf(e, AccessControlException.class) != null) {
+            if (StringUtils.containsIgnoreCase(instanceOf(e, AccessControlException.class).getMessage(), "expired")) {
+                errorParameters.add("errorMessage", SyncopeEnduserSession.Error.SESSION_EXPIRED.fallback());
+            } else {
+                errorParameters.add("errorMessage", SyncopeEnduserSession.Error.AUTHORIZATION.fallback());
+            }
+            errorPage = new Login(errorParameters);
+        } else if (instanceOf(e, PageExpiredException.class) != null || !SyncopeEnduserSession.get().isSignedIn()) {
+            errorParameters.add("errorMessage", SyncopeEnduserSession.Error.SESSION_EXPIRED.fallback());
+            errorPage = new Login(errorParameters);
+        } else if (instanceOf(e, BadRequestException.class) != null
+                || instanceOf(e, WebServiceException.class) != null
+                || instanceOf(e, SyncopeClientException.class) != null) {
+
+            errorParameters.add("errorMessage", SyncopeEnduserSession.Error.REST.fallback());
+            errorPage = new Login(errorParameters);
+        } else {
+            Throwable cause = instanceOf(e, ForbiddenException.class);
+            if (cause == null) {
+                // redirect to default Wicket error page
+                errorPage = new ExceptionErrorPage(e, null);
+            } else {
+                errorParameters.add("errorMessage", cause.getMessage());
+                errorPage = new Login(errorParameters);
+            }
+        }
+
+        if (errorPage instanceof Login) {
+            try {
+                SyncopeEnduserSession.get().invalidate();
+            } catch (Throwable t) {
+                // ignore
+                LOG.debug("Unexpected error while forcing logout after error", t);
+            }
+        }
+
+        return new RenderPageRequestHandler(new PageProvider(errorPage));
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
index 7ea4470..81f8a25 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserSession.java
@@ -18,21 +18,6 @@
  */
 package org.apache.syncope.client.enduser;
 
-import java.security.AccessControlException;
-import java.text.DateFormat;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Future;
-import java.util.stream.Collectors;
-import javax.ws.rs.BadRequestException;
-import javax.ws.rs.ForbiddenException;
-import javax.ws.rs.core.EntityTag;
-import javax.ws.rs.core.MediaType;
-import javax.xml.ws.WebServiceException;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.commons.lang3.time.FastDateFormat;
@@ -43,24 +28,63 @@ import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
 import org.apache.syncope.client.ui.commons.BaseSession;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.info.PlatformInfo;
+import org.apache.syncope.common.lib.info.SystemInfo;
 import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.SyncopeService;
 import org.apache.wicket.Session;
-import org.apache.wicket.protocol.http.WebSession;
+import org.apache.wicket.authroles.authentication.AuthenticatedWebSession;
+import org.apache.wicket.authroles.authorization.strategies.role.Roles;
 import org.apache.wicket.request.Request;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.core.task.TaskRejectedException;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.ForbiddenException;
+import javax.ws.rs.core.EntityTag;
+import javax.ws.rs.core.MediaType;
+import javax.xml.ws.WebServiceException;
+import java.security.AccessControlException;
+import java.text.DateFormat;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
 
-/**
- * Custom Syncope Enduser Session class.
- */
-public class SyncopeEnduserSession extends WebSession implements BaseSession {
+public class SyncopeEnduserSession extends AuthenticatedWebSession implements BaseSession {
+
+    private static final long serialVersionUID = 747562246415852166L;
+
+    public enum Error {
+        INVALID_SECURITY_ANSWER("invalid.security.answer", "Invalid Security Answer"),
+        SESSION_EXPIRED("error.session.expired", "Session expired: please login again"),
+        AUTHORIZATION("error.authorization", "Insufficient access rights when performing the requested operation"),
+        REST("error.rest", "There was an error while contacting the Core server");
+
+        private final String key;
 
-    private static final long serialVersionUID = 1284946129513378647L;
+        private final String fallback;
+
+        Error(final String key, final String fallback) {
+            this.key = key;
+            this.fallback = fallback;
+        }
+
+        public String key() {
+            return key;
+        }
+
+        public String fallback() {
+            return fallback;
+        }
+    }
 
     private static final Logger LOG = LoggerFactory.getLogger(SyncopeEnduserSession.class);
 
@@ -68,9 +92,9 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
 
     private final SyncopeClient anonymousClient;
 
-    private SyncopeClient client;
+    private final PlatformInfo platformInfo;
 
-    private UserTO selfTO;
+    private final SystemInfo systemInfo;
 
     private final Map<Class<?>, Object> services = Collections.synchronizedMap(new HashMap<>());
 
@@ -78,6 +102,10 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
 
     private String domain;
 
+    private SyncopeClient client;
+
+    private UserTO selfTO;
+
     public static SyncopeEnduserSession get() {
         return (SyncopeEnduserSession) Session.get();
     }
@@ -86,22 +114,39 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
         super(request);
 
         clientFactory = SyncopeWebApplication.get().newClientFactory();
-        anonymousClient = clientFactory.create(new AnonymousAuthenticationHandler(
-                SyncopeWebApplication.get().getAnonymousUser(),
-                SyncopeWebApplication.get().getAnonymousKey()));
+        anonymousClient = clientFactory.
+                create(new AnonymousAuthenticationHandler(
+                        SyncopeWebApplication.get().getAnonymousUser(),
+                        SyncopeWebApplication.get().getAnonymousKey()));
+
+        platformInfo = getAnonymousService(SyncopeService.class).platform();
+        systemInfo = getAnonymousService(SyncopeService.class).system();
 
         executor = new ThreadPoolTaskExecutor();
         executor.setWaitForTasksToCompleteOnShutdown(false);
-        executor.setCorePoolSize(SyncopeWebApplication.get().getCorePoolSize());
-        executor.setMaxPoolSize(SyncopeWebApplication.get().getMaxPoolSize());
-        executor.setQueueCapacity(SyncopeWebApplication.get().getQueueCapacity());
         executor.initialize();
     }
 
     protected String message(final SyncopeClientException sce) {
-        return sce.getType().name() + ": " + sce.getElements().stream().collect(Collectors.joining(", "));
+        Error error = null;
+        if (sce.getType() == ClientExceptionType.InvalidSecurityAnswer) {
+            error = Error.INVALID_SECURITY_ANSWER;
+        }
+        if (error == null) {
+            return sce.getType().name() + ": " + sce.getElements().stream().collect(Collectors.joining(", "));
+        }
+        return getApplication().getResourceSettings().getLocalizer().
+                getString(error.key(), null, null, null, null, error.fallback());
     }
 
+    /**
+     * Extract and localize (if translation available) the actual message from the given exception; then, report it
+     * via {@link Session#error(java.io.Serializable)}.
+     *
+     * @see org.apache.syncope.client.lib.RestClientExceptionMapper
+     *
+     * @param e raised exception
+     */
     @Override
     public void onException(final Exception e) {
         Throwable root = ExceptionUtils.getRootCause(e);
@@ -111,8 +156,6 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
             SyncopeClientException sce = (SyncopeClientException) root;
             message = sce.isComposite()
                     ? sce.asComposite().getExceptions().stream().map(this::message).collect(Collectors.joining("; "))
-                    : sce.getType() == ClientExceptionType.InvalidSecurityAnswer
-                    ? getApplication().getResourceSettings().getLocalizer().getString("invalid.security.answer", null)
                     : message(sce);
         } else if (root instanceof AccessControlException || root instanceof ForbiddenException) {
             Error error = StringUtils.containsIgnoreCase(message, "expired")
@@ -130,15 +173,31 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
         error(message);
     }
 
-    public void cleanup() {
-        client = null;
-        selfTO = null;
-        services.clear();
+    public MediaType getMediaType() {
+        return clientFactory.getContentType().getMediaType();
+    }
+
+    public SyncopeClient getAnonymousClient() {
+        return anonymousClient;
+    }
+
+    public void execute(final Runnable command) {
+        try {
+            executor.execute(command);
+        } catch (TaskRejectedException e) {
+            LOG.error("Could not execute {}", command, e);
+        }
     }
 
     @Override
-    public String getJWT() {
-        return Optional.ofNullable(client).map(SyncopeClient::getJWT).orElse(null);
+    public <T> Future<T> execute(final Callable<T> command) {
+        try {
+            return executor.submit(command);
+        } catch (TaskRejectedException e) {
+            LOG.error("Could not execute {}", command, e);
+
+            return new CompletableFuture<>();
+        }
     }
 
     @Override
@@ -151,24 +210,30 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
         return StringUtils.isBlank(domain) ? SyncopeConstants.MASTER_DOMAIN : domain;
     }
 
-    private void afterAuthentication(final String username) {
-        try {
-            selfTO = client.self().getRight();
-        } catch (ForbiddenException e) {
-            LOG.warn("Could not read self(), probably in a {} scenario", IdRepoEntitlement.MUST_CHANGE_PASSWORD, e);
+    @Override
+    public String getJWT() {
+        return client == null ? null : client.getJWT();
+    }
 
-            selfTO = new UserTO();
-            selfTO.setUsername(username);
-            selfTO.setMustChangePassword(true);
-        }
+    @Override
+    public Roles getRoles() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
 
-        // bind explicitly this session to have a stateful behavior during http requests, unless session will
-        // expire at each request
-        this.bind();
+    public PlatformInfo getPlatformInfo() {
+        return platformInfo;
     }
 
+    public SystemInfo getSystemInfo() {
+        return systemInfo;
+    }
+
+    @Override
     public boolean authenticate(final String username, final String password) {
         boolean authenticated = false;
+        if (SyncopeWebApplication.get().getAdminUser().equalsIgnoreCase(username)) {
+            return authenticated;
+        }
 
         try {
             client = clientFactory.setDomain(getDomain()).create(username, password);
@@ -199,6 +264,36 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
         return authenticated;
     }
 
+    private void afterAuthentication(final String username) {
+        try {
+            selfTO = client.self().getRight();
+        } catch (ForbiddenException e) {
+            LOG.warn("Could not read self(), probably in a {} scenario", IdRepoEntitlement.MUST_CHANGE_PASSWORD, e);
+
+            selfTO = new UserTO();
+            selfTO.setUsername(username);
+            selfTO.setMustChangePassword(true);
+        }
+
+        // bind explicitly this session to have a stateful behavior during http requests, unless session will
+        // expire at each request
+        this.bind();
+    }
+
+    protected boolean isAuthenticated() {
+        return client != null && client.getJWT() != null;
+    }
+
+    protected boolean isMustChangePassword() {
+        return selfTO != null && selfTO.isMustChangePassword();
+    }
+
+    public void cleanup() {
+        client = null;
+        selfTO = null;
+        services.clear();
+    }
+
     @Override
     public void invalidate() {
         if (isAuthenticated()) {
@@ -214,49 +309,15 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
         super.invalidate();
     }
 
-    @Override
-    public <T> T getAnonymousService(final Class<T> serviceClass) {
-        return getService(serviceClass);
-    }
-
-    @Override
-    public <T> T getService(final Class<T> serviceClass) {
-        T service = (client == null || !isAuthenticated())
-                ? anonymousClient.getService(serviceClass)
-                : client.getService(serviceClass);
-        WebClient.client(service).header(RESTHeaders.DOMAIN, getDomain());
-        return service;
-    }
-
-    @Override
-    public <T> T getService(final String etag, final Class<T> serviceClass) {
-        T serviceInstance = getService(serviceClass);
-        WebClient.client(serviceInstance).match(new EntityTag(etag), false).
-                type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
-
-        return serviceInstance;
-    }
-
     public UserTO getSelfTO() {
-        if (selfTO == null) {
-            throw new IllegalArgumentException("User not authenticated");
-        }
-        return selfTO;
+        return getSelfTO(false);
     }
 
-    @Override
-    public <T> Future<T> execute(final Callable<T> command) {
-        try {
-            return executor.submit(command);
-        } catch (TaskRejectedException e) {
-            LOG.error("Could not execute {}", command, e);
-
-            return new CompletableFuture<>();
+    public UserTO getSelfTO(final boolean reload) {
+        if (reload) {
+            afterAuthentication(selfTO.getUsername());
         }
-    }
-
-    public boolean isAuthenticated() {
-        return client != null && client.getJWT() != null;
+        return selfTO;
     }
 
     @SuppressWarnings("unchecked")
@@ -275,6 +336,24 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
     }
 
     @Override
+    public <T> T getService(final Class<T> serviceClass) {
+        T service = (client == null || !isAuthenticated())
+                ? anonymousClient.getService(serviceClass)
+                : client.getService(serviceClass);
+        WebClient.client(service).header(RESTHeaders.DOMAIN, getDomain());
+        return service;
+    }
+
+    @Override
+    public <T> T getService(final String etag, final Class<T> serviceClass) {
+        T serviceInstance = getService(serviceClass);
+        WebClient.client(serviceInstance).match(new EntityTag(etag), false).
+                type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
+
+        return serviceInstance;
+    }
+
+    @Override
     public <T> void resetClient(final Class<T> service) {
         T serviceInstance = getCachedService(service);
         WebClient.client(serviceInstance).reset();
@@ -284,4 +363,9 @@ public class SyncopeEnduserSession extends WebSession implements BaseSession {
     public FastDateFormat getDateFormat() {
         return FastDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, getLocale());
     }
+
+    @Override
+    public <T> T getAnonymousService(final Class<T> serviceClass) {
+        return getAnonymousClient().getService(serviceClass);
+    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
index af4fa3a..37ca700 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeWebApplication.java
@@ -25,9 +25,11 @@ import com.google.common.net.HttpHeaders;
 import de.agilecoders.wicket.core.Bootstrap;
 import de.agilecoders.wicket.core.settings.BootstrapSettings;
 import de.agilecoders.wicket.core.settings.IBootstrapSettings;
+import de.agilecoders.wicket.core.settings.SingleThemeProvider;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.List;
@@ -40,26 +42,26 @@ import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
 import org.apache.commons.io.monitor.FileAlterationMonitor;
 import org.apache.commons.io.monitor.FileAlterationObserver;
 import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.enduser.init.ClassPathScanImplementationLookup;
-import org.apache.syncope.client.enduser.assets.SyncopeEnduserCss;
-import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
+import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
+import org.apache.syncope.client.enduser.pages.BasePage;
+import org.apache.syncope.client.enduser.pages.Dashboard;
 import org.apache.syncope.client.enduser.pages.Login;
 import org.apache.syncope.client.enduser.pages.MustChangePassword;
-import org.apache.syncope.client.enduser.pages.Self;
 import org.apache.syncope.client.enduser.pages.SelfConfirmPasswordReset;
+import org.apache.syncope.client.enduser.panels.Sidebar;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
 import org.apache.syncope.client.ui.commons.SyncopeUIRequestCycleListener;
 import org.apache.syncope.client.ui.commons.annotations.Resource;
+import org.apache.syncope.client.ui.commons.themes.AdminLTE;
 import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.apache.syncope.common.keymaster.client.api.ServiceOps;
 import org.apache.syncope.common.lib.PropertyUtils;
 import org.apache.wicket.Page;
 import org.apache.wicket.Session;
 import org.apache.wicket.WicketRuntimeException;
-import org.apache.wicket.markup.head.CssHeaderItem;
-import org.apache.wicket.markup.head.IHeaderResponse;
-import org.apache.wicket.markup.html.IHeaderContributor;
 import org.apache.wicket.markup.html.WebPage;
 import org.apache.wicket.protocol.http.ResourceIsolationRequestCycleListener;
 import org.apache.wicket.protocol.http.WebApplication;
@@ -74,7 +76,6 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.AbstractResource;
 import org.apache.wicket.request.resource.IResource;
 import org.apache.wicket.request.resource.ResourceReference;
-import org.apache.wicket.resource.JQueryResourceReference;
 import org.apache.wicket.util.lang.Args;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -88,7 +89,7 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
 
     private static final String ENDUSER_PROPERTIES = "enduser.properties";
 
-    private static final String CUSTOM_FORM_ATTRIBUTES_FILE = "customFormAttributes.json";
+    private static final String CUSTOM_FORM_LAYOUT_FILE = "customFormLayout.json";
 
     public static final List<Locale> SUPPORTED_LOCALES = List.of(
             Locale.ENGLISH, Locale.ITALIAN, new Locale("pt", "BR"), new Locale("ru"), Locale.JAPANESE);
@@ -125,11 +126,36 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
 
     private Integer maxUploadFileSizeMB;
 
-    private FileAlterationMonitor customFormAttributesMonitor;
+    private FileAlterationMonitor customFormLayoutMonitor;
 
-    private Map<String, CustomAttributesInfo> customFormAttributes;
+    private Map<String, Class<? extends BasePage>> pageClasses;
 
-    protected void setSecurityHeaders(final Properties props, final WebResponse response) {
+    private Class<? extends Sidebar> sidebar;
+
+    private UserFormLayoutInfo customFormLayout;
+
+    @SuppressWarnings("unchecked")
+    protected void populatePageClasses(final Properties props) {
+        Enumeration<String> propNames = (Enumeration<String>) props.propertyNames();
+        while (propNames.hasMoreElements()) {
+            String className = propNames.nextElement();
+            if (className.startsWith("page.")) {
+                try {
+                    Class<?> clazz = ClassUtils.getClass(props.getProperty(className));
+                    if (BasePage.class.isAssignableFrom(clazz)) {
+                        pageClasses.put(
+                                StringUtils.substringAfter(className, "page."), (Class<? extends BasePage>) clazz);
+                    } else {
+                        LOG.warn("{} does not extend {}, ignoring...", clazz.getName(), BasePage.class.getName());
+                    }
+                } catch (ClassNotFoundException e) {
+                    LOG.error("While looking for class identified by property '{}'", className, e);
+                }
+            }
+        }
+    }
+
+    protected static void setSecurityHeaders(final Properties props, final WebResponse response) {
         @SuppressWarnings("unchecked")
         Enumeration<String> propNames = (Enumeration<String>) props.propertyNames();
         while (propNames.hasMoreElements()) {
@@ -199,33 +225,31 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
             maxPoolSize = 50;
         }
 
-        // read customFormAttributes.json
-        File enduserDir;
-        try (InputStream is = SyncopeWebApplication.class.getResourceAsStream('/' + CUSTOM_FORM_ATTRIBUTES_FILE)) {
-            customFormAttributes = MAPPER.readValue(is,
-                    new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+        // read customFormLayout.json
+        try (InputStream is = SyncopeWebApplication.class.getResourceAsStream('/' + CUSTOM_FORM_LAYOUT_FILE)) {
+            customFormLayout = MAPPER.readValue(is, new TypeReference<UserFormLayoutInfo>() {
             });
-            enduserDir = new File(props.getProperty("enduser.directory"));
+            File enduserDir = new File(props.getProperty("enduser.directory"));
             boolean existsEnduserDir = enduserDir.exists() && enduserDir.canRead() && enduserDir.isDirectory();
             if (existsEnduserDir) {
-                File customFormAttributesFile = FileUtils.getFile(enduserDir, CUSTOM_FORM_ATTRIBUTES_FILE);
-                if (customFormAttributesFile.exists()
-                        && customFormAttributesFile.canRead()
-                        && customFormAttributesFile.isFile()) {
-                    customFormAttributes = MAPPER.readValue(FileUtils.openInputStream(customFormAttributesFile),
-                            new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+                File customFormLayoutFile = FileUtils.getFile(enduserDir, CUSTOM_FORM_LAYOUT_FILE);
+                if (customFormLayoutFile.exists()
+                        && customFormLayoutFile.canRead()
+                        && customFormLayoutFile.isFile()) {
+                    customFormLayout = MAPPER.readValue(FileUtils.openInputStream(customFormLayoutFile),
+                            new TypeReference<UserFormLayoutInfo>() {
                     });
                 }
             }
             FileAlterationObserver observer = existsEnduserDir
                     ? new FileAlterationObserver(
                             enduserDir,
-                            pathname -> StringUtils.contains(pathname.getPath(), CUSTOM_FORM_ATTRIBUTES_FILE))
+                            pathname -> StringUtils.contains(pathname.getPath(), CUSTOM_FORM_LAYOUT_FILE))
                     : new FileAlterationObserver(
-                            SyncopeWebApplication.class.getResource('/' + CUSTOM_FORM_ATTRIBUTES_FILE).getFile(),
-                            pathname -> StringUtils.contains(pathname.getPath(), CUSTOM_FORM_ATTRIBUTES_FILE));
+                            SyncopeWebApplication.class.getResource('/' + CUSTOM_FORM_LAYOUT_FILE).getFile(),
+                            pathname -> StringUtils.contains(pathname.getPath(), CUSTOM_FORM_LAYOUT_FILE));
 
-            customFormAttributesMonitor = new FileAlterationMonitor(5000);
+            customFormLayoutMonitor = new FileAlterationMonitor(5000);
 
             FileAlterationListener listener = new FileAlterationListenerAdaptor() {
 
@@ -233,13 +257,13 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
                 public void onFileChange(final File file) {
                     try {
                         LOG.trace("{} has changed. Reloading form attributes customization configuration.",
-                                CUSTOM_FORM_ATTRIBUTES_FILE);
-                        customFormAttributes = MAPPER.readValue(FileUtils.openInputStream(file),
-                                new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+                                CUSTOM_FORM_LAYOUT_FILE);
+                        customFormLayout = MAPPER.readValue(FileUtils.openInputStream(file),
+                                new TypeReference<UserFormLayoutInfo>() {
                         });
                     } catch (IOException e) {
                         LOG.error("{} While reading app customization configuration.",
-                                CUSTOM_FORM_ATTRIBUTES_FILE, e);
+                                CUSTOM_FORM_LAYOUT_FILE, e);
                     }
                 }
 
@@ -247,34 +271,44 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
                 public void onFileCreate(final File file) {
                     try {
                         LOG.trace("{} has been created. Loading form attributes customization configuration.",
-                                CUSTOM_FORM_ATTRIBUTES_FILE);
-                        customFormAttributes = MAPPER.readValue(FileUtils.openInputStream(file),
-                                new TypeReference<HashMap<String, CustomAttributesInfo>>() {
+                                CUSTOM_FORM_LAYOUT_FILE);
+                        customFormLayout = MAPPER.readValue(FileUtils.openInputStream(file),
+                                new TypeReference<UserFormLayoutInfo>() {
                         });
                     } catch (IOException e) {
                         LOG.error("{} While reading app customization configuration.",
-                                CUSTOM_FORM_ATTRIBUTES_FILE, e);
+                                CUSTOM_FORM_LAYOUT_FILE, e);
                     }
                 }
 
                 @Override
                 public void onFileDelete(final File file) {
                     LOG.trace("{} has been deleted. Resetting form attributes customization configuration.",
-                            CUSTOM_FORM_ATTRIBUTES_FILE);
-                    customFormAttributes = null;
+                            CUSTOM_FORM_LAYOUT_FILE);
+                    customFormLayout = null;
                 }
             };
 
             observer.addListener(listener);
-            customFormAttributesMonitor.addObserver(observer);
-            customFormAttributesMonitor.start();
+            customFormLayoutMonitor.addObserver(observer);
+            customFormLayoutMonitor.start();
         } catch (Exception e) {
-            throw new WicketRuntimeException("Could not read " + CUSTOM_FORM_ATTRIBUTES_FILE, e);
+            throw new WicketRuntimeException("Could not read " + CUSTOM_FORM_LAYOUT_FILE, e);
         }
 
+        // process page properties
+        pageClasses = new HashMap<>();
+        populatePageClasses(props);
+        pageClasses = Collections.unmodifiableMap(pageClasses);
+
+        buildSidebarClass(props);
+
         // Application settings
         IBootstrapSettings settings = new BootstrapSettings();
 
+        // set theme provider
+        settings.setThemeProvider(new SingleThemeProvider(new AdminLTE()));
+
         // install application settings
         Bootstrap.install(this, settings);
 
@@ -282,25 +316,8 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
         getResourceSettings().setUseDefaultOnMissingResource(true);
         getResourceSettings().setThrowExceptionOnMissingResource(false);
 
-        getJavaScriptLibrarySettings().setJQueryReference(JQueryResourceReference.getV2());
-
-        getResourceSettings().setThrowExceptionOnMissingResource(true);
-
         getMarkupSettings().setStripWicketTags(true);
         getMarkupSettings().setCompressWhitespace(true);
-        getMarkupSettings().setStripComments(true);
-
-        // add some css assets as Java Wicket resource in order to set Bootstrap css as a dependency of those
-        // and stop it to override the custom css rules
-        getHeaderContributorListeners().add(new IHeaderContributor() {
-
-            private static final long serialVersionUID = -8955205747168484695L;
-
-            @Override
-            public void renderHead(final IHeaderResponse response) {
-                response.render(CssHeaderItem.forReference(SyncopeEnduserCss.INSTANCE));
-            }
-        });
 
         getRequestCycleListeners().add(new SyncopeUIRequestCycleListener() {
 
@@ -318,7 +335,6 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
             protected IRequestablePage getErrorPage(final PageParameters errorParameters) {
                 return new Login(errorParameters);
             }
-
         });
 
         if (BooleanUtils.toBoolean(props.getProperty("x-forward"))) {
@@ -388,11 +404,11 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
 
     @Override
     protected void onDestroy() {
-        if (customFormAttributesMonitor != null) {
+        if (customFormLayoutMonitor != null) {
             try {
-                customFormAttributesMonitor.stop(0);
+                customFormLayoutMonitor.stop(0);
             } catch (Exception e) {
-                LOG.error("{} While stopping file monitor", CUSTOM_FORM_ATTRIBUTES_FILE, e);
+                LOG.error("{} While stopping file monitor", CUSTOM_FORM_LAYOUT_FILE, e);
             }
         }
     }
@@ -400,11 +416,37 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
     @Override
     public Class<? extends Page> getHomePage() {
         return SyncopeEnduserSession.get().isAuthenticated()
-                && SyncopeEnduserSession.get().getSelfTO().isMustChangePassword()
+                && SyncopeEnduserSession.get().isMustChangePassword()
                 ? MustChangePassword.class
                 : SyncopeEnduserSession.get().isAuthenticated()
-                ? Self.class
-                : Login.class;
+                ? getPageClass("profile", Dashboard.class)
+                : getSignInPageClass();
+    }
+
+    public ClassPathScanImplementationLookup getLookup() {
+        return lookup;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void buildSidebarClass(final Properties props) {
+        try {
+            Class<?> clazz = ClassUtils.getClass(props.getProperty("sidebar", Sidebar.class.getCanonicalName()));
+            if (Sidebar.class.isAssignableFrom(clazz)) {
+                sidebar = (Class<? extends Sidebar>) clazz;
+            } else {
+                LOG.warn("{} does not extend {}, ignoring...", clazz.getName(), Sidebar.class.getName());
+            }
+        } catch (ClassNotFoundException e) {
+            LOG.error("While looking for class identified by property 'sidebar'", e);
+        }
+    }
+
+    public UserFormLayoutInfo getCustomFormLayout() {
+        return customFormLayout;
+    }
+
+    public Class<? extends Sidebar> getSidebar() {
+        return sidebar;
     }
 
     @Override
@@ -418,7 +460,15 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
                 setUseCompression(useGZIPCompression);
     }
 
-    protected static Class<? extends WebPage> getSignInPageClass() {
+    public Class<? extends BasePage> getPageClass(final String key) {
+        return pageClasses.get(key);
+    }
+
+    public Class<? extends BasePage> getPageClass(final String key, final Class<? extends BasePage> defaultValue) {
+        return pageClasses.getOrDefault(key, defaultValue);
+    }
+
+    protected Class<? extends WebPage> getSignInPageClass() {
         return Login.class;
     }
 
@@ -458,12 +508,4 @@ public class SyncopeWebApplication extends WicketBootStandardWebApplication {
         return maxWaitTime;
     }
 
-    public Map<String, CustomAttributesInfo> getCustomFormAttributes() {
-        return customFormAttributes;
-    }
-
-    public void setCustomFormAttributes(final Map<String, CustomAttributesInfo> customFormAttributes) {
-        this.customFormAttributes.clear();
-        this.customFormAttributes.putAll(customFormAttributes);
-    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/assets/SyncopeEnduserCss.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/assets/SyncopeEnduserCss.java
deleted file mode 100644
index bc25789..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/assets/SyncopeEnduserCss.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.assets;
-
-import de.agilecoders.wicket.core.Bootstrap;
-import java.util.ArrayList;
-import java.util.List;
-import org.apache.wicket.markup.head.CssHeaderItem;
-import org.apache.wicket.markup.head.HeaderItem;
-import org.apache.wicket.request.resource.CssResourceReference;
-
-public class SyncopeEnduserCss extends CssResourceReference {
-
-    private static final long serialVersionUID = 7244898174745686253L;
-
-    /**
-     * Singleton instance of this reference.
-     */
-    public static final SyncopeEnduserCss INSTANCE = new SyncopeEnduserCss();
-
-    public SyncopeEnduserCss() {
-        super(SyncopeEnduserCss.class, "css/syncopeEnduser.css");
-    }
-
-    @Override
-    public List<HeaderItem> getDependencies() {
-        final List<HeaderItem> dependencies = new ArrayList<>();
-        dependencies.add(CssHeaderItem.forReference(Bootstrap.getSettings().getCssResourceReference()));
-        dependencies.addAll(super.getDependencies());
-        return dependencies;
-    }
-
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/commons/EnduserConstants.java
similarity index 60%
copy from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
copy to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/commons/EnduserConstants.java
index 0a683a9..abdf652 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/commons/EnduserConstants.java
@@ -16,20 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser;
+package org.apache.syncope.client.enduser.commons;
 
-public final class SyncopeEnduserConstants {
+public final class EnduserConstants {
 
-    public static final String CAPTCHA_SESSION_KEY = "captcha";
+    public static final String STATUS = "status";
 
-    public static final String XSRF_COOKIE = "XSRF-TOKEN";
+    public static final String SUCCESS = "success";
 
-    public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
+    public static final String LANDING_PAGE = "landingPage";
 
-    public static final String MEMBERSHIP_ATTR_SEPARATOR = "#";
+    public static final String CONTENT_PANEL = "contentPanel";
 
-    private SyncopeEnduserConstants() {
-        // private constructor for utility class
-    }
+    public static final String SELF_ALLOWED = "selfRegistration.allowed";
+
+    public static final String PAGE_TITLE = "pageTitle";
 
+    private EnduserConstants() {
+        // private constructor for static utility class
+    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/init/ClassPathScanImplementationLookup.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/init/ClassPathScanImplementationLookup.java
index c89afff..6f1e334 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/init/ClassPathScanImplementationLookup.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/init/ClassPathScanImplementationLookup.java
@@ -24,8 +24,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import org.apache.commons.lang3.ArrayUtils;
-import org.apache.syncope.client.enduser.pages.BaseEnduserWebPage;
-import org.apache.syncope.client.enduser.pages.BaseExtPage;
+import org.apache.syncope.client.enduser.pages.BasePage;
 import org.apache.syncope.client.ui.commons.annotations.BinaryPreview;
 import org.apache.syncope.client.ui.commons.annotations.ExtPage;
 import org.apache.syncope.client.ui.commons.annotations.Resource;
@@ -50,9 +49,7 @@ public class ClassPathScanImplementationLookup {
 
     private List<Class<? extends BinaryPreviewer>> previewers;
 
-    private List<Class<? extends BaseExtPage>> extPages;
-
-    private List<Class<? extends BaseEnduserWebPage>> pages;
+    private List<Class<? extends BasePage>> extPages;
 
     /**
      * This method can be overridden by subclasses to customize classpath scan.
@@ -63,9 +60,8 @@ public class ClassPathScanImplementationLookup {
         return DEFAULT_BASE_PACKAGE;
     }
 
-    @SuppressWarnings("unchecked")
+    @SuppressWarnings({ "unchecked", "unchecked" })
     public void load() {
-        pages = new ArrayList<>();
         previewers = new ArrayList<>();
         extPages = new ArrayList<>();
         ssoLoginFormPanels = new ArrayList<>();
@@ -73,10 +69,9 @@ public class ClassPathScanImplementationLookup {
 
         ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
         scanner.addIncludeFilter(new AssignableTypeFilter(AbstractResource.class));
-        scanner.addIncludeFilter(new AssignableTypeFilter(BaseExtPage.class));
+        scanner.addIncludeFilter(new AssignableTypeFilter(BasePage.class));
         scanner.addIncludeFilter(new AssignableTypeFilter(BaseSSOLoginFormPanel.class));
         scanner.addIncludeFilter(new AssignableTypeFilter(BinaryPreviewer.class));
-        scanner.addIncludeFilter(new AssignableTypeFilter(BaseEnduserWebPage.class));
 
         scanner.findCandidateComponents(getBasePackage()).forEach(bd -> {
             try {
@@ -84,9 +79,9 @@ public class ClassPathScanImplementationLookup {
                         ClassUtils.getDefaultClassLoader());
                 boolean isAbstractClazz = Modifier.isAbstract(clazz.getModifiers());
                 if (!isAbstractClazz) {
-                    if (BaseExtPage.class.isAssignableFrom(clazz)) {
+                    if (BasePage.class.isAssignableFrom(clazz)) {
                         if (clazz.isAnnotationPresent(ExtPage.class)) {
-                            extPages.add((Class<? extends BaseExtPage>) clazz);
+                            extPages.add((Class<? extends BasePage>) clazz);
                         } else {
                             LOG.error("Could not find annotation {} in {}, ignoring",
                                     ExtPage.class.getName(), clazz.getName());
@@ -102,8 +97,6 @@ public class ClassPathScanImplementationLookup {
                         previewers.add((Class<? extends BinaryPreviewer>) clazz);
                     } else if (BaseSSOLoginFormPanel.class.isAssignableFrom(clazz)) {
                         ssoLoginFormPanels.add((Class<? extends BaseSSOLoginFormPanel>) clazz);
-                    } else if (BaseEnduserWebPage.class.isAssignableFrom(clazz)) {
-                        pages.add((Class<? extends BaseEnduserWebPage>) clazz);
                     }
                 }
             } catch (Throwable t) {
@@ -114,8 +107,6 @@ public class ClassPathScanImplementationLookup {
 
         ssoLoginFormPanels = Collections.unmodifiableList(ssoLoginFormPanels);
 
-        pages = Collections.unmodifiableList(pages);
-
         LOG.debug("Binary previewers found: {}", previewers);
         LOG.debug("Extension pages found: {}", extPages);
         LOG.debug("SSO Login pages found: {}", ssoLoginFormPanels);
@@ -144,11 +135,7 @@ public class ClassPathScanImplementationLookup {
         return this.ssoLoginFormPanels;
     }
 
-    public List<Class<? extends BaseExtPage>> getExtPageClasses() {
+    public List<Class<? extends BasePage>> getExtPageClasses() {
         return extPages;
     }
-
-    public List<Class<? extends BaseEnduserWebPage>> getPageClasses() {
-        return pages;
-    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/AnyLayoutUtils.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/AnyLayoutUtils.java
deleted file mode 100644
index 5739f02..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/AnyLayoutUtils.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.layout;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.util.List;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.client.enduser.SyncopeEnduserSession;
-import org.apache.syncope.client.ui.commons.wizards.ModalPanelBuilder;
-import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
-import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
-import org.apache.wicket.PageReference;
-
-/**
- * Utility methods for dealing with form layout information.
- */
-public final class AnyLayoutUtils {
-
-    private static final ObjectMapper MAPPER;
-
-    private static final String DEFAULT_USER_FORM_LAYOUT_INFO;
-
-    static {
-        MAPPER = new ObjectMapper();
-        try {
-            DEFAULT_USER_FORM_LAYOUT_INFO = MAPPER.writeValueAsString(new UserFormLayoutInfo());
-        } catch (IOException e) {
-            throw new IllegalArgumentException("While generating default enduser layout info for "
-                    + SyncopeEnduserSession.get().getSelfTO().getUsername(), e);
-        }
-    }
-
-    public static String getDefaultValue() {
-        return DEFAULT_USER_FORM_LAYOUT_INFO;
-    }
-
-    public static UserFormLayoutInfo fromJsonString(final String content) {
-        try {
-            return MAPPER.readValue(content, UserFormLayoutInfo.class);
-        } catch (IOException e) {
-            throw new IllegalArgumentException("While parsing console layout info for "
-                    + SyncopeEnduserSession.get().getSelfTO().getUsername(), e);
-        }
-    }
-
-    public static String defaultConsoleLayoutInfoIfEmpty(final String content) {
-        String result;
-
-        if (StringUtils.isBlank(content)) {
-            try {
-                ObjectNode tree = MAPPER.createObjectNode();
-
-                tree.set(AnyTypeKind.USER.name(), MAPPER.valueToTree(new UserFormLayoutInfo()));
-
-                result = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(tree);
-            } catch (IOException e) {
-                throw new IllegalArgumentException("While generating default console layout info for "
-                        + SyncopeEnduserSession.get().getSelfTO().getUsername(), e);
-            }
-        } else {
-            try {
-                result = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(MAPPER.readTree(content));
-            } catch (IOException e) {
-                result = content;
-            }
-        }
-
-        return result;
-    }
-
-    public static ModalPanelBuilder<AnyWrapper<UserTO>> newUserWizardBuilder(
-            final UserTO userTO,
-            final List<String> anyTypeClasses,
-            final UserFormLayoutInfo userFormLayoutInfo,
-            final PageReference pageRef) {
-
-        try {
-            return userFormLayoutInfo.getFormClass().getConstructor(
-                    userTO.getClass(), // previous
-                    userTO.getClass(), // actual
-                    List.class,
-                    userFormLayoutInfo.getClass(),
-                    pageRef.getClass()).
-                    newInstance(null, userTO, anyTypeClasses, userFormLayoutInfo, pageRef);
-
-        } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
-                | IllegalArgumentException | InvocationTargetException e) {
-            throw new IllegalArgumentException(
-                    "Could not instantiate " + userFormLayoutInfo.getFormClass().getName(), e);
-        }
-    }
-
-    private AnyLayoutUtils() {
-        // private constructor for static utility class
-    }
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/CustomizationOption.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/CustomizationOption.java
index c716f07..0fe471f 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/CustomizationOption.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/CustomizationOption.java
@@ -30,9 +30,6 @@ public class CustomizationOption implements Serializable {
 
     private List<String> defaultValues = new ArrayList<>();
 
-    public CustomizationOption() {
-    }
-
     public boolean isReadonly() {
         return readonly;
     }
@@ -48,15 +45,4 @@ public class CustomizationOption implements Serializable {
     public void setDefaultValues(final List<String> defaultValues) {
         this.defaultValues = defaultValues;
     }
-
-    public CustomizationOption readonly(final Boolean value) {
-        this.readonly = value;
-        return this;
-    }
-
-    public CustomizationOption defaultValues(final List<String> value) {
-        this.defaultValues = value;
-        return this;
-    }
-
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/UserFormLayoutInfo.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/UserFormLayoutInfo.java
index 477c1d1..0c81adc 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/UserFormLayoutInfo.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/layout/UserFormLayoutInfo.java
@@ -20,10 +20,10 @@ package org.apache.syncope.client.enduser.layout;
 
 import java.util.HashMap;
 import java.util.Map;
-import org.apache.syncope.client.enduser.wizards.any.UserWizardBuilder;
+import org.apache.syncope.client.enduser.panels.UserFormPanel;
 import org.apache.syncope.client.ui.commons.layout.AbstractAnyFormBaseLayout;
-import org.apache.syncope.client.ui.commons.layout.UserForm;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.client.ui.commons.layout.UserForm;
 
 public class UserFormLayoutInfo extends AbstractAnyFormBaseLayout<UserTO, UserForm> {
 
@@ -37,6 +37,8 @@ public class UserFormLayoutInfo extends AbstractAnyFormBaseLayout<UserTO, UserFo
 
     private boolean passwordManagement = true;
 
+    private boolean detailsManagement = true;
+
     public Map<String, CustomizationOption> getWhichPlainAttrs() {
         return whichPlainAttrs;
     }
@@ -51,7 +53,7 @@ public class UserFormLayoutInfo extends AbstractAnyFormBaseLayout<UserTO, UserFo
 
     @Override
     protected Class<? extends UserForm> getDefaultFormClass() {
-        return UserWizardBuilder.class;
+        return UserFormPanel.class;
     }
 
     public boolean isPasswordManagement() {
@@ -61,4 +63,12 @@ public class UserFormLayoutInfo extends AbstractAnyFormBaseLayout<UserTO, UserFo
     public void setPasswordManagement(final boolean passwordManagement) {
         this.passwordManagement = passwordManagement;
     }
+
+    public boolean isDetailsManagement() {
+        return detailsManagement;
+    }
+
+    public void setDetailsManagement(final boolean detailsManagement) {
+        this.detailsManagement = detailsManagement;
+    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/markup/html/form/AjaxDownload.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/markup/html/form/AjaxDownload.java
deleted file mode 100644
index 8e1367a..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/markup/html/form/AjaxDownload.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.markup.html.form;
-
-import java.time.Duration;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.client.ui.commons.HttpResourceStream;
-import org.apache.syncope.client.ui.commons.MIMETypesLoader;
-import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.behavior.AbstractAjaxBehavior;
-import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
-import org.apache.wicket.request.resource.ContentDisposition;
-import org.apache.wicket.spring.injection.annot.SpringBean;
-
-public abstract class AjaxDownload extends AbstractAjaxBehavior {
-
-    private static final long serialVersionUID = 7203445884857810583L;
-
-    @SpringBean
-    private MIMETypesLoader mimeTypesLoader;
-
-    private final String name;
-
-    private String fileKey;
-
-    private String mimeType;
-
-    private final boolean addAntiCache;
-
-    public AjaxDownload(final String name, final boolean addAntiCache) {
-        super();
-        this.name = name;
-        this.addAntiCache = addAntiCache;
-    }
-
-    public AjaxDownload(final String name, final String fileKey, final String mimeType, final boolean addAntiCache) {
-        this(name, addAntiCache);
-        this.fileKey = fileKey;
-        this.mimeType = mimeType;
-    }
-
-    public void initiate(final AjaxRequestTarget target) {
-
-        String url = getCallbackUrl().toString();
-        if (addAntiCache) {
-            url = url + (url.contains("?") ? "&" : "?");
-            url = url + "antiCache=" + System.currentTimeMillis();
-        }
-        target.appendJavaScript("setTimeout(\"window.location.href='" + url + "'\", 100);");
-    }
-
-    @Override
-    public void onRequest() {
-        HttpResourceStream stream = getResourceStream();
-        ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(stream);
-        String key = StringUtils.isNotBlank(fileKey) ? fileKey + '_' : "";
-        String ext = "";
-        if (StringUtils.isNotBlank(mimeType)) {
-            String extByMimeType = mimeTypesLoader.getFileExt(mimeType);
-            ext = StringUtils.isBlank(extByMimeType) ? ".bin" : ('.' + extByMimeType);
-        }
-        String fileName = key + (stream.getFilename() == null ? name : stream.getFilename()) + ext;
-
-        handler.setFileName(fileName);
-        handler.setContentDisposition(ContentDisposition.ATTACHMENT);
-        handler.setCacheDuration(Duration.ZERO);
-        getComponent().getRequestCycle().scheduleRequestHandlerAfterCurrent(handler);
-    }
-
-    protected abstract HttpResourceStream getResourceStream();
-
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/markup/html/form/BinaryFieldPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/markup/html/form/BinaryFieldPanel.java
index 512283d..f8d88a2 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/markup/html/form/BinaryFieldPanel.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/markup/html/form/BinaryFieldPanel.java
@@ -30,17 +30,19 @@ import java.util.ArrayList;
 import java.util.Base64;
 import java.util.Locale;
 import java.util.Optional;
+import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.client.ui.commons.HttpResourceStream;
 import org.apache.syncope.client.enduser.SyncopeWebApplication;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.commons.PreviewUtils;
 import org.apache.syncope.client.ui.commons.Constants;
-import org.apache.syncope.client.ui.commons.markup.html.form.BaseBinaryFieldPanel;
+import org.apache.syncope.client.ui.commons.HttpResourceStream;
 import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.preview.BinaryPreviewer;
+import org.apache.syncope.client.ui.commons.markup.html.form.BaseBinaryFieldPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.BinaryFieldDownload;
 import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
 import org.apache.syncope.client.ui.commons.rest.ResponseHolder;
 import org.apache.wicket.Component;
@@ -82,7 +84,7 @@ public class BinaryFieldPanel extends BaseBinaryFieldPanel {
 
     private final BootstrapFileInputField fileUpload;
 
-    private final AjaxDownload fileDownload;
+    private final BinaryFieldDownload fileDownload;
 
     private final BinaryPreviewer previewer;
 
@@ -100,6 +102,7 @@ public class BinaryFieldPanel extends BaseBinaryFieldPanel {
             final IModel<String> model,
             final String mimeType,
             final String fileKey) {
+
         super(id, name, model);
         this.model = model;
         this.fileKey = fileKey;
@@ -122,7 +125,7 @@ public class BinaryFieldPanel extends BaseBinaryFieldPanel {
             public void renderHead(final IHeaderResponse response) {
                 if (previewer == null) {
                     FileinputJsReference.INSTANCE.renderHead(response);
-                    final JQuery fileinputJS = $(fileUpload).chain(new IFunction() {
+                    JQuery fileinputJS = $(fileUpload).chain(new IFunction() {
 
                         private static final long serialVersionUID = -2285418135375523652L;
 
@@ -150,7 +153,7 @@ public class BinaryFieldPanel extends BaseBinaryFieldPanel {
 
         uploadForm.add(new Label("preview", StringUtils.isBlank(mimeType) ? StringUtils.EMPTY : '(' + mimeType + ')'));
 
-        fileDownload = new AjaxDownload(name, fileKey, mimeType, true) {
+        fileDownload = new BinaryFieldDownload(name, fileKey, mimeType, true) {
 
             private static final long serialVersionUID = 7203445884857810583L;
 
@@ -185,10 +188,7 @@ public class BinaryFieldPanel extends BaseBinaryFieldPanel {
         if (!Locale.ENGLISH.getLanguage().equals(language)) {
             config.withLocale(language);
         }
-
         fileUpload = new BootstrapFileInputField("fileUpload", new ListModel<>(new ArrayList<>()), config);
-        fileUpload.setOutputMarkupId(true);
-
         fileUpload.add(new AjaxFormSubmitBehavior(Constants.ON_CHANGE) {
 
             private static final long serialVersionUID = -1107858522700306810L;
@@ -253,11 +253,13 @@ public class BinaryFieldPanel extends BaseBinaryFieldPanel {
 
     private Response buildResponse() {
         return Response.ok(new ByteArrayInputStream(Base64.getMimeDecoder().decode(getModelObject()))).
-                type(StringUtils.isBlank(mimeType) ? MediaType.APPLICATION_OCTET_STREAM : mimeType).build();
+                type(StringUtils.isBlank(mimeType) ? MediaType.APPLICATION_OCTET_STREAM : mimeType).
+                header(HttpHeaders.LOCATION, StringUtils.EMPTY).
+                build();
     }
 
     private void changePreviewer(final Component panelPreview) {
-        final Fragment fragment = new Fragment("panelPreview", "previewFragment", container);
+        Fragment fragment = new Fragment("panelPreview", "previewFragment", container);
         fragment.add(panelPreview);
         container.addOrReplace(fragment);
         uploadForm.addOrReplace(container);
@@ -297,4 +299,11 @@ public class BinaryFieldPanel extends BaseBinaryFieldPanel {
     protected Integer getMaxUploadFileSizeMB() {
         return SyncopeWebApplication.get().getMaxUploadFileSizeMB();
     }
+
+    @Override
+    public FieldPanel<String> setReadOnly(final boolean readOnly) {
+        super.setReadOnly(readOnly);
+        fileUpload.setEnabled(!readOnly);
+        return this;
+    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/navigation/Navbar.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/navigation/Navbar.java
deleted file mode 100644
index fa87444..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/navigation/Navbar.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.navigation;
-
-import java.util.ArrayList;
-import java.util.List;
-import org.apache.syncope.client.enduser.SyncopeEnduserSession;
-import org.apache.syncope.client.enduser.pages.BaseExtPage;
-import org.apache.syncope.client.enduser.pages.Logout;
-import org.apache.syncope.client.enduser.pages.Self;
-import org.apache.syncope.client.ui.commons.Constants;
-import org.apache.syncope.client.ui.commons.annotations.ExtPage;
-import org.apache.wicket.Component;
-import org.apache.wicket.Page;
-import org.apache.wicket.ajax.AjaxEventBehavior;
-import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.ajax.attributes.AjaxCallListener;
-import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
-import org.apache.wicket.ajax.markup.html.AjaxLink;
-import org.apache.wicket.behavior.Behavior;
-import org.apache.wicket.markup.ComponentTag;
-import org.apache.wicket.markup.html.WebMarkupContainer;
-import org.apache.wicket.markup.html.WebPage;
-import org.apache.wicket.markup.html.basic.Label;
-import org.apache.wicket.markup.html.link.BookmarkablePageLink;
-import org.apache.wicket.markup.html.list.ListItem;
-import org.apache.wicket.markup.html.list.ListView;
-import org.apache.wicket.markup.html.panel.Panel;
-
-public class Navbar extends Panel {
-
-    private static final long serialVersionUID = 1323251762654401168L;
-
-    private final ListView<Class<? extends BaseExtPage>> extPages;
-
-    private final List<WebMarkupContainer> navbarItems = new ArrayList<>();
-
-    public Navbar(final String id, final List<Class<? extends BaseExtPage>> extPageClasses) {
-        super(id);
-        setOutputMarkupId(true);
-
-        WebMarkupContainer detailsLI = new WebMarkupContainer("detailsLI");
-        detailsLI.setMarkupId("self");
-        navbarItems.add(detailsLI);
-        add(detailsLI);
-
-        BookmarkablePageLink<Page> detailsLink = new BookmarkablePageLink<>("detailsLILink", Self.class);
-        detailsLink.setOutputMarkupId(true);
-        detailsLink.add(new Label("detailsLILabel", getString("details")));
-        detailsLI.add(detailsLink);
-
-        WebMarkupContainer extLI = new WebMarkupContainer("extensionsLI");
-        extLI.setOutputMarkupPlaceholderTag(true);
-        extLI.setVisible(!extPageClasses.isEmpty());
-        add(extLI);
-
-        extPages = new ListView<Class<? extends BaseExtPage>>("extPages", extPageClasses) {
-
-            private static final long serialVersionUID = 4949588177564901031L;
-
-            @Override
-            protected void populateItem(final ListItem<Class<? extends BaseExtPage>> item) {
-                WebMarkupContainer extPageLI = new WebMarkupContainer("extPageLI");
-                item.add(extPageLI);
-                extPageLI.setMarkupId(item.getModelObject().getSimpleName().toLowerCase());
-                navbarItems.add(extPageLI);
-
-                ExtPage ann = item.getModelObject().getAnnotation(ExtPage.class);
-
-                BookmarkablePageLink<Page> extLIPageLink =
-                        new BookmarkablePageLink<>("extPageLILink", item.getModelObject());
-                extLIPageLink.setOutputMarkupId(true);
-                extLIPageLink.add(new Label("extPageLabel", ann.label()));
-                extPageLI.add(extLIPageLink);
-            }
-        };
-        extPages.setOutputMarkupId(true);
-        extPages.setVisible(true);
-        extLI.add(extPages);
-
-        WebMarkupContainer logoLinkWmc = new WebMarkupContainer("logoIcon");
-        logoLinkWmc.add(new AjaxEventBehavior("click") {
-
-            private static final long serialVersionUID = -4255753643957306394L;
-
-            @Override
-            protected void onEvent(final AjaxRequestTarget target) {
-                setResponsePage(getApplication().getHomePage());
-            }
-        });
-        add(logoLinkWmc);
-
-        @SuppressWarnings("unchecked")
-        final Class<? extends WebPage> beforeLogout = (Class<? extends WebPage>) SyncopeEnduserSession.get().
-                getAttribute(Constants.BEFORE_LOGOUT_PAGE);
-        if (beforeLogout == null) {
-            add(new BookmarkablePageLink<>("logout", Logout.class));
-        } else {
-            add(new AjaxLink<Page>("logout") {
-
-                private static final long serialVersionUID = -4889563567201424183L;
-
-                @Override
-                protected void updateAjaxAttributes(final AjaxRequestAttributes attributes) {
-                    super.updateAjaxAttributes(attributes);
-
-                    AjaxCallListener ajaxCallListener = new AjaxCallListener();
-                    ajaxCallListener.onPrecondition("return confirm('" + getString("confirmGlobalLogout") + "');");
-                    attributes.getAjaxCallListeners().add(ajaxCallListener);
-                }
-
-                @Override
-                public void onClick(final AjaxRequestTarget target) {
-                    setResponsePage(beforeLogout);
-                }
-            });
-        }
-    }
-
-    public ListView<Class<? extends BaseExtPage>> getExtPages() {
-        return extPages;
-    }
-
-    public void setActiveNavItem(final String id) {
-        navbarItems.stream().
-                filter(containingLI -> containingLI.getMarkupId().equals(id)).findFirst().
-                ifPresent(found -> found.add(new Behavior() {
-
-            private static final long serialVersionUID = -5775607340182293596L;
-
-            @Override
-            public void onComponentTag(final Component component, final ComponentTag tag) {
-                tag.put("class", "active");
-            }
-        }));
-    }
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AbstractChangePassword.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AbstractChangePassword.java
new file mode 100644
index 0000000..5944754
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/AbstractChangePassword.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.pages;
+
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPasswordFieldPanel;
+import org.apache.syncope.client.enduser.panels.ChangePasswordPanel;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+public abstract class AbstractChangePassword extends BasePage {
+
+    private static final long serialVersionUID = 5889157642852559004L;
+
+    private static final String CHANGE_PASSWORD = "page.changePassword";
+
+    public AbstractChangePassword(final PageParameters parameters) {
+        super(parameters, CHANGE_PASSWORD);
+
+        WebMarkupContainer content = new WebMarkupContainer("content");
+        content.setOutputMarkupId(true);
+        contentWrapper.add(content);
+
+        ChangePasswordPanel changePasswordPanel = getPasswordPanel();
+        content.add(changePasswordPanel);
+        content.add(new AttributeModifier("style", "height: \"100%\""));
+    }
+
+    protected ChangePasswordPanel getPasswordPanel() {
+        ChangePasswordPanel changePasswordPanel = new ChangePasswordPanel("changePasswordPanel", notificationPanel) {
+
+            private static final long serialVersionUID = 5195544218030499386L;
+
+            @Override
+            protected void doSubmit(final AjaxRequestTarget target, final AjaxPasswordFieldPanel passwordField) {
+                boolean checked = true;
+                if (SyncopeWebApplication.get().isCaptchaEnabled()) {
+                    checked = captcha.check();
+                }
+                if (!checked) {
+                    SyncopeEnduserSession.get().error(getString(Constants.CAPTCHA_ERROR));
+                    getNotificationPanel().refresh(target);
+                } else {
+                    doPwdSubmit(target, passwordField);
+                }
+            }
+
+            @Override
+            protected void doCancel() {
+                doPwdCancel();
+            }
+
+            @Override
+            protected UserTO getLoggedUser() {
+                return getPwdLoggedUser();
+            }
+        };
+
+        changePasswordPanel.setOutputMarkupId(true);
+        return changePasswordPanel;
+    }
+
+    protected abstract void doPwdSubmit(AjaxRequestTarget target, AjaxPasswordFieldPanel passwordField);
+
+    protected abstract void doPwdCancel();
+
+    protected abstract UserTO getPwdLoggedUser();
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BasePage.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BasePage.java
new file mode 100644
index 0000000..eb238da
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BasePage.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.pages;
+
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+import org.apache.syncope.client.enduser.init.ClassPathScanImplementationLookup;
+import org.apache.syncope.client.enduser.wicket.markup.head.MetaHeaderItem;
+import org.apache.syncope.client.ui.commons.BaseSession;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.enduser.panels.Sidebar;
+import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.Page;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.Session;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.attributes.AjaxCallListener;
+import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.behavior.AttributeAppender;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.spring.injection.annot.SpringBean;
+
+public class BasePage extends BaseWebPage {
+
+    private static final long serialVersionUID = 1571997737305598502L;
+
+    @SpringBean
+    private ClassPathScanImplementationLookup lookup;
+
+    protected static final HeaderItem META_IE_EDGE = new MetaHeaderItem("X-UA-Compatible", "IE=edge");
+
+    protected final Sidebar sidebar;
+
+    protected final WebMarkupContainer contentWrapper;
+
+    protected final AjaxLink<Void> collapse;
+
+    public BasePage() {
+        this(null, null);
+    }
+
+    public BasePage(final PageParameters parameters, final String name) {
+        super(parameters);
+
+        Serializable leftMenuCollapse = SyncopeEnduserSession.get().getAttribute(Constants.MENU_COLLAPSE);
+        if ((leftMenuCollapse instanceof Boolean) && ((Boolean) leftMenuCollapse)) {
+            body.add(new AttributeAppender("class", " sidebar-collapse"));
+        }
+
+        // sidebar
+        Class<? extends Sidebar> clazz = SyncopeWebApplication.get().getSidebar();
+
+        try {
+            sidebar = clazz.getConstructor(
+                    String.class,
+                    PageReference.class,
+                    List.class).
+                    newInstance("sidebar", getPageReference(), lookup.getExtPageClasses());
+        } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
+                | IllegalArgumentException | InvocationTargetException e) {
+            throw new IllegalArgumentException("Could not instantiate " + clazz.getName(), e);
+        }
+
+        sidebar.setOutputMarkupPlaceholderTag(true);
+        body.add(sidebar);
+
+        // contentWrapper
+        contentWrapper = new WebMarkupContainer("contentWrapper");
+        contentWrapper.setOutputMarkupPlaceholderTag(true);
+        body.add(contentWrapper);
+
+        //pageTitle
+        addPageTitle(name);
+
+        // collapse
+        collapse = new AjaxLink<Void>("collapse") {
+
+            private static final long serialVersionUID = -7978723352517770644L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target) {
+                Session.get().setAttribute(Constants.MENU_COLLAPSE,
+                        Session.get().getAttribute(Constants.MENU_COLLAPSE) == null
+                        ? true
+                        : !(Boolean) Session.get().getAttribute(Constants.MENU_COLLAPSE));
+            }
+        };
+        collapse.setOutputMarkupPlaceholderTag(true);
+        body.add(collapse);
+
+        @SuppressWarnings("unchecked")
+        Class<? extends WebPage> beforeLogout = (Class<? extends WebPage>) Session.get().
+                getAttribute(Constants.BEFORE_LOGOUT_PAGE);
+        if (beforeLogout == null) {
+            body.add(new BookmarkablePageLink<>("logout", Logout.class));
+        } else {
+            body.add(new AjaxLink<Page>("logout") {
+
+                private static final long serialVersionUID = -7978723352517770644L;
+
+                @Override
+                protected void updateAjaxAttributes(final AjaxRequestAttributes attributes) {
+                    super.updateAjaxAttributes(attributes);
+
+                    AjaxCallListener ajaxCallListener = new AjaxCallListener();
+                    ajaxCallListener.onPrecondition("return confirm('" + getString("confirmGlobalLogout") + "');");
+                    attributes.getAjaxCallListeners().add(ajaxCallListener);
+                }
+
+                @Override
+                public void onClick(final AjaxRequestTarget target) {
+                    setResponsePage(beforeLogout);
+                }
+            });
+        }
+    }
+
+    protected void addPageTitle(final String title) {
+        contentWrapper.addOrReplace(new Label(EnduserConstants.PAGE_TITLE, new ResourceModel(title, title)));
+    }
+
+    protected void disableSidebar() {
+        sidebar.setVisible(false);
+        collapse.setVisible(false);
+        contentWrapper.add(new AttributeModifier("style", "margin-left: 0px"));
+    }
+
+    protected void setDomain(final PageParameters parameters) {
+        if (parameters != null && !parameters.get("domain").isEmpty()) {
+            BaseSession.class.cast(Session.get()).setDomain(parameters.get("domain").toString());
+        }
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BaseEnduserWebPage.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Dashboard.java
similarity index 55%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BaseEnduserWebPage.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Dashboard.java
index e7503bf..c99c789 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BaseEnduserWebPage.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Dashboard.java
@@ -18,31 +18,28 @@
  */
 package org.apache.syncope.client.enduser.pages;
 
-import org.apache.syncope.client.enduser.init.ClassPathScanImplementationLookup;
-import org.apache.syncope.client.enduser.navigation.Navbar;
-import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
+import org.apache.syncope.client.enduser.widgets.UserProfileWidget;
+import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
-import org.apache.wicket.spring.injection.annot.SpringBean;
 
-public class BaseEnduserWebPage extends BaseWebPage {
+public class Dashboard extends BasePage {
 
-    private static final long serialVersionUID = 5760583420031293480L;
+    private static final long serialVersionUID = -1100228004207271270L;
 
-    protected final Navbar navbar;
+    protected static final String HOME = "home";
 
-    @SpringBean
-    protected ClassPathScanImplementationLookup lookup;
+    protected final WebMarkupContainer content;
 
-    public BaseEnduserWebPage() {
-        this(null);
+    public Dashboard(final PageParameters parameters) {
+        super(parameters, HOME);
 
-        body.add(navbar);
-    }
+        content = new WebMarkupContainer("content");
+        content.setOutputMarkupId(true);
 
-    public BaseEnduserWebPage(final PageParameters parameters) {
-        super(parameters);
+        UserProfileWidget userProfileWidget = new UserProfileWidget("userProfileInfo");
+        userProfileWidget.setOutputMarkupId(true);
+        content.add(userProfileWidget);
 
-        navbar = new Navbar("navbar", lookup.getExtPageClasses());
-        body.add(navbar);
+        contentWrapper.add(content);
     }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditChangePassword.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditChangePassword.java
new file mode 100644
index 0000000..fb54b7a
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditChangePassword.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.pages;
+
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPasswordFieldPanel;
+import org.apache.syncope.common.lib.request.PasswordPatch;
+import org.apache.syncope.common.lib.request.UserUR;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+public class EditChangePassword extends AbstractChangePassword {
+
+    private static final long serialVersionUID = -537205681762708502L;
+
+    private final UserSelfRestClient userSelfRestClient = new UserSelfRestClient();
+
+    public EditChangePassword(final PageParameters parameters) {
+        super(parameters);
+    }
+
+    @Override
+    protected void doPwdSubmit(final AjaxRequestTarget target, final AjaxPasswordFieldPanel passwordField) {
+        PageParameters parameters = new PageParameters();
+        try {
+            UserTO userTO = getPwdLoggedUser();
+
+            UserUR req = new UserUR();
+            req.setKey(userTO.getKey());
+            req.setPassword(new PasswordPatch.Builder().
+                    value(passwordField.getModelObject()).onSyncope(true).resources(userTO.getResources()).build());
+            userSelfRestClient.update(userTO.getETagValue(), req);
+
+            parameters.add(EnduserConstants.STATUS, Constants.OPERATION_SUCCEEDED);
+            parameters.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.pwd.change.success.msg"));
+            parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.pwd.change.success"));
+            SyncopeEnduserSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
+            parameters.add(
+                    EnduserConstants.LANDING_PAGE,
+                    SyncopeWebApplication.get().getPageClass("profile", Dashboard.class).getName());
+            setResponsePage(SelfResult.class, parameters);
+        } catch (Exception e) {
+            LOG.error("While changing password for {}",
+                    SyncopeEnduserSession.get().getSelfTO().getUsername(), e);
+            SyncopeEnduserSession.get().onException(e);
+            notificationPanel.refresh(target);
+        }
+    }
+
+    @Override
+    protected UserTO getPwdLoggedUser() {
+        return SyncopeEnduserSession.get().getSelfTO(true);
+    }
+
+    @Override
+    protected void doPwdCancel() {
+        setResponsePage(getApplication().getHomePage());
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditSecurityQuestion.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditSecurityQuestion.java
new file mode 100644
index 0000000..3a63cad
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditSecurityQuestion.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.pages;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+import org.apache.syncope.client.enduser.panels.captcha.CaptchaPanel;
+import org.apache.syncope.client.enduser.rest.SecurityQuestionRestClient;
+import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
+import org.apache.syncope.client.ui.commons.panels.CardPanel;
+import org.apache.syncope.common.lib.request.StringReplacePatchItem;
+import org.apache.syncope.common.lib.request.UserUR;
+import org.apache.syncope.common.lib.to.SecurityQuestionTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.IChoiceRenderer;
+import org.apache.wicket.markup.html.form.StatelessForm;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.PropertyModel;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+public class EditSecurityQuestion extends BasePage {
+
+    private static final long serialVersionUID = -537205681762708502L;
+
+    private static final String EDIT_SECURITY_QUESTION = "page.editSecurityQuestion";
+
+    private final UserSelfRestClient userSelfRestClient = new UserSelfRestClient();
+
+    private final AjaxDropDownChoicePanel<String> securityQuestion;
+
+    private final FieldPanel<String> securityAnswer;
+
+    private final UserTO userTO;
+
+    public EditSecurityQuestion(final PageParameters parameters) {
+        super(parameters, EDIT_SECURITY_QUESTION);
+
+        userTO = SyncopeEnduserSession.get().getSelfTO(true);
+
+        WebMarkupContainer content = new WebMarkupContainer("content");
+        content.setOutputMarkupId(true);
+        contentWrapper.add(content);
+
+        StatelessForm<Void> form = new StatelessForm<>("securityQuestionForm");
+        form.setOutputMarkupId(true);
+        content.add(form);
+
+        securityQuestion = new AjaxDropDownChoicePanel<>("securityQuestion",
+                "securityQuestion", new PropertyModel<>(userTO, "securityQuestion"));
+        securityQuestion.setNullValid(true);
+
+        List<SecurityQuestionTO> securityQuestions = SecurityQuestionRestClient.list();
+        securityQuestion.setChoices(securityQuestions.stream().
+                map(SecurityQuestionTO::getKey).collect(Collectors.toList()));
+        securityQuestion.setChoiceRenderer(new IChoiceRenderer<String>() {
+
+            private static final long serialVersionUID = -4421146737845000747L;
+
+            @Override
+            public Object getDisplayValue(final String value) {
+                return securityQuestions.stream().filter(sq -> value.equals(sq.getKey())).
+                        map(SecurityQuestionTO::getContent).findFirst().orElse(null);
+            }
+
+            @Override
+            public String getIdValue(final String value, final int index) {
+                return value;
+            }
+
+            @Override
+            public String getObject(final String id, final IModel<? extends List<? extends String>> choices) {
+                return id;
+            }
+        });
+
+        securityQuestion.add(new AjaxEventBehavior(Constants.ON_CHANGE) {
+
+            private static final long serialVersionUID = 192359260308762078L;
+
+            @Override
+            protected void onEvent(final AjaxRequestTarget target) {
+                securityAnswer.setEnabled(StringUtils.isNotBlank(securityQuestion.getModelObject()));
+                target.add(securityAnswer);
+            }
+        });
+
+        form.add(securityQuestion);
+
+        securityAnswer = new AjaxTextFieldPanel("securityAnswer", "securityAnswer",
+                new PropertyModel<>(userTO, "securityAnswer"), false);
+        form.add(securityAnswer.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true).
+                setEnabled(StringUtils.isNotBlank(securityQuestion.getModelObject())));
+
+        CaptchaPanel<Void> captcha = new CaptchaPanel<>(EnduserConstants.CONTENT_PANEL);
+        captcha.setOutputMarkupPlaceholderTag(true);
+
+        form.add(new CardPanel.Builder<CaptchaPanel<Void>>()
+                .setName("captcha")
+                .setComponent(captcha)
+                .isVisible(SyncopeWebApplication.get().isCaptchaEnabled()).build("captchaPanelCard"));
+
+        AjaxButton submitButton = new AjaxButton("submit", new Model<>(getString("submit"))) {
+
+            private static final long serialVersionUID = 429178684321093953L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target) {
+                if (StringUtils.isBlank(securityQuestion.getModelObject())
+                        || StringUtils.isBlank(securityAnswer.getModelObject())) {
+
+                    SyncopeEnduserSession.get().error(getString(Constants.CAPTCHA_ERROR));
+                    ((BasePage) getPageReference().getPage()).getNotificationPanel().refresh(target);
+                } else {
+                    boolean checked = true;
+                    if (SyncopeWebApplication.get().isCaptchaEnabled()) {
+                        checked = captcha.check();
+                    }
+                    if (!checked) {
+                        SyncopeEnduserSession.get().error(getString(Constants.CAPTCHA_ERROR));
+                        ((BasePage) getPageReference().getPage()).getNotificationPanel().refresh(target);
+                    } else {
+                        PageParameters parameters = new PageParameters();
+                        try {
+                            UserUR req = new UserUR();
+                            req.setKey(userTO.getKey());
+                            req.setSecurityQuestion(new StringReplacePatchItem.Builder().
+                                    value(securityQuestion.getModelObject()).build());
+                            req.setSecurityAnswer(new StringReplacePatchItem.Builder().
+                                    value(securityAnswer.getModelObject()).build());
+                            userSelfRestClient.update(userTO.getETagValue(), req);
+
+                            parameters.add(EnduserConstants.STATUS, Constants.OPERATION_SUCCEEDED);
+                            parameters.add(Constants.NOTIFICATION_TITLE_PARAM,
+                                    getString("self.securityquestion.change.success"));
+                            parameters.add(Constants.NOTIFICATION_MSG_PARAM,
+                                    getString("self.securityquestion.change.success.msg"));
+                            SyncopeEnduserSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
+                        } catch (Exception e) {
+                            LOG.error("While changing password for {}",
+                                    SyncopeEnduserSession.get().getSelfTO().getUsername(), e);
+                            parameters.add(EnduserConstants.STATUS, Constants.OPERATION_ERROR);
+                            parameters.add(Constants.NOTIFICATION_TITLE_PARAM,
+                                    getString("self.securityquestion.change.error"));
+                            parameters.add(Constants.NOTIFICATION_MSG_PARAM,
+                                    getString("self.securityquestion.change.error.msg"));
+                            SyncopeEnduserSession.get().onException(e);
+                            notificationPanel.refresh(target);
+                        }
+                        parameters.add(
+                                EnduserConstants.LANDING_PAGE,
+                                SyncopeWebApplication.get().getPageClass("profile", Dashboard.class).getName());
+                        setResponsePage(SelfResult.class, parameters);
+                    }
+                }
+            }
+
+            @Override
+            protected void onError(final AjaxRequestTarget target) {
+                notificationPanel.refresh(target);
+            }
+        };
+        form.add(submitButton);
+
+        form.setDefaultButton(submitButton);
+
+        Button cancel = new Button("cancel") {
+
+            private static final long serialVersionUID = 3669569969172391336L;
+
+            @Override
+            public void onSubmit() {
+                setResponsePage(getApplication().getHomePage());
+            }
+        };
+
+        cancel.setOutputMarkupId(true);
+        cancel.setDefaultFormProcessing(false);
+        form.add(cancel);
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditUser.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditUser.java
new file mode 100644
index 0000000..341f185
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/EditUser.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.pages;
+
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
+import org.apache.syncope.client.enduser.panels.UserFormPanel;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.rest.api.service.SyncopeService;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+public class EditUser extends BasePage {
+
+    private static final long serialVersionUID = -1100228004207271270L;
+
+    private static final String EDIT_USER = "page.edituser";
+
+    protected WebMarkupContainer content;
+
+    public EditUser(final PageParameters parameters) {
+        super(parameters, EDIT_USER);
+
+        content = new WebMarkupContainer("content");
+        content.setOutputMarkupId(true);
+        contentWrapper.add(content);
+
+        UserTO userTO = SyncopeEnduserSession.get().getSelfTO(true);
+
+        UserFormPanel editUserPanel = new UserFormPanel(
+                "editUserPanel",
+                userTO,
+                userTO,
+                SyncopeEnduserSession.get().getService(SyncopeService.class).platform().getUserClasses(),
+                buildFormLayout(),
+                getPageReference());
+        editUserPanel.setOutputMarkupId(true);
+        content.add(editUserPanel);
+    }
+
+    protected UserFormLayoutInfo buildFormLayout() {
+        UserFormLayoutInfo customlayoutInfo = SyncopeWebApplication.get().getCustomFormLayout();
+        return customlayoutInfo != null ? customlayoutInfo : new UserFormLayoutInfo();
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Login.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Login.java
index c9f9a91..bcc26ad 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Login.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Login.java
@@ -22,12 +22,10 @@ import java.security.AccessControlException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.Locale;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import org.apache.syncope.client.enduser.SyncopeWebApplication;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
-import org.apache.syncope.client.enduser.init.ClassPathScanImplementationLookup;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
 import org.apache.syncope.client.ui.commons.BaseLogin;
 import org.apache.syncope.client.ui.commons.BaseSession;
 import org.apache.wicket.Component;
@@ -35,27 +33,27 @@ import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
-import org.apache.wicket.spring.injection.annot.SpringBean;
 
 public class Login extends BaseLogin {
 
-    private static final long serialVersionUID = -3422492668689122688L;
+    private static final long serialVersionUID = 5889157642852559004L;
 
-    @SpringBean
-    private ClassPathScanImplementationLookup lookup;
+    private final BookmarkablePageLink<Void> selfPwdReset;
 
     private final BookmarkablePageLink<Void> selfRegistration;
 
-    private final BookmarkablePageLink<Void> selfPwdReset;
-
     public Login(final PageParameters parameters) {
         super(parameters);
 
-        selfRegistration = new BookmarkablePageLink<>("self-registration", Self.class);
-        add(selfRegistration.setOutputMarkupId(true));
-
         selfPwdReset = new BookmarkablePageLink<>("self-pwd-reset", SelfPasswordReset.class);
-        add(selfPwdReset.setOutputMarkupId(true));
+        selfPwdReset.getPageParameters().add("domain", SyncopeEnduserSession.get().getDomain());
+        selfPwdReset.setVisible(SyncopeEnduserSession.get().getPlatformInfo().isPwdResetAllowed());
+        add(selfPwdReset.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
+
+        selfRegistration = new BookmarkablePageLink<>("self-registration", SelfRegistration.class);
+        selfRegistration.getPageParameters().add("domain", SyncopeEnduserSession.get().getDomain());
+        selfRegistration.setVisible(SyncopeEnduserSession.get().getPlatformInfo().isSelfRegAllowed());
+        add(selfRegistration.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true));
     }
 
     @Override
@@ -74,10 +72,10 @@ public class Login extends BaseLogin {
     @Override
     protected List<Panel> getSSOLoginFormPanels() {
         List<Panel> ssoLoginFormPanels = new ArrayList<>();
-        lookup.getSSOLoginFormPanels().forEach(ssoLoginFormPanel -> {
+        SyncopeWebApplication.get().getLookup().getSSOLoginFormPanels().forEach(ssoLoginFormPanel -> {
             try {
-                ssoLoginFormPanels.add(ssoLoginFormPanel.getConstructor(String.class, BaseSession.class).
-                        newInstance("ssoLogin", SyncopeEnduserSession.get()));
+                ssoLoginFormPanels.add(ssoLoginFormPanel.getConstructor(String.class, BaseSession.class).newInstance(
+                        "ssoLogin", SyncopeEnduserSession.get()));
             } catch (Exception e) {
                 LOG.error("Could not initialize the provided SSO login form panel", e);
             }
@@ -96,26 +94,22 @@ public class Login extends BaseLogin {
     }
 
     @Override
-    protected List<Locale> getSupportedLocales() {
-        return SyncopeWebApplication.SUPPORTED_LOCALES;
-    }
+    protected void authenticate(final String username, final String password, final AjaxRequestTarget target)
+            throws AccessControlException {
 
-    @Override
-    protected void authenticate(
-            final String username,
-            final String password,
-            final AjaxRequestTarget target) throws AccessControlException {
+        if (SyncopeWebApplication.get().getAnonymousUser().equals(username)
+                || SyncopeWebApplication.get().getAdminUser().equals(username)) {
 
-        if (!SyncopeWebApplication.get().getAdminUser().equalsIgnoreCase(username)
-                && !SyncopeWebApplication.get().getAnonymousUser().equalsIgnoreCase(username)
-                && SyncopeEnduserSession.get().authenticate(username, password)) {
+            throw new AccessControlException("Illegal username");
+        }
 
-            // user has been authenticated successfully
+        if (SyncopeEnduserSession.get().authenticate(username, password)) {
+            // If login has been called because the user was not yet logged in, than continue to the
+            // original destination, otherwise to the Home page
             continueToOriginalDestination();
             setResponsePage(getApplication().getHomePage());
         } else {
-            // not authenticated
-            sendError(getString("login-error"));
+            SyncopeEnduserSession.get().error(getString("login-error"));
             notificationPanel.refresh(target);
         }
     }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/MustChangePassword.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/MustChangePassword.java
index 0480618..4649c41 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/MustChangePassword.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/MustChangePassword.java
@@ -19,48 +19,57 @@
 package org.apache.syncope.client.enduser.pages;
 
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
-import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
 import org.apache.syncope.client.ui.commons.Constants;
-import org.apache.syncope.client.ui.commons.pages.AbstractMustChangePassword;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPasswordFieldPanel;
+import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 
-public class MustChangePassword extends AbstractMustChangePassword {
+public class MustChangePassword extends AbstractChangePassword {
 
     private static final long serialVersionUID = 8581970794722709800L;
 
     public MustChangePassword(final PageParameters parameters) {
         super(parameters);
+
+        setDomain(parameters);
+        disableSidebar();
     }
 
     @Override
-    protected void doSubmit(final AjaxRequestTarget target) {
+    protected void doPwdSubmit(final AjaxRequestTarget target, final AjaxPasswordFieldPanel passwordField) {
+        PageParameters parameters = new PageParameters();
         try {
             UserSelfRestClient.changePassword(passwordField.getModelObject());
 
             SyncopeEnduserSession.get().invalidate();
-
-            final PageParameters parameters = new PageParameters();
+            parameters.add(EnduserConstants.STATUS, Constants.OPERATION_SUCCEEDED);
+            parameters.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.pwd.change.success"));
             parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.pwd.change.success"));
-            setResponsePage(getApplication().getHomePage(), parameters);
-
-            setResponsePage(getApplication().getHomePage(), parameters);
+            SyncopeEnduserSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
         } catch (Exception e) {
             LOG.error("While changing password for {}",
                     SyncopeEnduserSession.get().getSelfTO().getUsername(), e);
+            parameters.add(EnduserConstants.STATUS, Constants.OPERATION_ERROR);
+            parameters.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.pwd.change.error"));
+            parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.pwd.change.error.msg"));
             SyncopeEnduserSession.get().onException(e);
-            notificationPanel.refresh(target);
         }
+        notificationPanel.refresh(target);
+        setResponsePage(SelfResult.class, parameters);
     }
 
     @Override
-    protected UserTO getLoggedUser() {
+    protected UserTO getPwdLoggedUser() {
         return SyncopeEnduserSession.get().getSelfTO();
     }
 
     @Override
-    protected void doCancel() {
-        setResponsePage(getApplication().getHomePage());
+    protected void doPwdCancel() {
+        SyncopeEnduserSession.get().invalidate();
+        final PageParameters parameters = new PageParameters();
+        setResponsePage(getApplication().getHomePage(), parameters);
     }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Self.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Self.java
deleted file mode 100644
index aab4197..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/Self.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.pages;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.client.enduser.SyncopeEnduserSession;
-import org.apache.syncope.client.enduser.layout.AnyLayoutUtils;
-import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
-import org.apache.syncope.client.ui.commons.Constants;
-import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
-import org.apache.syncope.client.ui.commons.wizards.AjaxWizardBuilder;
-import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
-import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
-import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
-import org.apache.syncope.common.lib.SyncopeConstants;
-import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.syncope.common.rest.api.service.SyncopeService;
-import org.apache.wicket.event.IEvent;
-import org.apache.wicket.event.IEventSource;
-import org.apache.wicket.markup.html.WebPage;
-import org.apache.wicket.request.mapper.parameter.PageParameters;
-import org.apache.wicket.spring.injection.annot.SpringBean;
-
-public class Self extends BaseEnduserWebPage implements IEventSource {
-
-    private static final long serialVersionUID = 164651008547631054L;
-
-    public static final String NEW_USER_PARAM = "newUser";
-
-    private static final ObjectMapper MAPPER = new ObjectMapper();
-
-    @SpringBean
-    private ConfParamOps confParamOps;
-
-    private AjaxWizardBuilder<AnyWrapper<UserTO>> wizardBuilder;
-
-    protected static final String WIZARD_ID = "wizard";
-
-    public Self(final PageParameters parameters) {
-        super(parameters);
-
-        body.add(buildWizard(SyncopeEnduserSession.get().isAuthenticated()
-                ? SyncopeEnduserSession.get().getSelfTO()
-                : buildNewUserTO(parameters),
-                SyncopeEnduserSession.get().isAuthenticated()
-                ? AjaxWizard.Mode.EDIT
-                : AjaxWizard.Mode.CREATE));
-    }
-
-    @Override
-    @SuppressWarnings("unchecked")
-    public void onEvent(final IEvent<?> event) {
-        if (event.getPayload() instanceof AjaxWizard.NewItemEvent) {
-            if (event.getPayload() instanceof AjaxWizard.NewItemCancelEvent) {
-                @SuppressWarnings("unchecked")
-                final Class<? extends WebPage> beforeLogout = (Class<? extends WebPage>) SyncopeEnduserSession.get().
-                        getAttribute(Constants.BEFORE_LOGOUT_PAGE);
-                if (beforeLogout == null) {
-                    SyncopeEnduserSession.get().invalidate();
-                    setResponsePage(getApplication().getHomePage());
-                } else {
-                    setResponsePage(beforeLogout);
-                }
-            } else if (event.getPayload() instanceof AjaxWizard.NewItemFinishEvent) {
-                SyncopeEnduserSession.get().invalidate();
-
-                final PageParameters parameters = new PageParameters();
-                parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.wizard.success"));
-                setResponsePage(getApplication().getHomePage(), parameters);
-            }
-        }
-        super.onEvent(event);
-    }
-
-    @Override
-    protected void onBeforeRender() {
-        super.onBeforeRender();
-        navbar.setActiveNavItem(getClass().getSimpleName().toLowerCase());
-    }
-
-    protected final AjaxWizard<AnyWrapper<UserTO>> buildWizard(final UserTO userTO, final AjaxWizard.Mode mode) {
-        String formLayoutConfParam = confParamOps.get(
-                SyncopeEnduserSession.get().getDomain(),
-                Constants.ENDUSER_ANYLAYOUT,
-                AnyLayoutUtils.getDefaultValue(),
-                String.class);
-
-        UserFormLayoutInfo formLayoutInfo =
-                StringUtils.isBlank(formLayoutConfParam)
-                ? new UserFormLayoutInfo()
-                : AnyLayoutUtils.fromJsonString(formLayoutConfParam);
-
-        wizardBuilder = (AjaxWizardBuilder<AnyWrapper<UserTO>>) AnyLayoutUtils.newUserWizardBuilder(
-                userTO,
-                SyncopeEnduserSession.get().getService(SyncopeService.class).platform().getUserClasses(),
-                formLayoutInfo,
-                this.getPageReference());
-        wizardBuilder.setItem(new UserWrapper(userTO));
-        return wizardBuilder.build(WIZARD_ID, mode);
-    }
-
-    private static UserTO buildNewUserTO(final PageParameters parameters) {
-        UserTO userTO = null;
-        if (parameters != null) {
-            if (!parameters.get(NEW_USER_PARAM).isNull()) {
-                try {
-                    userTO = MAPPER.readValue(parameters.get(NEW_USER_PARAM).toString(), UserTO.class);
-                } catch (JsonProcessingException e) {
-                    LOG.error("While reading user data from social registration", e);
-                }
-            }
-        }
-        if (userTO == null) {
-            userTO = new UserTO();
-        }
-        userTO.setRealm(SyncopeConstants.ROOT_REALM);
-        return userTO;
-    }
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfConfirmPasswordReset.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfConfirmPasswordReset.java
index f937507..6038d73 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfConfirmPasswordReset.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfConfirmPasswordReset.java
@@ -18,127 +18,95 @@
  */
 package org.apache.syncope.client.enduser.pages;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.password.strength.PasswordStrengthBehavior;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.password.strength.PasswordStrengthConfig;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
 import org.apache.syncope.client.ui.commons.Constants;
-import org.apache.syncope.client.ui.commons.DomainDropDown;
-import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPasswordFieldPanel;
-import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
-import org.apache.syncope.common.keymaster.client.api.DomainOps;
-import org.apache.syncope.common.keymaster.client.api.model.Domain;
+import org.apache.syncope.client.ui.commons.panels.CardPanel;
+import org.apache.syncope.client.ui.commons.wizards.any.PasswordPanel;
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
 import org.apache.syncope.common.lib.SyncopeClientException;
-import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.rest.api.service.UserSelfService;
 import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
 import org.apache.wicket.ajax.markup.html.form.AjaxButton;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.form.Button;
 import org.apache.wicket.markup.html.form.Form;
-import org.apache.wicket.markup.html.form.PasswordTextField;
 import org.apache.wicket.markup.html.form.StatelessForm;
-import org.apache.wicket.markup.html.form.validation.EqualPasswordInputValidator;
-import org.apache.wicket.model.LoadableDetachableModel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
-import org.apache.wicket.spring.injection.annot.SpringBean;
 
-public class SelfConfirmPasswordReset extends BaseEnduserWebPage {
+public class SelfConfirmPasswordReset extends BasePage {
 
     private static final long serialVersionUID = -2166782304542750726L;
 
-    @SpringBean
-    private DomainOps domainOps;
+    private static final String CONFIRM_PASSWORD_RESET = "confirmPasswordReset";
 
-    private final LoadableDetachableModel<List<String>> domains = new LoadableDetachableModel<List<String>>() {
+    public SelfConfirmPasswordReset(final PageParameters parameters) {
+        super(parameters, CONFIRM_PASSWORD_RESET);
 
-        private static final long serialVersionUID = 4659376149825914247L;
+        setDomain(parameters);
+        disableSidebar();
 
-        @Override
-        protected List<String> load() {
-            List<String> current = new ArrayList<>();
-            current.addAll(domainOps.list().stream().map(Domain::getKey).sorted().collect(Collectors.toList()));
-            current.add(0, SyncopeConstants.MASTER_DOMAIN);
-            return current;
-        }
-    };
+        if (parameters == null || parameters.get("token").isEmpty()) {
+            LOG.error("No token parameter found in the request url");
 
-    public SelfConfirmPasswordReset(final PageParameters parameters) {
-        super(parameters);
-
-        if (parameters.get("token").isEmpty()) {
-            LOG.debug("No token parameter found in the request url");
-            parameters.add("errorMessage", getString("self.confirm.pwd.reset.error.empty"));
-            setResponsePage(getApplication().getHomePage(), parameters);
+            PageParameters homeParameters = new PageParameters();
+            homeParameters.add("errorMessage", getString("self.confirm.pwd.reset.error.empty"));
+            setResponsePage(getApplication().getHomePage(), homeParameters);
         }
 
-        navbar.setEnabled(false);
-        navbar.setVisible(false);
-
         WebMarkupContainer content = new WebMarkupContainer("content");
         content.setOutputMarkupId(true);
-        body.add(content);
+        contentWrapper.add(content);
 
         Form<?> form = new StatelessForm<>("selfConfirmPwdResetForm");
         form.setOutputMarkupId(true);
         content.add(form);
 
-        DomainDropDown domainSelect = new DomainDropDown("domain", domains);
-        domainSelect.add(new AjaxFormComponentUpdatingBehavior(Constants.ON_BLUR) {
-
-            private static final long serialVersionUID = -1107858522700306810L;
-
-            @Override
-            protected void onUpdate(final AjaxRequestTarget target) {
-                // nothing to do
-            }
-        }).add(new AjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
-
-            private static final long serialVersionUID = -1107858522700306810L;
-
-            @Override
-            protected void onUpdate(final AjaxRequestTarget target) {
-                // nothing to do
-            }
-        });
-        form.add(domainSelect);
-
-        AjaxPasswordFieldPanel passwordField = new AjaxPasswordFieldPanel(
-                "password", getString("password"), new Model<>());
-        passwordField.setRequired(true);
-        passwordField.setMarkupId("password");
-        passwordField.setPlaceholder(getString("password"));
-        ((PasswordTextField) passwordField.getField()).setResetPassword(false);
-        form.add(passwordField);
-
-        FieldPanel<String> confirmPasswordField = new AjaxPasswordFieldPanel(
-                "confirmPassword", getString("confirm-password"), new Model<>());
-        confirmPasswordField.setRequired(true);
-        confirmPasswordField.setMarkupId("confirmPassword");
-        confirmPasswordField.setPlaceholder(getString("confirm-password"));
-        ((PasswordTextField) confirmPasswordField.getField()).setResetPassword(false);
-        form.add(confirmPasswordField);
-
-        form.add(new EqualPasswordInputValidator(passwordField.getField(), confirmPasswordField.getField()));
-
-        AjaxButton submitButton = new AjaxButton("submit", new Model<>(getString("submit"))) {
+        UserTO fakeUserTO = new UserTO();
+        PasswordPanel passwordPanel = new PasswordPanel(
+                EnduserConstants.CONTENT_PANEL,
+                new UserWrapper(fakeUserTO),
+                false,
+                false,
+                new PasswordStrengthBehavior(new PasswordStrengthConfig().
+                        withDebug(false).
+                        withShowVerdictsInsideProgressBar(true).
+                        withShowProgressBar(true)));
+        passwordPanel.setOutputMarkupId(true);
+
+        form.add(new CardPanel.Builder<PasswordPanel>()
+                .setName("selfConfirmPasswordResetPanel")
+                .setComponent(passwordPanel)
+                .isVisible(true)
+                .build("selfConfirmPasswordResetPanelCard"));
+
+        AjaxButton submit = new AjaxButton("submit", new Model<>(getString("submit"))) {
 
             private static final long serialVersionUID = 509325877101838812L;
 
             @Override
             protected void onSubmit(final AjaxRequestTarget target) {
+                PageParameters params = new PageParameters();
                 try {
                     SyncopeEnduserSession.get().getService(UserSelfService.class).confirmPasswordReset(
-                            parameters.get("token").toString(), passwordField.getDefaultModelObjectAsString());
-                    PageParameters parameters = new PageParameters();
-                    parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.confirm.pwd.reset.success"));
-                    setResponsePage(getApplication().getHomePage(), parameters);
+                            parameters.get("token").toString(), fakeUserTO.getPassword());
+                    params.add(EnduserConstants.STATUS, Constants.OPERATION_SUCCEEDED);
+                    params.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.confirm.pwd.reset.success"));
+                    params.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.confirm.pwd.reset.success.msg"));
+                    SyncopeEnduserSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
+                    parameters.add(EnduserConstants.LANDING_PAGE, Login.class.getName());
+                    setResponsePage(SelfResult.class, params);
                 } catch (SyncopeClientException sce) {
                     LOG.error("Unable to complete the 'Password Reset Confirmation' process", sce);
+                    params.add(EnduserConstants.STATUS, Constants.OPERATION_ERROR);
+                    params.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.confirm.pwd.reset.error"));
+                    params.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.confirm.pwd.reset.error.msg"));
                     SyncopeEnduserSession.get().onException(sce);
-                    ((BaseEnduserWebPage) getPageReference().getPage()).getNotificationPanel().refresh(target);
+                    ((BasePage) getPageReference().getPage()).getNotificationPanel().refresh(target);
                 }
             }
 
@@ -147,8 +115,8 @@ public class SelfConfirmPasswordReset extends BaseEnduserWebPage {
                 notificationPanel.refresh(target);
             }
         };
-        form.setDefaultButton(submitButton);
-        form.add(submitButton);
+        form.setDefaultButton(submit);
+        form.add(submit);
 
         Button cancel = new Button("cancel") {
 
@@ -158,7 +126,6 @@ public class SelfConfirmPasswordReset extends BaseEnduserWebPage {
             public void onSubmit() {
                 setResponsePage(getApplication().getHomePage());
             }
-
         };
         cancel.setOutputMarkupId(true);
         cancel.setDefaultFormProcessing(false);
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfPasswordReset.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfPasswordReset.java
index 0d53e99..3d34ed0 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfPasswordReset.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfPasswordReset.java
@@ -18,33 +18,219 @@
  */
 package org.apache.syncope.client.enduser.pages;
 
-import org.apache.syncope.client.enduser.panels.SelfPwdResetPanel;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+import org.apache.syncope.client.enduser.panels.captcha.CaptchaPanel;
+import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.ui.commons.panels.CardPanel;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SecurityQuestionTO;
+import org.apache.syncope.common.rest.api.service.SecurityQuestionService;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.AjaxLink;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
 import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.Button;
 import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.PropertyModel;
+import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 
-public class SelfPasswordReset extends BaseEnduserWebPage {
+public class SelfPasswordReset extends BasePage {
 
     private static final long serialVersionUID = 164651008547631054L;
 
+    private static final String SELF_PWD_RESET = "page.selfPwdReset";
+
+    private String usernameValue;
+
+    private String securityAnswerValue;
+
+    private final CaptchaPanel<Void> captcha;
+
     private final SelfPwdResetPanel pwdResetPanel;
 
     public SelfPasswordReset(final PageParameters parameters) {
-        super(parameters);
+        super(parameters, SELF_PWD_RESET);
+
+        setDomain(parameters);
+        disableSidebar();
 
-        navbar.setEnabled(false);
-        navbar.setVisible(false);
+        captcha = new CaptchaPanel<>("captchaPanel");
+        captcha.setOutputMarkupPlaceholderTag(true);
+        captcha.setVisible(SyncopeWebApplication.get().isCaptchaEnabled());
 
         WebMarkupContainer content = new WebMarkupContainer("content");
         content.setOutputMarkupId(true);
-        body.add(content);
+        contentWrapper.add(content);
 
         Form<?> form = new Form<>("selfPwdResetForm");
         content.add(form);
 
-        pwdResetPanel = new SelfPwdResetPanel("selfPwdResetPanel", getPageReference());
+        pwdResetPanel = new SelfPwdResetPanel(EnduserConstants.CONTENT_PANEL, captcha, getPageReference());
         pwdResetPanel.setOutputMarkupId(true);
 
-        form.add(pwdResetPanel);
+        form.add(new CardPanel.Builder<SelfPwdResetPanel>()
+                .setName("selfPasswordResetPanel")
+                .setComponent(pwdResetPanel)
+                .isVisible(true)
+                .build("selfPasswordResetPanelCard"));
+
+        AjaxButton submitButton = new AjaxButton("submit") {
+
+            private static final long serialVersionUID = 4284361595033427185L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target) {
+                boolean checked = true;
+                if (SyncopeWebApplication.get().isCaptchaEnabled()) {
+                    checked = captcha.check();
+                }
+                if (!checked) {
+                    SyncopeEnduserSession.get().error(getString(Constants.CAPTCHA_ERROR));
+                    SelfPasswordReset.this.getNotificationPanel().refresh(target);
+                } else {
+                    PageParameters parameters = new PageParameters();
+                    try {
+                        UserSelfRestClient.requestPasswordReset(usernameValue, securityAnswerValue);
+                        parameters.add(EnduserConstants.STATUS, Constants.OPERATION_SUCCEEDED);
+                        parameters.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.pwd.reset.success"));
+                        parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.pwd.reset.success.msg"));
+                        parameters.add(EnduserConstants.LANDING_PAGE, Login.class.getName());
+                        setResponsePage(SelfResult.class, parameters);
+                    } catch (SyncopeClientException sce) {
+                        LOG.error("Unable to reset password of [{}]", usernameValue, sce);
+                        SyncopeEnduserSession.get().onException(sce);
+                        SelfPasswordReset.this.getNotificationPanel().refresh(target);
+                    }
+                }
+            }
+
+        };
+        submitButton.setOutputMarkupId(true);
+        submitButton.setDefaultFormProcessing(false);
+        form.add(submitButton);
+
+        Button cancel = new Button("cancel") {
+
+            private static final long serialVersionUID = 3669569969172391336L;
+
+            @Override
+            public void onSubmit() {
+                setResponsePage(getApplication().getHomePage());
+            }
+
+        };
+        cancel.setOutputMarkupId(true);
+        cancel.setDefaultFormProcessing(false);
+        form.add(cancel);
+    }
+
+    public class SelfPwdResetPanel extends Panel {
+
+        private static final long serialVersionUID = -2841210052053545578L;
+
+        private final TextField<String> securityQuestion;
+
+        SelfPwdResetPanel(final String id, final CaptchaPanel<Void> captcha, final PageReference pageRef) {
+            super(id);
+
+            boolean isSecurityQuestionEnabled =
+                    SyncopeEnduserSession.get().getPlatformInfo().isPwdResetRequiringSecurityQuestions();
+
+            TextField<String> username = new TextField<>("username",
+                    new PropertyModel<>(SelfPasswordReset.this, "usernameValue"), String.class);
+            username.add(new AjaxFormComponentUpdatingBehavior(Constants.ON_BLUR) {
+
+                private static final long serialVersionUID = -1107858522700306810L;
+
+                @Override
+                protected void onUpdate(final AjaxRequestTarget target) {
+                    if (isSecurityQuestionEnabled) {
+                        loadSecurityQuestion(pageRef, target);
+                    }
+                }
+            });
+            username.setRequired(true);
+            add(username);
+
+            Label sqLabel =
+                    new Label("securityQuestionLabel", new ResourceModel("securityQuestion", "securityQuestion"));
+            sqLabel.setOutputMarkupPlaceholderTag(true);
+            sqLabel.setVisible(isSecurityQuestionEnabled);
+            add(sqLabel);
+
+            securityQuestion =
+                    new TextField<>("securityQuestion", new PropertyModel<>(Model.of(), "content"), String.class);
+            securityQuestion.setOutputMarkupId(true);
+            securityQuestion.setEnabled(false);
+            securityQuestion.setOutputMarkupPlaceholderTag(true);
+            securityQuestion.setVisible(isSecurityQuestionEnabled);
+            add(securityQuestion);
+
+            Label notLoading = new Label("not.loading", new ResourceModel("not.loading", "not.loading"));
+            notLoading.setOutputMarkupPlaceholderTag(true);
+            notLoading.setVisible(isSecurityQuestionEnabled);
+            add(notLoading);
+
+            AjaxLink<Void> reloadLink = new AjaxLink<Void>("reloadLink") {
+
+                private static final long serialVersionUID = -817438685948164787L;
+
+                @Override
+                public void onClick(final AjaxRequestTarget target) {
+                    loadSecurityQuestion(pageRef, target);
+                }
+            };
+            reloadLink.setOutputMarkupPlaceholderTag(true);
+            reloadLink.setVisible(isSecurityQuestionEnabled);
+            add(reloadLink);
+
+            Label saLabel = new Label("securityAnswerLabel", new ResourceModel("securityAnswer", "securityAnswer"));
+            saLabel.setOutputMarkupPlaceholderTag(true);
+            saLabel.setVisible(isSecurityQuestionEnabled);
+            add(saLabel);
+
+            TextField<String> securityAnswer =
+                    new TextField<>("securityAnswer", new PropertyModel<>(SelfPasswordReset.this,
+                            "securityAnswerValue"), String.class);
+            securityAnswer.add(new AjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
+
+                private static final long serialVersionUID = -1107858522700306810L;
+
+                @Override
+                protected void onUpdate(final AjaxRequestTarget target) {
+                    // do nothing
+                }
+            });
+            securityAnswer.setRequired(isSecurityQuestionEnabled);
+            securityAnswer.setOutputMarkupPlaceholderTag(true);
+            securityAnswer.setVisible(isSecurityQuestionEnabled);
+            add(securityAnswer);
+
+            add(captcha);
+        }
+
+        protected void loadSecurityQuestion(final PageReference pageRef, final AjaxRequestTarget target) {
+            try {
+                SecurityQuestionTO securityQuestionTO = SyncopeEnduserSession.get().getService(
+                        SecurityQuestionService.class).readByUser(usernameValue);
+                // set security question field model
+                securityQuestion.setModel(Model.of(securityQuestionTO.getContent()));
+                target.add(securityQuestion);
+            } catch (Exception e) {
+                LOG.error("Unable to get security question for [{}]", usernameValue, e);
+                SyncopeEnduserSession.get().onException(e);
+                ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
+            }
+        }
     }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfRegistration.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfRegistration.java
new file mode 100644
index 0000000..ec85d59
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfRegistration.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.pages;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
+import org.apache.syncope.client.enduser.panels.UserSelfFormPanel;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.rest.api.service.SyncopeService;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+public class SelfRegistration extends BasePage {
+
+    private static final long serialVersionUID = -1100228004207271270L;
+
+    private static final String SELF_REGISTRATION = "page.selfRegistration";
+
+    public static final String NEW_USER_PARAM = "newUser";
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    public SelfRegistration(final PageParameters parameters) {
+        super(parameters, SELF_REGISTRATION);
+
+        setDomain(parameters);
+        disableSidebar();
+
+        WebMarkupContainer content = new WebMarkupContainer("content");
+        content.setOutputMarkupId(true);
+        contentWrapper.add(content);
+
+        UserSelfFormPanel selfRegistrationPanel = new UserSelfFormPanel(
+                "selfRegistrationPanel",
+                buildNewUserTO(parameters),
+                buildNewUserTO(parameters),
+                SyncopeEnduserSession.get().getService(SyncopeService.class).platform().getUserClasses(),
+                buildFormLayout(),
+                getPageReference());
+        selfRegistrationPanel.setOutputMarkupId(true);
+        content.add(selfRegistrationPanel);
+    }
+
+    private UserFormLayoutInfo buildFormLayout() {
+        UserFormLayoutInfo customlayoutInfo = SyncopeWebApplication.get().getCustomFormLayout();
+        return customlayoutInfo != null ? customlayoutInfo : new UserFormLayoutInfo();
+    }
+
+    private static UserTO buildNewUserTO(final PageParameters parameters) {
+        UserTO userTO = null;
+        if (parameters != null) {
+            if (!parameters.get(NEW_USER_PARAM).isNull()) {
+                try {
+                    userTO = MAPPER.readValue(parameters.get(NEW_USER_PARAM).toString(), UserTO.class);
+                } catch (JsonProcessingException e) {
+                    LOG.error("While reading user data from social registration", e);
+                }
+            }
+        }
+        if (userTO == null) {
+            userTO = new UserTO();
+        }
+        userTO.setRealm(SyncopeConstants.ROOT_REALM);
+        return userTO;
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfResult.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfResult.java
new file mode 100644
index 0000000..4d415c4
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfResult.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.pages;
+
+import org.apache.syncope.client.enduser.BookmarkablePageLinkBuilder;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+public class SelfResult extends BasePage {
+
+    private static final long serialVersionUID = 3804053409052140145L;
+
+    private static final String RESULT_PAGE = "page.resultPage";
+
+    @SuppressWarnings("unchecked")
+    public SelfResult(final PageParameters parameters) {
+        super(parameters, RESULT_PAGE);
+
+        WebMarkupContainer content = new WebMarkupContainer("content");
+        content.setOutputMarkupId(true);
+        contentWrapper.add(content);
+        Class<? extends WebPage> page;
+        try {
+            page = (Class<? extends WebPage>) Class.forName(parameters.get(EnduserConstants.LANDING_PAGE).
+                    toString("org.apache.syncope.client.enduser.pages.Login"));
+        } catch (ClassNotFoundException e) {
+            LOG.debug("Login page not found", e);
+            page = Login.class;
+        }
+        if (page.equals(Login.class)) {
+            BookmarkablePageLink<WebPage> login =
+                    new BookmarkablePageLink<>("login", Login.class);
+            content.add(login.setOutputMarkupId(true));
+            disableSidebar();
+        } else {
+            content.add(BookmarkablePageLinkBuilder.build("login", page));
+        }
+
+        content.add(new Label("resultTitle", parameters.get(Constants.NOTIFICATION_TITLE_PARAM).toString()));
+        content.add(new Label("resultMessage", parameters.get(Constants.NOTIFICATION_MSG_PARAM).toString()));
+        content.add(new Fragment("statusIcon",
+                Constants.OPERATION_SUCCEEDED.equals(parameters.get(EnduserConstants.STATUS).toString())
+                ? "successIcon" : "errorIcon", content));
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AbstractAnyFormPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AbstractAnyFormPanel.java
new file mode 100644
index 0000000..18c461a
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AbstractAnyFormPanel.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels;
+
+import java.io.Serializable;
+import org.apache.syncope.client.enduser.pages.BasePage;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.model.CompoundPropertyModel;
+
+public abstract class AbstractAnyFormPanel<T extends Serializable> extends AbstractFormPanel<T> {
+
+    private static final long serialVersionUID = -5976166731584959275L;
+
+    protected final Form<T> form;
+
+    public AbstractAnyFormPanel(final String id, final T defaultItem, final PageReference pageReference) {
+        super(id, defaultItem, pageReference);
+
+        form = new Form<>("form");
+        form.setOutputMarkupId(true);
+        add(form);
+        AjaxButton submitButton = new AjaxButton("submit") {
+
+            private static final long serialVersionUID = 4284361595033427185L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target) {
+                onFormSubmit(target);
+            }
+
+            @Override
+            protected void onError(final AjaxRequestTarget target) {
+                ((BasePage) getPage()).getNotificationPanel().refresh(target);
+            }
+        };
+
+        submitButton.setOutputMarkupId(true);
+        submitButton.setDefaultFormProcessing(true);
+        form.add(submitButton);
+
+        Button cancel = new Button("cancel") {
+
+            private static final long serialVersionUID = 3669569969172391336L;
+
+            @Override
+            public void onSubmit() {
+                setResponsePage(getApplication().getHomePage());
+            }
+
+        };
+        cancel.setOutputMarkupId(true);
+        cancel.setDefaultFormProcessing(false);
+        form.add(cancel);
+    }
+
+    public Form<T> getForm() {
+        return form;
+    }
+
+    public void setFormModel(final T modelObject) {
+        form.setModel(new CompoundPropertyModel<>(modelObject));
+    }
+
+    protected void onCancelInternal(final T modelObject) {
+    }
+
+    protected Serializable onApplyInternal(final T modelObject) {
+        // do nothing
+        return null;
+    }
+
+    protected abstract void buildLayout(T modelObject);
+
+    protected abstract void onFormSubmit(AjaxRequestTarget target);
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AbstractFormPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AbstractFormPanel.java
new file mode 100644
index 0000000..312ea27
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AbstractFormPanel.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels;
+
+import java.io.Serializable;
+import org.apache.commons.lang3.SerializationUtils;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractFormPanel<T extends Serializable> extends Panel {
+
+    private static final long serialVersionUID = 6650311507433421554L;
+
+    protected static final Logger LOG = LoggerFactory.getLogger(AbstractFormPanel.class);
+
+    protected final PageReference pageRef;
+
+    protected final T defaultItem;
+
+    protected T item;
+
+    public AbstractFormPanel(final String id, final T defaultItem, final PageReference pageReference) {
+        super(id);
+        this.defaultItem = defaultItem;
+        this.pageRef = pageReference;
+    }
+
+    protected T getOriginalItem() {
+        return item;
+    }
+
+    protected T newModelObject() {
+        if (item == null) {
+            // keep the original item: the which one before the changes performed during wizard browsing
+            item = SerializationUtils.clone(defaultItem);
+        }
+
+        // instantiate a new model object and return it
+        return SerializationUtils.clone(item);
+    }
+
+    public PageReference getPageReference() {
+        return pageRef;
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AnyFormPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AnyFormPanel.java
new file mode 100644
index 0000000..ed9e79b
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/AnyFormPanel.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels;
+
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
+import org.apache.syncope.client.enduser.panels.captcha.CaptchaPanel;
+import org.apache.syncope.client.ui.commons.panels.CardPanel;
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
+import org.apache.syncope.common.lib.Attr;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.GroupableRelatableTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.PageReference;
+import java.util.List;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+import org.apache.syncope.client.enduser.panels.any.DerAttrs;
+import org.apache.syncope.client.enduser.panels.any.Details;
+import org.apache.syncope.client.enduser.panels.any.Groups;
+import org.apache.syncope.client.enduser.panels.any.PlainAttrs;
+import org.apache.syncope.client.enduser.panels.any.Resources;
+import org.apache.syncope.client.enduser.panels.any.VirAttrs;
+
+public abstract class AnyFormPanel extends AbstractAnyFormPanel<UserWrapper> {
+
+    private static final long serialVersionUID = -2720486919461006370L;
+
+    protected final List<String> anyTypeClasses;
+
+    protected CaptchaPanel<Void> captcha;
+
+    protected UserFormLayoutInfo formLayoutInfo;
+
+    public AnyFormPanel(final String id,
+            final UserTO anyTO,
+            final List<String> anyTypeClasses,
+            final UserFormLayoutInfo formLayoutInfo,
+            final PageReference pageReference) {
+
+        super(id, new UserWrapper(anyTO), pageReference);
+
+        this.formLayoutInfo = formLayoutInfo;
+        this.anyTypeClasses = anyTypeClasses;
+    }
+
+    @SuppressWarnings("unchecked")
+    public AnyFormPanel(final String id,
+            final UserWrapper wrapper,
+            final List<String> anyTypeClasses,
+            final UserFormLayoutInfo formLayoutInfo,
+            final PageReference pageReference) {
+
+        super(id, wrapper, pageReference);
+
+        this.formLayoutInfo = formLayoutInfo;
+        this.anyTypeClasses = anyTypeClasses;
+    }
+
+    protected Details<UserTO> addOptionalDetailsPanel(final UserWrapper modelObject) {
+        Details<UserTO> details = new Details<>(EnduserConstants.CONTENT_PANEL, modelObject, false, true, pageRef);
+        details.setOutputMarkupId(true);
+        return details;
+    }
+
+    @Override
+    protected void buildLayout(final UserWrapper modelObject) {
+        form.add(new CardPanel.Builder<>()
+                .setName("details")
+                .setComponent(addOptionalDetailsPanel(modelObject))
+                .isVisible(formLayoutInfo.isDetailsManagement()).build("userDetailsPanelCard"));
+
+        Groups groups = new Groups(EnduserConstants.CONTENT_PANEL, modelObject, false);
+        setOutputMarkupId(true);
+
+        form.add(new CardPanel.Builder<Groups>()
+                .setName("groups")
+                .setComponent(groups)
+                .isVisible(formLayoutInfo.isGroups()).build("groupsPanelCard"));
+
+        PlainAttrs plainAttrs = new PlainAttrs(EnduserConstants.CONTENT_PANEL,
+                modelObject, anyTypeClasses, formLayoutInfo.getWhichPlainAttrs());
+        plainAttrs.setOutputMarkupId(true);
+
+        form.add(new CardPanel.Builder<PlainAttrs>()
+                .setName("attributes.plain")
+                .setComponent(plainAttrs)
+                .isVisible(formLayoutInfo.isPlainAttrs() && plainAttrs.isPanelVisible()).build("plainAttrsPanelCard"));
+
+        DerAttrs derAttrs = new DerAttrs(EnduserConstants.CONTENT_PANEL,
+                modelObject, anyTypeClasses, formLayoutInfo.getWhichDerAttrs());
+        derAttrs.setOutputMarkupId(true);
+
+        form.add(new CardPanel.Builder<DerAttrs>()
+                .setName("attributes.derived")
+                .setComponent(derAttrs)
+                .isVisible(formLayoutInfo.isVirAttrs() && derAttrs.isPanelVisible()).build("derAttrsPanelCard"));
+
+        VirAttrs virAttrs = new VirAttrs(EnduserConstants.CONTENT_PANEL,
+                modelObject, anyTypeClasses, formLayoutInfo.getWhichVirAttrs());
+        virAttrs.setOutputMarkupId(true);
+
+        form.add(new CardPanel.Builder<VirAttrs>()
+                .setName("attributes.virtual")
+                .setComponent(virAttrs)
+                .isVisible(formLayoutInfo.isVirAttrs() && virAttrs.isPanelVisible()).build("virAttrsPanelCard"));
+
+        Resources resources = new Resources(EnduserConstants.CONTENT_PANEL, modelObject);
+        resources.setOutputMarkupId(true);
+
+        form.add(new CardPanel.Builder<Resources>()
+                .setName("resources")
+                .setComponent(resources)
+                .isVisible(formLayoutInfo.isResources()).build("resourcesPanelCard"));
+
+        // add captcha
+        captcha = new CaptchaPanel<>(EnduserConstants.CONTENT_PANEL);
+        captcha.setOutputMarkupPlaceholderTag(true);
+
+        form.add(new CardPanel.Builder<CaptchaPanel<Void>>()
+                .setName("captcha")
+                .setComponent(captcha)
+                .isVisible(SyncopeWebApplication.get().isCaptchaEnabled()).build("captchaPanelCard"));
+    }
+
+    protected void fixPlainAndVirAttrs(final AnyTO updated, final AnyTO original) {
+        // re-add to the updated object any missing plain or virtual attribute (compared to original): this to cope with
+        // form layout, which might have not included some plain or virtual attributes
+        for (Attr plainAttr : original.getPlainAttrs()) {
+            if (!updated.getPlainAttr(plainAttr.getSchema()).isPresent()) {
+                updated.getPlainAttrs().add(plainAttr);
+            }
+        }
+        for (Attr virAttr : original.getVirAttrs()) {
+            if (!updated.getVirAttr(virAttr.getSchema()).isPresent()) {
+                updated.getVirAttrs().add(virAttr);
+            }
+        }
+
+        if (updated instanceof GroupableRelatableTO && original instanceof GroupableRelatableTO) {
+            GroupableRelatableTO.class
+                    .cast(original).getMemberships().forEach(oMemb -> {
+                GroupableRelatableTO.class
+                        .cast(updated).getMembership(oMemb.getGroupKey()).ifPresent(uMemb -> {
+                    oMemb.getPlainAttrs()
+                            .stream().
+                            filter(attr -> !uMemb.getPlainAttr(attr.getSchema()).isPresent()).
+                            forEach(attr -> uMemb.getPlainAttrs().add(attr));
+                    oMemb.getVirAttrs()
+                            .stream().
+                            filter(attr -> !uMemb.getVirAttr(attr.getSchema()).isPresent()).
+                            forEach(attr -> uMemb.getVirAttrs().add(attr));
+                }
+                );
+            });
+        }
+
+        // remove from the updated object any plain or virtual attribute without values, thus triggering for removal in
+        // the generated patch
+        updated.getPlainAttrs().removeIf(attr -> attr.getValues().isEmpty());
+        updated.getVirAttrs().removeIf(attr -> attr.getValues().isEmpty());
+        if (updated instanceof GroupableRelatableTO) {
+            GroupableRelatableTO.class.cast(updated).getMemberships().forEach(memb -> {
+                memb.getPlainAttrs().removeIf(attr -> attr.getValues().isEmpty());
+                memb.getVirAttrs().removeIf(attr -> attr.getValues().isEmpty());
+            });
+        }
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/ChangePasswordPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/ChangePasswordPanel.java
new file mode 100644
index 0000000..c5e7fc1
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/ChangePasswordPanel.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels;
+
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.password.strength.PasswordStrengthBehavior;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.password.strength.PasswordStrengthConfig;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+import org.apache.syncope.client.enduser.panels.captcha.CaptchaPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.AbstractFieldPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPasswordFieldPanel;
+import org.apache.syncope.client.ui.commons.panels.CardPanel;
+import org.apache.syncope.client.ui.commons.panels.NotificationPanel;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.core.util.string.CssUtils;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.PasswordTextField;
+import org.apache.wicket.markup.html.form.StatelessForm;
+import org.apache.wicket.markup.html.form.validation.EqualPasswordInputValidator;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.util.string.AppendingStringBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class ChangePasswordPanel extends Panel {
+
+    protected static final Logger LOG = LoggerFactory.getLogger(ChangePasswordPanel.class);
+
+    private static final long serialVersionUID = -8937593602426944714L;
+
+    protected static final String FORM_SUFFIX = "form_";
+
+    protected StatelessForm<Void> form;
+
+    protected AjaxPasswordFieldPanel passwordField;
+
+    protected AjaxPasswordFieldPanel confirmPasswordField;
+
+    protected CaptchaPanel<Void> captcha;
+
+    public ChangePasswordPanel(final String id, final NotificationPanel notificationPanel) {
+        super(id);
+        form = new StatelessForm<Void>("changePassword") {
+
+            private static final long serialVersionUID = 418292023846536149L;
+
+            @Override
+            protected void appendDefaultButtonField() {
+                AppendingStringBuffer buffer = new AppendingStringBuffer();
+
+                String cssClass = getString(CssUtils.key(Form.class, "hidden-fields"));
+
+                // div that is not visible (but not display:none either)
+                buffer.append(String.format(
+                        "<div style=\"width:0px;height:0px;position:absolute;"
+                        + "left:-100px;top:-100px;overflow:hidden\" class=\"%s\">",
+                        cssClass));
+
+                // add an empty textfield (otherwise IE doesn't work)
+                buffer.append("<input title=\"text_hidden\" "
+                        + "aria-label=\"text_hidden\" type=\"text\" "
+                        + "tabindex=\"-1\" autocomplete=\"off\"/>");
+
+                // add the submitting component
+                final Component submittingComponent = (Component) getDefaultButton();
+                buffer.append("<input title=\"submit_hidden\" aria-label=\"submit_hidden\" "
+                        + "type=\"submit\" tabindex=\"-1\" name=\"");
+                buffer.append(getDefaultButton().getInputName());
+                buffer.append("\" onclick=\" var b=document.getElementById('");
+                buffer.append(submittingComponent.getMarkupId());
+                buffer.append(
+                        "'); if (b!=null&amp;&amp;b.onclick!=null&amp;&amp;typeof(b.onclick) != 'undefined') "
+                        + "{  var r = Wicket.bind(b.onclick, b)(); if (r != false) b.click(); } "
+                        + "else { b.click(); };  return false;\" ");
+                buffer.append(" />");
+
+                // close div
+                buffer.append("</div>");
+
+                getResponse().write(buffer);
+            }
+        };
+        form.setOutputMarkupId(true);
+        add(form);
+
+        passwordField = new AjaxPasswordFieldPanel(
+                "password",
+                getString("password"),
+                new Model<>(),
+                false,
+                new PasswordStrengthBehavior(
+                        new PasswordStrengthConfig()
+                                .withDebug(true)
+                                .withShowVerdictsInsideProgressBar(true)
+                                .withShowProgressBar(true)));
+        passwordField.setRequired(true);
+        passwordField.setMarkupId("password");
+        passwordField.setPlaceholder("password");
+
+        Label passwordLabel = (Label) passwordField.get(AbstractFieldPanel.LABEL);
+        passwordLabel.add(new AttributeModifier("for", FORM_SUFFIX + "password"));
+
+        ((PasswordTextField) passwordField.getField()).setResetPassword(true);
+        form.add(passwordField);
+
+        confirmPasswordField = new AjaxPasswordFieldPanel("confirmPassword",
+                getString("confirmPassword"), new Model<>());
+        confirmPasswordField.setRequired(true);
+        confirmPasswordField.setMarkupId("confirmPassword");
+        confirmPasswordField.setPlaceholder("confirmPassword");
+
+        Label confirmPasswordLabel = (Label) confirmPasswordField.get(AbstractFieldPanel.LABEL);
+        confirmPasswordLabel.add(new AttributeModifier("for", FORM_SUFFIX + "confirmPassword"));
+
+        ((PasswordTextField) confirmPasswordField.getField()).setResetPassword(true);
+        form.add(confirmPasswordField);
+
+        form.add(new EqualPasswordInputValidator(passwordField.getField(), confirmPasswordField.getField()));
+
+        captcha = new CaptchaPanel<>(EnduserConstants.CONTENT_PANEL);
+        captcha.setOutputMarkupPlaceholderTag(true);
+
+        form.add(new CardPanel.Builder<CaptchaPanel<Void>>()
+                .setName("captcha")
+                .setComponent(captcha)
+                .isVisible(SyncopeWebApplication.get().isCaptchaEnabled()).build("captchaPanelCard"));
+
+        AjaxButton submitButton = new AjaxButton("submit", new Model<>(getString("submit"))) {
+
+            private static final long serialVersionUID = 429178684321093953L;
+
+            @Override
+            protected void onSubmit(final AjaxRequestTarget target) {
+                doSubmit(target, passwordField);
+            }
+
+            @Override
+            protected void onError(final AjaxRequestTarget target) {
+                notificationPanel.refresh(target);
+            }
+        };
+        form.add(submitButton);
+        form.setDefaultButton(submitButton);
+
+        Button cancel = new Button("cancel") {
+
+            private static final long serialVersionUID = 3669569969172391336L;
+
+            @Override
+            public void onSubmit() {
+                doCancel();
+            }
+        };
+        cancel.setOutputMarkupId(true);
+        cancel.setDefaultFormProcessing(false);
+        form.add(cancel);
+    }
+
+    public StatelessForm<Void> getForm() {
+        return form;
+    }
+
+    public AjaxPasswordFieldPanel getPasswordField() {
+        return passwordField;
+    }
+
+    public AjaxPasswordFieldPanel getConfirmPasswordField() {
+        return confirmPasswordField;
+    }
+
+    protected abstract void doSubmit(AjaxRequestTarget target, AjaxPasswordFieldPanel passwordField);
+
+    protected abstract void doCancel();
+
+    protected abstract UserTO getLoggedUser();
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/SelfPwdResetPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/SelfPwdResetPanel.java
deleted file mode 100644
index 595ff0c..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/SelfPwdResetPanel.java
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.panels;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.apache.syncope.client.enduser.SyncopeEnduserSession;
-import org.apache.syncope.client.enduser.SyncopeWebApplication;
-import org.apache.syncope.client.enduser.pages.BaseEnduserWebPage;
-import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
-import org.apache.syncope.client.enduser.wizards.any.CaptchaPanel;
-import org.apache.syncope.client.ui.commons.Constants;
-import org.apache.syncope.client.ui.commons.DomainDropDown;
-import org.apache.syncope.common.keymaster.client.api.DomainOps;
-import org.apache.syncope.common.keymaster.client.api.model.Domain;
-import org.apache.syncope.common.lib.SyncopeClientException;
-import org.apache.syncope.common.lib.SyncopeConstants;
-import org.apache.syncope.common.lib.to.SecurityQuestionTO;
-import org.apache.syncope.common.rest.api.service.SecurityQuestionService;
-import org.apache.wicket.PageReference;
-import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
-import org.apache.wicket.ajax.markup.html.AjaxLink;
-import org.apache.wicket.ajax.markup.html.form.AjaxButton;
-import org.apache.wicket.event.IEventSource;
-import org.apache.wicket.markup.html.form.Button;
-import org.apache.wicket.markup.html.form.TextField;
-import org.apache.wicket.markup.html.panel.Panel;
-import org.apache.wicket.model.LoadableDetachableModel;
-import org.apache.wicket.model.Model;
-import org.apache.wicket.model.PropertyModel;
-import org.apache.wicket.request.mapper.parameter.PageParameters;
-import org.apache.wicket.spring.injection.annot.SpringBean;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class SelfPwdResetPanel extends Panel implements IEventSource {
-
-    private static final long serialVersionUID = -2841210052053545578L;
-
-    private static final Logger LOG = LoggerFactory.getLogger(SelfPwdResetPanel.class);
-
-    @SpringBean
-    private DomainOps domainOps;
-
-    private final LoadableDetachableModel<List<String>> domains = new LoadableDetachableModel<List<String>>() {
-
-        private static final long serialVersionUID = 4659376149825914247L;
-
-        @Override
-        protected List<String> load() {
-            List<String> current = new ArrayList<>();
-            current.addAll(domainOps.list().stream().map(Domain::getKey).sorted().collect(Collectors.toList()));
-            current.add(0, SyncopeConstants.MASTER_DOMAIN);
-            return current;
-        }
-    };
-
-    private String usernameText;
-
-    private String securityAnswerText;
-
-    private final TextField<String> securityQuestion;
-
-    private final CaptchaPanel<Void> captcha;
-
-    public SelfPwdResetPanel(final String id, final PageReference pageRef) {
-        super(id);
-
-        DomainDropDown domainSelect = new DomainDropDown("domain", domains);
-        domainSelect.add(new AjaxFormComponentUpdatingBehavior(Constants.ON_BLUR) {
-
-            private static final long serialVersionUID = -1107858522700306810L;
-
-            @Override
-            protected void onUpdate(final AjaxRequestTarget target) {
-                // nothing to do
-            }
-        }).add(new AjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
-
-            private static final long serialVersionUID = -1107858522700306810L;
-
-            @Override
-            protected void onUpdate(final AjaxRequestTarget target) {
-                // nothing to do
-            }
-        });
-        add(domainSelect);
-
-        TextField<String> username =
-                new TextField<>("username", new PropertyModel<>(this, "usernameText"), String.class);
-        username.add(new AjaxFormComponentUpdatingBehavior(Constants.ON_BLUR) {
-
-            private static final long serialVersionUID = -1107858522700306810L;
-
-            @Override
-            protected void onUpdate(final AjaxRequestTarget target) {
-                loadSecurityQuestion(pageRef, target);
-            }
-        });
-        username.setRequired(true);
-        add(username);
-
-        securityQuestion =
-                new TextField<>("securityQuestion", new PropertyModel<>(Model.of(), "content"), String.class);
-        securityQuestion.setOutputMarkupId(true);
-        securityQuestion.setEnabled(false);
-        add(securityQuestion);
-
-        AjaxLink<Void> reloadLink = new AjaxLink<>("reloadLink") {
-
-            private static final long serialVersionUID = -817438685948164787L;
-
-            @Override
-            public void onClick(final AjaxRequestTarget target) {
-                loadSecurityQuestion(pageRef, target);
-            }
-        };
-        add(reloadLink);
-
-        TextField<String> securityAnswer =
-                new TextField<>("securityAnswer", new PropertyModel<>(this, "securityAnswerText"), String.class);
-        securityAnswer.add(new AjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
-
-            private static final long serialVersionUID = -1107858522700306810L;
-
-            @Override
-            protected void onUpdate(final AjaxRequestTarget target) {
-                // do nothing
-            }
-        });
-        securityAnswer.setRequired(true);
-        add(securityAnswer);
-
-        captcha = new CaptchaPanel<>("captchaPanel");
-        captcha.setOutputMarkupPlaceholderTag(true);
-        captcha.setVisible(SyncopeWebApplication.get().isCaptchaEnabled());
-        add(captcha);
-
-        AjaxButton submitButton = new AjaxButton("submit") {
-
-            private static final long serialVersionUID = 4284361595033427185L;
-
-            @Override
-            protected void onSubmit(final AjaxRequestTarget target) {
-                boolean checked = true;
-                if (SyncopeWebApplication.get().isCaptchaEnabled()) {
-                    checked = captcha.captchaCheck();
-                }
-                if (!checked) {
-                    SyncopeEnduserSession.get().error(getString(Constants.CAPTCHA_ERROR));
-                    ((BaseEnduserWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
-                } else {
-                    try {
-                        UserSelfRestClient.requestPasswordReset(usernameText, securityAnswerText);
-                        PageParameters parameters = new PageParameters();
-                        parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.pwd.reset.success"));
-                        setResponsePage(getApplication().getHomePage(), parameters);
-                    } catch (SyncopeClientException sce) {
-                        LOG.error("Unable to reset password of [{}]", usernameText, sce);
-                        SyncopeEnduserSession.get().onException(sce);
-                        ((BaseEnduserWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
-                    }
-                }
-            }
-
-        };
-        submitButton.setOutputMarkupId(true);
-        submitButton.setDefaultFormProcessing(false);
-        add(submitButton);
-
-        Button cancel = new Button("cancel") {
-
-            private static final long serialVersionUID = 3669569969172391336L;
-
-            @Override
-            public void onSubmit() {
-                setResponsePage(getApplication().getHomePage());
-            }
-
-        };
-        cancel.setOutputMarkupId(true);
-        cancel.setDefaultFormProcessing(false);
-        add(cancel);
-    }
-
-    protected void loadSecurityQuestion(final PageReference pageRef, final AjaxRequestTarget target) {
-        try {
-            SecurityQuestionTO securityQuestionTO = SyncopeEnduserSession.get().getService(
-                    SecurityQuestionService.class).readByUser(usernameText);
-            // set security question field model
-            securityQuestion.setModel(Model.of(securityQuestionTO.getContent()));
-            target.add(securityQuestion);
-        } catch (Exception e) {
-            LOG.error("Unable to get security question for [{}]", usernameText, e);
-            SyncopeEnduserSession.get().onException(e);
-            ((BaseEnduserWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
-        }
-    }
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/Sidebar.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/Sidebar.java
new file mode 100644
index 0000000..bd31754
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/Sidebar.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels;
+
+import java.util.List;
+import java.util.stream.StreamSupport;
+import org.apache.syncope.client.enduser.BookmarkablePageLinkBuilder;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.pages.BasePage;
+import org.apache.syncope.client.enduser.pages.Dashboard;
+import org.apache.syncope.client.enduser.pages.EditChangePassword;
+import org.apache.syncope.client.enduser.pages.EditSecurityQuestion;
+import org.apache.syncope.client.enduser.pages.EditUser;
+import org.apache.syncope.client.ui.commons.annotations.ExtPage;
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.Component;
+import org.apache.wicket.Page;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.behavior.Behavior;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.markup.html.panel.Panel;
+
+public class Sidebar extends Panel {
+
+    private static final long serialVersionUID = 8091307811313529503L;
+
+    protected WebMarkupContainer dashboardLIContainer;
+
+    protected WebMarkupContainer profileULContainer;
+
+    protected WebMarkupContainer profileLIContainer;
+
+    public Sidebar(
+            final String id,
+            final PageReference pageRef,
+            final List<Class<? extends BasePage>> extPageClasses) {
+
+        super(id);
+
+        buildBaseSidebar();
+
+        // set 'active' menu item for everything but extensions
+        // 1. check if current class is set to top-level menu        
+        WebMarkupContainer containingLI = null;
+        if (dashboardLIContainer.getId().equals(
+                getLIContainerId(pageRef.getPage().getClass().getSimpleName().toLowerCase()))) {
+
+            containingLI = dashboardLIContainer;
+        }
+        // 2. if not, check if it is under 'Configuration'
+        if (containingLI == null) {
+            containingLI = (WebMarkupContainer) profileULContainer.get(
+                    getLIContainerId(pageRef.getPage().getClass().getSimpleName().toLowerCase()));
+        }
+        // 3. when found, set CSS coordinates for menu
+        if (containingLI != null) {
+            StreamSupport.stream(containingLI.spliterator(), false).filter(Link.class::isInstance).
+                    forEach(child -> child.add(new Behavior() {
+
+                private static final long serialVersionUID = -5775607340182293596L;
+
+                @Override
+                public void onComponentTag(final Component component, final ComponentTag tag) {
+                    tag.append("class", "active", " ");
+                }
+            }));
+
+            if (profileULContainer.getId().equals(containingLI.getParent().getId())) {
+                profileULContainer.add(new Behavior() {
+
+                    private static final long serialVersionUID = 3109256773218160485L;
+
+                    @Override
+                    public void renderHead(final Component component, final IHeaderResponse response) {
+                        response.render(OnDomReadyHeaderItem.forScript(
+                                "$('#profileLink').addClass('active')"));
+                    }
+
+                    @Override
+                    public void onComponentTag(final Component component, final ComponentTag tag) {
+                        tag.put("class", "nav nav-treeview");
+                        tag.put("style", "display: block;");
+                    }
+                });
+
+                profileLIContainer.add(new Behavior() {
+
+                    private static final long serialVersionUID = 3109256773218160485L;
+
+                    @Override
+                    public void onComponentTag(final Component component, final ComponentTag tag) {
+                        tag.put("class", "nav-item has-treeview menu-open");
+                    }
+                });
+            }
+        }
+
+        ListView<Class<? extends BasePage>> extPages =
+                new ListView<Class<? extends BasePage>>("extPages", extPageClasses) {
+
+            private static final long serialVersionUID = 4949588177564901031L;
+
+            @Override
+            protected void populateItem(final ListItem<Class<? extends BasePage>> item) {
+                WebMarkupContainer containingLI = new WebMarkupContainer("extPageLI");
+                item.add(containingLI);
+
+                ExtPage ann = item.getModelObject().getAnnotation(ExtPage.class);
+
+                BookmarkablePageLink<Page> link = new BookmarkablePageLink<>("extPage", item.getModelObject());
+
+                link.add(new Label("extPageLabel", ann.label()));
+
+                if (item.getModelObject().equals(pageRef.getPage().getClass())) {
+                    link.add(new Behavior() {
+
+                        private static final long serialVersionUID = 1469628524240283489L;
+
+                        @Override
+                        public void renderHead(final Component component, final IHeaderResponse response) {
+                            response.render(OnDomReadyHeaderItem.forScript(
+                                    "$('#extensionsLink').addClass('active')"));
+                        }
+
+                        @Override
+                        public void onComponentTag(final Component component, final ComponentTag tag) {
+                            tag.append("class", "active", " ");
+                        }
+                    });
+                }
+                containingLI.add(link);
+
+                Label extPageIcon = new Label("extPageIcon");
+                extPageIcon.add(new AttributeModifier("class", "nav-icon " + ann.icon()));
+                link.add(extPageIcon);
+            }
+        };
+
+        add(extPages.setRenderBodyOnly(true).setOutputMarkupId(true));
+    }
+
+    protected void buildBaseSidebar() {
+        dashboardLIContainer = new WebMarkupContainer(getLIContainerId("dashboard"));
+        add(dashboardLIContainer);
+        dashboardLIContainer.add(BookmarkablePageLinkBuilder.build(
+                "home", SyncopeWebApplication.get().getPageClass("profile", Dashboard.class)));
+
+        profileLIContainer = new WebMarkupContainer(getLIContainerId("profile"));
+        add(profileLIContainer);
+        profileULContainer = new WebMarkupContainer(getULContainerId("profile"));
+        profileLIContainer.add(profileULContainer);
+
+        WebMarkupContainer liContainer = new WebMarkupContainer(getLIContainerId("edituser"));
+        profileULContainer.add(liContainer);
+        liContainer.add(BookmarkablePageLinkBuilder.build("edituser", EditUser.class));
+
+        liContainer = new WebMarkupContainer(getLIContainerId("editchangepassword"));
+        profileULContainer.add(liContainer);
+        liContainer.add(BookmarkablePageLinkBuilder.build("editchangepassword", EditChangePassword.class));
+
+        liContainer = new WebMarkupContainer(getLIContainerId("editsecurityquestion"));
+        profileULContainer.add(liContainer);
+        liContainer.add(BookmarkablePageLinkBuilder.build("editsecurityquestion", EditSecurityQuestion.class));
+        liContainer.setOutputMarkupPlaceholderTag(true);
+        liContainer.setVisible(SyncopeEnduserSession.get().getPlatformInfo().isPwdResetRequiringSecurityQuestions());
+    }
+
+    protected String getLIContainerId(final String linkId) {
+        return linkId + "LI";
+    }
+
+    protected String getULContainerId(final String linkId) {
+        return linkId + "UL";
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/UserFormPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/UserFormPanel.java
new file mode 100644
index 0000000..0a91fa7
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/UserFormPanel.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels;
+
+import java.util.List;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
+import org.apache.syncope.client.enduser.pages.BasePage;
+import org.apache.syncope.client.enduser.pages.Dashboard;
+import org.apache.syncope.client.enduser.pages.SelfResult;
+import org.apache.syncope.client.enduser.panels.any.Details;
+import org.apache.syncope.client.enduser.panels.any.UserDetails;
+import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.ui.commons.layout.UserForm;
+import org.apache.syncope.client.ui.commons.panels.WizardModalPanel;
+import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
+import org.apache.syncope.client.ui.commons.wizards.ModalPanelBuilder;
+import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
+import org.apache.syncope.common.lib.AnyOperations;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.request.UserUR;
+import org.apache.syncope.common.lib.to.ProvisioningResult;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.event.IEventSink;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+
+public class UserFormPanel extends AnyFormPanel implements UserForm {
+
+    private static final long serialVersionUID = 6763365006334514387L;
+
+    private final UserSelfRestClient userSelfRestClient = new UserSelfRestClient();
+
+    public UserFormPanel(
+            final String id,
+            final UserTO userTO,
+            final List<String> anyTypeClasses,
+            final UserFormLayoutInfo formLayoutInfo,
+            final PageReference pageReference) {
+        super(id, new UserWrapper(userTO), anyTypeClasses, formLayoutInfo, pageReference);
+
+        UserWrapper modelObj = newModelObject();
+        buildLayout(modelObj);
+    }
+
+    public UserFormPanel(
+            final String id,
+            final UserTO previousUserTO,
+            final UserTO userTO,
+            final List<String> anyTypeClasses,
+            final UserFormLayoutInfo formLayoutInfo,
+            final PageReference pageReference) {
+        super(id, new UserWrapper(previousUserTO, userTO), anyTypeClasses, formLayoutInfo, pageReference);
+
+        UserWrapper modelObj = newModelObject();
+        setFormModel(modelObj);
+        buildLayout(modelObj);
+
+    }
+
+    @Override
+    protected Details<UserTO> addOptionalDetailsPanel(final UserWrapper modelObject) {
+        return new UserDetails(
+                EnduserConstants.CONTENT_PANEL,
+                UserWrapper.class.cast(modelObject),
+                false,
+                false,
+                pageRef);
+    }
+
+    @Override
+    protected void onFormSubmit(final AjaxRequestTarget target) {
+        // captcha check
+        boolean checked = true;
+        if (SyncopeWebApplication.get().isCaptchaEnabled()) {
+            checked = captcha.check();
+        }
+        if (!checked) {
+            SyncopeEnduserSession.get().error(getString(Constants.CAPTCHA_ERROR));
+            ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
+        } else {
+            ProvisioningResult<UserTO> result;
+            PageParameters parameters = new PageParameters();
+            try {
+                AnyWrapper<UserTO> updatedWrapper = form.getModelObject();
+                UserTO userTO = updatedWrapper.getInnerObject();
+
+                fixPlainAndVirAttrs(userTO, getOriginalItem().getInnerObject());
+                UserUR req = AnyOperations.diff(userTO, getOriginalItem().getInnerObject(), false);
+
+                // update just if it is changed
+                if (req.isEmpty()) {
+                    result = new ProvisioningResult<>();
+                    result.setEntity(userTO);
+                } else {
+                    result = userSelfRestClient.update(getOriginalItem().getInnerObject().getETagValue(), req);
+                    LOG.debug("User {} has been modified", result.getEntity().getUsername());
+                }
+                parameters.add(EnduserConstants.STATUS, Constants.OPERATION_SUCCEEDED);
+                parameters.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.profile.change.success"));
+                parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.profile.change.success.msg"));
+            } catch (SyncopeClientException sce) {
+                parameters.add(EnduserConstants.STATUS, Constants.ERROR);
+                parameters.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.profile.change.error"));
+                parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.profile.change.error.msg"));
+                SyncopeEnduserSession.get().onException(sce);
+                ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
+            }
+            parameters.add(
+                    EnduserConstants.LANDING_PAGE,
+                    SyncopeWebApplication.get().getPageClass("profile", Dashboard.class).getName());
+            setResponsePage(SelfResult.class, parameters);
+        }
+    }
+
+    @Override
+    public IEventSink getEventSink() {
+        return null;
+    }
+
+    @Override
+    public ModalPanelBuilder<AnyWrapper<UserTO>> setEventSink(final IEventSink eventSink) {
+        return null;
+    }
+
+    @Override
+    public ModalPanelBuilder<AnyWrapper<UserTO>> setItem(final AnyWrapper<UserTO> item) {
+        return null;
+    }
+
+    @Override
+    public AnyWrapper<UserTO> getDefaultItem() {
+        return null;
+    }
+
+    @Override
+    public WizardModalPanel<AnyWrapper<UserTO>> build(final String id, final int index, final AjaxWizard.Mode mode) {
+        return null;
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/UserSelfFormPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/UserSelfFormPanel.java
new file mode 100644
index 0000000..a3a610d
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/UserSelfFormPanel.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.enduser.SyncopeWebApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
+import org.apache.syncope.client.enduser.pages.BasePage;
+import org.apache.syncope.client.enduser.pages.Login;
+import org.apache.syncope.client.enduser.pages.SelfResult;
+import org.apache.syncope.client.enduser.panels.any.Details;
+import org.apache.syncope.client.enduser.panels.any.SelfUserDetails;
+import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
+import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
+import org.apache.syncope.common.lib.EntityTOUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.request.UserCR;
+import org.apache.syncope.common.lib.to.ProvisioningResult;
+import org.apache.syncope.common.lib.to.SecurityQuestionTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.rest.api.service.SecurityQuestionService;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import java.util.List;
+import org.apache.syncope.client.enduser.commons.EnduserConstants;
+
+public class UserSelfFormPanel extends UserFormPanel {
+
+    private static final long serialVersionUID = 6763365006334514387L;
+
+    private final UserSelfRestClient userSelfRestClient = new UserSelfRestClient();
+
+    private TextField<String> securityQuestion;
+
+    private String usernameText;
+
+    public UserSelfFormPanel(
+            final String id,
+            final UserTO previousUserTO,
+            final UserTO userTO,
+            final List<String> anyTypeClasses,
+            final UserFormLayoutInfo formLayoutInfo,
+            final PageReference pageReference) {
+        super(id, previousUserTO, userTO, anyTypeClasses, formLayoutInfo, pageReference);
+    }
+
+    @Override
+    protected Details<UserTO> addOptionalDetailsPanel(final UserWrapper modelObject) {
+        return new SelfUserDetails(
+                EnduserConstants.CONTENT_PANEL,
+                UserWrapper.class.cast(modelObject),
+                false,
+                false,
+                UserFormLayoutInfo.class.cast(formLayoutInfo).isPasswordManagement(),
+                pageRef);
+    }
+
+    @Override
+    protected void onFormSubmit(final AjaxRequestTarget target) {
+        // captcha check
+        boolean checked = true;
+        if (SyncopeWebApplication.get().isCaptchaEnabled()) {
+            checked = captcha.check();
+        }
+        if (!checked) {
+            SyncopeEnduserSession.get().error(getString(Constants.CAPTCHA_ERROR));
+            ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
+        } else {
+            ProvisioningResult<UserTO> result;
+            PageParameters parameters = new PageParameters();
+            try {
+                AnyWrapper<UserTO> updatedWarapper = form.getModelObject();
+                UserTO userTO = updatedWarapper.getInnerObject();
+
+                UserCR req = new UserCR();
+                EntityTOUtils.toAnyCR(userTO, req);
+                req.setStorePassword(updatedWarapper instanceof UserWrapper
+                        ? UserWrapper.class.cast(updatedWarapper).isStorePasswordInSyncope()
+                        : StringUtils.isNotBlank(userTO.getPassword()));
+
+                result = userSelfRestClient.create(req, true);
+                LOG.debug("User {} has been created", result.getEntity().getUsername());
+
+                parameters.add(EnduserConstants.STATUS, Constants.OPERATION_SUCCEEDED);
+                parameters.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.profile.change.success"));
+                parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.profile.change.success.msg"));
+            } catch (SyncopeClientException sce) {
+                parameters.add(EnduserConstants.STATUS, Constants.ERROR);
+                parameters.add(Constants.NOTIFICATION_TITLE_PARAM, getString("self.profile.change.error"));
+                parameters.add(Constants.NOTIFICATION_MSG_PARAM, getString("self.profile.change.error.msg"));
+                SyncopeEnduserSession.get().onException(sce);
+                ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
+            }
+            parameters.add(EnduserConstants.LANDING_PAGE, Login.class);
+            setResponsePage(SelfResult.class, parameters);
+        }
+    }
+
+    protected void loadSecurityQuestion(final PageReference pageRef, final AjaxRequestTarget target) {
+        try {
+            SecurityQuestionTO securityQuestionTO = SyncopeEnduserSession.get().getService(
+                    SecurityQuestionService.class).readByUser(usernameText);
+            // set security question field model
+            securityQuestion.setModel(Model.of(securityQuestionTO.getContent()));
+            target.add(securityQuestion);
+        } catch (Exception e) {
+            LOG.error("Unable to get security question for [{}]", usernameText, e);
+            SyncopeEnduserSession.get().onException(e);
+            ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
+        }
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/AbstractAttrs.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/AbstractAttrs.java
similarity index 85%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/AbstractAttrs.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/AbstractAttrs.java
index be2c63d..9f6e4ec 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/AbstractAttrs.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/AbstractAttrs.java
@@ -16,41 +16,41 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.wizards.any;
+package org.apache.syncope.client.enduser.panels.any;
 
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.cxf.common.util.StringUtils;
 import org.apache.syncope.client.enduser.layout.CustomizationOption;
 import org.apache.syncope.client.enduser.rest.SchemaRestClient;
 import org.apache.syncope.client.enduser.rest.SyncopeRestClient;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPalettePanel;
 import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
-import org.apache.syncope.common.lib.to.SchemaTO;
-import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.Attr;
+import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
+import org.apache.syncope.common.lib.to.SchemaTO;
 import org.apache.syncope.common.lib.types.SchemaType;
-import org.apache.wicket.PageReference;
 import org.apache.wicket.WicketRuntimeException;
 import org.apache.wicket.core.util.lang.PropertyResolver;
-import org.apache.wicket.extensions.wizard.WizardModel.ICondition;
-import org.apache.wicket.extensions.wizard.WizardStep;
-import org.apache.wicket.markup.head.IHeaderResponse;
-import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
+import org.apache.wicket.event.IEvent;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.util.ListModel;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
-public abstract class AbstractAttrs<S extends SchemaTO> extends WizardStep implements ICondition {
+public abstract class AbstractAttrs<S extends SchemaTO> extends Panel {
 
     private static final long serialVersionUID = -5387344116983102292L;
 
+    protected static final String FORM_SUFFIX = "form_";
+
     protected final Comparator<Attr> attrComparator = new AttrComparator();
 
     protected final AnyTO anyTO;
@@ -68,18 +68,21 @@ public abstract class AbstractAttrs<S extends SchemaTO> extends WizardStep imple
     private final List<String> anyTypeClasses;
 
     public AbstractAttrs(
+            final String id,
             final AnyWrapper<?> modelObject,
             final List<String> anyTypeClasses,
             final Map<String, CustomizationOption> whichAttrs) {
-        super();
+        super(id);
         this.anyTypeClasses = anyTypeClasses;
-        this.attrs = new ListModel<>(List.of());
-        this.membershipTOs = new ListModel<>(List.of());
+        this.attrs = new ListModel<>(Collections.emptyList());
+        this.membershipTOs = new ListModel<>(Collections.emptyList());
 
         this.setOutputMarkupId(true);
 
         this.anyTO = modelObject.getInnerObject();
         this.whichAttrs = whichAttrs;
+
+        evaluate();
     }
 
     private List<Attr> loadAttrs() {
@@ -99,9 +102,8 @@ public abstract class AbstractAttrs<S extends SchemaTO> extends WizardStep imple
 
             for (MembershipTO membership : (List<MembershipTO>) PropertyResolver.getPropertyField(
                     "memberships", anyTO).get(anyTO)) {
-                setSchemas(
-                        Pair.of(membership.getGroupKey(), membership.getGroupName()),
-                        getMembershipAuxClasses(membership));
+                setSchemas(Pair.of(membership.getGroupKey(), membership.getGroupName()), getMembershipAuxClasses(
+                        membership, anyTO.getType()));
                 setAttrs(membership);
 
                 if (AbstractAttrs.this instanceof PlainAttrs && !membership.getPlainAttrs().isEmpty()) {
@@ -142,7 +144,7 @@ public abstract class AbstractAttrs<S extends SchemaTO> extends WizardStep imple
                 : groupName + '#')
                 + schema;
         return whichAttrs.get(schemaName) == null
-                ? List.of()
+                ? Collections.emptyList()
                 : whichAttrs.get(schemaName).getDefaultValues();
     }
 
@@ -185,15 +187,8 @@ public abstract class AbstractAttrs<S extends SchemaTO> extends WizardStep imple
         allSchemas.forEach(schemaTO -> scs.put(schemaTO.getKey(), schemaTO));
     }
 
-    @Override
-    public void renderHead(final IHeaderResponse response) {
-        super.renderHead(response);
-        if (org.apache.cxf.common.util.CollectionUtils.isEmpty(attrs.getObject())
-                && org.apache.cxf.common.util.CollectionUtils.isEmpty(membershipTOs.getObject())) {
-            response.render(OnDomReadyHeaderItem.forScript(
-                    String.format("$('#emptyPlaceholder').append(\"%s\"); $('#attributes').hide();",
-                            getString("attribute.empty.list"))));
-        }
+    public boolean isPanelVisible() {
+        return !attrs.getObject().isEmpty() || !membershipTOs.getObject().isEmpty();
     }
 
     protected abstract void setAttrs();
@@ -204,27 +199,26 @@ public abstract class AbstractAttrs<S extends SchemaTO> extends WizardStep imple
 
     protected abstract List<Attr> getAttrsFromTO(MembershipTO membershipTO);
 
-    protected static List<String> getMembershipAuxClasses(final MembershipTO membershipTO) {
+    protected static List<String> getMembershipAuxClasses(final MembershipTO membershipTO, final String anyType) {
         try {
             return SyncopeRestClient.searchUserTypeExtensions(membershipTO.getGroupName());
         } catch (Exception e) {
-            return List.of();
+            return Collections.emptyList();
         }
     }
 
     @Override
+    protected void onInitialize() {
+        evaluate();
+        super.onInitialize();
+    }
+
     public boolean evaluate() {
         this.attrs.setObject(loadAttrs());
         this.membershipTOs.setObject(loadMembershipAttrs());
         return !attrs.getObject().isEmpty() || !membershipTOs.getObject().isEmpty();
     }
 
-    public PageReference getPageReference() {
-        // SYNCOPE-1213
-        // default implementation does not require to pass page reference, override this method of want otherwise
-        return null;
-    }
-
     private class AttrComparator implements Comparator<Attr>, Serializable {
 
         private static final long serialVersionUID = -5105030477767941060L;
@@ -253,6 +247,16 @@ public abstract class AbstractAttrs<S extends SchemaTO> extends WizardStep imple
         }
     }
 
+    @Override
+    public void onEvent(final IEvent<?> event) {
+        super.onEvent(event);
+        if (event.getPayload() instanceof AjaxPalettePanel.UpdateActionEvent) {
+            evaluate();
+            AjaxPalettePanel.UpdateActionEvent updateEvent = (AjaxPalettePanel.UpdateActionEvent) event.getPayload();
+            updateEvent.getTarget().add(this);
+        }
+    }
+
     public static class Schemas extends Panel {
 
         private static final long serialVersionUID = -2447602429647965090L;
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/DerAttrs.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/DerAttrs.java
similarity index 91%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/DerAttrs.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/DerAttrs.java
index 952b834..fea60ce 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/DerAttrs.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/DerAttrs.java
@@ -16,27 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.wizards.any;
+package org.apache.syncope.client.enduser.panels.any;
 
-import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.layout.CustomizationOption;
-import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
+import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
+import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.EntityTOUtils;
 import org.apache.syncope.common.lib.to.AnyTO;
-import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.to.DerSchemaTO;
 import org.apache.syncope.common.lib.to.GroupableRelatableTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
+import org.apache.wicket.extensions.markup.html.tabs.ITab;
 import org.apache.wicket.markup.ComponentTag;
 import org.apache.wicket.markup.MarkupStream;
 import org.apache.wicket.markup.html.WebMarkupContainer;
@@ -47,30 +48,21 @@ import org.apache.wicket.model.Model;
 import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.model.StringResourceModel;
 import org.apache.wicket.model.util.ListModel;
+import java.util.stream.Collectors;
 
 public class DerAttrs extends AbstractAttrs<DerSchemaTO> {
 
     private static final long serialVersionUID = -5387344116983102292L;
 
     public <T extends AnyTO> DerAttrs(
+            final String id,
             final AnyWrapper<T> modelObject,
             final List<String> anyTypeClasses,
             final Map<String, CustomizationOption> whichDerAttrs) {
 
-        super(modelObject, anyTypeClasses, whichDerAttrs);
-        setTitleModel(new ResourceModel("attributes.derived"));
-
-        add(new Accordion("derSchemas", List.of(new AbstractTab(
-                new ResourceModel("attributes.accordion", "Derived Attributes")) {
-
-            private static final long serialVersionUID = 1037272333056449378L;
-
-            @Override
-            public WebMarkupContainer getPanel(final String panelId) {
-                return new DerAttrs.DerSchemas(panelId, schemas, attrs);
-            }
-        }), Model.of(0)).setOutputMarkupId(true));
+        super(id, modelObject, anyTypeClasses, whichDerAttrs);
 
+        add(new DerAttrs.DerSchemas("derSchemas", schemas, attrs).setOutputMarkupId(true));
         add(new ListView<MembershipTO>("membershipsDerSchemas", membershipTOs) {
 
             private static final long serialVersionUID = 6741044372185745296L;
@@ -78,7 +70,7 @@ public class DerAttrs extends AbstractAttrs<DerSchemaTO> {
             @Override
             protected void populateItem(final ListItem<MembershipTO> item) {
                 final MembershipTO membershipTO = item.getModelObject();
-                item.add(new Accordion("membershipDerSchemas", List.of(new AbstractTab(
+                item.add(new Accordion("membershipDerSchemas", Collections.<ITab>singletonList(new AbstractTab(
                         new StringResourceModel(
                                 "attributes.membership.accordion",
                                 DerAttrs.this,
@@ -159,7 +151,7 @@ public class DerAttrs extends AbstractAttrs<DerSchemaTO> {
         membershipTO.getDerAttrs().addAll(derAttrs);
     }
 
-    public static class DerSchemas extends Schemas {
+    public class DerSchemas extends Schemas {
 
         private static final long serialVersionUID = -4730563859116024676L;
 
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Resources.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Details.java
similarity index 56%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Resources.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Details.java
index c4e7857..23c9bdf 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Resources.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Details.java
@@ -16,25 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.wizards.any;
+package org.apache.syncope.client.enduser.panels.any;
 
-import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
-import org.apache.syncope.client.ui.commons.wizards.any.AbstractResources;
 import org.apache.syncope.common.lib.to.AnyTO;
-import org.apache.syncope.common.rest.api.service.SyncopeService;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-public class Resources extends AbstractResources {
+public class Details<T extends AnyTO> extends Panel {
 
-    private static final long serialVersionUID = 702900610508752856L;
+    private static final long serialVersionUID = -8995647450549098844L;
 
-    public <T extends AnyTO> Resources(final AnyWrapper<T> modelObject) {
-        super(modelObject);
-    }
+    protected static final Logger LOG = LoggerFactory.getLogger(Details.class);
+
+    protected final PageReference pageRef;
 
-    @Override
-    public boolean evaluate() {
-        available.setObject(SyncopeEnduserSession.get().getService(SyncopeService.class).platform().getResources());
-        return !available.getObject().isEmpty();
+    public Details(
+            final String id,
+            final AnyWrapper<T> wrapper,
+            final boolean templateMode,
+            final boolean includeStatusPanel,
+            final PageReference pageRef) {
+        super(id);
+        this.pageRef = pageRef;
     }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Groups.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Groups.java
similarity index 71%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Groups.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Groups.java
index 7626f73..5b3912a 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Groups.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Groups.java
@@ -16,35 +16,79 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.wizards.any;
+package org.apache.syncope.client.enduser.panels.any;
 
-import java.util.List;
-import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.enduser.rest.GroupRestClient;
-import org.apache.syncope.client.lib.SyncopeClient;
-import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPalettePanel;
-import org.apache.syncope.client.ui.commons.wizards.any.AbstractGroups;
 import org.apache.syncope.client.ui.commons.wizards.any.AbstractGroupsModel;
 import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
-import org.apache.syncope.common.lib.to.AnyTO;
-import org.apache.syncope.common.lib.to.GroupTO;
-import org.apache.syncope.common.lib.to.GroupableRelatableTO;
-import org.apache.syncope.common.lib.to.MembershipTO;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.event.Broadcast;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.form.IChoiceRenderer;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.util.ListModel;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.commons.collections4.ListUtils;
+import org.apache.syncope.client.ui.commons.ajax.markup.html.LabelInfo;
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.GroupTO;
+import org.apache.syncope.common.lib.to.GroupableRelatableTO;
+import org.apache.syncope.common.lib.to.MembershipTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Panel;
 
-public class Groups extends AbstractGroups {
+public class Groups extends Panel {
 
     private static final long serialVersionUID = 552437609667518888L;
 
+    protected static final int MAX_GROUP_LIST_CARDINALITY = 30;
+
     private final EnduserGroupsModel groupsModel;
 
-    public <T extends AnyTO> Groups(final AnyWrapper<T> modelObject) {
-        super(modelObject);
+    protected final AnyTO anyTO;
+
+    protected WebMarkupContainer dyngroupsContainer;
+
+    protected WebMarkupContainer dynrealmsContainer;
+
+    protected WebMarkupContainer groupsContainer;
+
+    public <T extends AnyTO> Groups(final String id,
+            final AnyWrapper<T> modelObject,
+            final boolean templateMode) {
+
+        super(id);
+        this.anyTO = modelObject.getInnerObject();
+
+        setOutputMarkupId(true);
+
+        groupsContainer = new WebMarkupContainer("groupsContainer");
+        groupsContainer.setOutputMarkupId(true);
+        groupsContainer.setOutputMarkupPlaceholderTag(true);
+        add(groupsContainer);
+
+        // ------------------
+        // insert changed label if needed
+        // ------------------
+        if (modelObject instanceof UserWrapper
+                && UserWrapper.class.cast(modelObject).getPreviousUserTO() != null
+                && !ListUtils.isEqualList(
+                        UserWrapper.class.cast(modelObject).getInnerObject().getMemberships(),
+                        UserWrapper.class.cast(modelObject).getPreviousUserTO().getMemberships())) {
+            groupsContainer.add(new LabelInfo("changed", StringUtils.EMPTY));
+        } else {
+            groupsContainer.add(new Label("changed", StringUtils.EMPTY));
+        }
+        // ------------------
+
         this.groupsModel = new EnduserGroupsModel();
 
         setOutputMarkupId(true);
@@ -54,7 +98,14 @@ public class Groups extends AbstractGroups {
         addDynamicRealmsContainer();
     }
 
-    @Override
+    private Function<AjaxRequestTarget, Boolean> getEventFunction() {
+        return (Function<AjaxRequestTarget, Boolean> & Serializable) (target) -> {
+            send(Groups.this.getPage(), Broadcast.BREADTH,
+                    new AjaxPalettePanel.UpdateActionEvent((UserTO) anyTO, target));
+            return true;
+        };
+    }
+
     protected void addGroupsPanel() {
         if (anyTO instanceof GroupTO) {
             groupsContainer.add(new Label("groups").setVisible(false));
@@ -82,7 +133,7 @@ public class Groups extends AbstractGroups {
                             return choices.getObject().stream().
                                     filter(object -> id.equalsIgnoreCase(object.getGroupName())).findAny().orElse(null);
                         }
-                    });
+                    }).event(getEventFunction());
 
             groupsContainer.add(builder.setAllowOrder(true).withFilter().build("groups",
                     new ListModel<MembershipTO>() {
@@ -104,8 +155,7 @@ public class Groups extends AbstractGroups {
                             ? groupsModel.getObject()
                             : GroupRestClient.searchAssignableGroups(
                                     anyTO.getRealm(),
-                                    SyncopeClient.getGroupSearchConditionBuilder().
-                                            isAssignable().and().is(Constants.NAME_FIELD_NAME).equalTo(filter).query(),
+                                    filter,
                                     1, MAX_GROUP_LIST_CARDINALITY)).stream()
                             .map(input -> new MembershipTO.Builder(input.getKey())
                             .groupName(input.getName()).build()).collect(Collectors.toList());
@@ -115,11 +165,9 @@ public class Groups extends AbstractGroups {
         }
     }
 
-    @Override
     protected void addDynamicRealmsContainer() {
     }
 
-    @Override
     protected void addDynamicGroupsContainer() {
     }
 
@@ -167,7 +215,7 @@ public class Groups extends AbstractGroups {
 
         @Override
         public List<String> getDynMemberships() {
-            return List.of();
+            return Collections.emptyList();
         }
 
         /**
@@ -193,4 +241,7 @@ public class Groups extends AbstractGroups {
             }
         }
     }
+
+    public interface SerializableFunction extends Function<AjaxRequestTarget, Boolean>, Serializable {
+    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/PlainAttrs.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/PlainAttrs.java
similarity index 75%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/PlainAttrs.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/PlainAttrs.java
index 281ca92..a25c76d 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/PlainAttrs.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/PlainAttrs.java
@@ -16,113 +16,86 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.wizards.any;
+package org.apache.syncope.client.enduser.panels.any;
 
 import java.util.ArrayList;
-import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
-import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.time.FastDateFormat;
 import org.apache.syncope.client.enduser.layout.CustomizationOption;
-import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateFieldPanel;
 import org.apache.syncope.client.enduser.markup.html.form.BinaryFieldPanel;
 import org.apache.syncope.client.enduser.markup.html.form.MultiFieldPanel;
-import org.apache.syncope.client.ui.commons.markup.html.form.EncryptedFieldPanel;
 import org.apache.syncope.client.ui.commons.SchemaUtils;
+import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
+import org.apache.syncope.common.lib.Attr;
+import org.apache.syncope.common.lib.Attributable;
+import org.apache.syncope.common.lib.EntityTOUtils;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.common.lib.types.SchemaType;
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
+import org.apache.wicket.extensions.markup.html.tabs.ITab;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.IChoiceRenderer;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.model.util.ListModel;
+import java.util.stream.Collectors;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.ui.commons.markup.html.form.AbstractFieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxCheckBoxPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateFieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDateTimeFieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxSpinnerFieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.EncryptedFieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
-import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
-import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
-import org.apache.syncope.common.lib.EntityTOUtils;
-import org.apache.syncope.common.lib.SyncopeConstants;
-import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.AnyTO;
-import org.apache.syncope.common.lib.Attr;
-import org.apache.syncope.common.lib.Attributable;
-import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.GroupableRelatableTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.to.PlainSchemaTO;
-import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.syncope.common.lib.types.AttrSchemaType;
-import org.apache.syncope.common.lib.types.SchemaType;
-import org.apache.wicket.PageReference;
-import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
-import org.apache.wicket.markup.html.WebMarkupContainer;
-import org.apache.wicket.markup.html.form.IChoiceRenderer;
-import org.apache.wicket.markup.html.list.ListItem;
-import org.apache.wicket.markup.html.list.ListView;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.LoadableDetachableModel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.PropertyModel;
-import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.model.StringResourceModel;
-import org.apache.wicket.model.util.ListModel;
 
 public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
 
     private static final long serialVersionUID = 552437609667518888L;
 
-    protected final AjaxWizard.Mode mode;
-
     protected final AnyTO previousObject;
 
     protected String fileKey = "";
 
-    public <T extends AnyTO> PlainAttrs(
-            final AnyWrapper<T> modelObject,
-            final AjaxWizard.Mode mode,
+    public PlainAttrs(
+            final String id,
+            final UserWrapper modelObject,
             final List<String> anyTypeClasses,
             final Map<String, CustomizationOption> whichPlainAttrs) throws IllegalArgumentException {
 
-        super(modelObject, anyTypeClasses, whichPlainAttrs);
-        this.mode = mode;
+        super(id, modelObject, anyTypeClasses, whichPlainAttrs);
 
-        if (modelObject.getInnerObject() instanceof UserTO) {
-            fileKey = UserTO.class.cast(modelObject.getInnerObject()).getUsername();
-        } else if (modelObject.getInnerObject() instanceof GroupTO) {
-            fileKey = GroupTO.class.cast(modelObject.getInnerObject()).getName();
-        } else if (modelObject.getInnerObject() instanceof AnyObjectTO) {
-            fileKey = AnyObjectTO.class.cast(modelObject.getInnerObject()).getName();
-        }
+        fileKey = modelObject.getInnerObject().getUsername();
 
-        if (modelObject instanceof UserWrapper) {
-            previousObject = UserWrapper.class.cast(modelObject).getPreviousUserTO();
-        } else {
-            previousObject = null;
-        }
-
-        setTitleModel(new ResourceModel("attributes.plain"));
-
-        add(new Accordion("plainSchemas", List.of(new AbstractTab(
-                new ResourceModel("attributes.accordion", "Plain Attributes")) {
-
-            private static final long serialVersionUID = 1037272333056449378L;
-
-            @Override
-            public WebMarkupContainer getPanel(final String panelId) {
-                return new PlainSchemasOwn(panelId, schemas, attrs);
-            }
-        }), Model.of(0)).setOutputMarkupId(true));
+        previousObject = modelObject.getPreviousUserTO();
 
+        add(new PlainSchemasOwn("plainSchemas", schemas, attrs).setOutputMarkupId(true));
         add(new ListView<MembershipTO>("membershipsPlainSchemas", membershipTOs) {
 
-            private static final long serialVersionUID = 1749643897846L;
+            private static final long serialVersionUID = 6741044372185745296L;
 
             @Override
             protected void populateItem(final ListItem<MembershipTO> item) {
                 final MembershipTO membershipTO = item.getModelObject();
-                item.add(new Accordion("membershipPlainSchemas", List.of(new AbstractTab(
+                item.add(new Accordion("membershipPlainSchemas", Collections.<ITab>singletonList(new AbstractTab(
                         new StringResourceModel(
                                 "attributes.membership.accordion",
                                 PlainAttrs.this,
@@ -158,11 +131,6 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
     }
 
     @Override
-    protected boolean filterSchemas() {
-        return super.filterSchemas() && mode != AjaxWizard.Mode.TEMPLATE;
-    }
-
-    @Override
     protected List<Attr> getAttrsFromTO() {
         return anyTO.getPlainAttrs().stream().sorted(attrComparator).collect(Collectors.toList());
     }
@@ -174,11 +142,11 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
 
     @Override
     protected void setAttrs() {
-        List<Attr> attrs = new ArrayList<>();
+        List<Attr> plainAttrs = new ArrayList<>();
 
         Map<String, Attr> attrMap = EntityTOUtils.buildAttrMap(anyTO.getPlainAttrs());
 
-        attrs.addAll(schemas.values().stream().map(schema -> {
+        plainAttrs.addAll(schemas.values().stream().map(schema -> {
             Attr attrTO = new Attr();
             attrTO.setSchema(schema.getKey());
             if (attrMap.get(schema.getKey()) == null || attrMap.get(schema.getKey()).getValues().isEmpty()) {
@@ -190,7 +158,7 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
         }).collect(Collectors.toList()));
 
         anyTO.getPlainAttrs().clear();
-        anyTO.getPlainAttrs().addAll(attrs);
+        anyTO.getPlainAttrs().addAll(plainAttrs);
     }
 
     @Override
@@ -205,17 +173,16 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
             attrMap = new HashMap<>();
         }
 
-        plainAttrs.addAll(membershipSchemas.get(membershipTO.getGroupKey()).values().stream().
-                map(schema -> {
-                    Attr attrTO = new Attr();
-                    attrTO.setSchema(schema.getKey());
-                    if (attrMap.get(schema.getKey()) == null || attrMap.get(schema.getKey()).getValues().isEmpty()) {
-                        attrTO.getValues().add(StringUtils.EMPTY);
-                    } else {
-                        attrTO.getValues().addAll(attrMap.get(schema.getKey()).getValues());
-                    }
-                    return attrTO;
-                }).collect(Collectors.toList()));
+        plainAttrs.addAll(membershipSchemas.get(membershipTO.getGroupKey()).values().stream().map(schema -> {
+            Attr attr = new Attr();
+            attr.setSchema(schema.getKey());
+            if (attrMap.get(schema.getKey()) == null || attrMap.get(schema.getKey()).getValues().isEmpty()) {
+                attr.getValues().add(StringUtils.EMPTY);
+            } else {
+                attr.getValues().addAll(attrMap.get(schema.getKey()).getValues());
+            }
+            return attr;
+        }).collect(Collectors.toList()));
 
         membershipTO.getPlainAttrs().clear();
         membershipTO.getPlainAttrs().addAll(plainAttrs);
@@ -238,7 +205,7 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
             case Boolean:
                 panel = new AjaxCheckBoxPanel(
                         "panel",
-                        schemaTO.getLabel(getLocale()),
+                        schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()),
                         new Model<>(),
                         true);
                 panel.setRequired(required);
@@ -252,13 +219,13 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
                 if (datePattern.contains("H")) {
                     panel = new AjaxDateTimeFieldPanel(
                             "panel",
-                            schemaTO.getLabel(getLocale()),
+                            schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()),
                             new Model<>(),
                             FastDateFormat.getInstance(datePattern));
                 } else {
                     panel = new AjaxDateFieldPanel(
                             "panel",
-                            schemaTO.getLabel(getLocale()),
+                            schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()),
                             new Model<>(),
                             FastDateFormat.getInstance(datePattern));
                 }
@@ -271,7 +238,7 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
 
             case Enum:
                 panel = new AjaxDropDownChoicePanel<>("panel",
-                        schemaTO.getLabel(getLocale()), new Model<>(), true);
+                        schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()), new Model<>(), true);
                 ((AjaxDropDownChoicePanel<String>) panel).setChoices(SchemaUtils.getEnumeratedValues(schemaTO));
 
                 if (StringUtils.isNotBlank(schemaTO.getEnumerationKeys())) {
@@ -307,7 +274,7 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
             case Long:
                 panel = new AjaxSpinnerFieldPanel.Builder<Long>().enableOnChange().build(
                         "panel",
-                        schemaTO.getLabel(getLocale()),
+                        schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()),
                         Long.class,
                         new Model<>());
 
@@ -319,7 +286,7 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
             case Double:
                 panel = new AjaxSpinnerFieldPanel.Builder<Double>().enableOnChange().step(0.1).build(
                         "panel",
-                        schemaTO.getLabel(getLocale()),
+                        schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()),
                         Double.class,
                         new Model<>());
 
@@ -329,22 +296,12 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
                 break;
 
             case Binary:
-                final PageReference pageRef = getPageReference();
                 panel = new BinaryFieldPanel(
                         "panel",
-                        schemaTO.getLabel(getLocale()),
+                        schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()),
                         new Model<>(),
                         schemaTO.getMimeType(),
-                        fileKey) {
-
-                    private static final long serialVersionUID = -3268213909514986831L;
-
-                    @Override
-                    protected PageReference getPageReference() {
-                        return pageRef;
-                    }
-
-                };
+                        fileKey);
                 if (required) {
                     panel.addRequiredLabel();
                 }
@@ -352,7 +309,7 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
 
             case Encrypted:
                 panel = new EncryptedFieldPanel("panel",
-                        schemaTO.getLabel(getLocale()), new Model<>(), true);
+                        schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()), new Model<>(), true);
 
                 if (required) {
                     panel.addRequiredLabel();
@@ -361,7 +318,7 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
 
             default:
                 panel = new AjaxTextFieldPanel("panel",
-                        schemaTO.getLabel(getLocale()), new Model<>(), true);
+                        schemaTO.getLabel(SyncopeEnduserSession.get().getLocale()), new Model<>(), true);
 
                 if (jexlHelp) {
                     AjaxTextFieldPanel.class.cast(panel).enableJexlHelp();
@@ -375,6 +332,10 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
         panel.setReadOnly(readOnly);
         panel.setMarkupId(StringUtils.isBlank(groupName) ? schemaTO.getKey() : groupName + '.' + schemaTO.getKey());
 
+        Label label = (Label) panel.get(AbstractFieldPanel.LABEL);
+        label.add(new AttributeModifier("for", FORM_SUFFIX
+                + (StringUtils.isBlank(groupName) ? schemaTO.getKey() : groupName + '.' + schemaTO.getKey())));
+
         return panel;
     }
 
@@ -400,25 +361,25 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
                 @Override
                 @SuppressWarnings({ "unchecked", "rawtypes" })
                 protected void populateItem(final ListItem<Attr> item) {
-                    Attr attr = item.getModelObject();
-                    PlainSchemaTO schema = schemas.get(attr.getSchema());
+                    Attr attrTO = item.getModelObject();
+                    PlainSchemaTO schema = schemas.get(attrTO.getSchema());
 
                     // set default values, if any
-                    if (attr.getValues().stream().noneMatch(StringUtils::isNotBlank)) {
-                        attr.getValues().clear();
-                        attr.getValues().addAll(getDefaultValues(attr.getSchema(), groupName));
+                    if (attrTO.getValues().stream().noneMatch(StringUtils::isNotBlank)) {
+                        attrTO.getValues().clear();
+                        attrTO.getValues().addAll(getDefaultValues(attrTO.getSchema(), groupName));
                     }
 
-                    AbstractFieldPanel<?> panel = getFieldPanel(schemas.get(attr.getSchema()));
-                    if (schemas.get(attr.getSchema()).isMultivalue()) {
+                    AbstractFieldPanel<?> panel = getFieldPanel(schemas.get(attrTO.getSchema()));
+                    if (schemas.get(attrTO.getSchema()).isMultivalue()) {
                         panel = new MultiFieldPanel.Builder<>(
                                 new PropertyModel<>(
-                                        attributableTO.getObject().getPlainAttr(attr.getSchema()), "values"))
-                                .build("panel", attr.getSchema(), FieldPanel.class.cast(panel));
+                                        attributableTO.getObject().getPlainAttr(attrTO.getSchema()), "values"))
+                                .build("panel", attrTO.getSchema(), FieldPanel.class.cast(panel));
                         // SYNCOPE-1215 the entire multifield panel must be readonly, not only its field
                         ((MultiFieldPanel) panel).setReadOnly(schema == null ? false : schema.isReadonly());
                     } else {
-                        FieldPanel.class.cast(panel).setNewModel(attr.getValues()).
+                        FieldPanel.class.cast(panel).setNewModel(attrTO.getValues()).
                                 setReadOnly(schema == null ? false : schema.isReadonly());
                     }
 
@@ -446,26 +407,26 @@ public class PlainAttrs extends AbstractAttrs<PlainSchemaTO> {
                 @Override
                 @SuppressWarnings({ "unchecked", "rawtypes" })
                 protected void populateItem(final ListItem<Attr> item) {
-                    Attr attr = item.getModelObject();
-                    PlainSchemaTO schema = schemas.get(attr.getSchema());
+                    Attr attrTO = item.getModelObject();
+                    PlainSchemaTO schema = schemas.get(attrTO.getSchema());
 
                     // set default values, if any
-                    if (attr.getValues().stream().noneMatch(StringUtils::isNotBlank)) {
-                        attr.getValues().clear();
-                        attr.getValues().addAll(getDefaultValues(attr.getSchema()));
+                    if (attrTO.getValues().stream().noneMatch(StringUtils::isNotBlank)) {
+                        attrTO.getValues().clear();
+                        attrTO.getValues().addAll(getDefaultValues(attrTO.getSchema()));
                     }
 
-                    AbstractFieldPanel<?> panel = getFieldPanel(schemas.get(attr.getSchema()));
-                    if (schemas.get(attr.getSchema()).isMultivalue()) {
+                    AbstractFieldPanel<?> panel = getFieldPanel(schemas.get(attrTO.getSchema()));
+                    if (schemas.get(attrTO.getSchema()).isMultivalue()) {
                         panel = new MultiFieldPanel.Builder<>(
-                                new PropertyModel<>(attr, "values")).build(
+                                new PropertyModel<>(attrTO, "values")).build(
                                 "panel",
-                                attr.getSchema(),
+                                attrTO.getSchema(),
                                 FieldPanel.class.cast(panel));
                         // SYNCOPE-1215 the entire multifield panel must be readonly, not only its field
                         ((MultiFieldPanel) panel).setReadOnly(schema == null ? false : schema.isReadonly());
                     } else {
-                        FieldPanel.class.cast(panel).setNewModel(attr.getValues()).
+                        FieldPanel.class.cast(panel).setNewModel(attrTO.getValues()).
                                 setReadOnly(schema == null ? false : schema.isReadonly());
                     }
                     item.add(panel);
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Resources.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Resources.java
new file mode 100644
index 0000000..93265e8
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/Resources.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels.any;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.ui.commons.ajax.markup.html.LabelInfo;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPalettePanel;
+import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.rest.api.service.SyncopeService;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.PropertyModel;
+import org.apache.wicket.model.util.ListModel;
+
+public class Resources extends Panel {
+
+    private static final long serialVersionUID = 702900610508752856L;
+
+    protected final ListModel<String> available;
+
+    public <T extends AnyTO> Resources(final String id, final AnyWrapper<T> modelObject) {
+        super(id);
+        final T entityTO = modelObject.getInnerObject();
+
+        if (modelObject instanceof UserWrapper
+                && UserWrapper.class.cast(modelObject).getPreviousUserTO() != null
+                && !modelObject.getInnerObject().getResources().equals(
+                        UserWrapper.class.cast(modelObject).getPreviousUserTO().getResources())) {
+
+            add(new LabelInfo("changed", StringUtils.EMPTY));
+        } else {
+            add(new Label("changed", StringUtils.EMPTY));
+        }
+
+        this.setOutputMarkupId(true);
+        this.available = new ListModel<>(List.of());
+
+        add(new AjaxPalettePanel.Builder<String>().build("resources",
+                new PropertyModel<List<String>>(entityTO, "resources") {
+
+            private static final long serialVersionUID = 3799387950428254072L;
+
+            @Override
+            public List<String> getObject() {
+                return new ArrayList<>(entityTO.getResources());
+            }
+
+            @Override
+            public void setObject(final List<String> object) {
+                entityTO.getResources().clear();
+                entityTO.getResources().addAll(object);
+            }
+        }, available).hideLabel().setOutputMarkupId(true));
+    }
+
+    @Override
+    protected void onInitialize() {
+        super.onInitialize();
+        available.setObject(SyncopeEnduserSession.get().getService(SyncopeService.class).platform().getResources());
+    }
+
+    public boolean evaluate() {
+        available.setObject(SyncopeEnduserSession.get().getService(SyncopeService.class).platform().getResources());
+        return !available.getObject().isEmpty();
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/SelfUserDetails.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/SelfUserDetails.java
new file mode 100644
index 0000000..3918934
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/SelfUserDetails.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels.any;
+
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
+import org.apache.wicket.PageReference;
+
+public class SelfUserDetails extends UserDetails {
+
+    private static final long serialVersionUID = 6592027822510220469L;
+
+    public SelfUserDetails(
+            final String id,
+            final UserWrapper wrapper,
+            final boolean templateMode,
+            final boolean includeStatusPanel,
+            final boolean showPasswordManagement,
+            final PageReference pageRef) {
+
+        super(id, wrapper, templateMode, includeStatusPanel, pageRef);
+
+        // ------------------------
+        // Password
+        // ------------------------
+        EditUserPasswordPanel panel = new EditUserPasswordPanel("password", wrapper, templateMode);
+        panel.setVisible(showPasswordManagement);
+
+        add(panel);
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/UserDetails.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/UserDetails.java
new file mode 100644
index 0000000..8897db7
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/UserDetails.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.panels.any;
+
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.password.strength.PasswordStrengthBehavior;
+import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.password.strength.PasswordStrengthConfig;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.enduser.rest.RealmRestClient;
+import org.apache.syncope.client.ui.commons.ajax.markup.html.LabelInfo;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
+import org.apache.syncope.client.ui.commons.wizards.any.PasswordPanel;
+import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
+import org.apache.syncope.common.lib.to.RealmTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.PropertyModel;
+import org.apache.wicket.model.ResourceModel;
+
+public class UserDetails extends Details<UserTO> {
+
+    private static final long serialVersionUID = 6592027822510220463L;
+
+    private final FieldPanel<String> realm;
+
+    protected final AjaxTextFieldPanel username;
+
+    protected final UserTO userTO;
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public UserDetails(
+            final String id,
+            final UserWrapper wrapper,
+            final boolean templateMode,
+            final boolean includeStatusPanel,
+            final PageReference pageRef) {
+
+        super(id, wrapper, templateMode, includeStatusPanel, pageRef);
+
+        userTO = wrapper.getInnerObject();
+        // ------------------------
+        // Username
+        // ------------------------
+        username = new AjaxTextFieldPanel("username", "username", new PropertyModel<>(userTO, "username"), false);
+
+        if (wrapper.getPreviousUserTO() != null && StringUtils.
+                compare(wrapper.getPreviousUserTO().getUsername(), wrapper.getInnerObject().getUsername()) != 0) {
+            username.showExternAction(new LabelInfo("externalAction", wrapper.getPreviousUserTO().getUsername()));
+        }
+
+        if (templateMode) {
+            username.enableJexlHelp();
+        } else {
+            username.addRequiredLabel();
+        }
+        add(username);
+        // ------------------------
+
+        // ------------------------
+        // Realm
+        // ------------------------
+        realm = new AjaxDropDownChoicePanel<>(
+                "destinationRealm", "destinationRealm", new PropertyModel<>(userTO, "realm"), false);
+
+        ((AjaxDropDownChoicePanel<String>) realm).setChoices(
+                RealmRestClient.list().stream().map(RealmTO::getFullPath).collect(Collectors.toList()));
+        add(realm);
+    }
+
+    protected static class EditUserPasswordPanel extends Panel {
+
+        private static final long serialVersionUID = -8198836979773590078L;
+
+        protected EditUserPasswordPanel(
+                final String id,
+                final UserWrapper wrapper,
+                final boolean templateMode) {
+
+            super(id);
+            setOutputMarkupId(true);
+            add(new Label("warning", new ResourceModel("password.change.warning")));
+            add(new PasswordPanel(
+                    "passwordPanel",
+                    wrapper,
+                    templateMode,
+                    wrapper.getInnerObject().getKey() == null,
+                    new PasswordStrengthBehavior(new PasswordStrengthConfig().
+                            withDebug(false).
+                            withShowVerdictsInsideProgressBar(true).
+                            withShowProgressBar(true))));
+        }
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/VirAttrs.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/VirAttrs.java
similarity index 82%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/VirAttrs.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/VirAttrs.java
index 86c55b2..65386d9 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/VirAttrs.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/any/VirAttrs.java
@@ -16,86 +16,77 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.wizards.any;
+package org.apache.syncope.client.enduser.panels.any;
 
-import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.layout.CustomizationOption;
 import org.apache.syncope.client.enduser.markup.html.form.MultiFieldPanel;
-import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
 import org.apache.syncope.client.ui.commons.markup.html.form.AbstractFieldPanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
+import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
+import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.EntityTOUtils;
 import org.apache.syncope.common.lib.to.AnyTO;
-import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.to.GroupableRelatableTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.to.VirSchemaTO;
 import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
+import org.apache.wicket.extensions.markup.html.tabs.ITab;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.list.ListItem;
 import org.apache.wicket.markup.html.list.ListView;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.PropertyModel;
-import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.model.StringResourceModel;
 import org.apache.wicket.model.util.ListModel;
+import java.util.stream.Collectors;
 
 public class VirAttrs extends AbstractAttrs<VirSchemaTO> {
 
     private static final long serialVersionUID = -7982691107029848579L;
 
     public <T extends AnyTO> VirAttrs(
+            final String id,
             final AnyWrapper<T> modelObject,
             final List<String> anyTypeClasses,
             final Map<String, CustomizationOption> whichVirAttrs) {
 
-        super(modelObject, anyTypeClasses, whichVirAttrs);
-
-        setTitleModel(new ResourceModel("attributes.virtual"));
-
-        add(new Accordion("virSchemas", List.of(new AbstractTab(
-                new ResourceModel("attributes.accordion", "Virtual Attributes")) {
-
-            private static final long serialVersionUID = 1037272333056449378L;
-
-            @Override
-            public WebMarkupContainer getPanel(final String panelId) {
-                return new VirAttrs.VirSchemas(panelId, schemas, attrs);
-            }
-        }), Model.of(0)).setOutputMarkupId(true));
+        super(id, modelObject, anyTypeClasses, whichVirAttrs);
 
+        add(new VirAttrs.VirSchemas("virSchemas", schemas, attrs).setOutputMarkupId(true));
         add(new ListView<MembershipTO>("membershipsVirSchemas", membershipTOs) {
 
             private static final long serialVersionUID = 9101744072914090143L;
 
             @Override
             protected void populateItem(final ListItem<MembershipTO> item) {
-                MembershipTO membTO = item.getModelObject();
-                item.add(new Accordion("membershipVirSchemas", List.of(new AbstractTab(
-                        new StringResourceModel("attributes.membership.accordion", VirAttrs.this, Model.of(membTO))) {
-
-                    private static final long serialVersionUID = 1037272333056449378L;
-
-                    @Override
-                    public WebMarkupContainer getPanel(final String panelId) {
-                        return new VirAttrs.VirSchemas(
-                                panelId,
-                                membTO.getGroupName(),
-                                membershipSchemas.get(membTO.getGroupKey()),
-                                new ListModel<>(getAttrsFromTO(membTO)));
-                    }
-                }), Model.of(-1)).setOutputMarkupId(true));
+                final MembershipTO membershipTO = item.getModelObject();
+                item.add(new Accordion("membershipVirSchemas",
+                        Collections.<ITab>singletonList(new AbstractTab(new StringResourceModel(
+                                "attributes.membership.accordion", VirAttrs.this, Model.of(membershipTO))) {
+
+                            private static final long serialVersionUID = 1037272333056449378L;
+
+                            @Override
+                            public WebMarkupContainer getPanel(final String panelId) {
+                                return new VirAttrs.VirSchemas(
+                                        panelId,
+                                        membershipTO.getGroupName(),
+                                        membershipSchemas.get(membershipTO.getGroupKey()),
+                                        new ListModel<>(getAttrsFromTO(membershipTO)));
+                            }
+                        }), Model.of(-1)).setOutputMarkupId(true));
             }
-        });
+        }).setOutputMarkupId(true);
     }
 
     @Override
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/CaptchaPanel.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/captcha/CaptchaPanel.java
similarity index 83%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/CaptchaPanel.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/captcha/CaptchaPanel.java
index 4b886bf..007e26b 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/CaptchaPanel.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/panels/captcha/CaptchaPanel.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.wizards.any;
+package org.apache.syncope.client.enduser.panels.captcha;
 
 import java.security.SecureRandom;
 import org.apache.commons.lang3.StringUtils;
@@ -31,7 +31,7 @@ import org.apache.wicket.model.Model;
 
 public class CaptchaPanel<T> extends Panel {
 
-    private static final long serialVersionUID = 1169850573252481471L;
+    private static final long serialVersionUID = -450657681453274465L;
 
     private static final SecureRandom RANDOM = new SecureRandom();
 
@@ -40,8 +40,6 @@ public class CaptchaPanel<T> extends Panel {
             withinRange('a', 'z').
             build();
 
-    private String randomText;
-
     private final Model<String> captchaText = new Model<>();
 
     private final CaptchaImageResource captchaImageResource;
@@ -55,8 +53,7 @@ public class CaptchaPanel<T> extends Panel {
 
             @Override
             protected byte[] render() {
-                randomText = RANDOM_LETTERS.generate(6);
-                getChallengeIdModel().setObject(randomText);
+                getChallengeIdModel().setObject(RANDOM_LETTERS.generate(6));
                 return super.render();
             }
         };
@@ -82,13 +79,13 @@ public class CaptchaPanel<T> extends Panel {
                 setOutputMarkupPlaceholderTag(true));
     }
 
-    public void reload() {
-        this.captchaImageResource.invalidate();
-    }
-
-    public boolean captchaCheck() {
-        return StringUtils.isBlank(captchaText.getObject()) || StringUtils.isBlank(randomText)
+    public boolean check() {
+        boolean check = StringUtils.isBlank(captchaText.getObject())
+                || StringUtils.isBlank(captchaImageResource.getChallengeId())
                 ? false
-                : captchaText.getObject().equals(randomText);
+                : captchaText.getObject().equals(captchaImageResource.getChallengeId());
+
+        captchaImageResource.invalidate();
+        return check;
     }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/resources/CaptchaResource.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/resources/CaptchaResource.java
deleted file mode 100644
index 7cbb5f5..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/resources/CaptchaResource.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.resources;
-
-import java.security.SecureRandom;
-import javax.servlet.http.HttpServletRequest;
-import org.apache.commons.text.RandomStringGenerator;
-import org.apache.syncope.client.enduser.SyncopeEnduserConstants;
-import org.apache.wicket.extensions.markup.html.captcha.CaptchaImageResource;
-import org.apache.wicket.request.cycle.RequestCycle;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class CaptchaResource extends CaptchaImageResource {
-
-    private static final long serialVersionUID = 8293404296348102926L;
-
-    private static final SecureRandom RANDOM = new SecureRandom();
-
-    private static final Logger LOG = LoggerFactory.getLogger(CaptchaResource.class);
-
-    private static final RandomStringGenerator RANDOM_LETTERS = new RandomStringGenerator.Builder().
-            usingRandom(RANDOM::nextInt).
-            withinRange('a', 'z').
-            build();
-
-    @Override
-    protected byte[] render() {
-        LOG.debug("Generate captcha");
-
-        String captcha = RANDOM_LETTERS.generate(6);
-        HttpServletRequest request = ((HttpServletRequest) RequestCycle.get().getRequest().getContainerRequest());
-        // store the captcha in the current session
-        request.getSession().setAttribute(SyncopeEnduserConstants.CAPTCHA_SESSION_KEY, captcha);
-
-        getChallengeIdModel().setObject(captcha);
-        return super.render();
-    }
-
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AnyTypeRestClient.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AnyTypeRestClient.java
index 8f3c823..09c6ea9 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AnyTypeRestClient.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/AnyTypeRestClient.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.client.enduser.rest;
 
 import java.io.Serializable;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import org.apache.commons.lang3.ObjectUtils;
@@ -47,7 +48,7 @@ public class AnyTypeRestClient extends BaseRestClient {
     }
 
     public static List<AnyTypeTO> listAnyTypes() {
-        List<AnyTypeTO> types = List.of();
+        List<AnyTypeTO> types = Collections.emptyList();
 
         try {
             types = getService(AnyTypeService.class).list();
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/BaseRestClient.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/BaseRestClient.java
index 6f9e4ff..97196c0 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/BaseRestClient.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/BaseRestClient.java
@@ -18,10 +18,17 @@
  */
 package org.apache.syncope.client.enduser.rest;
 
+import java.net.URI;
+import javax.ws.rs.core.HttpHeaders;
+import org.apache.cxf.jaxrs.client.WebClient;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.rest.RestClient;
 import org.apache.syncope.common.lib.search.OrderByClauseBuilder;
+import org.apache.syncope.common.lib.types.ExecStatus;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
 import org.apache.syncope.common.rest.api.service.SyncopeService;
 import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
 import org.slf4j.Logger;
@@ -49,7 +56,7 @@ public abstract class BaseRestClient implements RestClient {
         SyncopeEnduserSession.get().resetClient(serviceClass);
     }
 
-    protected static String toOrderBy(final SortParam<String> sort) {
+    public static String toOrderBy(final SortParam<String> sort) {
         OrderByClauseBuilder builder = SyncopeClient.getOrderByClauseBuilder();
 
         String property = sort.getProperty();
@@ -65,4 +72,22 @@ public abstract class BaseRestClient implements RestClient {
 
         return builder.build();
     }
+
+    protected static <E extends JAXRSService, T> T getObject(
+            final E service, final URI location, final Class<T> resultClass) {
+
+        WebClient webClient = WebClient.fromClient(WebClient.client(service));
+        webClient.accept(SyncopeEnduserSession.get().getMediaType()).to(location.toASCIIString(), false);
+        return webClient.
+                header(RESTHeaders.DOMAIN, SyncopeEnduserSession.get().getDomain()).
+                header(HttpHeaders.AUTHORIZATION, "Bearer " + SyncopeEnduserSession.get().getJWT()).
+                get(resultClass);
+    }
+
+    protected static String getStatus(final int httpStatus) {
+        ExecStatus execStatus = ExecStatus.fromHttpStatus(httpStatus);
+        return execStatus == null
+                ? Constants.UNKNOWN
+                : execStatus.name();
+    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/GroupRestClient.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/GroupRestClient.java
index f9e22c3..23c98fd 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/GroupRestClient.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/GroupRestClient.java
@@ -18,9 +18,8 @@
  */
 package org.apache.syncope.client.enduser.rest;
 
-import java.util.List;
-import javax.ws.rs.core.GenericType;
-import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.request.GroupCR;
 import org.apache.syncope.common.lib.request.GroupUR;
 import org.apache.syncope.common.lib.to.GroupTO;
@@ -29,6 +28,9 @@ import org.apache.syncope.common.rest.api.beans.AnyQuery;
 import org.apache.syncope.common.rest.api.service.AnyService;
 import org.apache.syncope.common.rest.api.service.GroupService;
 import org.apache.syncope.common.rest.api.service.SyncopeService;
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+import java.util.List;
 
 /**
  * Console client for invoking Rest Group's services.
@@ -42,16 +44,16 @@ public class GroupRestClient extends AbstractAnyRestClient<GroupTO> {
         return GroupService.class;
     }
 
-    public static ProvisioningResult<GroupTO> create(final GroupCR groupCR) {
-        Response response = getService(GroupService.class).create(groupCR);
+    public static ProvisioningResult<GroupTO> create(final GroupCR groupTO) {
+        Response response = getService(GroupService.class).create(groupTO);
         return response.readEntity(new GenericType<ProvisioningResult<GroupTO>>() {
         });
     }
 
-    public ProvisioningResult<GroupTO> update(final String etag, final GroupUR updateReq) {
+    public ProvisioningResult<GroupTO> update(final String etag, final GroupUR groupPatch) {
         ProvisioningResult<GroupTO> result;
         synchronized (this) {
-            result = getService(etag, GroupService.class).update(updateReq).
+            result = getService(etag, GroupService.class).update(groupPatch).
                     readEntity(new GenericType<ProvisioningResult<GroupTO>>() {
                     });
             resetClient(getAnyServiceClass());
@@ -65,7 +67,8 @@ public class GroupRestClient extends AbstractAnyRestClient<GroupTO> {
         final int page,
         final int size) {
 
-        return getService(SyncopeService.class).searchAssignableGroups(realm, term, page, size).getResult();
+        return getService(SyncopeService.class).searchAssignableGroups(
+                StringUtils.isNotEmpty(realm) ? realm : SyncopeConstants.ROOT_REALM, term, page, size).getResult();
     }
 
     @Override
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/RoleRestClient.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/RoleRestClient.java
deleted file mode 100644
index 6b3aea3..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/RoleRestClient.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.rest;
-
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.common.lib.to.RoleTO;
-import org.apache.syncope.common.rest.api.service.RoleService;
-
-/**
- * Console client for invoking Rest Role's services.
- */
-public class RoleRestClient extends BaseRestClient {
-
-    private static final long serialVersionUID = -3161863874876938094L;
-
-    public static void delete(final String key) {
-        getService(RoleService.class).delete(key);
-    }
-
-    public static RoleTO read(final String key) {
-        return getService(RoleService.class).read(key);
-    }
-
-    public static void update(final RoleTO roleTO) {
-        getService(RoleService.class).update(roleTO);
-    }
-
-    public static void create(final RoleTO roleTO) {
-        getService(RoleService.class).create(roleTO);
-    }
-
-    public static List<RoleTO> list() {
-        return getService(RoleService.class).list();
-    }
-
-    public static String readAnyLayout(final String roleKey) {
-        try {
-            return IOUtils.toString(InputStream.class.cast(
-                    getService(RoleService.class).getAnyLayout(roleKey).getEntity()),
-                    StandardCharsets.UTF_8);
-        } catch (Exception e) {
-            LOG.error("Error retrieving console layout info for role {}", roleKey, e);
-            return StringUtils.EMPTY;
-        }
-    }
-
-    public static void setAnyLayout(final String roleKey, final String content) {
-        getService(RoleService.class).setAnyLayout(
-                roleKey, IOUtils.toInputStream(content, StandardCharsets.UTF_8));
-    }
-
-    public static void removeAnyLayout(final String roleKey) {
-        getService(RoleService.class).removeAnyLayout(roleKey);
-    }
-
-    public static List<String> getAllAvailableEntitlements() {
-        return getSyncopeService().platform().getEntitlements().stream().sorted().collect(Collectors.toList());
-    }
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/SchemaRestClient.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/SchemaRestClient.java
index c89e35b..666b273 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/SchemaRestClient.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/SchemaRestClient.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.client.enduser.rest;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
@@ -86,7 +87,7 @@ public class SchemaRestClient extends BaseRestClient {
     }
 
     public static List<String> getSchemaNames(final SchemaType schemaType) {
-        List<String> schemaNames = List.of();
+        List<String> schemaNames = Collections.emptyList();
 
         try {
             schemaNames = getSchemas(schemaType, null, new String[0]).stream().
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/SyncopeRestClient.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/SyncopeRestClient.java
index 74429c8..233dd51 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/SyncopeRestClient.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/SyncopeRestClient.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.client.enduser.rest;
 
+import java.util.Collections;
 import java.util.List;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.TypeExtensionTO;
@@ -28,7 +29,7 @@ public class SyncopeRestClient extends BaseRestClient {
     private static final long serialVersionUID = -2211371717449597247L;
 
     public static List<String> listAnyTypeClasses() {
-        List<String> types = List.of();
+        List<String> types = Collections.emptyList();
 
         try {
             types = getService(SyncopeService.class).platform().getAnyTypeClasses();
@@ -39,13 +40,14 @@ public class SyncopeRestClient extends BaseRestClient {
     }
 
     public static List<String> searchUserTypeExtensions(final String groupName) {
-        List<String> types = List.of();
+        List<String> types = Collections.emptyList();
         try {
             TypeExtensionTO typeExtensionTO = getService(SyncopeService.class).readUserTypeExtension(groupName);
             types = typeExtensionTO == null ? types : typeExtensionTO.getAuxClasses();
-        } catch (SyncopeClientException e) {
+        } catch (Exception e) {
             LOG.error("While reading all any type classes for group [{}]", groupName, e);
         }
         return types;
     }
+
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/UserSelfRestClient.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/UserSelfRestClient.java
index 95a35ce..f7cf9cf 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/UserSelfRestClient.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/UserSelfRestClient.java
@@ -18,51 +18,41 @@
  */
 package org.apache.syncope.client.enduser.rest;
 
-import javax.ws.rs.core.GenericType;
-import javax.ws.rs.core.Response;
-import org.apache.syncope.common.lib.request.BooleanReplacePatchItem;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.rest.api.service.UserSelfService;
+import javax.ws.rs.core.GenericType;
 
-/**
- * Console client for invoking rest users services.
- */
 public class UserSelfRestClient extends BaseRestClient {
 
     private static final long serialVersionUID = -1575748964398293968L;
 
-    public static ProvisioningResult<UserTO> create(final UserCR createReq) {
-        Response response = getService(UserSelfService.class).create(createReq);
-        return response.readEntity(new GenericType<ProvisioningResult<UserTO>>() {
+    public static void changePassword(final String password) {
+        getService(UserSelfService.class).mustChangePassword(password);
+    }
+
+    public static void requestPasswordReset(final String username, final String securityAnswer) {
+        getService(UserSelfService.class).requestPasswordReset(username, securityAnswer);
+    }
+
+    public ProvisioningResult<UserTO> create(final UserCR createReq, final boolean storePassword) {
+        ProvisioningResult<UserTO> result;
+        result = getService(UserSelfService.class).create(createReq).readEntity(
+                new GenericType<ProvisioningResult<UserTO>>() {
         });
+        return result;
     }
 
-    public ProvisioningResult<UserTO> update(final String etag, final UserUR updateReq) {
+    public ProvisioningResult<UserTO> update(final String etag, final UserUR userPatch) {
         ProvisioningResult<UserTO> result;
         synchronized (this) {
-            result = getService(etag, UserSelfService.class).update(updateReq).
+            result = getService(etag, UserSelfService.class).update(userPatch).
                     readEntity(new GenericType<ProvisioningResult<UserTO>>() {
                     });
             resetClient(UserSelfService.class);
         }
         return result;
     }
-
-    public ProvisioningResult<UserTO> mustChangePassword(final String etag, final boolean value, final String key) {
-        UserUR userUR = new UserUR();
-        userUR.setKey(key);
-        userUR.setMustChangePassword(new BooleanReplacePatchItem.Builder().value(value).build());
-        return update(etag, userUR);
-    }
-
-    public static void changePassword(final String password) {
-        getService(UserSelfService.class).mustChangePassword(password);
-    }
-
-    public static void requestPasswordReset(final String username, final String securityAnswer) {
-        getService(UserSelfService.class).requestPasswordReset(username, securityAnswer);
-    }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Captcha.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wicket/markup/head/MetaHeaderItem.java
similarity index 50%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Captcha.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wicket/markup/head/MetaHeaderItem.java
index c46755c..90ccf60 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/Captcha.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wicket/markup/head/MetaHeaderItem.java
@@ -16,34 +16,34 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.wizards.any;
+package org.apache.syncope.client.enduser.wicket.markup.head;
 
-import org.apache.syncope.client.enduser.SyncopeWebApplication;
-import org.apache.wicket.extensions.wizard.WizardModel;
-import org.apache.wicket.extensions.wizard.WizardStep;
+import java.io.Serializable;
+import java.util.Arrays;
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.request.Response;
 
-public class Captcha extends WizardStep implements WizardModel.ICondition {
+public class MetaHeaderItem extends HeaderItem implements Serializable {
 
-    private static final long serialVersionUID = 702900610508752856L;
+    private static final long serialVersionUID = 7578609827530302053L;
 
-    private final CaptchaPanel<Void> captchaPanel;
+    private final String key;
 
-    public Captcha() {
-        captchaPanel = new CaptchaPanel<>("captchaPanel");
-        captchaPanel.setOutputMarkupId(true);
-        add(captchaPanel);
-    }
+    private final String value;
 
-    public boolean captchaCheck() {
-        return captchaPanel.captchaCheck();
+    public MetaHeaderItem(final String key, final String value) {
+        this.key = key;
+        this.value = value;
     }
 
-    public void reload() {
-        captchaPanel.reload();
+    @Override
+    public Iterable<?> getRenderTokens() {
+        return Arrays.asList("meta-" + key + "-" + value);
     }
 
     @Override
-    public boolean evaluate() {
-        return SyncopeWebApplication.get().isCaptchaEnabled();
+    public void render(final Response response) {
+        response.write("<meta http-equiv=\"" + key + "\" content=\"" + value + "\"/>");
+        response.write("\n");
     }
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfUpdate.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/widgets/BaseWidget.java
similarity index 71%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfUpdate.java
rename to client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/widgets/BaseWidget.java
index 7c86c2d..8426ae5 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/SelfUpdate.java
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/widgets/BaseWidget.java
@@ -16,16 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.pages;
+package org.apache.syncope.client.enduser.widgets;
 
-import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.markup.html.panel.Panel;
 
-public class SelfUpdate extends BaseEnduserWebPage {
+public abstract class BaseWidget extends Panel {
 
-    private static final long serialVersionUID = 164651008547631054L;
+    private static final long serialVersionUID = -4186604985011430091L;
 
-    public SelfUpdate(final PageParameters parameters) {
-        super(parameters);
+    public BaseWidget(final String id) {
+        super(id);
     }
 
 }
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/widgets/UserProfileWidget.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/widgets/UserProfileWidget.java
new file mode 100644
index 0000000..9f52674
--- /dev/null
+++ b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/widgets/UserProfileWidget.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.widgets;
+
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+
+public class UserProfileWidget extends BaseWidget {
+
+    private static final long serialVersionUID = 4437711189800676363L;
+
+    protected UserTO userTO;
+
+    public UserProfileWidget(final String id) {
+        super(id);
+
+        userTO = SyncopeEnduserSession.get().getSelfTO(true);
+
+        WebMarkupContainer userProfile = new WebMarkupContainer("userProfile");
+        userProfile.setOutputMarkupId(true);
+        add(userProfile);
+
+        Label welcome = new Label("welcome", userTO.getUsername());
+        welcome.setOutputMarkupId(true);
+        userProfile.add(welcome);
+
+        addBaseFields(userProfile);
+        addExtFields(userProfile);
+    }
+
+    protected void addBaseFields(final WebMarkupContainer userProfile) {
+        Label username = new Label("username", userTO.getUsername());
+        username.setOutputMarkupId(true);
+        userProfile.add(username);
+
+        Label changePwdDate = new Label("changePwdDate", userTO.getChangePwdDate());
+        changePwdDate.setOutputMarkupId(true);
+        userProfile.add(changePwdDate);
+
+        Label lastLoginDate = new Label("lastLoginDate", userTO.getLastLoginDate());
+        lastLoginDate.setOutputMarkupId(true);
+        userProfile.add(lastLoginDate);
+    }
+
+    protected void addExtFields(final WebMarkupContainer userProfile) {
+    }
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/AnyWizardBuilder.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/AnyWizardBuilder.java
deleted file mode 100644
index 922554e..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/AnyWizardBuilder.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.wizards.any;
-
-import java.io.Serializable;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.syncope.client.enduser.SyncopeWebApplication;
-import org.apache.syncope.client.enduser.SyncopeEnduserSession;
-import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
-import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
-import org.apache.syncope.client.ui.commons.wizards.AjaxWizardMgtButtonBar;
-import org.apache.syncope.client.ui.commons.wizards.any.AbstractAnyWizardBuilder;
-import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
-import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
-import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.wicket.Component;
-import org.apache.wicket.PageReference;
-import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.extensions.wizard.FinishButton;
-import org.apache.wicket.extensions.wizard.IWizard;
-import org.apache.wicket.extensions.wizard.IWizardModel;
-import org.apache.wicket.extensions.wizard.IWizardStep;
-import org.apache.wicket.extensions.wizard.WizardModel;
-
-public abstract class AnyWizardBuilder extends AbstractAnyWizardBuilder<UserTO> {
-
-    private static final long serialVersionUID = -2480279868319546243L;
-
-    protected final List<String> anyTypeClasses;
-
-    protected UserFormLayoutInfo formLayoutInfo;
-
-    protected Captcha captcha;
-
-    /**
-     * Construct.
-     *
-     * @param anyTO any
-     * @param anyTypeClasses any type classes
-     * @param formLayoutInfo form layout info
-     * @param pageRef caller page reference.
-     */
-    public AnyWizardBuilder(
-            final UserTO anyTO,
-            final List<String> anyTypeClasses,
-            final UserFormLayoutInfo formLayoutInfo,
-            final PageReference pageRef) {
-
-        super(new AnyWrapper<>(anyTO), pageRef);
-        this.anyTypeClasses = anyTypeClasses;
-        this.formLayoutInfo = formLayoutInfo;
-    }
-
-    /**
-     * Construct.
-     *
-     * @param wrapper any wrapper
-     * @param anyTypeClasses any type classes
-     * @param formLayoutInfo form layout info
-     * @param pageRef caller page reference.
-     */
-    public AnyWizardBuilder(
-            final UserWrapper wrapper,
-            final List<String> anyTypeClasses,
-            final UserFormLayoutInfo formLayoutInfo,
-            final PageReference pageRef) {
-
-        super(wrapper, pageRef);
-        this.anyTypeClasses = anyTypeClasses;
-        this.formLayoutInfo = formLayoutInfo;
-    }
-
-    @Override
-    protected WizardModel buildModelSteps(final AnyWrapper<UserTO> modelObject, final WizardModel wizardModel) {
-        wizardModel.add(new UserDetails(
-                UserWrapper.class.cast(modelObject),
-                mode == AjaxWizard.Mode.TEMPLATE,
-                UserFormLayoutInfo.class.cast(formLayoutInfo).isPasswordManagement()));
-
-        if (formLayoutInfo.isAuxClasses()) {
-            wizardModel.add(new EnduserAuxClasses(modelObject, anyTypeClasses));
-        }
-
-        if (formLayoutInfo.isGroups()) {
-            wizardModel.add(new Groups(modelObject));
-        }
-
-        // attributes panel steps
-        if (formLayoutInfo.isPlainAttrs()) {
-            wizardModel.add(new PlainAttrs(
-                    modelObject,
-                    mode,
-                    anyTypeClasses,
-                    formLayoutInfo.getWhichPlainAttrs()) {
-
-                private static final long serialVersionUID = 8167894751609598306L;
-
-                @Override
-                public PageReference getPageReference() {
-                    return pageRef;
-                }
-
-            });
-        }
-        if (formLayoutInfo.isDerAttrs()) {
-            wizardModel.add(new DerAttrs(modelObject, anyTypeClasses, formLayoutInfo.getWhichDerAttrs()));
-        }
-        if (formLayoutInfo.isVirAttrs()) {
-            wizardModel.add(new VirAttrs(modelObject, anyTypeClasses, formLayoutInfo.getWhichVirAttrs()));
-        }
-        if (formLayoutInfo.isResources()) {
-            wizardModel.add(new Resources(modelObject));
-        }
-        if (SyncopeWebApplication.get().isCaptchaEnabled()) {
-            // add captcha
-            captcha = new Captcha();
-            captcha.setOutputMarkupId(true);
-            wizardModel.add(captcha);
-        }
-
-        return wizardModel;
-    }
-
-    @Override
-    protected long getMaxWaitTimeInSeconds() {
-        return SyncopeWebApplication.get().getMaxWaitTimeInSeconds();
-    }
-
-    @Override
-    protected void sendError(final Exception exception) {
-        SyncopeEnduserSession.get().onException(exception);
-    }
-
-    @Override
-    protected void sendWarning(final String message) {
-        SyncopeEnduserSession.get().warn(message);
-    }
-
-    @Override
-    protected Future<Pair<Serializable, Serializable>> execute(
-            final Callable<Pair<Serializable, Serializable>> future) {
-        return SyncopeEnduserSession.get().execute(future);
-    }
-
-    @Override
-    public AjaxWizard<AnyWrapper<UserTO>> build(final String id, final AjaxWizard.Mode mode) {
-        this.mode = mode;
-
-        // get the specified item if available
-        final AnyWrapper<UserTO> modelObject = newModelObject();
-
-        return new AjaxWizard<AnyWrapper<UserTO>>(
-                id, modelObject, buildModelSteps(modelObject, new WizardModel()), mode, this.pageRef) {
-
-            private static final long serialVersionUID = 7770507663760640735L;
-
-            @Override
-            protected void onCancelInternal() {
-                AnyWizardBuilder.this.onCancelInternal(modelObject);
-            }
-
-            @Override
-            protected Pair<Serializable, Serializable> onApplyInternal(final AjaxRequestTarget target) {
-                Serializable res = AnyWizardBuilder.this.onApplyInternal(modelObject);
-
-                Serializable payload;
-                switch (mode) {
-                    case CREATE:
-                        payload = getCreateCustomPayloadEvent(res, target);
-                        break;
-                    case EDIT:
-                    case TEMPLATE:
-                        payload = getEditCustomPayloadEvent(res, target);
-                        break;
-                    default:
-                        payload = null;
-                }
-
-                return Pair.of(payload, res);
-            }
-
-            @Override
-            protected long getMaxWaitTimeInSeconds() {
-                return AnyWizardBuilder.this.getMaxWaitTimeInSeconds();
-            }
-
-            @Override
-            protected void sendError(final Exception exception) {
-                SyncopeEnduserSession.get().onException(exception);
-            }
-
-            @Override
-            protected void sendWarning(final String message) {
-                AnyWizardBuilder.this.sendWarning(message);
-            }
-
-            @Override
-            protected Future<Pair<Serializable, Serializable>> execute(
-                    final Callable<Pair<Serializable, Serializable>> future) {
-                return AnyWizardBuilder.this.execute(future);
-            }
-
-            @Override
-            protected Component newButtonBar(final String id) {
-                return new AjaxWizardMgtButtonBar<>(id, this, mode) {
-
-                    private static final long serialVersionUID = -3041152400413815333L;
-
-                    @Override
-                    protected FinishButton newFinishButton(final String id, final IWizard wizard) {
-                        return new FinishButton(id, wizard) {
-
-                            private static final long serialVersionUID = 864248301720764819L;
-
-                            @Override
-                            public boolean isEnabled() {
-                                switch (mode) {
-                                    case EDIT:
-                                    case TEMPLATE:
-                                        return true;
-                                    case READONLY:
-                                        return false;
-                                    default:
-                                        if (!completed) {
-                                            final IWizardStep activeStep = getWizardModel().getActiveStep();
-                                            completed = (activeStep != null)
-                                                    && getWizardModel().isLastStep(activeStep)
-                                                    && super.isEnabled();
-                                        }
-                                        return completed;
-                                }
-                            }
-
-                            @Override
-                            public boolean isVisible() {
-                                switch (mode) {
-                                    case READONLY:
-                                        return false;
-                                    default:
-                                        return true;
-                                }
-                            }
-
-                            @Override
-                            public void onClick() {
-                                IWizardModel wizardModel = getWizardModel();
-                                IWizardStep activeStep = wizardModel.getActiveStep();
-
-                                // let the step apply any state
-                                activeStep.applyState();
-
-                                // if the step completed after applying the state, notify the wizard
-                                if (activeStep.isComplete()
-                                        && SyncopeWebApplication.get().isCaptchaEnabled()
-                                        && !getWizardModel().isLastStep(activeStep)) {
-                                    // go to last step
-                                    getWizardModel().last();
-                                } else if (activeStep.isComplete()) {
-                                    getWizardModel().finish();
-                                } else {
-                                    error(getLocalizer().getString(
-                                            "org.apache.wicket.extensions.wizard.FinishButton.step.did.not.complete",
-                                            this));
-                                }
-                            }
-                        };
-                    }
-
-                };
-            }
-
-        }.setEventSink(eventSink).addOuterObject(outerObjects);
-    }
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/EnduserAuxClasses.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/EnduserAuxClasses.java
deleted file mode 100644
index bdd6f44..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/EnduserAuxClasses.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.wizards.any;
-
-import org.apache.syncope.client.ui.commons.wizards.any.AbstractAuxClasses;
-import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.apache.syncope.client.enduser.rest.SyncopeRestClient;
-import org.apache.syncope.common.lib.to.AnyTO;
-import org.apache.syncope.common.lib.to.AnyTypeClassTO;
-
-public class EnduserAuxClasses extends AbstractAuxClasses {
-
-    private static final long serialVersionUID = 552437609667518888L;
-
-    public <T extends AnyTO> EnduserAuxClasses(final AnyWrapper<T> modelObject, final List<String> anyTypeClasses) {
-        super(modelObject, anyTypeClasses);
-    }
-
-    @Override
-    protected final List<AnyTypeClassTO> listAnyTypecClasses() {
-        return SyncopeRestClient.listAnyTypeClasses().stream().map(name -> {
-            AnyTypeClassTO anyTypeClassTO = new AnyTypeClassTO();
-            anyTypeClassTO.setKey(name);
-            return anyTypeClassTO;
-        }).collect(Collectors.toList());
-    }
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/UserDetails.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/UserDetails.java
deleted file mode 100644
index 8f07432..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/UserDetails.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.wizards.any;
-
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.password.strength.PasswordStrengthBehavior;
-import de.agilecoders.wicket.extensions.markup.html.bootstrap.form.password.strength.PasswordStrengthConfig;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.client.enduser.rest.RealmRestClient;
-import org.apache.syncope.client.enduser.rest.SecurityQuestionRestClient;
-import org.apache.syncope.client.ui.commons.Constants;
-import org.apache.syncope.client.ui.commons.ajax.markup.html.LabelInfo;
-import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
-import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
-import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
-import org.apache.syncope.client.ui.commons.markup.html.form.FieldPanel;
-import org.apache.syncope.client.ui.commons.wizards.any.PasswordPanel;
-import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
-import org.apache.syncope.common.lib.to.RealmTO;
-import org.apache.syncope.common.lib.to.SecurityQuestionTO;
-import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.wicket.Component;
-import org.apache.wicket.ajax.AjaxEventBehavior;
-import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.ajax.markup.html.AjaxLink;
-import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
-import org.apache.wicket.extensions.markup.html.tabs.ITab;
-import org.apache.wicket.extensions.wizard.WizardStep;
-import org.apache.wicket.markup.ComponentTag;
-import org.apache.wicket.markup.html.basic.Label;
-import org.apache.wicket.markup.html.form.IChoiceRenderer;
-import org.apache.wicket.markup.html.panel.Panel;
-import org.apache.wicket.model.IModel;
-import org.apache.wicket.model.Model;
-import org.apache.wicket.model.PropertyModel;
-import org.apache.wicket.model.ResourceModel;
-
-public class UserDetails extends WizardStep {
-
-    private static final long serialVersionUID = 6592027822510220463L;
-
-    private static final String PASSWORD_CONTENT_PATH = "body:content";
-
-    private final FieldPanel<String> realm;
-
-    protected final AjaxTextFieldPanel username;
-
-    private final FieldPanel<String> securityQuestion;
-
-    private final FieldPanel<String> securityAnswer;
-
-    protected final UserTO userTO;
-
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    public UserDetails(
-            final UserWrapper wrapper,
-            final boolean templateMode,
-            final boolean showPasswordManagement) {
-
-        super();
-
-        userTO = wrapper.getInnerObject();
-        // ------------------------
-        // Username
-        // ------------------------
-        username = new AjaxTextFieldPanel(Constants.USERNAME_FIELD_NAME, Constants.USERNAME_FIELD_NAME,
-                new PropertyModel<>(userTO, Constants.USERNAME_FIELD_NAME), false);
-
-        if (wrapper.getPreviousUserTO() != null && StringUtils.
-                compare(wrapper.getPreviousUserTO().getUsername(), wrapper.getInnerObject().getUsername()) != 0) {
-            username.showExternAction(new LabelInfo("externalAction", wrapper.getPreviousUserTO().getUsername()));
-        }
-
-        if (templateMode) {
-            username.enableJexlHelp();
-        } else {
-            username.addRequiredLabel();
-        }
-        add(username);
-        // ------------------------
-
-        // ------------------------
-        // Realm
-        // ------------------------
-        realm = new AjaxDropDownChoicePanel<>(
-                "destinationRealm", "destinationRealm", new PropertyModel<>(userTO, "realm"), false);
-
-        ((AjaxDropDownChoicePanel<String>) realm).setChoices(
-                RealmRestClient.list().stream().map(RealmTO::getFullPath).collect(Collectors.toList()));
-        add(realm);
-
-        // ------------------------
-        // Password
-        // ------------------------
-        Model<Integer> model = Model.of(-1);
-
-        Accordion accordion = new Accordion("accordionPanel", List.of(
-                new AbstractTab(new ResourceModel("password.change", "Change password")) {
-
-            private static final long serialVersionUID = 1037272333056449378L;
-
-            @Override
-            public Panel getPanel(final String panelId) {
-                EditUserPasswordPanel panel = new EditUserPasswordPanel(panelId, wrapper, templateMode);
-                panel.setEnabled(model.getObject() >= 0);
-                return panel;
-            }
-        }), model) {
-
-            private static final long serialVersionUID = -2898628183677758699L;
-
-            @Override
-            protected Component newTitle(final String markupId, final ITab tab, final Accordion.State state) {
-                return new AjaxLink<Integer>(markupId) {
-
-                    private static final long serialVersionUID = 7021195294339489084L;
-
-                    @Override
-                    protected void onComponentTag(final ComponentTag tag) {
-                        super.onComponentTag(tag);
-                        tag.put("style", "color: #337ab7");
-                    }
-
-                    @Override
-                    public void onClick(final AjaxRequestTarget target) {
-                        model.setObject(model.getObject() == 0 ? -1 : 0);
-                        Component passwordPanel = getParent().get(PASSWORD_CONTENT_PATH);
-                        passwordPanel.setEnabled(model.getObject() >= 0);
-                        target.add(passwordPanel);
-                    }
-                }.setBody(new ResourceModel("password.change", "Change password ..."));
-            }
-        };
-
-        accordion.setOutputMarkupId(true);
-        accordion.setVisible(showPasswordManagement);
-        add(accordion);
-        // ------------------------
-
-        // ------------------------
-        // Security Question
-        // ------------------------
-        securityQuestion = new AjaxDropDownChoicePanel("securityQuestion", "securityQuestion", new PropertyModel<>(
-                userTO, "securityQuestion"));
-        ((AjaxDropDownChoicePanel) securityQuestion).setNullValid(true);
-
-        final List<SecurityQuestionTO> securityQuestions = SecurityQuestionRestClient.list();
-        ((AjaxDropDownChoicePanel<String>) securityQuestion).setChoices(securityQuestions.stream().map(
-                SecurityQuestionTO::getKey).collect(Collectors.toList()));
-        ((AjaxDropDownChoicePanel<String>) securityQuestion).setChoiceRenderer(
-                new IChoiceRenderer<String>() {
-
-            private static final long serialVersionUID = -4421146737845000747L;
-
-            @Override
-            public Object getDisplayValue(final String value) {
-                return securityQuestions.stream().filter(sq -> value.equals(sq.getKey()))
-                        .map(SecurityQuestionTO::getContent).findFirst().orElse(null);
-            }
-
-            @Override
-            public String getIdValue(final String value, final int index) {
-                return value;
-            }
-
-            @Override
-            public String getObject(
-                    final String id,
-                    final IModel<? extends List<? extends String>> choices) {
-                return id;
-            }
-        });
-
-        securityQuestion.add(new AjaxEventBehavior(Constants.ON_CHANGE) {
-
-            private static final long serialVersionUID = 192359260308762078L;
-
-            @Override
-            protected void onEvent(final AjaxRequestTarget target) {
-                securityAnswer.setEnabled(StringUtils.isNotBlank(securityQuestion.getModelObject()));
-                target.add(securityAnswer);
-            }
-        });
-
-        add(securityQuestion);
-        // ------------------------
-
-        // ------------------------
-        // Security Answer
-        // ------------------------
-        securityAnswer =
-                new AjaxTextFieldPanel("securityAnswer", "securityAnswer",
-                        new PropertyModel<>(userTO, "securityAnswer"), false);
-        add(securityAnswer.setOutputMarkupId(true).setOutputMarkupPlaceholderTag(true).setEnabled(StringUtils.
-                isNotBlank(securityQuestion.getModelObject())));
-        // ------------------------
-    }
-
-    public static class EditUserPasswordPanel extends Panel {
-
-        private static final long serialVersionUID = -8198836979773590078L;
-
-        public EditUserPasswordPanel(
-                final String id,
-                final UserWrapper wrapper,
-                final boolean templateMode) {
-            super(id);
-            setOutputMarkupId(true);
-            add(new Label("warning", new ResourceModel("password.change.warning")));
-            add(new PasswordPanel("passwordPanel", wrapper, templateMode, new PasswordStrengthBehavior(
-                    new PasswordStrengthConfig()
-                            .withDebug(true)
-                            .withShowVerdictsInsideProgressBar(true)
-                            .withShowProgressBar(true))));
-        }
-
-    }
-}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/UserWizardBuilder.java b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/UserWizardBuilder.java
deleted file mode 100644
index 714cacb..0000000
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/wizards/any/UserWizardBuilder.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.enduser.wizards.any;
-
-import java.io.Serializable;
-import java.util.List;
-import java.util.Optional;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.client.ui.commons.layout.UserForm;
-import org.apache.syncope.client.enduser.layout.UserFormLayoutInfo;
-import org.apache.syncope.client.enduser.rest.UserSelfRestClient;
-import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
-import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
-import org.apache.syncope.client.ui.commons.wizards.exception.CaptchaNotMatchingException;
-import org.apache.syncope.common.lib.AnyOperations;
-import org.apache.syncope.common.lib.EntityTOUtils;
-import org.apache.syncope.common.lib.request.PasswordPatch;
-import org.apache.syncope.common.lib.request.UserCR;
-import org.apache.syncope.common.lib.request.UserUR;
-import org.apache.syncope.common.lib.to.ProvisioningResult;
-import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.wicket.PageReference;
-
-public class UserWizardBuilder extends AnyWizardBuilder implements UserForm {
-
-    private static final long serialVersionUID = 6716803168859873877L;
-
-    protected final UserSelfRestClient userSelfRestClient = new UserSelfRestClient();
-
-    /**
-     * Constructor to be used for templating only.
-     *
-     * @param anyTypeClasses any type classes.
-     * @param formLayoutInfo form layout.
-     * @param pageRef reference page.
-     */
-    public UserWizardBuilder(
-            final List<String> anyTypeClasses,
-            final UserFormLayoutInfo formLayoutInfo,
-            final PageReference pageRef) {
-
-        super(new UserWrapper(null), anyTypeClasses, formLayoutInfo, pageRef);
-    }
-
-    /**
-     * Constructor to be used for Approval and Remediation details only.
-     *
-     * @param previousUserTO previous user status.
-     * @param userTO new user status to be approved.
-     * @param anyTypeClasses any type classes.
-     * @param formLayoutInfo from layout.
-     * @param pageRef reference page.
-     */
-    public UserWizardBuilder(
-            final UserTO previousUserTO,
-            final UserTO userTO,
-            final List<String> anyTypeClasses,
-            final UserFormLayoutInfo formLayoutInfo,
-            final PageReference pageRef) {
-
-        super(new UserWrapper(previousUserTO, userTO), anyTypeClasses, formLayoutInfo, pageRef);
-    }
-
-    @Override
-    protected Serializable onApplyInternal(final AnyWrapper<UserTO> modelObject) {
-        // captcha check
-        if (captcha != null && captcha.evaluate() && !captcha.captchaCheck()) {
-            throw new CaptchaNotMatchingException();
-        }
-        UserTO inner = modelObject.getInnerObject();
-
-        ProvisioningResult<UserTO> result;
-        if (inner.getKey() == null) {
-            UserCR req = new UserCR();
-            EntityTOUtils.toAnyCR(inner, req);
-            req.setStorePassword(modelObject instanceof UserWrapper
-                    ? UserWrapper.class.cast(modelObject).isStorePasswordInSyncope()
-                    : StringUtils.isNotBlank(inner.getPassword()));
-
-            result = UserSelfRestClient.create(req);
-        } else {
-            fixPlainAndVirAttrs(inner, getOriginalItem().getInnerObject());
-            UserUR userUR = AnyOperations.diff(inner, getOriginalItem().getInnerObject(), false);
-
-            if (StringUtils.isNotBlank(inner.getPassword())) {
-                PasswordPatch passwordPatch = new PasswordPatch.Builder().
-                        value(inner.getPassword()).onSyncope(true).resources(inner.getResources()).build();
-                userUR.setPassword(passwordPatch);
-            }
-
-            // update just if it is changed
-            if (userUR.isEmpty()) {
-                result = new ProvisioningResult<>();
-                result.setEntity(inner);
-            } else {
-                result = userSelfRestClient.update(getOriginalItem().getInnerObject().getETagValue(), userUR);
-            }
-        }
-
-        return result;
-    }
-
-    /**
-     * Overrides default setItem() in order to clean statusModel as well.
-     *
-     * @param item item to be set.
-     * @return the current wizard.
-     */
-    @Override
-    public UserWizardBuilder setItem(final AnyWrapper<UserTO> item) {
-        super.setItem(Optional.ofNullable(item)
-            .map(userTOAnyWrapper -> new UserWrapper(userTOAnyWrapper.getInnerObject())).orElse(null));
-        return this;
-    }
-
-}
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/AdminLTE_plugins/dataTables.bootstrap4.min.css b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/AdminLTE_plugins/dataTables.bootstrap4.min.css
new file mode 100644
index 0000000..f1930be
--- /dev/null
+++ b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/AdminLTE_plugins/dataTables.bootstrap4.min.css
@@ -0,0 +1 @@
+table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important;border-spacing:0}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-ali [...]
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BaseExtPage.java b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility.css
similarity index 67%
rename from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BaseExtPage.java
rename to client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility.css
index ace1d2f..21b9201 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/pages/BaseExtPage.java
+++ b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility.css
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser.pages;
 
-import org.apache.wicket.request.mapper.parameter.PageParameters;
+.btn-accessibility {
+  position: absolute;
+  right: 0;
+  font-size: 12px;
+  padding: 15px 15px 0 0;
+}
 
-public abstract class BaseExtPage extends BaseEnduserWebPage {
+.btn-accessibility i {
+  font-size: 30px;
+}
 
-    private static final long serialVersionUID = 4627828052717627159L;
+.btn-accessibility:hover {
+  cursor:pointer;
+}
 
-    public BaseExtPage() {
-        super();
-    }
+.control-sidebar-menu a:focus {
+  outline: 1px #949494 solid;
+}
 
-    public BaseExtPage(final PageParameters parameters) {
-        super(parameters);
-    }
+#change_contrast {
+  top: 0;
+}
+#change_fontSize {
+  top: 40px;
 }
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility/accessibilityFont.css b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility/accessibilityFont.css
new file mode 100644
index 0000000..da26456
--- /dev/null
+++ b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility/accessibilityFont.css
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+body,
+p {
+  font-size: 200%;
+}
+
+.menu a,
+.treeview-menu a,
+.control-sidebar-subheading {
+  font-size: 80% !important;
+}
+
+.box-header > .fa, 
+.box-header > .glyphicon, 
+.box-header > .ion, .box-header .box-title,
+.dropdown-toggle .filter-option,
+.dropdown-menu a,
+.menu-info span,
+.user-header,
+.header,
+.modal-title,
+.small-box p,
+.alert-widget,
+.alert-widget > a > .label,
+.alert h4,
+#mappings .fa,
+.popover-content,
+.box .dropdown-toggle .glyphicon,
+.box-header button,
+.toggle .toggle-group .btn,
+.input-group .form-control label {
+  font-size: 120%;
+}
+
+input,
+select,
+.footer a,
+.modal-footer button,
+.dropdown-menu:not([role='menu']),
+.dropdown-menu > li.header,
+div#tablehandling ul.menu i,
+.content-header > .breadcrumb,
+.btn-primary:not(.btn-circle),
+.modal-content .box .dialog pre {
+  font-size: 100% !important;
+}
+
+#topology .window {
+  height: 90px;
+}
+
+.dataTables_length select {
+  font-size: 85% !important;
+}
+
+button.close {
+  font-size: 2em;
+}
+
+.details-footer .information {
+  font-size: 12px;
+}
+
+.btn-file i,
+.btn-file span,
+.input-group-btn button.btn-primary {
+  font-size: 20px !important;
+}
+
+.checkbox input[type=checkbox], 
+.checkbox-inline input[type=checkbox], 
+.radio input[type=radio], 
+.radio-inline input[type=radio],
+input[type=checkbox], 
+input[type=radio] {
+  width: 20px;
+  height: 20px;
+}
+
+.k-timepicker, 
+.k-datepicker {
+  width: 200px !important;
+}
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility/accessibilityHC.css b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility/accessibilityHC.css
new file mode 100644
index 0000000..aa4d5b9
--- /dev/null
+++ b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/accessibility/accessibilityHC.css
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* General
+============================================================================= */
+.content-wrapper,
+.content-wrapper .box,
+.modal-content,
+.modal-header,
+.background-footer,
+input:not(:disabled):not([type="file"]),
+select,
+select option,
+button:not(.close),
+.table .sorting,
+.dataTables_paginate a,
+.modal-footer,
+.box-header,
+.dropdown-menu,
+.main-footer,
+.circular-actions a,
+.k-select,
+.file-caption.kv-fileinput-caption {
+  color: #f7f7f7 !important;
+  background-color: #0f1417 !important;
+}
+
+
+.dataTable thead th:after,
+.close {
+  color: #f7f7f7 !important;
+  opacity: 0.8 !important;
+}
+
+
+.input-group-addon a,
+.input-group-addon i,
+.dropdown-menu > li:not(.disabled) > a:hover,
+.dropdown-menu > li:not(.disabled) > a:hover span,
+.dropdown-menu > li:not(.disabled) > a:focus,
+.dropdown-menu > li:not(.disabled) > a:focus span,
+#startAtContainer .input-group-addon span,
+#startAtContainer .k-widget span,
+#startAtContainer .k-widget i,
+#startAtContainer .k-widget input,
+.modal-footer i,
+.navbar-nav > .user-menu > .dropdown-menu > .user-footer .btn-default,
+.content-wrapper .nav-tabs a,
+.wrapper .content-wrapper .nav.nav-tabs li.active a span,
+.wrapper .content-wrapper .nav.nav-tabs li.active a,
+.wrapper .content-wrapper .nav.nav-tabs li a:hover span,
+.wrapper .content-wrapper .nav.nav-tabs li a:hover,
+.wrapper .content-wrapper .nav.nav-tabs li a:active span,
+.wrapper .content-wrapper .nav.nav-tabs li a:active,
+.wrapper .content-wrapper .nav.nav-tabs li a:focus span,
+.wrapper .content-wrapper .nav.nav-tabs li a:focus,
+.wrapper .content-wrapper .tab-content .btn-primary .fa-download {
+  color: #000000 !important;
+}
+
+
+.content-wrapper a:not(.btn-primary),
+.content-wrapper span:not(.label-info):not([role="presentation"]):not([class^='cm-']),
+.content-wrapper p,
+.content-wrapper .box-title a,
+.realm-choice .dropdown-menu > li:not(.disabled) > a:hover,
+.realm-choice .dropdown-menu > li:not(.disabled) > a:hover span,
+.realm-choice .dropdown-menu > li:not(.disabled) > a:focus,
+.realm-choice .dropdown-menu > li:not(.disabled) > a:focus span,
+.box-header,
+.breadcrumb .active,
+table tbody tr:hover button span,
+.dropdown-menu > li > a,
+.modal-header button,
+.circular-actions a i,
+.wizard-form .input-group-addon i,
+#startAtContainer .input-group-addon i,
+.attribute .input-group-addon i {
+  color: #f7f7f7 !important;
+}
+
+
+.box.box-primary {
+  border-top-color: #f7f7f7;
+}
+
+
+img,
+.content-wrapper div.btn.btn-file span.hidden-xs {
+  background-color: #f7f7f7;
+  color: #000000 !important;
+}
+.btn-primary,
+.callout.callout-info,
+.alert-info,
+.label-info,
+.modal-info .modal-body {
+  background-color: #f7f7f7 !important;
+  color: #000000 !important;
+}
+
+
+.logo img,
+.modal-content .input-group .input-group-addon {
+  background-color: transparent;
+}
+
+
+.modal-dialog {
+  border: 2px solid white;
+}
+
+
+#veil:not(:required):after {
+  -webkit-box-shadow: #f7f7f7 1.5em 0 0 0, 
+    #f7f7f7 1.1em 1.1em 0 0, 
+    #f7f7f7 0 1.5em 0 0,
+    #f7f7f7 -1.1em 1.1em 0 0, 
+    rgba(0, 0, 0, 0.5) -1.5em 0 0 0, 
+    rgba(0, 0, 0, 0.5) -1.1em -1.1em 0 0, 
+    #f7f7f7 0 -1.5em 0 0, 
+    #f7f7f7 1.1em -1.1em 0 0;
+  box-shadow: #f7f7f7 1.5em 0 0 0, 
+    #f7f7f7 1.1em 1.1em 0 0, 
+    #f7f7f7 0 1.5em 0 0, 
+    #f7f7f7 -1.1em 1.1em 0 0, 
+    #f7f7f7 -1.5em 0 0 0, 
+    #f7f7f7 -1.1em -1.1em 0 0, 
+    #f7f7f7 0 -1.5em 0 0, 
+    #f7f7f7 1.1em -1.1em 0 0; 
+}
+
+
+.input-group input:disabled,
+.input-group input[disabled],
+.control-sidebar-dark .control-sidebar-menu > li > a:hover,
+.table-hover > tbody > tr:hover,
+.skin-blue .main-header .navbar .sidebar-toggle:hover,
+.main-header .navbar .sidebar-toggle:hover,
+.skin-blue .main-header .logo:hover,
+.main-header .logo:hover,
+div.toggle-menu ul li:hover,
+.skin-blue .main-header .navbar .nav > li > a:hover, 
+.skin-blue .main-header .navbar .nav > li > a:active, 
+.skin-blue .main-header .navbar .nav > li > a:focus, 
+.skin-blue .main-header .navbar .nav .open > a:hover, 
+.skin-blue .main-header .navbar .nav .open > a:focus,
+.skin-blue .sidebar-menu > li:hover > a, 
+.skin-blue .sidebar-menu > li.active > a,
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus, 
+.dropdown-menu > li > a:hover,
+.k-block, 
+.k-draghandle, 
+.k-grid-header, 
+.k-grouping-header, 
+.k-header, 
+.k-pager-wrap, 
+.k-toolbar, 
+.k-treemap-tile,
+.k-picker-wrap,
+.k-state-disabled,
+.km-pane-wrapper .k-header {
+  background-color: rgba(108, 115, 117, 0.55);
+}
+
+
+.sidebar-mini .main-header .navbar {
+  background-color: #222d32; /* color from 'skin-blue' */
+  border-bottom: 1px white solid;
+  box-sizing: border-box;
+}
+.sidebar-mini .main-header .logo,
+div.toggle-menu,
+.skin-blue .main-header li.user-header {
+  background-color: #222d32;
+}
+
+
+div.toggle-menu {
+  border: 1px solid #f7f7f7;
+}
+
+
+a,
+.pagination > .active > a, 
+.pagination > .active > a:focus, 
+.pagination > .active > a:hover, 
+.pagination > .active > span, 
+.pagination > .active > span:focus, 
+.pagination > .active > span:hover {
+  border-color: #76abd9;
+}
+a {
+  color: #76abd9;
+}
+
+
+.logs button.btn-primary {
+  border-color: #f7f7f7;
+}
+.logs button.btn-primary:hover {
+  border-color: #adadad;
+}
+div.infolabel,
+.input-group input:disabled,
+.input-group input[disabled] {
+  color: #d2d2d2;
+}
+
+
+.bg-red,
+.callout.callout-danger,
+.alert-danger,
+.alert-error,
+.label-danger,
+.modal-danger .modal-body,
+.btn-danger.active, 
+.btn-danger:active, 
+.open>.dropdown-toggle.btn-danger {
+  background-color: #942819 !important;
+}
+.bg-yellow {
+  background-color: #6F4706 !important;
+}
+.bg-green,
+.copy-clipboard-feedback,
+.btn-success,
+.callout.callout-success, 
+.alert-success, 
+.label-success, 
+.modal-success .modal-body {
+  background-color: #005C32 !important;
+}
+.bg-aqua {
+  background-color: #004E61 !important;
+}
+.bg-yellow, 
+.callout.callout-warning, 
+.alert-warning, 
+.label-warning, 
+.modal-warning .modal-body {
+  background-color: #6F4706 !important;
+}
+.callout.callout-warning {
+  border-color: #6F4706;
+}
+
+
+.bootstrap-select .btn.btn-default {
+  background-color: rgba(101, 101, 101, 0.7) !important;
+}
+
+
+/* Login page
+============================================================================= */
+.login-body {
+  background-image: linear-gradient(rgb(31, 109, 142), #004626);
+}
+
+.login-logo {
+  background: transparent;
+}
+
+.btn-accessibility {
+  color: #f7f7f7 !important;
+}
+
+.form-signin .btn-primary, 
+.form-signin .btn-primary {
+  border: 2px solid white;
+}
+.form-signin .btn-primary.focus, 
+.form-signin .btn-primary:focus {
+  border-color: #8c8c8c;
+}
+
+.login-card {
+  background-color: #0f1417;
+}
\ No newline at end of file
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts.css b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts.css
new file mode 100644
index 0000000..da7bdf8
--- /dev/null
+++ b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts.css
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* source-sans-pro-300 - latin-ext_cyrillic_latin_cyrillic-ext */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: normal;
+  font-weight: 300;
+  src: local('Source Sans Pro Light'), local('SourceSansPro-Light'),
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* source-sans-pro-300italic - latin-ext_cyrillic_latin_cyrillic-ext */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: italic;
+  font-weight: 300;
+  src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'),
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* source-sans-pro-regular - latin-ext_cyrillic_latin_cyrillic-ext */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'),
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* source-sans-pro-italic - latin-ext_cyrillic_latin_cyrillic-ext */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: italic;
+  font-weight: 400;
+  src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'),
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* source-sans-pro-600 - latin-ext_cyrillic_latin_cyrillic-ext */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: normal;
+  font-weight: 600;
+  src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'),
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* source-sans-pro-600italic - latin-ext_cyrillic_latin_cyrillic-ext */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: italic;
+  font-weight: 600;
+  src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'),
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* source-sans-pro-700 - latin-ext_cyrillic_latin_cyrillic-ext */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'),
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('./fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300.woff b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300.woff
new file mode 100644
index 0000000..2132b5e
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300.woff differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300.woff2 b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300.woff2
new file mode 100644
index 0000000..943f826
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300.woff2 differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300italic.woff b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300italic.woff
new file mode 100644
index 0000000..aa25cd3
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300italic.woff differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300italic.woff2 b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300italic.woff2
new file mode 100644
index 0000000..441997f
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-300italic.woff2 differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600.woff b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600.woff
new file mode 100644
index 0000000..24d2824
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600.woff differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600.woff2 b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600.woff2
new file mode 100644
index 0000000..9ec7d25
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600.woff2 differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600italic.woff b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600italic.woff
new file mode 100644
index 0000000..ce5a1cc
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600italic.woff differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600italic.woff2 b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600italic.woff2
new file mode 100644
index 0000000..7ed2f82
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-600italic.woff2 differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-700.woff b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-700.woff
new file mode 100644
index 0000000..9fbfe68
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-700.woff differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-700.woff2 b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-700.woff2
new file mode 100644
index 0000000..096dcb1
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-700.woff2 differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-italic.woff b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-italic.woff
new file mode 100644
index 0000000..c1cf1ea
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-italic.woff differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-italic.woff2 b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-italic.woff2
new file mode 100644
index 0000000..ff006be
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-italic.woff2 differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-regular.woff b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-regular.woff
new file mode 100644
index 0000000..e8a1ac7
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-regular.woff differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-regular.woff2 b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-regular.woff2
new file mode 100644
index 0000000..1b0bc46
Binary files /dev/null and b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/fonts/source-sans-pro-v11-latin-ext_cyrillic_latin_cyrillic-ext-regular.woff2 differ
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/login.css b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/login.css
new file mode 100644
index 0000000..931cb47
--- /dev/null
+++ b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/login.css
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+body, html {
+  height: 100% !important;
+  background-repeat: no-repeat;
+  background-image: linear-gradient(rgb(70, 30, 30), #dd4b39);
+}
+
+.card-container.card {
+  width: 350px;
+  padding: 40px 40px;
+}
+
+.btn {
+  font-weight: 700;
+  height: 36px;
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  user-select: none;
+  cursor: default;
+}
+
+/*
+ * Card component
+ */
+.card {
+  background-color: #F7F7F7;
+  /* just in case there no content*/
+  padding: 20px 25px 30px;
+  margin: 0 auto 25px;
+  margin-top: 50px;
+  /* shadows and rounded borders */
+  -moz-border-radius: 2px;
+  -webkit-border-radius: 2px;
+  border-radius: 2px;
+  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
+  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
+  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
+}
+
+.login-logo {
+  width: 200px;
+  margin: 0 auto 10px;
+  display: block;
+}
+
+/*
+ * Form styles
+ */
+.profile-name-card {
+  font-size: 16px;
+  font-weight: bold;
+  text-align: center;
+  margin: 10px 0 0;
+  min-height: 1em;
+}
+
+.form-signin #inputPassword {
+  direction: ltr;
+  height: 44px;
+  font-size: 16px;
+}
+
+.form-signin input[type=password],
+.form-signin input[type=text],
+.form-signin button {
+  width: 100%;
+  display: block;
+  margin-bottom: 10px;
+  z-index: 1;
+  position: relative;
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+.form-signin .form-control:focus {
+  border-color: rgb(104, 145, 162);
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162);
+  box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162);
+}
+
+.btn.btn-signin {
+  background-color: rgb(104, 145, 162);
+  padding: 0px;
+  font-weight: 700;
+  font-size: 14px;
+  height: 36px;
+  -moz-border-radius: 3px;
+  -webkit-border-radius: 3px;
+  border-radius: 3px;
+  border: none;
+  -o-transition: all 0.218s;
+  -moz-transition: all 0.218s;
+  -webkit-transition: all 0.218s;
+  transition: all 0.218s;
+}
+
+.btn.btn-signin:hover,
+.btn.btn-signin:active,
+.btn.btn-signin:focus {
+  background-color: #00a65a;
+}
+
+.btn.btn-sso {
+  padding: 0px;
+  font-weight: 700;
+  font-size: 14px;
+  height: 36px;
+  -moz-border-radius: 3px;
+  -webkit-border-radius: 3px;
+  border-radius: 3px;
+  border: none;
+  -o-transition: all 0.218s;
+  -moz-transition: all 0.218s;
+  -webkit-transition: all 0.218s;
+  transition: all 0.218s;
+}
+
+.btn.btn-sso:hover,
+.btn.btn-sso:active,
+.btn.btn-sso:focus {
+  background-color: #00a65a;
+}
diff --git a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/search.css
similarity index 50%
copy from client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
copy to client/idrepo/enduser/src/main/resources/META-INF/resources/css/search.css
index 0a683a9..ece4f4a 100644
--- a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserConstants.java
+++ b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/search.css
@@ -16,20 +16,65 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.enduser;
 
-public final class SyncopeEnduserConstants {
+.searchBox .col-xs-12{
+  padding-left: 0px !important;
+  padding-right: 0px !important;
+}
+
+.clause{
+  display: block;
+  line-height: 34px;
+  width: 100%;
+}
+
+.clause .operator{
+  width: 65px !important;
+}
+
+.clause .operator .checkbox{
+  margin: 0px !important;
+}
+
+.clause .field {
+  line-height: 34px;
+  float: left;
+  padding: 0 3px 0px 0px;
+  display: inline-block !important;
+}
+.clause .type{
+  width: 120px !important;
+}
 
-    public static final String CAPTCHA_SESSION_KEY = "captcha";
+.clause .property{
+  width: 190px;
+}
 
-    public static final String XSRF_COOKIE = "XSRF-TOKEN";
+.clause .comparator{
+  width: 100px;
+}
 
-    public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
+.clause .comparator button{
+  width: 100px !important;
+}
 
-    public static final String MEMBERSHIP_ATTR_SEPARATOR = "#";
+.clause .value{
+  width: 220px;
+}
 
-    private SyncopeEnduserConstants() {
-        // private constructor for utility class
-    }
+.clause .textvalue{
+  width: 45px;
+}
+
+.clause .action{
+  float: left;
+  padding: 0px 7px 0px;
+}
 
+.searchBox .input-group-addon:last-child{
+  border: 1px solid #ccc !important;
 }
+
+.searchBox .input-group{
+  margin-top: 1px;
+}
\ No newline at end of file
diff --git a/client/idrepo/enduser/src/main/resources/META-INF/resources/css/syncopeEnduser.css b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/syncopeEnduser.css
new file mode 100644
index 0000000..0b487d3
--- /dev/null
+++ b/client/idrepo/enduser/src/main/resources/META-INF/resources/css/syncopeEnduser.css
@@ -0,0 +1,1377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+pre {
+  white-space: -moz-pre-wrap; /* Mozilla, supported since 1999 */
+  white-space: -pre-wrap; /* Opera */
+  white-space: -o-pre-wrap; /* Opera */
+  white-space: pre-wrap; /* CSS3 - Text module (Candidate Recommendation) http://www.w3.org/TR/css3-text/#white-space */
+  word-wrap: break-word; /* IE 5.5+ */
+}
+
+/* Absolute Center Spinner */
+#veil {
+  display:none;
+  position: fixed;
+  z-index:99999;
+  height: 2em;
+  width: 2em;
+  overflow: show;
+  margin: auto;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+}
+
+/* Transparent Overlay */
+#veil:before {
+  content: '';
+  display: block;
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0,0,0,0.3);
+}
+
+/* :not(:required) hides these rules from IE9 and below */
+#veil:not(:required) {
+  /* hide "loading..." text */
+  font: 0/0 a;
+  color: transparent;
+  text-shadow: none;
+  background-color: transparent;
+  border: 0;
+}
+
+#veil:not(:required):after {
+  content: '';
+  display: block;
+  font-size: 10px;
+  width: 1em;
+  height: 1em;
+  margin-top: -0.5em;
+  -webkit-animation: spinner 2000ms infinite linear;
+  -moz-animation: spinner 2000ms infinite linear;
+  -ms-animation: spinner 2000ms infinite linear;
+  -o-animation: spinner 2000ms infinite linear;
+  animation: spinner 2000ms infinite linear;
+  border-radius: 0.5em;
+  -webkit-box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.5) -1.5em 0 0 0, rgba(0, 0, 0, 0.5) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
+  box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) -1.5em 0 0 0, rgba(0, 0, 0, 0.75) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0;
+}
+
+/* Animation */
+
+@-webkit-keyframes spinner {
+  0% {
+    -webkit-transform: rotate(0deg);
+    -moz-transform: rotate(0deg);
+    -ms-transform: rotate(0deg);
+    -o-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+    -moz-transform: rotate(360deg);
+    -ms-transform: rotate(360deg);
+    -o-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-moz-keyframes spinner {
+  0% {
+    -webkit-transform: rotate(0deg);
+    -moz-transform: rotate(0deg);
+    -ms-transform: rotate(0deg);
+    -o-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+    -moz-transform: rotate(360deg);
+    -ms-transform: rotate(360deg);
+    -o-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@-o-keyframes spinner {
+  0% {
+    -webkit-transform: rotate(0deg);
+    -moz-transform: rotate(0deg);
+    -ms-transform: rotate(0deg);
+    -o-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+    -moz-transform: rotate(360deg);
+    -ms-transform: rotate(360deg);
+    -o-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+@keyframes spinner {
+  0% {
+    -webkit-transform: rotate(0deg);
+    -moz-transform: rotate(0deg);
+    -ms-transform: rotate(0deg);
+    -o-transform: rotate(0deg);
+    transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+    -moz-transform: rotate(360deg);
+    -ms-transform: rotate(360deg);
+    -o-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+
+.block-sidebar {
+  max-height: 100% !important; 
+  overflow: auto !important; 
+  padding-top: 90px !important; 
+  padding-bottom: 50px !important; 
+  position: fixed !important;
+  width: 245px !important;
+}
+
+.inner-control-sidebar {
+  position: fixed; 
+  height: auto;
+}
+
+.content-margin-layout {
+  margin: 0px 230px 0px 0px !important;
+  padding: 20px !important;
+}
+
+.admin-content-page {
+  padding: 20px;
+  background: #ecf0f5
+}
+
+.realms {
+  min-height: 554px
+}
+
+.realm-header {
+  clear: both;
+  display:block;
+  display: inline-table;
+  margin: 0 0 10px;
+  line-height: 25px;
+}
+
+.realm-label {
+  float: left;
+  font-size: 16px;
+}
+
+.realm-label label {
+  font-weight: 600 !important;
+}
+
+.realm-choice {
+  right: 0px;
+  position: absolute;
+}
+
+.realm-header .dropdown-menu li a {
+  text-align: left !important;
+  white-space: pre !important;
+  line-height: 7px;
+}
+
+.block-header {
+  position: fixed !important;
+  width: 100% !important;
+  top: 0 !important;
+}
+
+.block-footer {
+  position: fixed !important;
+  width: 100% !important;
+  bottom: 0px !important;
+}
+
+.logo-pos {
+  padding-top: 4px !important;
+  overflow: visible !important;
+}
+
+.angle {
+  border: medium none !important;
+  cursor: pointer;
+  display: inline-table !important;
+  float: right;
+  height: 30px;
+  overflow: hidden;
+  position: relative !important;
+  right: 0;
+  top: -30px;
+  width: 25%;
+  z-index: 3;
+}
+
+.main-header .logo {
+  height: 55px !important;
+}
+
+.w_caption h3 {
+  font-size: 16px;
+}
+
+div.wicket-modal div.w_content_3 {
+  border: 1px solid #eee;
+  border-radius: 20px;
+  padding: 5px;
+}
+
+.tab-content {
+  margin-bottom: 10px;
+  margin-top: 5px;
+  position: relative;
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding: 20px 20px 5px 20px;
+}
+
+.modal-body .tab-content .information {
+  position: relative !important;
+}
+
+.scrollable-tab-content {
+  overflow-y: auto;
+  max-height: 480px;
+}
+
+.inner-scrollable-tab-content {
+  height: 430px;
+  margin-top: 20px;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+a.help {
+  position: relative;
+  display: inline;
+  text-decoration: none;
+}
+
+a.help span {
+  position: absolute;
+  width:19em;
+  color: #000000;
+  background: whitesmoke;
+  visibility: hidden;
+  border-radius: 0px;
+  padding: 3px;
+}
+
+a.help span:after {
+  position: absolute;
+  top: 50%;
+  left: 100%;
+  margin-top: -8px;
+  width: 0;
+  height: 0;
+}
+
+a.help span {
+  visibility: visible;
+  opacity: none;
+  right: 100%;
+  top: 50%;
+  margin-right: 4px;
+  margin-top: -11px;
+  border: 1px solid black;
+  z-index: 1000000;
+}
+
+a.help span a {
+  color: #463;
+  text-decoration: none;
+}
+
+.modal-open .modal {
+  overflow: hidden;
+}
+
+.modal-body {
+  max-height: 550px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+
+.modal {
+  background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
+  z-index: 7000 !important;
+}
+
+.wizard-step-title {
+  font-size: 18px !important;
+  font-weight: normal !important;
+  margin-bottom: 10px !important;
+}
+
+div.wizard-view div.wizard-view{
+  max-height: 380px;
+  height: 380px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 50px 20px 50px 20px;
+}
+
+.wizard-view .wizard-form{
+  max-height: 450px;
+  height: 450px;
+}
+
+.modal-body .tab-content .wizard-view {
+  max-height: 330px !important;
+  height: 330px !important;
+}
+
+.modal-body .tab-content .wizard-form {
+  max-height: 400px !important;
+  height: 400px !important;
+}
+
+.wizard-view > div {
+  display: block;
+  height: 95%;
+  position: relative;
+}
+
+.wizard-view {
+  padding: 0px 5px;
+}
+
+.wizard-form {
+  height:480px;
+  position: relative;
+}
+
+.wizard-form > div {
+  max-height: 440px;
+  height: 440px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 0px;
+}
+
+.box-body .wizard-form {
+  height:400px !important;
+}
+
+.box-body .wizard-form > div {
+  height: 360px !important;
+}
+
+div.modal-body div.box-body div.wizard-buttons {
+  bottom: 30px;
+}
+
+.wizard-buttons {
+  padding: 10px 0px 5px 0px;
+  position: absolute;
+  bottom: 4px;
+  width: 100%;
+}
+
+.wizard-buttons div.float-left {
+  position: absolute;
+  left: 15px;
+}
+
+.wizard-buttons div.float-right {
+  position: absolute;
+  right: 15px;
+}
+
+.wizard-step-title {
+  font-weight: bold; 
+  font-size:medium;
+}
+
+div.realms div.summarize {
+  margin: 50px 100px;
+}
+
+.navbar a {
+  height: 55px
+}
+
+.navbar .footer a {
+  height: 34px
+}
+
+.navbar .user-footer a {
+  height: 34px
+}
+
+span.overridable div.checkbox {
+  float: right; 
+  margin: 0px; 
+  padding: 0px;
+}
+
+span.overridable div.checkbox label div div.toggle-group label {
+  padding-left: 7px;
+}
+
+div#outer.modal-lg, div#utilityModal.modal-lg, section#notifications .modal-lg {
+  max-width: 1200px;
+  width: 97%;
+}
+
+.details {
+  max-height: 440px;
+  overflow-x: hidden;
+  overflow-y: auto;
+  display: block
+}
+
+th.checkGroupColumn {
+  width: 20px;
+  text-align: center;
+  padding-right: 8px !important;
+}
+
+td.checkGroupColumn {
+  text-align: center;
+}
+
+/**
+  BEGIN - Style for Information panel
+*/
+div.information{
+  margin: 30px 0px 0px 0px; 
+  border: 1px solid #EEE; 
+  font-size: 10px;
+  color: #888;
+  display: inline-table; 
+  width: 100%; 
+  clear: both; 
+  float:none;
+  position: absolute;
+  bottom: -90px;
+  left: 0px;
+  padding: 2px;
+}
+
+div.infolabel{
+  margin-left: 5px; 
+  float:left; 
+  width: 150px;
+  font-weight: bold;
+  color: #888;
+}
+
+div.infoleft{
+  float:left; 
+  display: inline-table; 
+  width: 50%
+}
+
+div.inforight{ 
+  display: inline-table; 
+  width: 50%
+}
+
+div.inforow{
+  display: inline-table;
+  width: 100%
+}
+
+div.wrap{
+  word-wrap: break-word; 
+  width: 550px; 
+  margin-left: 155px;
+}
+/**
+END - Style for Information panel
+*/
+
+#ownership div.toggle {
+  width: 110px !important;
+}
+
+.table > tbody > tr > td.list_view_panel_labels {
+  vertical-align: middle;
+}
+
+div.searchResult{
+  padding-top: 30px;
+  display: block;
+  clear: both;
+}
+
+.logs .input-group-addon .input-group-btn {
+  width: 130px !important;
+}
+
+.logs .col_width {
+  width: 90% !important;
+}
+
+.logs .box-header {
+  display: none !important;
+}
+
+.logs .box{
+  border-top: 0px !important;
+}
+
+*::after, *::before {
+  box-sizing: border-box;
+}
+
+.wicket-aa-container {
+  border-color: #eee;
+  box-shadow: none;
+  background-clip: padding-box;
+  background-color: #fff;
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  border-radius: 4px;
+  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.176);
+  float: left;
+  font-size: 14px;
+  min-width: 160px;
+  max-height: 250px;
+  z-index: 7001 !important;
+}
+
+div.wicket-aa ul {
+  list-style: none;
+  padding-left: 15px;
+}
+/**
+START - startAt
+*/
+div#startAt {
+  background-color: rgba(98, 98, 98, 0.98) !important;
+  color: #CCC;
+  right: 5px !important;
+  top: 100px !important;
+  min-width: 450px;
+  min-height: 130px !important;
+  z-index: 6000 !important;
+}
+
+div#itemTransformersTogglePanel {
+  min-width: 1000px;
+}
+
+div#startAtContainer {
+  padding: 15px;
+}
+
+div#startAtContainer input {
+  background-color: rgba(200, 200, 200, 0.60) !important;
+}
+/**
+END - startAt
+*/
+
+/**
+START - Notifications
+*/
+/*Temporany fix diagonal stacking*/
+.k-popup.k-notification {
+  box-shadow: none;
+}
+
+.k-notification-error.k-group {
+  background: rgba(100% , 0% , 0% , .7);
+  color: red;
+}
+
+.errorNotification {
+  width: 300px;
+  height: 100px;
+  vertical-align: middle;
+  display: table-cell;
+}
+
+.errorNotification #level {
+  float: left; 
+  padding-left: 10px; 
+  font-size: 2em; 
+  width: 2%;
+}
+
+.errorNotification #message {
+  float: right; 
+  padding-left: 0px; 
+  width:85%
+}
+
+.k-notification-success.k-group {
+  background: rgba(0% , 60% , 0% , .7);
+  color: #fff;
+}
+
+.successNotification {
+  width: 300px;
+  height: 100px;
+  vertical-align: middle;
+  display: table-cell;
+}
+
+.successNotification #level {
+  float: left; 
+  padding-left: 10px; 
+  font-size: 2em; 
+  width: 2%;
+}
+
+.successNotification #message {
+  float: right; 
+  padding-left: 0px; 
+  width:85%
+}
+/**
+EN - Notifications
+*/
+
+/**
+START - Actions
+*/
+.actions > li > a {
+  padding: 5px 0px 5px 0px !important;
+}
+
+div.listview-actions a {
+  float:left;
+  padding: 5px 0px 5px 0px !important;
+}
+
+.action a.btn {
+  padding: 0px;
+}
+
+.btn-circle, .circular-actions a {
+  border-radius: 15px !important;
+  font-size: 12px;
+  height: 30px;
+  line-height: 1.42857;
+  padding: 6px 0;
+  text-align: center;
+  width: 30px;
+}
+
+.circular-actions a.btn {
+  background-color: #3c8dbc;
+  border-color: #367fa9;
+  color: #fff;
+}
+
+.btn-circle i, .circular-actions a i {
+  margin: 0px;
+}
+
+.multipanel-btn-minus {
+  padding: 0px 0px 0px 6px;
+  border: 0 none !important;
+}
+
+.multipanel-btn-plus {
+  padding: 15px 0px 8px 6px;
+  border: 0 none !important;
+}
+
+.multipanel-box {
+  padding: 5px;
+  display: inline-table;
+  margin: 0px;
+}
+/**
+END - Actions
+*/
+
+/**
+START - DataTable
+*/
+.dataTable {
+  clear: both;
+}
+/**
+END - DataTable
+*/
+
+/**
+START - Result page
+*/
+.attribute {
+  padding: 0px 4px 0px 4px; 
+}
+
+span.highlight .attribute label {
+  color : red;
+}
+/**
+END - Result page
+*/
+
+.navbar-nav > .user-menu > .dropdown-menu > li.user-header {
+  height: auto !important;
+  padding: 10px;
+  text-align: center;
+}
+
+.nav-tabs-custom > .nav-tabs > li.active {
+  border-top-color: #d2d6de !important;
+}
+
+.code-deletion {
+  background-color: #ffdddd;
+  border-color: #f1c0c0;
+}
+
+.code-addition {
+  background-color: #dbffdb;
+  border-color: #c1e9c1;
+}
+
+/**
+START - AjaxDateTimePicker
+*/
+.input-auto-width {
+  width: auto !important;
+}
+
+.icon-top-position {
+  top: 5px !important;
+}
+
+/**
+END - AjaxDateTimePicker
+*/
+
+/**
+START - Search - AjaxDateTimePicker
+*/
+.searchBox .col-xs-12{
+  padding-left: 0px !important;
+  padding-right: 0px !important;
+}
+
+.clause{
+  display: block;
+  line-height: 34px;
+  width: 100%;
+}
+
+.clause .operator{
+  width: 65px !important;
+}
+
+.clause .operator .checkbox{
+  margin: 0px !important;
+}
+
+.clause .field {
+  line-height: 34px;
+  float: left;
+  padding: 0 3px 0px 0px;
+  display: inline-block !important;
+}
+
+.clause .type{
+  width: 170px !important;
+}
+
+.clause .type button{
+  width: 170px !important;
+}
+
+.clause .property{
+  width: 300px;
+}
+
+.clause .property button{
+  width: 300px;
+}
+
+.clause .comparator{
+  width: 100px;
+}
+
+.clause .comparator button{
+  width: 100px !important;
+}
+
+.clause .value{
+  width: 220px;
+}
+
+.clause .date{
+  width: 160px;
+}
+
+.clause .hours{
+  width: 45px;
+}
+
+.clause .separator{
+  width: 20px;
+  padding-left: 12px;
+}
+
+.clause .action{
+  float: left;
+  padding: 0px 7px 0px;
+}
+
+.searchBox .input-group-addon:last-child{
+  border: 1px solid #ccc !important;
+}
+
+.searchBox .input-group{
+  margin-top: 1px;
+}
+
+.custom-autocomplete-box li.selected {
+  background-color: #eee;
+}
+
+/**
+END - Search - AjaxDateTimePicker
+*/
+/**
+START - Parameters Details
+*/
+div#parametersForm{
+  min-height: 220px;
+}
+/**
... 7763 lines suppressed ...