From 83931d3722f86168bebf39aca5ca2055e6f176aa Mon Sep 17 00:00:00 2001
From: Dominik Frantisek Bucik <bucik@ics.muni.cz>
Date: Thu, 6 Jun 2024 18:56:26 +0200
Subject: [PATCH] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20Great=20refactor=20o?=
 =?UTF-8?q?f=20redirects?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

USe session attributes for passing data across redirects to have safe
passing mechanism
---
 .../localization/messages_cs.properties       |  40 ++-
 .../localization/messages_en.properties       |  40 ++-
 .../src/main/webapp/WEB-INF/views/aup.jsp     |   2 +-
 ...sTestSpWarning.jsp => test_rp_warning.jsp} |   8 +-
 .../{unapproved.jsp => unauthorized.jsp}      |   2 +-
 ....jsp => unauthorized_ensure_vo_member.jsp} |  23 +-
 ...ized_ensure_vo_member_notify_redirect.jsp} |  20 +-
 .../views/unauthorized_is_eligible.jsp        |  49 +++
 .../views/unauthorized_not_in_env_units.jsp   |  51 +++
 ...unauthorized_register_choose_vo_group.jsp} |  26 +-
 ...orized_register_notify_action_required.jsp |  51 +++
 ...pproved_spec.jsp => unauthorized_spec.jsp} |   2 +-
 .../src/main/webapp/WEB-INF/web-context.xml   |  25 +-
 ... unauthorized_register_choose_vo_group.js} |  12 +-
 .../java/cz/muni/ics/oidc/PerunConstants.java |   2 +
 .../java/cz/muni/ics/oidc/RedirectUtils.java  |  45 +++
 .../server/adapters/PerunAdapterMethods.java  |  24 +-
 .../adapters/PerunAdapterMethodsRpc.java      |   7 +
 .../adapters/impl/PerunAdapterImpl.java       |  24 +-
 .../adapters/impl/PerunAdapterLdap.java       |  89 +++--
 .../server/adapters/impl/PerunAdapterRpc.java | 123 +++++--
 .../EntitlementExtendedClaimSource.java       |   5 +-
 .../claims/sources/EntitlementSource.java     |   5 +-
 .../claims/sources/GroupNamesSource.java      |   7 +-
 .../server/connectors/PerunConnectorRpc.java  |   2 +
 .../ics/oidc/server/filters/FiltersUtils.java | 107 +-----
 .../server/filters/impl/IsEligibleFilter.java |  63 ++--
 .../impl/PerunAuthorizationFilter.java        | 121 ++++++-
 .../filters/impl/PerunEnsureVoMember.java     |  89 +++--
 .../filters/impl/PerunIsTestSpFilter.java     |  27 +-
 .../filters/impl/PerunLogIdentityFilter.java  |   9 +-
 .../filters/impl/ProxyStatisticsFilter.java   |   2 +-
 .../server/filters/impl/ValidUserFilter.java  |  81 +++--
 .../oidc/web/controllers/AupController.java   |  25 +-
 .../oidc/web/controllers/ControllerUtils.java |  87 +++--
 .../web/controllers/IsTestSpController.java   |  65 ----
 .../oidc/web/controllers/LoginController.java |   3 -
 .../PerunUnapprovedController.java            | 255 --------------
 ...PerunUnapprovedRegistrationController.java | 176 ----------
 .../PerunUnauthorizedController.java          | 324 ++++++++++++++++++
 .../controllers/RegistrationController.java   |  42 ---
 .../controllers/TestSpWarningController.java  |  63 ++++
 42 files changed, 1199 insertions(+), 1024 deletions(-)
 rename perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/{isTestSpWarning.jsp => test_rp_warning.jsp} (76%)
 rename perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/{unapproved.jsp => unauthorized.jsp} (96%)
 rename perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/{unapproved_is_eligible.jsp => unauthorized_ensure_vo_member.jsp} (56%)
 rename perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/{registrationFormContinue.jsp => unauthorized_ensure_vo_member_notify_redirect.jsp} (66%)
 create mode 100644 perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_is_eligible.jsp
 create mode 100644 perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_not_in_env_units.jsp
 rename perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/{registrationForm.jsp => unauthorized_register_choose_vo_group.jsp} (70%)
 create mode 100644 perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_register_notify_action_required.jsp
 rename perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/{unapproved_spec.jsp => unauthorized_spec.jsp} (89%)
 rename perun-oidc-server-webapp/src/main/webapp/resources/js/{reg_form_select.js => unauthorized_register_choose_vo_group.js} (61%)
 create mode 100644 perun-oidc-server/src/main/java/cz/muni/ics/oidc/RedirectUtils.java
 delete mode 100644 perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/IsTestSpController.java
 delete mode 100644 perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedController.java
 delete mode 100644 perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedRegistrationController.java
 create mode 100644 perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnauthorizedController.java
 delete mode 100644 perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/RegistrationController.java
 create mode 100644 perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/TestSpWarningController.java

diff --git a/perun-oidc-server-webapp/src/main/resources/localization/messages_cs.properties b/perun-oidc-server-webapp/src/main/resources/localization/messages_cs.properties
index d5c8744fa..0f0f2fad5 100644
--- a/perun-oidc-server-webapp/src/main/resources/localization/messages_cs.properties
+++ b/perun-oidc-server-webapp/src/main/resources/localization/messages_cs.properties
@@ -69,22 +69,25 @@ zoneinfo=Z\u00F3na
 phone_number=Telefon
 
 #UNAPPROVED
-contact_p=V p\u0159\u00EDpad\u011B nejasnost\u00ED n\u00E1s kontaktujte na
+403_aai_contact_text=V p\u0159\u00EDpad\u011B nejasnost\u00ED n\u00E1s kontaktujte na
 403_header=P\u0159\u00EDstup odm\u00EDtnut
 403_text=Nem\u00E1te dostate\u010Dn\u00E1 pr\u00E1va pro p\u0159\u00EDstup ke slu\u017Eb\u011B:
 403_informationPage=Pro v\u00EDce informac\u00ED o slu\u017Eb\u011B nav\u0161tivte
 403_contactSupport=Pokud si mysl\u00EDte \u017Ee m\u00E1te m\u00EDt p\u0159\u00EDstup, kontaktujte administr\u00E1tora:
 403_subject=Probl\u00E9m s p\u0159ihl\u00E1\u0161en\u00EDm do slu\u017Eby
+
 403_ensure_vo_hdr=P\u0159\u00EDstup zam\u00EDtnut
 403_ensure_vo_msg=Nem\u00E1te dostate\u010Dn\u00E1 pr\u00E1va pro p\u0159\u00EDstup ke slu\u017Eb\u011B
+403_ensure_vo_client_contact=Pokud si mysl\u00EDte, \u017Ee v\u00E1m byl p\u0159\u00EDstup odep\u0159en nepr\u00E1vem, nebo chcete p\u0159\u00EDstup z\u00EDskat, kontaktujte spr\u00E1vce slu\u017Eby pomoc\u00ED kontakt\u016F n\u00ED\u017Ee.
+
 403_authorization_hdr=P\u0159\u00EDstup zam\u00EDtnut
 403_authorization_msg=Tato str\u00E1nka se V\u00E1m zobrazuje, proto\u017Ee nem\u00E1te p\u0159\u00EDstup ke slu\u017Eb\u011B. To m\u016F\u017Ee b\u00FDt d\u016Fsledkem p\u0159\u00EDstupov\u00FDch omezen\u00ED nastaven\u00FDch administr\u00E1torem.
-403_not_in_test_vos_groups_hdr=P\u0159\u00EDstup zam\u00EDtnut
-403_not_in_test_vos_groups_msg=Tato str\u00E1nka se V\u00E1m zobrazuje, proto\u017Ee nem\u00E1te p\u0159\u00EDstup k testovac\u00EDm slu\u017Eb\u00E1m AAI.
-403_not_in_prod_vos_groups_hdr=P\u0159\u00EDstup zam\u00EDtnut
-403_not_in_prod_vos_groups_msg=Tato str\u00E1nka se V\u00E1m zobrazuje, proto\u017Ee nem\u00E1te p\u0159\u00EDstup ke slu\u017Eb\u00E1m AAI.
-403_not_in_mandatory_vos_groups_hdr=P\u0159\u00EDstup zam\u00EDtnut
-403_not_in_mandatory_vos_groups_msg=Tato str\u00E1nka se V\u00E1m zobrazuje, proto\u017Ee Va\u0161e po\u017Eadovan\u00E9 \u010Dlenstv\u00ED v organizaci je nevalidn\u00ED.
+
+403_not_in_env_vos_groups_hdr=P\u0159\u00EDstup zam\u00EDtnut
+403_not_in_env_vos_groups_msg=Tato str\u00E1nka se V\u00E1m zobrazuje, proto\u017Ee nespl\u0148ujete podm\u00EDnky \u010Dlenstv\u00ED v organiza\u010Dn\u00EDch jednotk\u00E1ch AAI.
+403_not_in_env_vos_groups_urls=Pomoc\u00ED n\u00E1sleduj\u00FA\u00EDc\u00EDch odkaz\u016F se m\u016F\u017Eete do organiza\u010Dn\u00EDch jednotek registrovat. Pokud nevid\u00EDte \u017E\u00E1dn\u00E9 odkazy, kontaktujte spr\u00E1ve slu\u017Eby pro p\u0159idelen\u00ED p\u0159\u00EDstupu.
+403_not_in_env_vos_groups_client_contact=Pokud si mysl\u00EDte, \u017Ee v\u00E1m byl p\u0159\u00EDstup odep\u0159en nepr\u00E1vem, nebo chcete p\u0159\u00EDstup z\u00EDskat, kontaktujte spr\u00E1vce slu\u017Eby pomoc\u00ED kontakt\u016F n\u00ED\u017Ee.
+
 403_not_logged_in_hdr=P\u0159\u00EDstup zam\u00EDtnut
 403_not_logged_in_msg=Zd\u00E1 se, \u017Ee p\u0159ihl\u00E1\u0161en\u00ED selhalo. Zkuste, pros\u00EDm, zav\u0159\u00EDt V\u00E1\u0161 prohl\u00ED\u017Ee\u010D a p\u0159ihl\u00E1sit se znovu.
 
@@ -92,21 +95,22 @@ contact_p=V p\u0159\u00EDpad\u011B nejasnost\u00ED n\u00E1s kontaktujte na
 403_is_eligible_default_text=P\u0159\u00EDstup ke slu\u017Eb\u011B byl zam\u00EDtnut, proto\u017Ee V\u00E1\u0161 \u00FA\u010Det nespl\u0148uje pomd\u00EDnky p\u0159\u00EDstupu. P\u0159ihlaste se, pros\u00EDme, pomoc\u00ED jin\u00E9ho \u00FA\u010Dtu.
 403_is_eligible_default_button_text=Pokra\u010Dovat
 403_is_eligible_default_contact_text=Pokud si mysl\u00EDte, \u017Ee pou\u017E\u00EDv\u00E1te spr\u00E1vn\u00FD \u00FA\u010Det a p\u0159\u00EDstup je V\u00E1m odm\u00EDtnut nepr\u00E1vem, pros\u00EDme kontakujte n\u00E1s na
+403_is_eligible_client_contact=Pokud si mysl\u00EDte, \u017Ee v\u00E1m byl p\u0159\u00EDstup odep\u0159en nepr\u00E1vem, nebo chcete p\u0159\u00EDstup z\u00EDskat, kontaktujte spr\u00E1vce slu\u017Eby pomoc\u00ED kontakt\u016F n\u00ED\u017Ee.
 
 #GO TO REGISTRATION
-go_to_registration_title=Je vy\u017Eadov\u00E1na Va\u0161e aktivita
-go_to_registration_header1=Pro p\u0159\u00EDstup ke slu\u017Eb\u011B
-go_to_registration_header2=je vy\u017Eadov\u00E1na Va\u0161e aktivita
-go_to_registration_continue=Pokra\u010Dovat na str\u00E1nku s dopl\u0148uj\u00EDc\u00EDmi informacemi
+unauthorized_register_notify_action_required_title=Je vy\u017Eadov\u00E1na Va\u0161e aktivita
+unauthorized_register_notify_action_required_header1=Pro p\u0159\u00EDstup ke slu\u017Eb\u011B
+unauthorized_register_notify_action_required_header2=je vy\u017Eadov\u00E1na Va\u0161e aktivita
+unauthorized_register_notify_action_required_continue=Pokra\u010Dovat na str\u00E1nku s dopl\u0148uj\u00EDc\u00EDmi informacemi
 
 #REGISTRATION
-registration_title=Registrace pro p\u0159\u00EDstup ke slu\u017Eb\u011B
-registration_header1=P\u0159\u00EDstup ke slu\u017Eb\u011B
-registration_header2=byl zam\u00EDtnut
-registration_message=Pro z\u00EDsk\u00E1n\u00ED p\u0159\u00EDstupu k dan\u00E9 slu\u017Eb\u011B je nutn\u00E9 b\u00FDt \u010Dlenem jedn\u00E9 z n\u00E1sleduj\u00EDc\u00EDch skupin. Pokra\u010Dujte v\u00FDb\u011Brem p\u0159\u00EDslu\u0161n\u00E9 organizace a skupiny.
-registration_select_vo=Vyberte virtu\u00E1ln\u00ED organizaci:
-registration_select_group=Vyberte skupinu pro registraci:
-registration_continue=Pokra\u010Dovat na registra\u010Dn\u00ED str\u00E1nku do vybran\u00E9 skupiny
+unauthorized_register_choose_vo_group_title=Registrace pro p\u0159\u00EDstup ke slu\u017Eb\u011B
+unauthorized_register_choose_vo_group_header1=P\u0159\u00EDstup ke slu\u017Eb\u011B
+unauthorized_register_choose_vo_group_header2=byl zam\u00EDtnut
+unauthorized_register_choose_vo_group_message=Pro z\u00EDsk\u00E1n\u00ED p\u0159\u00EDstupu k dan\u00E9 slu\u017Eb\u011B je nutn\u00E9 b\u00FDt \u010Dlenem jedn\u00E9 z n\u00E1sleduj\u00EDc\u00EDch skupin. Pokra\u010Dujte v\u00FDb\u011Brem p\u0159\u00EDslu\u0161n\u00E9 organizace a skupiny.
+unauthorized_register_choose_vo_group_select_vo=Vyberte virtu\u00E1ln\u00ED organizaci:
+unauthorized_register_choose_vo_group_select_group=Vyberte skupinu pro registraci:
+unauthorized_register_choose_vo_group_continue=Pokra\u010Dovat na registra\u010Dn\u00ED str\u00E1nku do vybran\u00E9 skupiny
 
 #CESNET footer specific
 footer_other_projects=OSTATN\u00CD PROJEKTY
diff --git a/perun-oidc-server-webapp/src/main/resources/localization/messages_en.properties b/perun-oidc-server-webapp/src/main/resources/localization/messages_en.properties
index a51323982..4e05cdcc7 100644
--- a/perun-oidc-server-webapp/src/main/resources/localization/messages_en.properties
+++ b/perun-oidc-server-webapp/src/main/resources/localization/messages_en.properties
@@ -68,22 +68,25 @@ zoneinfo=Zone
 phone_number=Phone
 
 #UNAPPROVED
-contact_p=In case of any questions, do not hesitate to contact us at
+403_aai_contact_text=In case of any questions, do not hesitate to contact us at
 403_header=Access forbidden
 403_text=You don't meet the prerequisites for accessing the service:
 403_informationPage=For more information about this service please visit this
 403_contactSupport=If you think you should have an access contact service operator at
 403_subject=Problem with login to service:
+
 403_ensure_vo_hdr=Access denied
 403_ensure_vo_msg=You don't meet the prerequisites to access the service.
+403_ensure_vo_client_contact=If you think you should have access to the service, you can contact the service operator via the email(s) below.
+
 403_authorization_hdr=Access denied
 403_authorization_msg=You see this page because you are not allowed to access the service. This situation can be a result of the access restrictions that the service administrator has set up.
-403_not_in_test_vos_groups_hdr=Access denied
-403_not_in_test_vos_groups_msg=You see this page because you are not allowed to access AAI's testing services.
-403_not_in_prod_vos_groups_hdr=Access denied
-403_not_in_prod_vos_groups_msg=You see this page because you are not allowed to access AAI's services.
-403_not_in_mandatory_vos_groups_hdr=Access denied
-403_not_in_mandatory_vos_groups_msg=You are seeing this page because your membership in the required organizational units is invalid.
+
+403_not_in_env_vos_groups_hdr=Access denied
+403_not_in_env_vos_groups_msg=You see this page because you are not allowed to access this service. The reason is that you are not member of the required organizational units.
+403_not_in_env_vos_groups_urls=Please register into the required units using the links below. If you see no links, please get in touch with the service operator to grant you access.
+403_not_in_env_vos_groups_client_contact=If you need more information about the access requirements, please use one of the contacts of the service operator below.
+
 403_not_logged_in_hdr=Access denied
 403_not_logged_in_msg=It appears the login process has failed. Please close your browser and try to log in again.
 
@@ -91,21 +94,22 @@ contact_p=In case of any questions, do not hesitate to contact us at
 403_is_eligible_default_text=Your account does not meet the criteria for accessing the service. Please log in with other account.
 403_is_eligible_default_button_text=Continue with other account.
 403_is_eligible_default_contact_text=If you think you have used an account which meets the criteria, and you are still prevented from logging in to the service, please contact us at
+403_is_eligible_client_contact=If you think you should have access to the service, you can contact the service operator via the email(s) below.
 
 #GO TO REGISTRATION
-go_to_registration_title=Your activity is necessary
-go_to_registration_header1=Your activity is necessary to access the
-go_to_registration_header2=service
-go_to_registration_continue=Continue to a page with additional information
+unauthorized_register_notify_action_required_title=Your activity is necessary
+unauthorized_register_notify_action_required_header1=Your activity is necessary to access the
+unauthorized_register_notify_action_required_header2=service
+unauthorized_register_notify_action_required_continue=Continue to a page with additional information
 
 #REGISTRATION
-registration_title=Registration for access to the service
-registration_header1=Access to the service
-registration_header2=has been forbidden
-registration_message=To access the service it is necessary to have a valid membership in one of the following groups. Please proceed with selection of organization and group for registration.
-registration_select_vo=Select virtual organization for registration:
-registration_select_group=Select group for registration:
-registration_continue=Continue to the registration page for selected group
+unauthorized_register_choose_vo_group_title=Registration for access to the service
+unauthorized_register_choose_vo_group_header1=Access to the service
+unauthorized_register_choose_vo_group_header2=has been forbidden
+unauthorized_register_choose_vo_group_message=To access the service it is necessary to have a valid membership in one of the following groups. Please proceed with selection of organization and group for registration.
+unauthorized_register_choose_vo_group_select_vo=Select virtual organization for registration:
+unauthorized_register_choose_vo_group_select_group=Select group for registration:
+unauthorized_register_choose_vo_group_continue=Continue to the registration page for selected group
 
 #CESNET+einfra footer specific
 footer_other_projects=OTHER CESNET PROJECTS
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/aup.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/aup.jsp
index 3a3c54cf6..3a6caab7f 100644
--- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/aup.jsp
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/aup.jsp
@@ -29,7 +29,7 @@ request.setAttribute("cssLinks", cssLinks);
             <div>
                 <p style="font-size: 16px; padding: 0; margin: 0;"><spring:message code="org_vo"/>${" "}<strong>${aup.key}</strong></p>
                 <p><spring:message code="see_aup"/>${" "}${aup.value.version}${" "}
-                    <a href="${aup.value.link}"><spring:message code="here"/></a></p>
+                    <a href="${aup.value.link}" target="_blank"><spring:message code="here"/></a></p>
             </div>
         </c:forEach>
         <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/isTestSpWarning.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/test_rp_warning.jsp
similarity index 76%
rename from perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/isTestSpWarning.jsp
rename to perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/test_rp_warning.jsp
index fc4be6d4d..4831f9c68 100644
--- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/isTestSpWarning.jsp
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/test_rp_warning.jsp
@@ -27,13 +27,11 @@
     </div>
     <p><spring:message code="is_test_sp_warning_text"/></p>
 
-    <form method="GET" action="${action}">
+    <form method="POST" action="">
         <hr/>
         <br/>
-        <input type="hidden" name="target" value="${fn:escapeXml(target)}">
-        <input type="hidden" name="accepted" value="true">
-        <spring:message code="is_test_sp_warning_continue" var="submit_value"/>
-        <input type="submit" name="continue" value="${submit_value}" class="btn btn-lg btn-primary btn-block">
+        <input type="submit" name="continue" value="<spring:message code="is_test_sp_warning_continue"/>"
+               class="btn btn-lg btn-primary btn-block">
     </form>
 </div>
 </div><!-- ENDWRAP -->
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized.jsp
similarity index 96%
rename from perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved.jsp
rename to perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized.jsp
index 5bfda5a7c..99420eb38 100644
--- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved.jsp
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized.jsp
@@ -32,7 +32,7 @@ request.setAttribute("cssLinks", cssLinks);
             <c:if test="${not empty client.clientUri}">
                 <br/>
                 <spring:message key="403_informationPage"/>${' '}
-                <a href="${fn:escapeXml(client.clientUri)}">
+                <a href="${fn:escapeXml(client.clientUri)}" target="_blank">
                     ${fn:escapeXml(client.clientUri)}
                 </a>
             </c:if>
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_is_eligible.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_ensure_vo_member.jsp
similarity index 56%
rename from perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_is_eligible.jsp
rename to perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_ensure_vo_member.jsp
index 6170db3e3..051db6d01 100644
--- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_is_eligible.jsp
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_ensure_vo_member.jsp
@@ -16,19 +16,26 @@ request.setAttribute("cssLinks", cssLinks);
 
 <t:header title="${title}" reqURL="${reqURL}" baseURL="${baseURL}"
           cssLinks="${cssLinks}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
-
 </div> <%-- header --%>
 
 <div id="content">
     <div class="error_message" style="word-wrap: break-word;">
-        <h1><spring:message code="${outHeader}"/></h1>
-        <p><spring:message code="${outMessage}"/></p>
-        <c:if test="${hasTarget}">
-            <form method="POST" action="" class="mb-4">
-                <button class="btn btn-primary btn-block"><spring:message code="${outButton}"/></button>
-            </form>
+        <h1><spring:message code="403_ensure_vo_hdr"/></h1>
+        <p><spring:message code="403_ensure_vo_msg"/></p>
+        <c:if test="${not empty client and not empty client.contacts}">
+            <p><spring:message code="403_ensure_vo_client_contact"/></p>
+            <ul>
+                <c:forEach items="${client.contacts}" var="contact">
+                    <li>
+                        <a href="mailto:${fn:escapeXml(contact)}">${fn:escapeXml(contact)}</a>
+                    </li>
+                </c:forEach>
+            </ul>
         </c:if>
-        <p><spring:message code="${outContactP}"/>${" "}<a href="mailto:${contactMail}">${contactMail}</a></p>
+        <p>
+            <spring:message code="403_aai_contact_text"/>${" "}
+            <a href="mailto:${fn:escapeXml(contactMail)}">${fn:escapeXml(contactMail)}</a>
+        </p>
     </div>
 </div>
 </div><!-- ENDWRAP -->
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/registrationFormContinue.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_ensure_vo_member_notify_redirect.jsp
similarity index 66%
rename from perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/registrationFormContinue.jsp
rename to perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_ensure_vo_member_notify_redirect.jsp
index 49ed92f2a..35c0c8833 100644
--- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/registrationFormContinue.jsp
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_ensure_vo_member_notify_redirect.jsp
@@ -1,25 +1,24 @@
 <%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" trimDirectiveWhitespaces="true" %>
 <%@ page import="java.util.ArrayList" %>
 <%@ page import="java.util.List" %>
+<%@ page import="cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController" %>
 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
 <%@ taglib prefix="t" tagdir="/WEB-INF/tags/common"%>
 <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
 
-
-
 <%
 
 String samlCssUrl = (String) request.getAttribute("samlResourcesURL");
 List<String> cssLinks = new ArrayList<>();
 
-cssLinks.add(samlCssUrl + "/module.php/perun/res/css/perun_identity_go_to_registration.css");
+cssLinks.add(samlCssUrl + "/module.php/perun/res/css/perun_identity_unauthorized_register_notify_action_required.css");
 
 request.setAttribute("cssLinks", cssLinks);
 
 %>
 
-<spring:message code="go_to_registration_title" var="title"/>
+<spring:message code="unauthorized_register_notify_action_required_title" var="title"/>
 <t:header title="${title}" reqURL="${reqURL}" baseURL="${baseURL}"
           cssLinks="${cssLinks}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
 
@@ -27,25 +26,22 @@ request.setAttribute("cssLinks", cssLinks);
 
 <div id="content">
     <div id="head">
-        <h1><spring:message code="go_to_registration_header1"/>
+        <h1><spring:message code="unauthorized_register_notify_action_required_header1"/>
             <c:choose>
                 <c:when test="${not empty client.clientName and not empty client.clientUri}">
-                    ${" "}<a href="${fn:escapeXml(client.uri)}">${fn:escapeXml(client.clientName)}</a>
+                    ${" "}<a href="${fn:escapeXml(client.clientUri)}" target="_blank">${fn:escapeXml(client.clientName)}</a>
                 </c:when>
                 <c:when test="${not empty client.clientName}">
                     ${" "}${fn:escapeXml(client.clientName)}
                 </c:when>
             </c:choose>
-            ${" "}<spring:message code="go_to_registration_header2"/>
+            ${" "}<spring:message code="unauthorized_register_notify_action_required_header2"/>
         </h1>
     </div>
-    <form method="GET" action="${action}">
+    <form action="${pageContext.request.contextPath}${PerunUnauthorizedController.UNAUTHORIZED_REGISTER_CHOOSE_VO_GROUP_MAPPING}" method="GET">
         <hr/>
         <br/>
-        <input type="hidden" name="client_id" value="${fn:escapeXml(client_id)}" />
-        <input type="hidden" name="facility_id" value="${fn:escapeXml(facility_id)}" />
-        <input type="hidden" name="user_id" value="${fn:escapeXml(user_id)}" />
-        <spring:message code="go_to_registration_continue" var="submit_value"/>
+        <spring:message code="unauthorized_register_notify_action_required_continue" var="submit_value"/>
         <input type="submit" name="continueToRegistration" value="${submit_value}"
                class="btn btn-lg btn-primary btn-block">
     </form>
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_is_eligible.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_is_eligible.jsp
new file mode 100644
index 000000000..8e8cd185b
--- /dev/null
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_is_eligible.jsp
@@ -0,0 +1,49 @@
+<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" trimDirectiveWhitespaces="true" %>
+<%@ page import="java.util.ArrayList" %>
+<%@ page import="java.util.List" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="t" tagdir="/WEB-INF/tags/common"%>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+<%
+
+List<String> cssLinks = new ArrayList<>();
+
+request.setAttribute("cssLinks", cssLinks);
+
+%>
+
+<t:header title="${title}" reqURL="${reqURL}" baseURL="${baseURL}"
+          cssLinks="${cssLinks}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
+
+</div> <%-- header --%>
+
+<div id="content">
+    <div class="error_message" style="word-wrap: break-word;">
+        <h1><spring:message code="${outHeader}"/></h1>
+        <p><spring:message code="${outMessage}"/></p>
+        <c:if test="${not empty target}">
+            <form method="GET" action="${target}" class="mb-4">
+                <input class="btn btn-primary btn-block" value="<spring:message code="${outButton}"/>" type="submit"/>
+            </form>
+        </c:if>
+        <c:if test="${not empty client and not empty client.contacts}">
+            <p><spring:message code="403_is_eligible_client_contact"/></p>
+            <ul>
+                <c:forEach items="${client.contacts}" var="contact">
+                    <li>
+                        <a href="mailto:${fn:escapeXml(contact)}">${fn:escapeXml(contact)}</a>
+                    </li>
+                </c:forEach>
+            </ul>
+        </c:if>
+        <p>
+            <spring:message code="${outContactP}"/>${" "}
+            <a href="mailto:${fn:escapeXml(contactMail)}">${fn:escapeXml(contactMail)}</a>
+        </p>
+    </div>
+</div>
+</div><!-- ENDWRAP -->
+
+<t:footer baseURL="${baseURL}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_not_in_env_units.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_not_in_env_units.jsp
new file mode 100644
index 000000000..0a6997700
--- /dev/null
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_not_in_env_units.jsp
@@ -0,0 +1,51 @@
+<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" trimDirectiveWhitespaces="true" %>
+<%@ page import="java.util.ArrayList" %>
+<%@ page import="java.util.List" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="t" tagdir="/WEB-INF/tags/common"%>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+<%
+
+List<String> cssLinks = new ArrayList<>();
+
+request.setAttribute("cssLinks", cssLinks);
+
+%>
+
+<t:header title="${title}" reqURL="${reqURL}" baseURL="${baseURL}"
+          cssLinks="${cssLinks}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
+</div> <%-- header --%>
+
+<div id="content">
+    <div class="error_message" style="word-wrap: break-word;">
+        <h1><spring:message code="403_not_in_env_vos_groups_hdr"/></h1>
+        <p><spring:message code="403_not_in_env_vos_groups_msg"/></p>
+        <p><spring:message code="403_not_in_env_vos_groups_urls"/></p>
+        <c:if test="${not empty registrationUrls}">
+            <ul>
+                <c:forEach var="entry" items="${registrationUrls}">
+                    <a href="<c:out value="${entry.key}" />" target="_blank">
+                        <c:out value="${entry.value}" />
+                    </a>
+                </c:forEach>
+            </ul>
+        </c:if>
+        <c:if test="${not empty client and not empty client.contacts}">
+            <p>
+                <spring:message code="403_not_in_env_vos_groups_client_contact"/>
+            </p>
+            <ul>
+                <c:forEach items="${client.contacts}" var="contact">
+                    <li>
+                        <a href="mailto:${fn:escapeXml(contact)}">${fn:escapeXml(contact)}</a>
+                    </li>
+                </c:forEach>
+            </ul>
+        </c:if>
+    </div>
+</div>
+</div><!-- ENDWRAP -->
+
+<t:footer baseURL="${baseURL}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/registrationForm.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_register_choose_vo_group.jsp
similarity index 70%
rename from perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/registrationForm.jsp
rename to perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_register_choose_vo_group.jsp
index 34c2775a2..3fbc92efc 100644
--- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/registrationForm.jsp
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_register_choose_vo_group.jsp
@@ -6,8 +6,6 @@
 <%@ taglib prefix="t" tagdir="/WEB-INF/tags/common"%>
 <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
 
-
-
 <%
 
 String samlCssUrl = (String) request.getAttribute("samlResourcesURL");
@@ -18,7 +16,7 @@ cssLinks.add(samlCssUrl + "/module.php/perun/res/css/perun_identity_choose_vo_an
 request.setAttribute("cssLinks", cssLinks);
 
 %>
-<spring:message code="registration_title" var="title"/>
+<spring:message code="unauthorized_register_choose_vo_group_title" var="title"/>
 <t:header title="${title}" reqURL="${reqURL}" baseURL="${baseURL}"
           cssLinks="${cssLinks}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
 
@@ -26,24 +24,24 @@ request.setAttribute("cssLinks", cssLinks);
 
 <div id="content">
     <div id="head">
-        <h1><spring:message code="registration_header1"/>
+        <h1><spring:message code="unauthorized_register_choose_vo_group_header1"/>
             <c:choose>
                 <c:when test="${not empty client.clientName and not empty client.clientUri}">
-                    &#32;<a href="${fn:escapeXml(client.clientUri)}">${fn:escapeXml(client.clientName)}</a>
+                    &#32;<a href="${fn:escapeXml(client.clientUri)}" target="_blank">${fn:escapeXml(client.clientName)}</a>
                 </c:when>
                 <c:when test="${not empty client.clientName}">
                     &#32;${fn:escapeXml(client.clientName)}
                 </c:when>
             </c:choose>
-            ${" "}<spring:message code="registration_header2"/>
+            ${" "}<spring:message code="unauthorized_register_choose_vo_group_header2"/>
         </h1>
     </div>
-    <div class="msg"><spring:message code="registration_message"/></div>
+    <div class="msg"><spring:message code="unauthorized_register_choose_vo_group_message"/></div>
 
     <div class="list-group">
-        <form action="${action}" method="get">
-            <h4><spring:message code="registration_select_vo"/></h4>
-            <select id="selectVo" class="form-control" name="selectedVo" onchange="filter()" required>
+        <form action="${registrarUrl}" method="GET">
+            <h4><spring:message code="unauthorized_register_choose_vo_group_select_vo"/></h4>
+            <select id="selectVo" class="form-control" onchange="filter()" required name="vo">
                 <c:forEach var="voGroupPair" items="${groupsForRegistration}">
                     <option value="${fn:escapeXml(voGroupPair.key.shortName)}">
                             ${fn:escapeXml(voGroupPair.key.name)}
@@ -51,8 +49,8 @@ request.setAttribute("cssLinks", cssLinks);
                 </c:forEach>
             </select>
 
-            <h4 class="selectGroup" style="display: none"><spring:message code="registration_select_group"/></h4>
-            <select  class="selectGroup form-control" name="selectedGroup" class="form-control" style="display: none" required>
+            <h4 class="selectGroup" style="display: none"><spring:message code="unauthorized_register_choose_vo_group_select_group"/></h4>
+            <select  class="selectGroup form-control" style="display: none" required name="group">
                 <c:forEach var="voGroupPair" items="${groupsForRegistration}">
                     <c:forEach var="group" items="${voGroupPair.value}">
                         <option class="groupOption" value="${fn:escapeXml(voGroupPair.key.shortName)}:${fn:escapeXml(group.name)}">
@@ -62,7 +60,7 @@ request.setAttribute("cssLinks", cssLinks);
                 </c:forEach>
             </select>
 
-            <spring:message code="registration_continue" var="submit_value"/>
+            <spring:message code="unauthorized_register_choose_vo_group_continue" var="submit_value"/>
             <input type="submit" value="${submit_value}" class="btn btn-lg btn-primary btn-block">
         </form>
     </div>
@@ -71,4 +69,4 @@ request.setAttribute("cssLinks", cssLinks);
 
 <t:footer baseURL="${baseURL}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
 
-<script type="text/javascript" src="resources/js/reg_form_select.js"></script>
+<script type="text/javascript" src="resources/js/unauthorized_register_choose_vo_group.js"></script>
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_register_notify_action_required.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_register_notify_action_required.jsp
new file mode 100644
index 000000000..35c0c8833
--- /dev/null
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_register_notify_action_required.jsp
@@ -0,0 +1,51 @@
+<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" trimDirectiveWhitespaces="true" %>
+<%@ page import="java.util.ArrayList" %>
+<%@ page import="java.util.List" %>
+<%@ page import="cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="t" tagdir="/WEB-INF/tags/common"%>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+<%
+
+String samlCssUrl = (String) request.getAttribute("samlResourcesURL");
+List<String> cssLinks = new ArrayList<>();
+
+cssLinks.add(samlCssUrl + "/module.php/perun/res/css/perun_identity_unauthorized_register_notify_action_required.css");
+
+request.setAttribute("cssLinks", cssLinks);
+
+%>
+
+<spring:message code="unauthorized_register_notify_action_required_title" var="title"/>
+<t:header title="${title}" reqURL="${reqURL}" baseURL="${baseURL}"
+          cssLinks="${cssLinks}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
+
+</div> <%-- header --%>
+
+<div id="content">
+    <div id="head">
+        <h1><spring:message code="unauthorized_register_notify_action_required_header1"/>
+            <c:choose>
+                <c:when test="${not empty client.clientName and not empty client.clientUri}">
+                    ${" "}<a href="${fn:escapeXml(client.clientUri)}" target="_blank">${fn:escapeXml(client.clientName)}</a>
+                </c:when>
+                <c:when test="${not empty client.clientName}">
+                    ${" "}${fn:escapeXml(client.clientName)}
+                </c:when>
+            </c:choose>
+            ${" "}<spring:message code="unauthorized_register_notify_action_required_header2"/>
+        </h1>
+    </div>
+    <form action="${pageContext.request.contextPath}${PerunUnauthorizedController.UNAUTHORIZED_REGISTER_CHOOSE_VO_GROUP_MAPPING}" method="GET">
+        <hr/>
+        <br/>
+        <spring:message code="unauthorized_register_notify_action_required_continue" var="submit_value"/>
+        <input type="submit" name="continueToRegistration" value="${submit_value}"
+               class="btn btn-lg btn-primary btn-block">
+    </form>
+</div>
+</div><!-- ENDWRAP -->
+
+<t:footer baseURL="${baseURL}" theme="${theme}" samlResourcesURL="${samlResourcesURL}"/>
\ No newline at end of file
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_spec.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_spec.jsp
similarity index 89%
rename from perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_spec.jsp
rename to perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_spec.jsp
index de564e6d0..ce1130d4e 100644
--- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_spec.jsp
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unauthorized_spec.jsp
@@ -22,7 +22,7 @@ request.setAttribute("cssLinks", cssLinks);
     <div class="error_message" style="word-wrap: break-word;">
         <h1><spring:message code="${outHeader}"/></h1>
         <p><spring:message code="${outMessage}"/></p>
-        <p><spring:message code="contact_p"/>${" "}<a href="mailto:${contactMail}">${contactMail}</a></p>
+        <p><spring:message code="403_aai_contact_text"/>${" "}<a href="mailto:${contactMail}">${contactMail}</a></p>
     </div>
 </div>
 </div><!-- ENDWRAP -->
diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/web-context.xml b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/web-context.xml
index b66ff2613..8cde01ada 100644
--- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/web-context.xml
+++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/web-context.xml
@@ -60,21 +60,18 @@
             <mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.endpoint.IntrospectionEndpoint).URL}**" />
             <mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.endpoint.RevocationEndpoint).URL}**" />
             <mvc:exclude-mapping path="/#{T(cz.muni.ics.oauth2.web.endpoint.DynamicRegistrationEndpoint).URL}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.IsTestSpController).MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.TestSpWarningController).MAPPING}**" />
             <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.AupController).URL}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_MAPPING}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_AUTHORIZATION}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_ENSURE_VO_MAPPING}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_IS_ELIGIBLE_MAPPING}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_MANDATORY_VOS_GROUPS}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_PROD_VOS_GROUPS}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_TEST_VOS_GROUPS}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_LOGGED_IN}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_SPECIFIC_MAPPING}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_CONTINUE_MAPPING}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_FORM_MAPPING}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController).REGISTRATION_FORM_SUBMIT_MAPPING}**" />
-            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.RegistrationController).CONTINUE_DIRECT_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_REGISTER_CHOOSE_VO_GROUP_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_AUTHORIZATION_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_ENSURE_VO_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_ENSURE_VO_REDIRECT_NOTIFY_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_IS_ELIGIBLE_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_NOT_IN_ENV_VOS_GROUPS_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_NOT_LOGGED_IN_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_REGISTER_NOTIFY_ACTION_REQUIRED_MAPPING}**" />
+            <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController).UNAUTHORIZED_SPECIFIC_MAPPING}**" />
             <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.LogoutController).MAPPING_SUCCESS}" />
             <mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.LoginController).MAPPING_FAILURE}" />
             <mvc:exclude-mapping path="/saml**" />
diff --git a/perun-oidc-server-webapp/src/main/webapp/resources/js/reg_form_select.js b/perun-oidc-server-webapp/src/main/webapp/resources/js/unauthorized_register_choose_vo_group.js
similarity index 61%
rename from perun-oidc-server-webapp/src/main/webapp/resources/js/reg_form_select.js
rename to perun-oidc-server-webapp/src/main/webapp/resources/js/unauthorized_register_choose_vo_group.js
index 86fc13cc1..f9d9a431e 100644
--- a/perun-oidc-server-webapp/src/main/webapp/resources/js/reg_form_select.js
+++ b/perun-oidc-server-webapp/src/main/webapp/resources/js/unauthorized_register_choose_vo_group.js
@@ -1,6 +1,8 @@
+const selectGroup = $(".selectGroup");
+
 function filter() {
   hideGroups();
-  $(".selectGroup").val("");
+  selectGroup.val("");
   const vo = $("#selectVo").val();
   if (vo !== "") {
     showGroups();
@@ -15,10 +17,14 @@ function filter() {
   }
 }
 function showGroups() {
-  $(".selectGroup").show();
+  selectGroup.show();
+  selectGroup.prop("required", true);
+  selectGroup.prop("disabled", false);
 }
 function hideGroups() {
-  $(".selectGroup").hide();
+  selectGroup.hide();
+  selectGroup.prop("required", false);
+  selectGroup.prop("disabled", true);
 }
 $(document).ready(function () {
   $("#selectVo").val("");
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/PerunConstants.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/PerunConstants.java
index b0463689c..128474767 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/PerunConstants.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/PerunConstants.java
@@ -7,5 +7,7 @@ public interface PerunConstants {
     String REGISTRAR_TARGET_EXTENDED = "targetextended";
 
     String REGISTRAR_PARAM_VO = "vo";
+    String REGISTRAR_PARAM_GROUP = "group";
 
+    String GROUP_NAME_MEMBERS = "members";
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/RedirectUtils.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/RedirectUtils.java
new file mode 100644
index 000000000..851b5cf16
--- /dev/null
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/RedirectUtils.java
@@ -0,0 +1,45 @@
+package cz.muni.ics.oidc;
+
+import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
+import cz.muni.ics.oidc.web.controllers.ControllerUtils;
+import org.springframework.http.HttpHeaders;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.util.Collections;
+import java.util.Map;
+
+public class RedirectUtils {
+
+
+    private RedirectUtils() {
+        // disallow instantiation
+    }
+    public static void redirectInternal(HttpServletRequest req,
+                                        HttpServletResponse res,
+                                        PerunOidcConfig config,
+                                        String endpoint,
+                                        Map<String, Object> sessionParams)
+    {
+        String redirectUrl = ControllerUtils.createRedirectUrl(config.getBaseURL(), endpoint, Collections.emptyMap());
+
+        HttpSession sess = req.getSession(true);
+        for (Map.Entry<String, Object> entry : sessionParams.entrySet()) {
+            sess.setAttribute(entry.getKey(), entry.getValue());
+        }
+
+        res.reset();
+        res.setStatus(HttpServletResponse.SC_FOUND);
+        res.setHeader(HttpHeaders.LOCATION, redirectUrl);
+    }
+
+    public static void redirectExternal(HttpServletResponse res,
+                                        String location)
+    {
+        res.reset();
+        res.setStatus(HttpServletResponse.SC_FOUND);
+        res.setHeader(HttpHeaders.LOCATION, location);
+    }
+
+}
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethods.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethods.java
index f0120bae2..0a6e6c381 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethods.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethods.java
@@ -314,34 +314,14 @@ public interface PerunAdapterMethods {
 	 */
 	Set<Long> getUserGroupsIds(Long userId, Long voId);
 
-	/**
-	 * Check if user is valid member of given VOs (identified by IDs)
-	 * @param userId ID of user in Perun
-	 * @param mandatoryVos Set of IDs identifying the VOs
-	 * @param mandatoryGroups Set of IDs identifying the Groups
-	 * @param envVos Set of IDs identifying the VOs
-	 * @param envGroups Set of IDs identifying the Groups
-	 * @return returns TRUE if:
-	 * 	User is member of at least one specified mandatory VO, and
-	 * 	User is member of at least one specified mandatory GROUP, and
-	 * 	User is member of at least one specified env VO, and
-	 * 	User is member of at least one specified env GROUP.
-	 * 	Returns FALSE otherwise.
-	 */
-	boolean isValidMemberInGroupsAndVos(Long userId, Set<Long> mandatoryVos, Set<Long> mandatoryGroups,
-										Set<Long> envVos, Set<Long> envGroups);
-
 	/**
 	 * Check if user is valid member of given VOs (identified by IDs)
 	 * @param userId ID of user in Perun
 	 * @param vos Set of IDs identifying the VOs
 	 * @param groups Set of IDs identifying the Groups
-	 * @return returns TRUE if:
-	 * 	User is member of at least one specified mandatory VO, and
-	 * 	User is member of at least one specified mandatory GROUP, and
-	 * 	Returns FALSE otherwise.
+	 * @return returns set of names (vo short_names and group full qualified names) where user is not valid member
 	 */
-	boolean isValidMemberInGroupsAndVos(Long userId, Set<Long> vos, Set<Long> groups);
+	Set<String> getVosAndGroupsNamesWhereUserInvalid(Long userId, Set<Long> vos, Set<Long> groups);
 
 	boolean isUserInVo(Long userId, String voShortName);
 
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethodsRpc.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethodsRpc.java
index 9dc800278..1c8ef3788 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethodsRpc.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/PerunAdapterMethodsRpc.java
@@ -11,6 +11,7 @@ import cz.muni.ics.oidc.server.connectors.Affiliation;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Interface with specific methods that only rpc interface can execute
@@ -229,4 +230,10 @@ public interface PerunAdapterMethodsRpc {
 
 	boolean hasApplicationForm(String voShortName);
 
+	/**
+	 * Get map of Vo to Set of Groups based on unique group names
+	 * @param invalidUnits set of strings in format vo_short_name[:g_name]*
+	 * @return Map of VOs to Set of Groups
+	 */
+	Map<Vo, Set<Group>> getRegistrationUnits(Set<String> invalidUnits);
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterImpl.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterImpl.java
index 1160ca0d2..10a1a5f0d 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterImpl.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterImpl.java
@@ -10,6 +10,8 @@ import cz.muni.ics.oidc.server.adapters.PerunAdapter;
 import cz.muni.ics.oidc.server.connectors.Affiliation;
 
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -365,28 +367,12 @@ public class PerunAdapterImpl extends PerunAdapter {
     }
 
     @Override
-    public boolean isValidMemberInGroupsAndVos(Long userId, Set<Long> mandatoryVos, Set<Long> mandatoryGroups,
-                                               Set<Long> envVos, Set<Long> envGroups) {
+    public Set<String> getVosAndGroupsNamesWhereUserInvalid(Long userId, Set<Long> vos, Set<Long> groups) {
         try {
-            return this.getAdapterPrimary().isValidMemberInGroupsAndVos(userId, mandatoryVos, mandatoryGroups,
-                    envVos, envGroups);
+            return this.getAdapterPrimary().getVosAndGroupsNamesWhereUserInvalid(userId, vos, groups);
         } catch (UnsupportedOperationException e) {
             if (this.isCallFallback()) {
-                return this.getAdapterFallback().isValidMemberInGroupsAndVos(userId, mandatoryVos, mandatoryGroups,
-                        envVos, envGroups);
-            } else {
-                throw e;
-            }
-        }
-    }
-
-    @Override
-    public boolean isValidMemberInGroupsAndVos(Long userId, Set<Long> vos, Set<Long> groups) {
-        try {
-            return this.getAdapterPrimary().isValidMemberInGroupsAndVos(userId, vos, groups);
-        } catch (UnsupportedOperationException e) {
-            if (this.isCallFallback()) {
-                return this.getAdapterFallback().isValidMemberInGroupsAndVos(userId, vos, groups);
+                return this.getAdapterFallback().getVosAndGroupsNamesWhereUserInvalid(userId, vos, groups);
             } else {
                 throw e;
             }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterLdap.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterLdap.java
index 6f93e3a18..e731865af 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterLdap.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterLdap.java
@@ -444,8 +444,14 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
 	}
 
 	@Override
-	public boolean isValidMemberInGroupsAndVos(Long userId, Set<Long> mandatoryVos, Set<Long> mandatoryGroups,
-											   Set<Long> envVos, Set<Long> envGroups) {
+	public Set<String> getVosAndGroupsNamesWhereUserInvalid(Long userId, Set<Long> vos, Set<Long> groups) {
+		if (vos == null) {
+			vos = new HashSet<>();
+		}
+		if (groups == null) {
+			groups = new HashSet<>();
+		}
+
 		final Set<Long> foundGroupIds = new HashSet<>();
 		final Set<Long> foundVoIds = new HashSet<>();
 		String dnPrefix = getDnPrefixForUserId(userId);
@@ -469,35 +475,66 @@ public class PerunAdapterLdap extends PerunAdapterWithMappingServices implements
 		};
 		connectorLdap.lookup(dnPrefix, attributes, mapper);
 
-		return PerunAdapter.decideAccess(foundVoIds, foundGroupIds, mandatoryVos, mandatoryGroups, envVos, envGroups);
-	}
+		Set<Long> voIdsToRegister = new HashSet<>(vos);
+		voIdsToRegister.removeAll(foundVoIds);
 
-	@Override
-	public boolean isValidMemberInGroupsAndVos(Long userId, Set<Long> vos, Set<Long> groups) {
-		final Set<Long> foundGroupIds = new HashSet<>();
-		final Set<Long> foundVoIds = new HashSet<>();
-		String dnPrefix = getDnPrefixForUserId(userId);
-		String[] attributes = new String[] { MEMBER_OF };
-		EntryMapper<Void> mapper = e -> {
-			if (checkHasAttributes(e, attributes)) {
-				Attribute a = e.get(MEMBER_OF);
-				a.iterator().forEachRemaining(id -> {
-					String fullVal = id.getString();
-					String[] parts = fullVal.split(",", 3);
+		Set<Long> groupIdsToRegister = new HashSet<>(groups);
+		groupIdsToRegister.removeAll(foundGroupIds);
 
-					String groupId = parts[0];
-					groupId = groupId.replace(PERUN_GROUP_ID + '=', "");
-					foundGroupIds.add(Long.parseLong(groupId));
-					String voIdStr = parts[1];
-					voIdStr = voIdStr.replace(PERUN_VO_ID + '=', "");
-					foundVoIds.add(Long.parseLong(voIdStr));
-				});
+		if (voIdsToRegister.isEmpty() && groupIdsToRegister.isEmpty()) {
+			return new HashSet<>();
+		}
+
+		Set<String> unitNames = new HashSet<>();
+		if (!voIdsToRegister.isEmpty()) {
+			unitNames.addAll(getVoShortNamesByVoIds(voIdsToRegister));
+		}
+		if (!groupIdsToRegister.isEmpty()) {
+			unitNames.addAll(getGroupShortNamesByVoIds(groupIdsToRegister));
+		}
+		return unitNames;
+	}
+
+	private Set<String> getVoShortNamesByVoIds(Set<Long> voIds) {
+		String[] attributes = new String[] { O };
+		List<FilterBuilder> voIdsFilter = new ArrayList<>();
+		for (Long voId : voIds) {
+			voIdsFilter.add(equal(PERUN_VO_ID, voId.toString()));
+		}
+		FilterBuilder filter = and(
+				equal(OBJECT_CLASS, PERUN_VO), or(voIdsFilter.toArray(new FilterBuilder[]{}))
+		);
+		EntryMapper<String> mapper = e -> {
+			if (!checkHasAttributes(e, attributes)) {
+				return null;
 			}
-			return null;
+			Attribute a = e.get(attributes[0]);
+			return a.getString();
 		};
-		connectorLdap.lookup(dnPrefix, attributes, mapper);
+		List<String> voShortNames = connectorLdap.search(null, filter, SearchScope.ONELEVEL, attributes, mapper);
+		voShortNames.removeIf(Objects::isNull);
+		return new HashSet<>(voShortNames);
+	}
 
-		return PerunAdapter.decideAccess(foundVoIds, foundGroupIds, vos, groups);
+	private Set<String> getGroupShortNamesByVoIds(Set<Long> groupIds) {
+		String[] attributes = new String[] { PERUN_UNIQUE_GROUP_NAME };
+		List<FilterBuilder> voIdsFilter = new ArrayList<>();
+		for (Long groupId : groupIds) {
+			voIdsFilter.add(equal(PERUN_GROUP_ID, groupId.toString()));
+		}
+		FilterBuilder filter = and(
+				equal(OBJECT_CLASS, PERUN_GROUP), or(voIdsFilter.toArray(new FilterBuilder[]{}))
+		);
+		EntryMapper<String> mapper = e -> {
+			if (!checkHasAttributes(e, attributes)) {
+				return null;
+			}
+			Attribute a = e.get(attributes[0]);
+			return a.getString();
+		};
+		List<String> groupUniqueNames = connectorLdap.search(null, filter, SearchScope.SUBTREE, attributes, mapper);
+		groupUniqueNames.removeIf(Objects::isNull);
+		return new HashSet<>(groupUniqueNames);
 	}
 
 	@Override
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterRpc.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterRpc.java
index 326e06ccf..0b02757b6 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterRpc.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/adapters/impl/PerunAdapterRpc.java
@@ -2,6 +2,7 @@ package cz.muni.ics.oidc.server.adapters.impl;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.NullNode;
+import cz.muni.ics.oidc.PerunConstants;
 import cz.muni.ics.oidc.models.AttributeMapping;
 import cz.muni.ics.oidc.models.Facility;
 import cz.muni.ics.oidc.models.Group;
@@ -57,6 +58,7 @@ import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.METHOD_GET_EN
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.METHOD_GET_FACILITIES_BY_ATTRIBUTE;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.METHOD_GET_GROUPS_WHERE_USER_IS_ACTIVE;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.METHOD_GET_GROUP_BY_ID;
+import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.METHOD_GET_GROUP_BY_NAME;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.METHOD_GET_MEMBERS_BY_USER;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.METHOD_GET_MEMBER_BY_USER;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.METHOD_GET_MEMBER_GROUPS;
@@ -80,6 +82,7 @@ import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.PARAM_FACILIT
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.PARAM_GROUP;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.PARAM_ID;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.PARAM_MEMBER;
+import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.PARAM_NAME;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.PARAM_RESOURCE;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.PARAM_SHORT_NAME;
 import static cz.muni.ics.oidc.server.connectors.PerunConnectorRpc.PARAM_USER;
@@ -902,31 +905,15 @@ public class PerunAdapterRpc
     }
 
     @Override
-    public boolean isValidMemberInGroupsAndVos(Long userId, Set<Long> mandatoryVos, Set<Long> mandatoryGroups,
-                                               Set<Long> envVos, Set<Long> envGroups) {
+    public Set<String> getVosAndGroupsNamesWhereUserInvalid(Long userId, Set<Long> vos, Set<Long> groups) {
         if (!connectorRpc.isEnabled()) {
-            return false;
+            return new HashSet<>();
         }
-        List<Member> members = getMembersByUser(userId);
-        Set<Long> foundVoIds = new HashSet<>();
-        Set<Long> foundGroupIds = new HashSet<>();
-        boolean skipGroups = mandatoryGroups.isEmpty() && envGroups.isEmpty();
-        for (Member m : members) {
-            if (MemberStatus.VALID.equals(m.getStatus())) {
-                foundVoIds.add(m.getVoId());
-            }
-            if (!skipGroups) {
-                foundGroupIds.addAll(getMemberGroups(m.getId()).stream().map(Model::getId).collect(Collectors.toList()));
-            }
+        if (vos == null) {
+            vos = new HashSet<>();
         }
-
-        return PerunAdapter.decideAccess(foundVoIds, foundGroupIds, mandatoryVos, mandatoryGroups, envVos, envGroups);
-    }
-
-    @Override
-    public boolean isValidMemberInGroupsAndVos(Long userId, Set<Long> vos, Set<Long> groups) {
-        if (!connectorRpc.isEnabled()) {
-            return false;
+        if (groups == null) {
+            groups = new HashSet<>();
         }
         List<Member> members = getMembersByUser(userId);
         Set<Long> foundVoIds = new HashSet<>();
@@ -942,7 +929,39 @@ public class PerunAdapterRpc
             }
         }
 
-        return PerunAdapter.decideAccess(foundVoIds, foundGroupIds, vos, groups);
+        Set<Long> voIdsToRegister = new HashSet<>(vos);
+        voIdsToRegister.removeAll(foundVoIds);
+
+        Set<Long> groupIdsToRegister = new HashSet<>(groups);
+        groupIdsToRegister.removeAll(foundGroupIds);
+
+        Set<String> unitNames = new HashSet<>();
+
+        final Map<Long, Vo> voCache = new HashMap<>();
+        voIdsToRegister.forEach(voId -> {
+            Vo vo = getVoById(voId);
+            if (vo != null) {
+                voCache.put(vo.getId(), vo);
+                unitNames.add(vo.getShortName());
+            }
+        });
+
+
+        groupIdsToRegister.forEach(groupId -> {
+            Group group = getGroupById(groupId);
+            if (group != null) {
+                Vo vo = voCache.getOrDefault(group.getVoId(), null);
+                if (vo == null) {
+                    vo = getVoById(group.getVoId());
+                    if (vo == null) {
+                        return;
+                    }
+                    voCache.put(group.getVoId(), vo);
+                }
+                unitNames.add((vo.getShortName() + ":" + group.getName()));
+            }
+        });
+        return unitNames;
     }
 
     @Override
@@ -1019,6 +1038,50 @@ public class PerunAdapterRpc
         return voIds;
     }
 
+    @Override
+    public Map<Vo, Set<Group>> getRegistrationUnits(Set<String> invalidUnits) {
+        if (!connectorRpc.isEnabled()) {
+            return Collections.emptyMap();
+        }
+        Map<String, Set<String>> voShortNamesToGroupNames = new HashMap<>();
+        for (String invalidUnit : invalidUnits) {
+            String[] parts = invalidUnit.split(":", 2);
+            String voShortName = parts[0];
+            voShortNamesToGroupNames.putIfAbsent(voShortName, new HashSet<>());
+            if (parts.length > 1 && !PerunConstants.GROUP_NAME_MEMBERS.equals(parts[1])) {
+                voShortNamesToGroupNames.get(voShortName).add(parts[1]);
+            }
+        }
+
+        Map<Vo, Set<Group>> res = new HashMap<>();
+        for (Map.Entry<String, Set<String>> e : voShortNamesToGroupNames.entrySet()) {
+            Vo vo = getVoByShortName(e.getKey());
+            if (vo != null && hasApplicationForm(vo.getId())) {
+                res.put(vo, new HashSet<>());
+                for (String groupName : e.getValue()) {
+                    Group group = getGroupByName(vo, groupName);
+                    if (group != null && hasApplicationForm(vo.getId())) {
+                        res.get(vo).add(group);
+                    }
+                }
+            }
+        }
+        return res;
+    }
+
+    private Group getGroupByName(Vo vo, String name) {
+        if (!connectorRpc.isEnabled()) {
+            return null;
+        }
+
+        Map<String, Object> params = new LinkedHashMap<>();
+        params.put(PARAM_VO, vo.getId());
+        params.put(PARAM_NAME, name);
+        JsonNode jsonNode = connectorRpc.post(GROUPS_MANAGER, METHOD_GET_GROUP_BY_NAME, params);
+
+        return RpcMapper.mapGroup(jsonNode);
+    }
+
     private Member getMemberByUser(Long userId, Long voId) {
         if (!connectorRpc.isEnabled()) {
             return null;
@@ -1122,6 +1185,18 @@ public class PerunAdapterRpc
         return RpcMapper.mapVo(res);
     }
 
+    private Group getGroupById(Long groupId) {
+        if (!connectorRpc.isEnabled()) {
+            return null;
+        }
+
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put(PARAM_ID, groupId);
+
+        JsonNode res = connectorRpc.post(GROUPS_MANAGER, METHOD_GET_GROUP_BY_ID, map);
+        return RpcMapper.mapGroup(res);
+    }
+
     private List<Group> getAssignedGroups(Long resourceId) {
         if (!connectorRpc.isEnabled()) {
             return new ArrayList<>();
@@ -1286,7 +1361,7 @@ public class PerunAdapterRpc
         Map<String, Object> map = new LinkedHashMap<>();
         map.put(PARAM_GROUP, group.getId());
         try {
-            if (group.getName().equalsIgnoreCase("members")) {
+            if (PerunConstants.GROUP_NAME_MEMBERS.equals(group.getName())) {
                 log.debug("hasApplicationForm({}) continues to call regForm for VO {}", group, group.getVoId());
                 return hasApplicationForm(group.getVoId());
             } else {
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/EntitlementExtendedClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/EntitlementExtendedClaimSource.java
index 4c8cb2279..33564198b 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/EntitlementExtendedClaimSource.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/EntitlementExtendedClaimSource.java
@@ -1,6 +1,7 @@
 package cz.muni.ics.oidc.server.claims.sources;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import cz.muni.ics.oidc.PerunConstants;
 import cz.muni.ics.oidc.models.Facility;
 import cz.muni.ics.oidc.models.Group;
 import cz.muni.ics.oidc.server.adapters.PerunAdapter;
@@ -63,8 +64,8 @@ public class EntitlementExtendedClaimSource extends EntitlementSource {
     private void fillUuidEntitlements(Set<Group> userGroups, Set<String> entitlements) {
         for (Group group : userGroups) {
             String displayName = group.getUniqueGroupName();
-            if (StringUtils.hasText(displayName) && MEMBERS.equals(group.getName())) {
-                displayName = displayName.replace(':' + MEMBERS, "");
+            if (StringUtils.hasText(displayName) && PerunConstants.GROUP_NAME_MEMBERS.equals(group.getName())) {
+                displayName = displayName.replace(':' + PerunConstants.GROUP_NAME_MEMBERS, "");
             }
             String entitlement = wrapGroupEntitlementToAARC(group.getUuid());
             log.trace("{} - added UUID entitlement: '{}'", getClaimName(), entitlement);
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/EntitlementSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/EntitlementSource.java
index 779f56e78..cbfc6c608 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/EntitlementSource.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/EntitlementSource.java
@@ -2,6 +2,7 @@ package cz.muni.ics.oidc.server.claims.sources;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.net.UrlEscapers;
+import cz.muni.ics.oidc.PerunConstants;
 import cz.muni.ics.oidc.models.Facility;
 import cz.muni.ics.oidc.models.Group;
 import cz.muni.ics.oidc.models.PerunAttributeValue;
@@ -137,8 +138,8 @@ public class EntitlementSource extends GroupNamesSource {
 			}
 
 			String[] parts = fullGname.split(":", 2);
-			if (parts.length == 2 && StringUtils.hasText(parts[1]) && MEMBERS.equals(parts[1])) {
-				parts[1] = parts[1].replace(MEMBERS, "");
+			if (parts.length == 2 && StringUtils.hasText(parts[1]) && PerunConstants.GROUP_NAME_MEMBERS.equals(parts[1])) {
+				parts[1] = parts[1].replace(PerunConstants.GROUP_NAME_MEMBERS, "");
 			}
 
 			String gname = parts[0];
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/GroupNamesSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/GroupNamesSource.java
index fa5db66a4..6a1dba456 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/GroupNamesSource.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/GroupNamesSource.java
@@ -1,6 +1,7 @@
 package cz.muni.ics.oidc.server.claims.sources;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import cz.muni.ics.oidc.PerunConstants;
 import cz.muni.ics.oidc.models.Facility;
 import cz.muni.ics.oidc.models.Group;
 import cz.muni.ics.oidc.server.claims.ClaimSource;
@@ -25,8 +26,6 @@ import java.util.Set;
 @Slf4j
 public class GroupNamesSource extends ClaimSource {
 
-	protected static final String MEMBERS = "members";
-
 	public GroupNamesSource(ClaimSourceInitContext ctx) {
 		super(ctx);
 		log.debug("{} - initialized", getClaimName());
@@ -57,8 +56,8 @@ public class GroupNamesSource extends ClaimSource {
 		Map<Long, String> idToNameMap = new HashMap<>();
 		userGroups.forEach(g -> {
 			String uniqueName = g.getUniqueGroupName();
-			if (trimMembers && StringUtils.hasText(uniqueName) && MEMBERS.equals(g.getName())) {
-				uniqueName = uniqueName.replace(':' + MEMBERS, "");
+			if (trimMembers && StringUtils.hasText(uniqueName) && PerunConstants.GROUP_NAME_MEMBERS.equals(g.getName())) {
+				uniqueName = uniqueName.replace(':' + PerunConstants.GROUP_NAME_MEMBERS, "");
 				g.setUniqueGroupName(uniqueName);
 			}
 
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/connectors/PerunConnectorRpc.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/connectors/PerunConnectorRpc.java
index e590c9acb..0f02c177b 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/connectors/PerunConnectorRpc.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/connectors/PerunConnectorRpc.java
@@ -64,6 +64,7 @@ public class PerunConnectorRpc implements InitializingBean {
     public static final String PARAM_EXT_LOGIN = "extLogin";
     public static final String PARAM_EXT_SOURCE_NAME = "extSourceName";
     public static final String PARAM_SHORT_NAME = "shortName";
+    public static final String PARAM_NAME = "name";
 
     // METHODS
 
@@ -94,6 +95,7 @@ public class PerunConnectorRpc implements InitializingBean {
     public static final String METHOD_GET_RICH_GROUPS_ASSIGNED_TO_RESOURCE_WITH_ATTRIBUTES_BY_NAMES =
             "getRichGroupsAssignedToResourceWithAttributesByNames";
     public static final String METHOD_GET_USER_EXT_SOURCES = "getUserExtSources";
+    public static final String METHOD_GET_GROUP_BY_NAME = "getGroupByName";
 
     // VARIABLES
 
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/FiltersUtils.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/FiltersUtils.java
index cf03b9c18..212c678ac 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/FiltersUtils.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/FiltersUtils.java
@@ -1,16 +1,14 @@
 package cz.muni.ics.oidc.server.filters;
 
+import com.google.common.net.HttpHeaders;
 import cz.muni.ics.oauth2.model.ClientDetailsEntity;
 import cz.muni.ics.oauth2.model.DeviceCode;
 import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
-import cz.muni.ics.oidc.models.Facility;
-import cz.muni.ics.oidc.models.PerunAttributeValue;
 import cz.muni.ics.oidc.models.PerunUser;
 import cz.muni.ics.oidc.saml.SamlProperties;
 import cz.muni.ics.oidc.server.adapters.PerunAdapter;
-import cz.muni.ics.oidc.server.configurations.FacilityAttrsConfig;
+import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
 import cz.muni.ics.oidc.web.controllers.ControllerUtils;
-import cz.muni.ics.oidc.web.controllers.PerunUnapprovedRegistrationController;
 import cz.muni.ics.openid.connect.request.OAuth2RequestFactory;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.providers.ExpiringUsernameAuthenticationToken;
@@ -19,8 +17,10 @@ import org.springframework.util.StringUtils;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 import java.util.AbstractMap;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -256,8 +256,8 @@ public class FiltersUtils {
 	 * @param req request wrapper object
 	 * @return Rebuilt URL.
 	 */
-	public static String buildRequestURL(HttpServletRequest req) {
-		return buildRequestURL(req, null);
+	public static String reconstructRequestUrl(HttpServletRequest req) {
+		return reconstructRequestUrl(req, null);
 	}
 
 	/**
@@ -266,7 +266,7 @@ public class FiltersUtils {
 	 * @param additionalParams parameters to be added
 	 * @return Rebuilt URL.
 	 */
-	public static String buildRequestURL(HttpServletRequest req, Map<String, String> additionalParams) {
+	public static String reconstructRequestUrl(HttpServletRequest req, Map<String, String> additionalParams) {
 		String returnURL = req.getRequestURL().toString();
 
 		if (req.getQueryString() != null) {
@@ -287,75 +287,6 @@ public class FiltersUtils {
 		return returnURL;
 	}
 
-	/**
-	 * Redirect user to the unapproved page.
-	 * @param base Base URL
-	 * @param response response object
-	 * @param clientId identifier of the service
-	 */
-	public static void redirectUnapproved(String base, HttpServletResponse response, String clientId, String redirectMapping)
-	{
-		// cannot register, redirect to unapproved
-		Map<String, String> params = new HashMap<>();
-		if (clientId != null) {
-			params.put("client_id", clientId);
-		}
-
-		String redirectUrl = ControllerUtils.createRedirectUrl(base, redirectMapping, params);
-		response.reset();
-		response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-		response.setHeader("Location", redirectUrl);
-	}
-
-	/**
-	 * Redirect user to the correct page when cannot access the service based on membership.
-	 * @param base base URL
-	 * @param response Response object
-	 * @param facility Facility representing the client
-	 * @param user User accessing the service
-	 * @param clientIdentifier ClientID
-	 * @param facilityAttrsConfig Config object for facility attributes
-	 * @param facilityAttributes Actual facility attributes
-	 * @param perunAdapter Adapter to call Perun
-	 */
-	public static void redirectUserCannotAccess(String base,
-												HttpServletResponse response,
-												Facility facility,
-												PerunUser user,
-												String clientIdentifier,
-												FacilityAttrsConfig facilityAttrsConfig,
-												PerunAdapter perunAdapter,
-												String redirectUrl)
-	{
-		Map<String, PerunAttributeValue> facilityAttributes = perunAdapter.getFacilityAttributeValues(
-				facility, facilityAttrsConfig.getMembershipAttrNames());
-		if (facilityAttributes.get(facilityAttrsConfig.getAllowRegistrationAttr()).valueAsBoolean()) {
-			boolean canRegister = perunAdapter.getAdapterRpc().groupWhereCanRegisterExists(facility);
-			if (canRegister) {
-				PerunAttributeValue customRegUrlAttr = facilityAttributes.get(facilityAttrsConfig.getRegistrationURLAttr());
-				if (customRegUrlAttr != null && customRegUrlAttr.valueAsString() != null) {
-					String customRegUrl = facilityAttributes.get(facilityAttrsConfig.getRegistrationURLAttr()).valueAsString();
-					customRegUrl = validateUrl(customRegUrl);
-					if (customRegUrl != null) {
-						// redirect to custom registration URL
-						FiltersUtils.redirectToCustomRegUrl(response, customRegUrl, user);
-						return;
-					}
-				}
-
-				if (facilityAttributes.get(facilityAttrsConfig.getDynamicRegistrationAttr()).valueAsBoolean()) {
-					// redirect to registration form
-					FiltersUtils.redirectToRegistrationForm(base, response, clientIdentifier, facility, user);
-					return;
-				}
-			}
-		}
-
-		// cannot register, redirect to unapproved
-		log.debug("user cannot register to obtain access, redirecting user '{}' to unapproved page", user);
-		FiltersUtils.redirectUnapproved(base, response, clientIdentifier, redirectUrl);
-	}
-
 	public static String fillStringMandatoryProperty(String propertyName,
 													 String filterName,
 													 AuthProcFilterInitContext params) {
@@ -376,28 +307,7 @@ public class FiltersUtils {
 		return filled;
 	}
 
-	private static void redirectToRegistrationForm(String base, HttpServletResponse response,
-												   String clientIdentifier, Facility facility, PerunUser user) {
-		Map<String, String> params = new HashMap<>();
-		params.put("client_id", clientIdentifier);
-		params.put("facility_id", facility.getId().toString());
-		params.put("user_id", String.valueOf(user.getId()));
-		String redirectUrl = ControllerUtils.createRedirectUrl(base,
-				PerunUnapprovedRegistrationController.REGISTRATION_CONTINUE_MAPPING, params);
-		log.debug("redirecting user '{}' to the registration form URL: {}", user, redirectUrl);
-		response.reset();
-		response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-		response.setHeader("Location", redirectUrl);
-	}
-
-	private static void redirectToCustomRegUrl(HttpServletResponse response, String customRegUrl, PerunUser user) {
-		log.debug("redirecting user '{}' to the custom registration URL: {}", user, customRegUrl);
-		response.reset();
-		response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-		response.setHeader("Location", customRegUrl);
-	}
-
-	private static String validateUrl(String customRegUrl) {
+	public static String validateUrl(String customRegUrl) {
 		return (customRegUrl == null || customRegUrl.isEmpty()) ? null : customRegUrl;
 	}
 
@@ -423,4 +333,5 @@ public class FiltersUtils {
 	public static String getClientId(HttpServletRequest req) {
 		return req.getParameter(AuthProcFilterConstants.PARAM_CLIENT_ID);
 	}
+
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/IsEligibleFilter.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/IsEligibleFilter.java
index 358586c95..e3dd1e739 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/IsEligibleFilter.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/IsEligibleFilter.java
@@ -1,6 +1,7 @@
 package cz.muni.ics.oidc.server.filters.impl;
 
 import cz.muni.ics.oidc.BeanUtil;
+import cz.muni.ics.oidc.RedirectUtils;
 import cz.muni.ics.oidc.exceptions.ConfigurationException;
 import cz.muni.ics.oidc.saml.ExtendedOAuth2Exception;
 import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
@@ -9,16 +10,12 @@ import cz.muni.ics.oidc.server.filters.AuthProcFilterCommonVars;
 import cz.muni.ics.oidc.server.filters.AuthProcFilterConstants;
 import cz.muni.ics.oidc.server.filters.AuthProcFilterInitContext;
 import cz.muni.ics.oidc.server.filters.FiltersUtils;
-import cz.muni.ics.oidc.web.controllers.ControllerUtils;
-import cz.muni.ics.oidc.web.controllers.PerunUnapprovedController;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.http.HttpHeaders;
 import org.springframework.security.saml.SAMLCredential;
 import org.springframework.util.StringUtils;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
@@ -26,6 +23,13 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.SESS_ATTR_BUTTON_TRANSLATION;
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.SESS_ATTR_CLIENT;
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.SESS_ATTR_CONTACT_TRANSLATION;
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.SESS_ATTR_HEADER_TRANSLATION;
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.SESS_ATTR_TARGET;
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.SESS_ATTR_TEXT_TRANSLATION;
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.UNAUTHORIZED_IS_ELIGIBLE_MAPPING;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT_NONE;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.REDIRECT_URI;
@@ -50,16 +54,6 @@ public class IsEligibleFilter extends AuthProcFilter {
 
     public static final String APPLIED = "APPLIED_" + IsEligibleFilter.class.getSimpleName();
 
-    public final static String DEFAULT_HEADER_TRANSLATION_KEY = "403_is_eligible_default_header_text";
-    public final static String DEFAULT_TEXT_TRANSLATION_KEY = "403_is_eligible_default_text";
-    public final static String DEFAULT_BUTTON_TRANSLATION_KEY = "403_is_eligible_default_button_text";
-    public final static String DEFAULT_CONTACT_TRANSLATION_KEY = "403_is_eligible_default_contact_text";
-
-    public static final String HEADER_TRANSLATION = "header_translation";
-    public static final String TEXT_TRANSLATION = "text_translation";
-    public static final String BUTTON_TRANSLATION = "button_translation";
-    public static final String CONTACT_TRANSLATION = "contact_translation";
-
     /* CONFIGURATION PROPERTIES */
     private static final String SAML_ATTRIBUTE = "samlAttribute";
     private static final String TRIGGER_SCOPE = "triggerScope";
@@ -78,6 +72,11 @@ public class IsEligibleFilter extends AuthProcFilter {
 
     /* END OF CONFIGURATION PROPERTIES */
 
+    private static final String DEFAULT_HEADER_TRANSLATION_KEY = "403_is_eligible_default_header_text";
+    private static final String DEFAULT_TEXT_TRANSLATION_KEY = "403_is_eligible_default_text";
+    private static final String DEFAULT_BUTTON_TRANSLATION_KEY = "403_is_eligible_default_button_text";
+    private static final String DEFAULT_CONTACT_TRANSLATION_KEY = "403_is_eligible_default_contact_text";
+
     private final String eligibleLastSeenSAMLAttributeName;
     private final String triggerScope;
     private final int validityPeriod;
@@ -112,6 +111,7 @@ public class IsEligibleFilter extends AuthProcFilter {
             }
         }
 
+
         this.oldValueHeaderTranslationKey = FiltersUtils.fillStringProperty(
                 OLD_VALUE_HEADER_TRANSLATION_KEY, ctx, DEFAULT_HEADER_TRANSLATION_KEY);
         this.oldValueTextTranslationKey = FiltersUtils.fillStringProperty(
@@ -185,17 +185,22 @@ public class IsEligibleFilter extends AuthProcFilter {
         }
 
         if (PROMPT_NONE.equals(req.getParameter(PROMPT))) {
-            throw new ExtendedOAuth2Exception("access_denied", "User does not meet the eligibility rules", req.getParameter(REDIRECT_URI), req.getParameter(STATE));
+            throw new ExtendedOAuth2Exception(
+                "access_denied", "User does not meet the eligibility rules",
+                req.getParameter(REDIRECT_URI), req.getParameter(STATE)
+            );
         }
 
-        HttpSession sess = req.getSession(true);
-        sess.setAttribute(HEADER_TRANSLATION, headerKey);
-        sess.setAttribute(TEXT_TRANSLATION, textKey);
-        sess.setAttribute(BUTTON_TRANSLATION, buttonKey);
-        sess.setAttribute(CONTACT_TRANSLATION, contactKey);
+        Map<String, Object> sessionAttributes = new HashMap<>();
+        sessionAttributes.put(SESS_ATTR_HEADER_TRANSLATION, headerKey);
+        sessionAttributes.put(SESS_ATTR_TEXT_TRANSLATION, textKey);
+        sessionAttributes.put(SESS_ATTR_BUTTON_TRANSLATION, buttonKey);
+        sessionAttributes.put(SESS_ATTR_CONTACT_TRANSLATION, contactKey);
+        sessionAttributes.put(SESS_ATTR_CLIENT, params.getClient());
+
 
         log.debug("{} - attribute '{}' value is invalid, stop user at this point", filterName, eligibleLastSeenTimestamp);
-        this.redirect(req, res);
+        this.redirect(req, res, sessionAttributes);
         return false;
     }
 
@@ -204,19 +209,11 @@ public class IsEligibleFilter extends AuthProcFilter {
         return false;
     }
 
-    private void redirect(HttpServletRequest req, HttpServletResponse res) {
-        Map<String, String> params = new HashMap<>();
-
-        String targetURL = FiltersUtils.buildRequestURL(req,
+    private void redirect(HttpServletRequest req, HttpServletResponse res, Map<String, Object> sessionAttributes) {
+        String targetURL = FiltersUtils.reconstructRequestUrl(req,
                 Collections.singletonMap(AuthProcFilterConstants.PARAM_PROMPT, "login"));
-        params.put(AuthProcFilterConstants.PARAM_TARGET, targetURL);
-
-        String redirectUrl = ControllerUtils.createRedirectUrl(config.getConfigBean().getIssuer(),
-                PerunUnapprovedController.UNAPPROVED_IS_ELIGIBLE_MAPPING, params);
-        log.debug("{} - redirecting user to unapproved: URL '{}'", filterName, redirectUrl);
-        res.reset();
-        res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-        res.setHeader(HttpHeaders.LOCATION, redirectUrl);
+        sessionAttributes.put(SESS_ATTR_TARGET, targetURL);
+        RedirectUtils.redirectInternal(req, res, config, UNAUTHORIZED_IS_ELIGIBLE_MAPPING, sessionAttributes);
     }
 
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunAuthorizationFilter.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunAuthorizationFilter.java
index 40929bae0..5de5efc0c 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunAuthorizationFilter.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunAuthorizationFilter.java
@@ -1,5 +1,6 @@
 package cz.muni.ics.oidc.server.filters.impl;
 
+import cz.muni.ics.oauth2.model.ClientDetailsEntity;
 import cz.muni.ics.oidc.exceptions.ConfigurationException;
 import cz.muni.ics.oidc.models.Facility;
 import cz.muni.ics.oidc.models.PerunAttributeValue;
@@ -11,14 +12,17 @@ import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
 import cz.muni.ics.oidc.server.filters.AuthProcFilter;
 import cz.muni.ics.oidc.server.filters.AuthProcFilterCommonVars;
 import cz.muni.ics.oidc.server.filters.AuthProcFilterInitContext;
-import cz.muni.ics.oidc.server.filters.FiltersUtils;
+import cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController;
 import lombok.extern.slf4j.Slf4j;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
 import java.util.Map;
 
-import static cz.muni.ics.oidc.web.controllers.PerunUnapprovedController.UNAPPROVED_AUTHORIZATION;
+import static cz.muni.ics.oidc.RedirectUtils.redirectExternal;
+import static cz.muni.ics.oidc.RedirectUtils.redirectInternal;
+import static cz.muni.ics.oidc.server.filters.FiltersUtils.validateUrl;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT_NONE;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.REDIRECT_URI;
@@ -73,8 +77,7 @@ public class PerunAuthorizationFilter extends AuthProcFilter {
 			return true;
 		}
 
-		return this.decideAccess(facility, user, req, res, params.getClientIdentifier(),
-				perunAdapter, facilityAttrsConfig, accessControlDisabledAttr);
+		return this.decideAccess(req, res, facility, user, params.getClient());
 	}
 
 	@Override
@@ -82,14 +85,11 @@ public class PerunAuthorizationFilter extends AuthProcFilter {
 		return false;
 	}
 
-	private boolean decideAccess(Facility facility,
+	private boolean decideAccess(HttpServletRequest req,
+								 HttpServletResponse res,
+								 Facility facility,
 								 PerunUser user,
-								 HttpServletRequest req,
-								 HttpServletResponse response,
-								 String clientIdentifier,
-								 PerunAdapter perunAdapter,
-								 FacilityAttrsConfig facilityAttrsConfig,
-								 String accessControlDisabledAttr)
+								 ClientDetailsEntity client)
 	{
 		Map<String, PerunAttributeValue> facilityAttributes = perunAdapter.getFacilityAttributeValues(
 				facility, facilityAttrsConfig.getMembershipAttrNames());
@@ -103,14 +103,107 @@ public class PerunAuthorizationFilter extends AuthProcFilter {
 			log.info("{} - user allowed to access the service", getFilterName());
 			return true;
 		} else {
+			log.info("{} - user is not allowed to access the service due to membership check", getFilterName());
 			if (PROMPT_NONE.equals(req.getParameter(PROMPT))) {
 				throw new ExtendedOAuth2Exception("interaction_required",
-						"User needs to make further steps to be able to access the service.", req.getParameter(REDIRECT_URI), req.getParameter(STATE));
+						"User needs to make further steps to be able to access the service.",
+						req.getParameter(REDIRECT_URI),
+						req.getParameter(STATE));
 			}
-			FiltersUtils.redirectUserCannotAccess(config.getConfigBean().getIssuer(), response, facility, user,
-					clientIdentifier, facilityAttrsConfig, perunAdapter, UNAPPROVED_AUTHORIZATION);
+
+			redirectUserCannotAccess(req, res, facility, user, client);
 			return false;
 		}
 	}
 
+	private void redirectUserCannotAccess(HttpServletRequest req,
+										  HttpServletResponse res,
+										  Facility facility,
+										  PerunUser user,
+										  ClientDetailsEntity client)
+	{
+		Map<String, PerunAttributeValue> facilityAttributes = perunAdapter.getFacilityAttributeValues(
+			facility, facilityAttrsConfig.getMembershipAttrNames()
+		);
+
+		if (allowsRegistration(facilityAttributes)) {
+			log.debug("{} - service '{}' allows registration", getFilterName(), client.getClientName());
+			PerunAttributeValue customRegUrlAttr = facilityAttributes.get(facilityAttrsConfig.getRegistrationURLAttr());
+			if (customRegUrlAttr != null && customRegUrlAttr.valueAsString() != null) {
+				String customRegUrl = facilityAttributes.get(facilityAttrsConfig.getRegistrationURLAttr())
+						.valueAsString();
+				log.debug("{} - service '{}' has a custom registration URL", getFilterName(), customRegUrl);
+				customRegUrl = validateUrl(customRegUrl);
+				if (customRegUrl != null) {
+					log.debug(
+						"{} - service '{}' registration URL '{}' passed validation. Redirecting user there.",
+						getFilterName(), client.getClientName(), customRegUrl
+					);
+					redirectExternal(res, customRegUrl);
+					return;
+				} else {
+					log.debug(
+						"{} - service '{}' registration URL did not pass validation. Redirecting to unauthorized.",
+						getFilterName(), client.getClientName()
+					);
+				}
+			} else {
+				boolean groupsForRegistrationExist = perunAdapter.getAdapterRpc().groupWhereCanRegisterExists(facility);
+				boolean dynamicRegistrationAllowed =
+						facilityAttributes.get(facilityAttrsConfig.getDynamicRegistrationAttr()).valueAsBoolean();
+				if (groupsForRegistrationExist && dynamicRegistrationAllowed) {
+					log.debug(
+						"{} - service '{}' allows dynamic registration and some registration units exist. Redirecting user to the form to select VO (and GROUP)",
+						getFilterName(), client.getClientName()
+					);
+					redirectChoosePerunEntities(req, res, user, client, facility);
+					return;
+				} else {
+					log.debug(
+							"{} - service '{}' has no registration option for user - units exist: {}, dynamic reg. allowed: {}",
+							getFilterName(), client.getClientName(), groupsForRegistrationExist, dynamicRegistrationAllowed
+					);
+				}
+			}
+		}
+
+		// cannot register, redirect to unauthorized
+		log.debug("User cannot register to obtain access (facility did not enable this), redirecting user '{}' to unauthorized page", user);
+		redirectUnauthorized(req, res, user, client);
+	}
+
+	private boolean allowsRegistration(Map<String, PerunAttributeValue> facilityAttributes) {
+		return facilityAttributes.getOrDefault(facilityAttrsConfig.getAllowRegistrationAttr(), null) != null
+			&& facilityAttributes.get(facilityAttrsConfig.getAllowRegistrationAttr()).valueAsBoolean();
+	}
+
+	private void redirectChoosePerunEntities(HttpServletRequest req,
+											 HttpServletResponse res,
+											 PerunUser user,
+											 ClientDetailsEntity client,
+											 Facility facility)
+	{
+		log.trace(
+			"{} - Redirecting user '{}' to the internal form to choose VO (and possibly GROUP)",
+			getFilterName(), user
+		);
+		Map<String, Object> sessAttributes = new HashMap<>();
+		sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_USER, user);
+		sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_CLIENT, client);
+		sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_FACILITY, facility);
+		redirectInternal(req, res, config, PerunUnauthorizedController.UNAUTHORIZED_REGISTER_NOTIFY_ACTION_REQUIRED_MAPPING, sessAttributes);
+	}
+
+	private void redirectUnauthorized(HttpServletRequest req,
+									HttpServletResponse res,
+									PerunUser user,
+									ClientDetailsEntity client)
+	{
+		log.debug("{} - Redirecting user '{}' to the internal page informing about unauthorized access",
+				getFilterName(), user);
+		Map<String, Object> sessAttributes = new HashMap<>();
+		sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_CLIENT, client);
+		redirectInternal(req, res, config, PerunUnauthorizedController.UNAUTHORIZED_AUTHORIZATION_MAPPING, sessAttributes);
+	}
+
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunEnsureVoMember.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunEnsureVoMember.java
index 12cb21e60..09c28bda5 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunEnsureVoMember.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunEnsureVoMember.java
@@ -1,6 +1,8 @@
 package cz.muni.ics.oidc.server.filters.impl;
 
+import cz.muni.ics.oauth2.model.ClientDetailsEntity;
 import cz.muni.ics.oidc.PerunConstants;
+import cz.muni.ics.oidc.RedirectUtils;
 import cz.muni.ics.oidc.exceptions.ConfigurationException;
 import cz.muni.ics.oidc.models.Facility;
 import cz.muni.ics.oidc.models.PerunAttributeValue;
@@ -12,10 +14,8 @@ import cz.muni.ics.oidc.server.filters.AuthProcFilterCommonVars;
 import cz.muni.ics.oidc.server.filters.AuthProcFilterInitContext;
 import cz.muni.ics.oidc.server.filters.FiltersUtils;
 import cz.muni.ics.oidc.web.controllers.ControllerUtils;
-import cz.muni.ics.oidc.web.controllers.PerunUnapprovedController;
-import cz.muni.ics.oidc.web.controllers.RegistrationController;
+import cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.http.HttpHeaders;
 import org.springframework.util.StringUtils;
 
 import javax.servlet.http.HttpServletRequest;
@@ -25,6 +25,9 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.SESS_ATTR_CLIENT;
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.SESS_ATTR_TARGET;
+import static cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController.UNAUTHORIZED_ENSURE_VO_MAPPING;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT_NONE;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.REDIRECT_URI;
@@ -104,9 +107,12 @@ public class PerunEnsureVoMember extends AuthProcFilter {
             return true;
         } else {
             if (PROMPT_NONE.equals(req.getParameter(PROMPT))) {
-                throw new ExtendedOAuth2Exception("interaction_required", "User needs to register to VO", req.getParameter(REDIRECT_URI), req.getParameter(STATE));
+                throw new ExtendedOAuth2Exception(
+                        "interaction_required", "User needs to register to VO",
+                        req.getParameter(REDIRECT_URI), req.getParameter(STATE)
+                );
             }
-            redirect(res, getLoginUrl(facility.getId()), voShortName);
+            redirect(req, res, getLoginUrl(facility.getId()), voShortName, params.getClient());
             return false;
         }
     }
@@ -116,15 +122,31 @@ public class PerunEnsureVoMember extends AuthProcFilter {
         return false;
     }
 
-    private void redirect(HttpServletResponse response, PerunAttributeValue loginUrlAttr, String voShortName) {
+    @Override
+    public String toString() {
+        return "PerunEnsureVoMember{" +
+                "voDefsAttr='" + voDefsAttr + '\'' +
+                ", loginUrlAttr='" + loginUrlAttr + '\'' +
+                '}';
+    }
+
+    private void redirect(HttpServletRequest req,
+                          HttpServletResponse res,
+                          PerunAttributeValue loginUrlAttr,
+                          String voShortName,
+                          ClientDetailsEntity client)
+    {
         String loginUrl = null;
         if (loginUrlAttr != null && StringUtils.hasText(loginUrlAttr.valueAsString())) {
             loginUrl = loginUrlAttr.valueAsString();
         }
+
         if (StringUtils.hasText(voShortName) && perunAdapter.getAdapterRpc().hasApplicationForm(voShortName)) {
-            redirectDirectly(response, loginUrl, voShortName);
+            log.debug("{} - redirecting user to registration page for VO '{}'", getFilterName(), voShortName);
+            redirectRegistration(req, res, loginUrl, voShortName, client);
         } else {
-            redirectUnapproved(response);
+            log.debug("{} - redirecting user to unauthorized", getFilterName());
+            redirectUnauthorized(req, res, client);
         }
     }
 
@@ -136,23 +158,18 @@ public class PerunEnsureVoMember extends AuthProcFilter {
     }
 
     private PerunAttributeValue getVoDefsAttrValue(PerunAttributeValue attrValue) {
-        if (attrValue == null) {
-            return null;
-        } else if (attrValue.valueAsJson().isArray() && attrValue.valueAsJson().size() < 1) {
+        if (attrValue == null || (attrValue.valueAsJson().isArray() && attrValue.valueAsJson().size() < 1)) {
             return null;
         }
         return attrValue;
     }
 
-    @Override
-    public String toString() {
-        return "PerunEnsureVoMember{" +
-                "voDefsAttr='" + voDefsAttr + '\'' +
-                ", loginUrlAttr='" + loginUrlAttr + '\'' +
-                '}';
-    }
-
-    private void redirectDirectly(HttpServletResponse res, String loginUrl, String voShortName) {
+    private void redirectRegistration(HttpServletRequest req,
+                                      HttpServletResponse res,
+                                      String loginUrl,
+                                      String voShortName,
+                                      ClientDetailsEntity client)
+    {
         String registrarUrl = perunOidcConfig.getRegistrarUrl();
         Map<String, String> params = new HashMap<>();
         params.put(PerunConstants.REGISTRAR_PARAM_VO, voShortName);
@@ -162,26 +179,24 @@ public class PerunEnsureVoMember extends AuthProcFilter {
             params.put(PerunConstants.REGISTRAR_TARGET_EXTENDED, loginUrl);
         }
         String target = ControllerUtils.createUrl(registrarUrl, params);
+        RedirectUtils.redirectInternal(
+                req,
+                res,
+                perunOidcConfig,
+                PerunUnauthorizedController.UNAUTHORIZED_REGISTER_NOTIFY_ACTION_REQUIRED_MAPPING,
+                Map.of(SESS_ATTR_TARGET, target, SESS_ATTR_CLIENT, client)
+        );
 
-        String url = ControllerUtils.constructRequestUrl(perunOidcConfig, RegistrationController.CONTINUE_DIRECT_MAPPING);
-        params.clear();
-        params.put(RegistrationController.PARAM_TARGET, target);
-
-        String redirectUrl = ControllerUtils.createUrl(url, params);
-        log.debug("{} - redirecting user to '{}'", getFilterName(), redirectUrl);
-        res.reset();
-        res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-        res.setHeader(HttpHeaders.LOCATION, redirectUrl);
     }
 
-    private void redirectUnapproved(HttpServletResponse res) {
-        String redirectUrl = ControllerUtils.constructRequestUrl(perunOidcConfig,
-                PerunUnapprovedController.UNAPPROVED_ENSURE_VO_MAPPING);
-
-        log.debug("{} - redirecting user to '{}'", getFilterName(), redirectUrl);
-        res.reset();
-        res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-        res.setHeader(HttpHeaders.LOCATION, redirectUrl);
+    private void redirectUnauthorized(HttpServletRequest req, HttpServletResponse res, ClientDetailsEntity client) {
+        RedirectUtils.redirectInternal(
+                req,
+                res,
+                perunOidcConfig,
+                UNAUTHORIZED_ENSURE_VO_MAPPING,
+                Map.of(SESS_ATTR_CLIENT, client)
+        );
     }
 
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunIsTestSpFilter.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunIsTestSpFilter.java
index 96e8bfc71..b91d6e3ba 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunIsTestSpFilter.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunIsTestSpFilter.java
@@ -1,5 +1,6 @@
 package cz.muni.ics.oidc.server.filters.impl;
 
+import cz.muni.ics.oidc.RedirectUtils;
 import cz.muni.ics.oidc.exceptions.ConfigurationException;
 import cz.muni.ics.oidc.models.Facility;
 import cz.muni.ics.oidc.models.PerunAttributeValue;
@@ -11,18 +12,18 @@ import cz.muni.ics.oidc.server.filters.AuthProcFilterCommonVars;
 import cz.muni.ics.oidc.server.filters.AuthProcFilterInitContext;
 import cz.muni.ics.oidc.server.filters.FiltersUtils;
 import cz.muni.ics.oidc.web.controllers.ControllerUtils;
-import cz.muni.ics.oidc.web.controllers.IsTestSpController;
+import cz.muni.ics.oidc.web.controllers.TestSpWarningController;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.HttpHeaders;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
-import java.util.HashMap;
 import java.util.Map;
 
 import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_TARGET;
-import static cz.muni.ics.oidc.web.controllers.IsTestSpController.IS_TEST_SP_APPROVED_SESS;
+import static cz.muni.ics.oidc.web.controllers.TestSpWarningController.SESS_ATTR_IS_TEST_SP_APPROVED;
+import static cz.muni.ics.oidc.web.controllers.TestSpWarningController.MAPPING;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT_NONE;
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.REDIRECT_URI;
@@ -77,7 +78,7 @@ public class PerunIsTestSpFilter extends AuthProcFilter {
                 throw new ExtendedOAuth2Exception("interaction_required",
                         "User needs to acknowledge accessing a test service", req.getParameter(REDIRECT_URI), req.getParameter(STATE));
             }
-            this.redirect(req, res);
+            redirect(req, res);
             return false;
         }
         log.debug("{} - service is not testing, let user access it", getFilterName());
@@ -94,24 +95,18 @@ public class PerunIsTestSpFilter extends AuthProcFilter {
             return false;
         }
         boolean approved = false;
-        if (req.getSession().getAttribute(IS_TEST_SP_APPROVED_SESS) != null) {
-            approved = (Boolean) req.getSession().getAttribute(IS_TEST_SP_APPROVED_SESS);
-            req.getSession().removeAttribute(IS_TEST_SP_APPROVED_SESS);
+        if (req.getSession().getAttribute(SESS_ATTR_IS_TEST_SP_APPROVED) != null) {
+            approved = (Boolean) req.getSession().getAttribute(SESS_ATTR_IS_TEST_SP_APPROVED);
+            req.getSession().removeAttribute(SESS_ATTR_IS_TEST_SP_APPROVED);
         }
         return approved;
     }
 
     private void redirect(HttpServletRequest req, HttpServletResponse res) {
-        String targetURL = FiltersUtils.buildRequestURL(req);
+        String target = FiltersUtils.reconstructRequestUrl(req);
 
-        Map<String, String> params = new HashMap<>();
-        params.put(PARAM_TARGET, targetURL);
-        String redirectUrl = ControllerUtils.createRedirectUrl(config.getConfigBean().getIssuer(),
-                IsTestSpController.MAPPING, params);
-        log.debug("{} - redirecting user to testSP warning page: {}", getFilterName(), redirectUrl);
-        res.reset();
-        res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-        res.setHeader(HttpHeaders.LOCATION, redirectUrl);
+        log.debug("{} - redirecting user to testSP warning page: {}", getFilterName(), target);
+        RedirectUtils.redirectInternal(req, res, config, MAPPING, Map.of(TestSpWarningController.SESS_ATTR_TARGET, target));
     }
 
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunLogIdentityFilter.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunLogIdentityFilter.java
index 7fe7bd203..01125beb1 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunLogIdentityFilter.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/PerunLogIdentityFilter.java
@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletResponse;
 @Slf4j
 public class PerunLogIdentityFilter extends AuthProcFilter {
 
+    public static final String EMPTY = "_empty";
     private final String userIdentifierAttr;
 
     public PerunLogIdentityFilter(AuthProcFilterInitContext params) throws ConfigurationException {
@@ -37,10 +38,10 @@ public class PerunLogIdentityFilter extends AuthProcFilter {
         SAMLCredential samlCredential = FiltersUtils.getSamlCredential(req);
 
         Long id = -1L;
-        String name = "_empty";
-        String identifier = "_empty";
-        String clientName = "_empty";
-        String clientId = "_empty";
+        String name = EMPTY;
+        String identifier = EMPTY;
+        String clientName = EMPTY;
+        String clientId = EMPTY;
         if (user != null) {
             name = user.getFirstName() + ' ' + user.getLastName();
             id = user.getId();
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/ProxyStatisticsFilter.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/ProxyStatisticsFilter.java
index 82db0e3d6..15034f6f9 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/ProxyStatisticsFilter.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/ProxyStatisticsFilter.java
@@ -127,7 +127,7 @@ public class ProxyStatisticsFilter extends AuthProcFilter {
 			log.warn("{} - skip execution: no authenticating idp identifier provided", getFilterName());
 			return true;
 		} else if (!StringUtils.hasText(samlCredential.getAttributeAsString(idpNameAttributeName))) {
-			log.warn("{} - skip execution: no authenticating idp identifier provided", getFilterName());
+			log.warn("{} - skip execution: no authenticating idp name provided", getFilterName());
 			return true;
 		}
 
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/ValidUserFilter.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/ValidUserFilter.java
index fb9d418b1..9c11d8164 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/ValidUserFilter.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/ValidUserFilter.java
@@ -1,9 +1,13 @@
 package cz.muni.ics.oidc.server.filters.impl;
 
+import cz.muni.ics.oauth2.model.ClientDetailsEntity;
+import cz.muni.ics.oidc.RedirectUtils;
 import cz.muni.ics.oidc.exceptions.ConfigurationException;
 import cz.muni.ics.oidc.models.Facility;
+import cz.muni.ics.oidc.models.Group;
 import cz.muni.ics.oidc.models.PerunAttributeValue;
 import cz.muni.ics.oidc.models.PerunUser;
+import cz.muni.ics.oidc.models.Vo;
 import cz.muni.ics.oidc.saml.ExtendedOAuth2Exception;
 import cz.muni.ics.oidc.server.adapters.PerunAdapter;
 import cz.muni.ics.oidc.server.configurations.FacilityAttrsConfig;
@@ -12,13 +16,15 @@ import cz.muni.ics.oidc.server.filters.AuthProcFilter;
 import cz.muni.ics.oidc.server.filters.AuthProcFilterCommonVars;
 import cz.muni.ics.oidc.server.filters.AuthProcFilterInitContext;
 import cz.muni.ics.oidc.server.filters.FiltersUtils;
-import cz.muni.ics.oidc.web.controllers.PerunUnapprovedController;
+import cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.util.StringUtils;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT;
@@ -60,7 +66,6 @@ public class ValidUserFilter extends AuthProcFilter {
 	private static final String TEST_ENV_VOS = "testEnvVos";
 	private static final String PROD_ENV_GROUPS = "prodEnvGroups";
 	private static final String PROD_ENV_VOS = "prodEnvVos";
-
 	private final Set<Long> allEnvGroups;
 	private final Set<Long> allEnvVos;
 	private final Set<Long> testEnvGroups;
@@ -111,42 +116,27 @@ public class ValidUserFilter extends AuthProcFilter {
 			return true;
 		}
 
-		if (!checkMemberValidInGroupsAndVos(user, allEnvVos, allEnvGroups)) {
-			if (PROMPT_NONE.equals(req.getParameter(PROMPT))) {
-				throw new ExtendedOAuth2Exception("interaction_required",
-						"User needs to make further steps to be able to access the service.", req.getParameter(REDIRECT_URI), req.getParameter(STATE));
-			}
-			redirectCannotAccess(res, facility, user, params.getClientIdentifier(), PerunUnapprovedController.UNAPPROVED_NOT_IN_MANDATORY_VOS_GROUPS);
-			return false;
-		}
-
-		PerunAttributeValue isTestSpAttrValue = perunAdapter.getFacilityAttributeValue(facility.getId(), facilityAttrsConfig.getTestSpAttr());
+		PerunAttributeValue isTestSpAttrValue = perunAdapter.getFacilityAttributeValue(
+				facility.getId(), facilityAttrsConfig.getTestSpAttr());
 		boolean testService = false;
 		if (isTestSpAttrValue != null) {
 			testService = isTestSpAttrValue.valueAsBoolean();
 		}
 
-		log.debug("{} - service {} in test env", getFilterName(), (testService ? "is" : "is not"));
-
-		Set<Long> vos = new HashSet<>();
-		Set<Long> groups = new HashSet<>();
-		String unapprovedMapping;
+		Set<Long> vos = new HashSet<>(allEnvVos);
+		Set<Long> groups = new HashSet<>(allEnvGroups);
 		if (testService) {
 			vos.addAll(testEnvVos);
 			groups.addAll(testEnvGroups);
-			unapprovedMapping = PerunUnapprovedController.UNAPPROVED_NOT_IN_TEST_VOS_GROUPS;
 		} else {
 			vos.addAll(prodEnvVos);
 			groups.addAll(prodEnvGroups);
-			unapprovedMapping = PerunUnapprovedController.UNAPPROVED_NOT_IN_PROD_VOS_GROUPS;
 		}
-		if (!checkMemberValidInGroupsAndVos(user, vos, groups)) {
-			log.info("{} - Redirecting to unapproved page with mapping '{}'", getFilterName(), unapprovedMapping);
-			if (PROMPT_NONE.equals(req.getParameter(PROMPT))) {
-				throw new ExtendedOAuth2Exception("interaction_required",
-						"User needs to make further steps to be able to access the service.", req.getParameter(REDIRECT_URI), req.getParameter(STATE));
-			}
-			redirectCannotAccess(res, facility, user, params.getClientIdentifier(), unapprovedMapping);
+
+		Set<String> invalidUnits = getVosAndGroupsNamesWhereUserInvalid(user, vos, groups);
+		if (!invalidUnits.isEmpty()) {
+			respondToPrompt(req);
+			redirectCannotAccess(req, res, invalidUnits, params.getClient());
 			return false;
 		}
 
@@ -154,16 +144,33 @@ public class ValidUserFilter extends AuthProcFilter {
 		return true;
 	}
 
+	private void redirectCannotAccess(HttpServletRequest req,
+									  HttpServletResponse res,
+									  Set<String> invalidUnits,
+									  ClientDetailsEntity client)
+	{
+		Map<Vo, Set<Group>> registrationUnits = perunAdapter.getAdapterRpc().getRegistrationUnits(invalidUnits);
+
+		Map<String, Object> sessParams = new HashMap<>();
+		sessParams.put(PerunUnauthorizedController.SESS_ATTR_REGISTRATION_UNITS, registrationUnits);
+		sessParams.put(PerunUnauthorizedController.SESS_ATTR_CLIENT, client);
+		RedirectUtils.redirectInternal(req, res, config, PerunUnauthorizedController.UNAUTHORIZED_NOT_IN_ENV_VOS_GROUPS_MAPPING, sessParams);
+	}
+
 	@Override
 	protected boolean oncePerSession() {
 		return false;
 	}
 
-	private void redirectCannotAccess(HttpServletResponse res, Facility facility, PerunUser user,
-									  String clientId, String mapping)
-	{
-		FiltersUtils.redirectUserCannotAccess(config.getConfigBean().getIssuer(), res, facility, user,
-				clientId, facilityAttrsConfig, perunAdapter, mapping);
+	private void respondToPrompt(HttpServletRequest req) {
+		if (PROMPT_NONE.equals(req.getParameter(PROMPT))) {
+			throw new ExtendedOAuth2Exception(
+					"interaction_required",
+					"User needs to make further steps to be able to access the service.",
+					req.getParameter(REDIRECT_URI),
+					req.getParameter(STATE)
+			);
+		}
 	}
 
 	private Set<Long> getIdsFromParam(AuthProcFilterInitContext params, String propKey) {
@@ -180,14 +187,14 @@ public class ValidUserFilter extends AuthProcFilter {
 		return result;
 	}
 
-	private boolean checkMemberValidInGroupsAndVos(PerunUser user, Set<Long> vos, Set<Long> groups) {
-		if (!perunAdapter.isValidMemberInGroupsAndVos(user.getId(), vos, groups)) {
+	private Set<String> getVosAndGroupsNamesWhereUserInvalid(PerunUser user, Set<Long> vos, Set<Long> groups) {
+		Set<String> invalidUnits = perunAdapter.getVosAndGroupsNamesWhereUserInvalid(user.getId(), vos, groups);
+		if (!invalidUnits.isEmpty()) {
 			log.info("{} - user is not member in required set of vos and groups", getFilterName());
-			log.debug("{} - user: '{}', vos: '{}', groups: '{}'",
-					getFilterName(), user.getId(), vos, groups);
-			return false;
+			log.debug("{} - user: '{}', vos: '{}', groups: '{}', invalid units: '{}",
+					getFilterName(), user.getId(), vos, groups, invalidUnits);
 		}
-		return true;
+		return invalidUnits;
 	}
 
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/AupController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/AupController.java
index 4367f0d35..07df0759a 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/AupController.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/AupController.java
@@ -41,19 +41,22 @@ public class AupController {
     public static final String RETURN_URL = "returnUrl";
     public static final String USER_ATTR = "userAttr";
 
-    private static final SimpleDateFormat formatter= new SimpleDateFormat("yyyy-MM-dd");
+    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd");
 
-    private final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance;
     private final ObjectMapper mapper = new ObjectMapper();
 
-    @Autowired
-    private PerunAdapter perunAdapter;
+    private final PerunAdapter perunAdapter;
 
-    @Autowired
-    private PerunOidcConfig perunOidcConfig;
+    private final PerunOidcConfig perunOidcConfig;
+
+    private final WebHtmlClasses htmlClasses;
 
     @Autowired
-    private WebHtmlClasses htmlClasses;
+    public AupController(PerunAdapter perunAdapter, PerunOidcConfig perunOidcConfig, WebHtmlClasses htmlClasses) {
+        this.perunAdapter = perunAdapter;
+        this.perunOidcConfig = perunOidcConfig;
+        this.htmlClasses = htmlClasses;
+    }
 
     @GetMapping(value = "/" + URL)
     public String showAup(HttpServletRequest req,
@@ -81,7 +84,7 @@ public class AupController {
                            @SessionAttribute(name = USER_ATTR) String userAupsAttrName) throws IOException
     {
         JsonNode aupsToApproveJson = mapper.readTree(newAupsString);
-        ObjectNode aupsToApproveJsonObject = new ObjectNode(jsonNodeFactory);
+        ObjectNode aupsToApproveJsonObject = new ObjectNode(JsonNodeFactory.instance);
 
         Iterator<Map.Entry<String, JsonNode>> iterator = aupsToApproveJson.fields();
         while (iterator.hasNext()) {
@@ -89,7 +92,7 @@ public class AupController {
             ObjectNode aup = (ObjectNode) keyAupPair.getValue();
 
             Date date = new Date(System.currentTimeMillis());
-            aup.put(Aup.SIGNED_ON, formatter.format(date));
+            aup.put(Aup.SIGNED_ON, FORMATTER.format(date));
 
             if (aupsToApproveJsonObject.has(keyAupPair.getKey())) {
                 aupsToApproveJsonObject.replace(keyAupPair.getKey(), aup);
@@ -128,7 +131,7 @@ public class AupController {
 
     private ObjectNode updateUserAupsAttrValue(ObjectNode userAups, ObjectNode newAups) throws IOException {
         if (userAups == null) {
-            userAups = new ObjectNode(jsonNodeFactory);
+            userAups = JsonNodeFactory.instance.objectNode();
         }
 
         Iterator<Map.Entry<String, JsonNode>> newAupsFields = newAups.fields();
@@ -146,7 +149,7 @@ public class AupController {
             }
 
             if (oldAupsArray  == null) {
-                oldAupsArray = new ArrayNode(jsonNodeFactory);
+                oldAupsArray = JsonNodeFactory.instance.arrayNode();
             }
 
             oldAupsArray.add(newApprovedAup);
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/ControllerUtils.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/ControllerUtils.java
index 5e0433c01..229da1011 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/ControllerUtils.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/ControllerUtils.java
@@ -46,14 +46,17 @@ public class ControllerUtils {
     private static final String LANG_KEY = "lang";
     private static final String REQ_URL_KEY = "reqURL";
     private static final String LANGS_MAP_KEY = "langsMap";
-    public static final String LANG_PROPS_KEY = "langProps";
+
+    private ControllerUtils() {
+        // disallow instantiation
+    }
 
     /**
      * Set language properties for page.
 
      * @param model model object
      * @param req request object
-     * @param localization localization with texts
+     * @param config configuration bean
      */
     public static void setLanguageForPage(Map<String, Object> model, HttpServletRequest req, PerunOidcConfig config) {
         String langFromParam = req.getParameter(LANG_KEY);
@@ -145,6 +148,16 @@ public class ControllerUtils {
         model.put("contactMail", perunOidcConfig.getEmailContact());
     }
 
+    public static void setPageOptions(Map<String, Object> model,
+                                      HttpServletRequest req,
+                                      WebHtmlClasses classes,
+                                      PerunOidcConfig perunOidcConfig,
+                                      String page) {
+        setLanguageForPage(model, req, perunOidcConfig);
+        setPageOptions(model, req, classes, perunOidcConfig);
+        model.put("page", page);
+    }
+
     /**
      * Set scopes and claims for consent page.
 
@@ -213,21 +226,20 @@ public class ControllerUtils {
         model.put(SCOPES, sortedScopes);
     }
 
-    private static String transformObject(JsonObject obj) {
-        StringJoiner sj = new StringJoiner(", ");
-        for (String s: obj.keySet()) {
-            sj.add(s + ": " + obj.get(s).getAsString());
-        }
-        return sj.toString();
-    }
+    public static Set<SystemScope> getSortedScopes(Set<String> requestedScopes, SystemScopeService scopeService) {
+        Set<SystemScope> scopes = scopeService.fromStrings(requestedScopes);
 
+        Set<SystemScope> sortedScopes = new LinkedHashSet<>(scopes.size());
+        Set<SystemScope> systemScopes = scopeService.getAll();
 
-    private static String transformArray(JsonArray arr) {
-        StringJoiner sj = new StringJoiner(", ");
-        for (int i = 0; i < arr.size(); i++) {
-            sj.add(arr.get(i).getAsString());
+        for (SystemScope s : systemScopes) {
+            if (scopes.contains(s)) {
+                sortedScopes.add(s);
+            }
         }
-        return sj.toString();
+
+        sortedScopes.addAll(Sets.difference(scopes, systemScopes));
+        return sortedScopes;
     }
 
     /**
@@ -246,14 +258,10 @@ public class ControllerUtils {
             if (it.hasNext()) {
                 while (it.hasNext()) {
                     Map.Entry<String, String> param = it.next();
-                    try {
-                        if (param.getKey() != null && param.getValue() != null) {
-                            String encodedValue = URLEncoder.encode(param.getValue(),
-                                StandardCharsets.UTF_8.toString());
-                            sb.append(param.getKey()).append('=').append(encodedValue);
-                        }
-                    } catch (UnsupportedEncodingException e) {
-                        //TODO: handle
+                    if (param.getKey() != null && param.getValue() != null) {
+                        String encodedValue = URLEncoder.encode(param.getValue(),
+                                StandardCharsets.UTF_8);
+                        sb.append(param.getKey()).append('=').append(encodedValue);
                     }
                     if (it.hasNext()) {
                         sb.append('&');
@@ -278,6 +286,25 @@ public class ControllerUtils {
         return url + newPath;
     }
 
+
+    private static String transformObject(JsonObject obj) {
+        StringJoiner sj = new StringJoiner(", ");
+        for (String s: obj.keySet()) {
+            sj.add(s + ": " + obj.get(s).getAsString());
+        }
+        return sj.toString();
+    }
+
+
+    private static String transformArray(JsonArray arr) {
+        StringJoiner sj = new StringJoiner(", ");
+        for (int i = 0; i < arr.size(); i++) {
+            sj.add(arr.get(i).getAsString());
+        }
+        return sj.toString();
+    }
+
+
     private static String removeQueryParameter(String url, String parameterName)
         throws URISyntaxException {
         URIBuilder uriBuilder = new URIBuilder(url);
@@ -311,20 +338,4 @@ public class ControllerUtils {
         }
     }
 
-    public static Set<SystemScope> getSortedScopes(Set<String> requestedScopes, SystemScopeService scopeService) {
-        Set<SystemScope> scopes = scopeService.fromStrings(requestedScopes);
-
-        Set<SystemScope> sortedScopes = new LinkedHashSet<>(scopes.size());
-        Set<SystemScope> systemScopes = scopeService.getAll();
-
-        for (SystemScope s : systemScopes) {
-            if (scopes.contains(s)) {
-                sortedScopes.add(s);
-            }
-        }
-
-        sortedScopes.addAll(Sets.difference(scopes, systemScopes));
-        return sortedScopes;
-    }
-
 }
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/IsTestSpController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/IsTestSpController.java
deleted file mode 100644
index 5c1dc2bb6..000000000
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/IsTestSpController.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package cz.muni.ics.oidc.web.controllers;
-
-import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
-import cz.muni.ics.oidc.web.WebHtmlClasses;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpSession;
-import java.util.Map;
-
-import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_ACCEPTED;
-import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_TARGET;
-
-/**
- *  Controller for IS TEST SP pages.
- *
- * @author Pavol Pluta <pavol.pluta1@gmail.com>
- */
-@Controller
-@Slf4j
-public class IsTestSpController {
-
-    public static final String MAPPING = "/testRpWarning";
-    public static final String IS_TEST_SP_APPROVED_SESS = "isTestSpApprovedSession";
-    private static final String TARGET = "target";
-    private static final String ACTION = "action";
-
-    private final WebHtmlClasses htmlClasses;
-    private final PerunOidcConfig perunOidcConfig;
-
-    @Autowired
-    public IsTestSpController(WebHtmlClasses htmlClasses, PerunOidcConfig perunOidcConfig) {
-        this.htmlClasses = htmlClasses;
-        this.perunOidcConfig = perunOidcConfig;
-    }
-
-    @GetMapping(value = MAPPING, params = PARAM_TARGET)
-    public String isTestSpWarning(HttpServletRequest req,
-                                  Map<String, Object> model,
-                                  @RequestParam(PARAM_TARGET) String returnUrl)
-    {
-        log.debug("Display warning page for isTestSp");
-        model.put(TARGET, returnUrl);
-        model.put(ACTION, req.getRequestURL().toString());
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-        return "isTestSpWarning";
-    }
-
-    @GetMapping(value = MAPPING, params = {PARAM_TARGET, PARAM_ACCEPTED})
-    public String warningApproved(HttpServletRequest request,
-                                  @RequestParam(PARAM_TARGET) String target)
-    {
-        log.debug("Warning approved, set session attribute and redirect to {}", target);
-        HttpSession sess = request.getSession();
-        if (sess != null) {
-            sess.setAttribute(IS_TEST_SP_APPROVED_SESS, true);
-        }
-        return "redirect:" + target;
-    }
-
-}
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/LoginController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/LoginController.java
index 536214c27..1e7174e57 100644
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/LoginController.java
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/LoginController.java
@@ -21,8 +21,6 @@ public class LoginController {
 
 	public static final String MAPPING_SUCCESS = "/login_success";
 	public static final String MAPPING_FAILURE = "/login_failure";
-
-	public static final String KEY_ERROR_MSG = "error_msg";
 	public static final String ATTR_EXCEPTION = "exception_in_auth";
 
 	private final WebHtmlClasses htmlClasses;
@@ -60,7 +58,6 @@ public class LoginController {
 					model.put(JsonErrorView.ERROR, "unmet_authentication_requirements");
 					model.put(JsonErrorView.ERROR_MESSAGE, "Cannot log in. MFA has been requested and not performed");
 					return JsonErrorView.VIEWNAME;
-					//model.put(KEY_ERROR_MSG, "login_failure.no_authn_context.msg");
 				}
 			}
 		}
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedController.java
deleted file mode 100644
index b9817c968..000000000
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedController.java
+++ /dev/null
@@ -1,255 +0,0 @@
-package cz.muni.ics.oidc.web.controllers;
-
-import cz.muni.ics.oauth2.model.ClientDetailsEntity;
-import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
-import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
-import cz.muni.ics.oidc.web.WebHtmlClasses;
-import cz.muni.ics.openid.connect.view.HttpCodeView;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
-import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
-import org.springframework.stereotype.Controller;
-import org.springframework.util.StringUtils;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.SessionAttribute;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import java.util.Map;
-
-import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_CLIENT_ID;
-import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_HEADER;
-import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_MESSAGE;
-import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_TARGET;
-import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.BUTTON_TRANSLATION;
-import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.CONTACT_TRANSLATION;
-import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.DEFAULT_BUTTON_TRANSLATION_KEY;
-import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.DEFAULT_CONTACT_TRANSLATION_KEY;
-import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.DEFAULT_HEADER_TRANSLATION_KEY;
-import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.DEFAULT_TEXT_TRANSLATION_KEY;
-import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.HEADER_TRANSLATION;
-import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.TEXT_TRANSLATION;
-
-/**
- * Ctonroller for the unapproved page.
- *
- * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
- */
-@Controller
-@Slf4j
-public class PerunUnapprovedController {
-
-    public static final String UNAPPROVED_MAPPING = "/unapproved";
-    public static final String UNAPPROVED_SPECIFIC_MAPPING = "/unapproved_spec";
-    public static final String UNAPPROVED_IS_ELIGIBLE_MAPPING = "/unapprovedNotEligible";
-    public static final String UNAPPROVED_ENSURE_VO_MAPPING = "/unapprovedEnsureVo";
-    public static final String UNAPPROVED_AUTHORIZATION = "/unapprovedAuthorization";
-    public static final String UNAPPROVED_NOT_IN_TEST_VOS_GROUPS = "/unapprovedNotInTestVosGroups";
-    public static final String UNAPPROVED_NOT_IN_PROD_VOS_GROUPS = "/unapprovedNotInProdVosGroups";
-    public static final String UNAPPROVED_NOT_IN_MANDATORY_VOS_GROUPS = "/unapprovedNotInMandatoryVosGroups";
-    public static final String UNAPPROVED_NOT_LOGGED_IN = "/unapprovedNotLoggedIn";
-
-    private static final String OUT_HEADER = "outHeader";
-    private static final String OUT_MESSAGE = "outMessage";
-    private static final String OUT_BUTTON = "outButton";
-    private static final String OUT_CONTACT_P = "outContactP";
-
-    private static final String ENSURE_VO_HDR = "403_ensure_vo_hdr";
-    private static final String ENSURE_VO_MSG = "403_ensure_vo_msg";
-
-    private static final String AUTHORIZATION_HDR = "403_authorization_hdr";
-    private static final String AUTHORIZATION_MSG = "403_authorization_msg";
-
-    private static final String NOT_IN_TEST_VOS_GROUPS_HDR = "403_not_in_test_vos_groups_hdr";
-    private static final String NOT_IN_TEST_VOS_GROUPS_MSG = "403_not_in_test_vos_groups_msg";
-
-    private static final String NOT_IN_PROD_VOS_GROUPS_HDR = "403_not_in_prod_vos_groups_hdr";
-    private static final String NOT_IN_PROD_VOS_GROUPS_MSG = "403_not_in_prod_vos_groups_msg";
-
-    private static final String NOT_IN_MANDATORY_VOS_GROUPS_HDR = "403_not_in_mandatory_vos_groups_hdr";
-    private static final String NOT_IN_MANDATORY_VOS_GROUPS_MSG = "403_not_in_mandatory_vos_groups_msg";
-
-    private static final String NOT_LOGGED_IN_HDR = "403_not_logged_in_hdr";
-    private static final String NOT_LOGGED_IN_MSG = "403_not_logged_in_msg";
-
-    private static final String CONTACT_LANG_PROP_KEY = "contact_p";
-    private static final String CONTACT_MAIL = "contactMail";
-    private static final String HAS_TARGET = "hasTarget";
-    private static final String REASON = "reason";
-
-    public static final String TARGET = "target";
-
-    @Autowired
-    private ClientDetailsEntityService clientService;
-
-    @Autowired
-    private PerunOidcConfig perunOidcConfig;
-
-    @Autowired
-    private WebHtmlClasses htmlClasses;
-
-    @Autowired
-    private SecurityContextLogoutHandler logoutHandler;
-
-    @GetMapping(value = UNAPPROVED_MAPPING)
-    public String showUnapproved(HttpServletRequest req,
-                                 Map<String, Object> model,
-                                 @RequestParam(PARAM_CLIENT_ID) String clientId)
-    {
-        ClientDetailsEntity client;
-
-        try {
-            client = clientService.loadClientByClientId(clientId);
-        } catch (OAuth2Exception e) {
-            log.error("showUnapproved: OAuth2Exception was thrown when attempting to load client", e);
-            model.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
-            return HttpCodeView.VIEWNAME;
-        } catch (IllegalArgumentException e) {
-            log.error("showUnapproved: IllegalArgumentException was thrown when attempting to load client", e);
-            model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
-            return HttpCodeView.VIEWNAME;
-        }
-
-        if (client == null) {
-            log.error("showUnapproved: could not find client " + clientId);
-            model.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
-            return HttpCodeView.VIEWNAME;
-        }
-
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-        model.put("client", client);
-        return "unapproved";
-    }
-
-    @GetMapping(value = UNAPPROVED_SPECIFIC_MAPPING)
-    public String showUnapprovedSpec(HttpServletRequest req, Map<String, Object> model,
-                                     @RequestParam(value = PARAM_HEADER, required = false) String header,
-                                     @RequestParam(value = PARAM_MESSAGE, required = false) String message)
-    {
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-
-        model.put(OUT_HEADER, header);
-        model.put(OUT_MESSAGE, message);
-        model.put(OUT_CONTACT_P, CONTACT_LANG_PROP_KEY);
-        model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact());
-        return "unapproved_spec";
-    }
-
-    @GetMapping(value = UNAPPROVED_IS_ELIGIBLE_MAPPING)
-    public String showUnapprovedIsEligible(HttpServletRequest req,
-                                                 Map<String, Object> model,
-                                                 @RequestParam(value = PARAM_TARGET, required = false) String target)
-    {
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-
-        HttpSession sess = req.getSession();
-        String header = loadSessionTranslationKey(sess, HEADER_TRANSLATION, DEFAULT_HEADER_TRANSLATION_KEY);
-        String message = loadSessionTranslationKey(sess, TEXT_TRANSLATION, DEFAULT_TEXT_TRANSLATION_KEY);
-        String button = loadSessionTranslationKey(sess, BUTTON_TRANSLATION, DEFAULT_BUTTON_TRANSLATION_KEY);
-        String contactP = loadSessionTranslationKey(sess, CONTACT_TRANSLATION, DEFAULT_CONTACT_TRANSLATION_KEY);
-
-        model.put(OUT_HEADER, header);
-        model.put(OUT_MESSAGE, message);
-        model.put(OUT_BUTTON, button);
-        model.put(OUT_CONTACT_P, contactP);
-        model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact());
-        model.put(HAS_TARGET, StringUtils.hasText(target));
-        req.getSession(true).setAttribute(TARGET, target);
-        return "unapproved_is_eligible";
-    }
-
-    @PostMapping(value = UNAPPROVED_IS_ELIGIBLE_MAPPING)
-    public String showUnapprovedIsEligibleHandle(HttpServletRequest req,
-                                                       HttpServletResponse res,
-                                                       Map<String, Object> model,
-                                                       @SessionAttribute(PARAM_TARGET) String target)
-    {
-        if (!StringUtils.hasText(target)) {
-            return showUnapprovedIsEligible(req, model, null);
-        } else {
-            logoutHandler.logout(req, res, null);
-            return "redirect:" + target;
-        }
-    }
-
-    @GetMapping(value = UNAPPROVED_ENSURE_VO_MAPPING)
-    public String showUnapprovedEnsureVo(HttpServletRequest req, Map<String, Object> model) {
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-
-        model.put(OUT_HEADER, ENSURE_VO_HDR);
-        model.put(OUT_MESSAGE, ENSURE_VO_MSG);
-        model.put(OUT_CONTACT_P, CONTACT_LANG_PROP_KEY);
-        model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact());
-
-        return "unapproved_spec";
-    }
-
-    @GetMapping(value = UNAPPROVED_AUTHORIZATION)
-    public String showUnapprovedAuthorization(HttpServletRequest req, Map<String, Object> model) {
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-
-        model.put(OUT_HEADER, AUTHORIZATION_HDR);
-        model.put(OUT_MESSAGE, AUTHORIZATION_MSG);
-        model.put(OUT_CONTACT_P, CONTACT_LANG_PROP_KEY);
-        model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact());
-        return "unapproved_spec";
-    }
-
-    @GetMapping(value = UNAPPROVED_NOT_IN_TEST_VOS_GROUPS)
-    public String showUnapprovedNotInTestVosGroups(HttpServletRequest req, Map<String, Object> model) {
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-
-        model.put(OUT_HEADER, NOT_IN_TEST_VOS_GROUPS_HDR);
-        model.put(OUT_MESSAGE, NOT_IN_TEST_VOS_GROUPS_MSG);
-        model.put(OUT_CONTACT_P, CONTACT_LANG_PROP_KEY);
-        model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact());
-        return "unapproved_spec";
-    }
-
-    @GetMapping(value = UNAPPROVED_NOT_IN_PROD_VOS_GROUPS)
-    public String showUnapprovedNotInProdVosGroups(HttpServletRequest req, Map<String, Object> model) {
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-
-        model.put(OUT_HEADER, NOT_IN_PROD_VOS_GROUPS_HDR);
-        model.put(OUT_MESSAGE, NOT_IN_PROD_VOS_GROUPS_MSG);
-        model.put(OUT_CONTACT_P, CONTACT_LANG_PROP_KEY);
-        model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact());
-        return "unapproved_spec";
-    }
-
-    @GetMapping(value = UNAPPROVED_NOT_IN_MANDATORY_VOS_GROUPS)
-    public String showUnapprovedNotInMandatoryVosGroups(HttpServletRequest req, Map<String, Object> model) {
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-
-        model.put(OUT_HEADER, NOT_IN_MANDATORY_VOS_GROUPS_HDR);
-        model.put(OUT_MESSAGE, NOT_IN_MANDATORY_VOS_GROUPS_MSG);
-        model.put(OUT_CONTACT_P, CONTACT_LANG_PROP_KEY);
-        model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact());
-        return "unapproved_spec";
-    }
-
-    @GetMapping(value = UNAPPROVED_NOT_LOGGED_IN)
-    public String showUnapprovedNotLoggedIn(HttpServletRequest req, Map<String, Object> model) {
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-
-        model.put(OUT_HEADER, NOT_LOGGED_IN_HDR);
-        model.put(OUT_MESSAGE, NOT_LOGGED_IN_MSG);
-        model.put(OUT_CONTACT_P, CONTACT_LANG_PROP_KEY);
-        model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact());
-        return "unapproved_spec";
-    }
-
-
-    private String loadSessionTranslationKey(HttpSession sess, String key, String fallbackValue) {
-        if (sess != null && StringUtils.hasText((String) sess.getAttribute(key))) {
-            return (String) sess.getAttribute(key);
-        }
-        return fallbackValue;
-    }
-
-}
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedRegistrationController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedRegistrationController.java
deleted file mode 100644
index 3fab8a35b..000000000
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedRegistrationController.java
+++ /dev/null
@@ -1,176 +0,0 @@
-package cz.muni.ics.oidc.web.controllers;
-
-import cz.muni.ics.oauth2.model.ClientDetailsEntity;
-import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
-import cz.muni.ics.oidc.models.Facility;
-import cz.muni.ics.oidc.models.Group;
-import cz.muni.ics.oidc.models.PerunAttributeValue;
-import cz.muni.ics.oidc.models.Vo;
-import cz.muni.ics.oidc.server.adapters.PerunAdapter;
-import cz.muni.ics.oidc.server.configurations.FacilityAttrsConfig;
-import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
-import cz.muni.ics.oidc.web.WebHtmlClasses;
-import cz.muni.ics.openid.connect.view.HttpCodeView;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Controller for the unapproved page which offers registration.
- *
- * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
- */
-@Controller
-@Slf4j
-public class PerunUnapprovedRegistrationController {
-
-    public static final String REGISTRATION_FORM_MAPPING = "/regForm";
-    public static final String REGISTRATION_FORM_SUBMIT_MAPPING = "/regForm/submit";
-    public static final String REGISTRATION_CONTINUE_MAPPING = "/regForm/continue";
-
-    @Autowired
-    private ClientDetailsEntityService clientService;
-
-    @Autowired
-    private PerunAdapter perunAdapter;
-
-    @Autowired
-    private FacilityAttrsConfig facilityAttrsConfig;
-
-    @Autowired
-    private PerunOidcConfig perunOidcConfig;
-
-    @Autowired
-    private WebHtmlClasses htmlClasses;
-
-    @GetMapping(value = REGISTRATION_FORM_MAPPING)
-    public String showRegistrationForm(Map<String, Object> model, ServletRequest req, ServletResponse res,
-                                       @RequestParam("client_id") String clientId,
-                                       @RequestParam("facility_id") Long facilityId,
-                                       @RequestParam("user_id") Long userId)
-    {
-        HttpServletRequest request = (HttpServletRequest) req;
-        HttpServletResponse response = (HttpServletResponse) res;
-        ClientDetailsEntity client;
-
-        try {
-            client = clientService.loadClientByClientId(clientId);
-        } catch (OAuth2Exception e) {
-            log.error("confirmAccess: OAuth2Exception was thrown when attempting to load client", e);
-            model.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
-            return HttpCodeView.VIEWNAME;
-        } catch (IllegalArgumentException e) {
-            log.error("confirmAccess: IllegalArgumentException was thrown when attempting to load client", e);
-            model.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
-            return HttpCodeView.VIEWNAME;
-        }
-
-        if (client == null) {
-            log.error("confirmAccess: could not find client {}", clientId);
-            model.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
-            return HttpCodeView.VIEWNAME;
-        }
-
-        Facility facility = perunAdapter.getFacilityByClientId(clientId);
-        Map<String, PerunAttributeValue> facilityAttributes = perunAdapter.getFacilityAttributeValues(facility,
-                facilityAttrsConfig.getMembershipAttrNames());
-        List<String> voShortNames = facilityAttributes.get(facilityAttrsConfig.getVoShortNamesAttr()).valueAsList();
-        Map<Vo, List<Group>> groupsForRegistration = perunAdapter.getAdapterRpc()
-                .getGroupsForRegistration(facility, userId, voShortNames);
-
-        if (groupsForRegistration.isEmpty()) {
-            String redirectUrl = ControllerUtils.createRedirectUrl(perunOidcConfig.getConfigBean().getIssuer(),
-                    PerunUnapprovedController.UNAPPROVED_MAPPING, Collections.singletonMap("client_id", clientId));
-            response.reset();
-            response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-            response.setHeader("Location", redirectUrl);
-            return null;
-        } else if (groupsForRegistration.keySet().size() == 1) {
-            for (Map.Entry<Vo, List<Group>> entry: groupsForRegistration.entrySet()) {
-                // no other way how to extract the first item (as it is the only)
-                List<Group> groupList = groupsForRegistration.get(entry.getKey());
-                if (groupList.size() == 1) {
-                    Group group = groupList.get(0);
-                    String redirectUrl = createRegistrarUrl(entry.getKey(), group.getName());
-                    response.reset();
-                    response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
-                    response.setHeader("Location", redirectUrl);
-                    return null;
-                }
-            }
-        }
-
-        ControllerUtils.setPageOptions(model, request, htmlClasses, perunOidcConfig);
-        model.put("client", client);
-        model.put("facilityId", facilityId);
-        model.put("action", buildActionUrl(request));
-        model.put("groupsForRegistration", groupsForRegistration);
-        model.put("page", "regForm");
-        return "registrationForm";
-    }
-
-    @GetMapping(value = REGISTRATION_FORM_SUBMIT_MAPPING)
-    public void processRegistrationForm(@RequestParam("selectedGroup") String groupName,
-                                        @RequestParam("selectedVo") String voName,
-                                        ServletResponse res) throws IOException {
-        HttpServletResponse request = (HttpServletResponse) res;
-
-        groupName = groupName.split(":", 2)[1];
-
-        String redirectUrl = createRegistrarUrl(voName, groupName);
-
-        request.sendRedirect(redirectUrl);
-    }
-
-    @GetMapping(value = REGISTRATION_CONTINUE_MAPPING)
-    public String showContinuePage(Map<String, Object> model, ServletRequest req,
-                                   @RequestParam("client_id") String clientId,
-                                   @RequestParam("facility_id") Long facilityId,
-                                   @RequestParam("user_id") Long userId)
-    {
-        HttpServletRequest request = (HttpServletRequest) req;
-
-        model.put("page", "regContinue");
-        model.put("client_id", clientId);
-        model.put("facility_id", facilityId);
-        model.put("user_id", userId);
-        model.put("action", request.getRequestURL().toString()
-                .replace(REGISTRATION_CONTINUE_MAPPING, REGISTRATION_FORM_MAPPING));
-        ControllerUtils.setPageOptions(model, request, htmlClasses, perunOidcConfig);
-        return "registrationFormContinue";
-    }
-
-    private String createRegistrarUrl(Vo vo, String groupName) {
-        return createRegistrarUrl(vo.getShortName(), groupName);
-    }
-
-    private String createRegistrarUrl(String vohortName, String groupName) {
-        String redirectUrl = perunOidcConfig.getRegistrarUrl().concat("?vo=").concat(vohortName);
-        if (groupName != null && !groupName.isEmpty() && !groupName.equalsIgnoreCase("members")) {
-            redirectUrl = redirectUrl.concat("&group=").concat(groupName);
-        }
-
-        return redirectUrl;
-    }
-
-    private String buildActionUrl(HttpServletRequest request) {
-        int startIndex = request.getRequestURL().lastIndexOf(REGISTRATION_FORM_MAPPING);
-        int length = request.getRequestURL().length();
-        return request.getRequestURL().delete(startIndex, length)
-                .append(REGISTRATION_FORM_SUBMIT_MAPPING).toString();
-    }
-
-}
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnauthorizedController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnauthorizedController.java
new file mode 100644
index 000000000..44f1574ba
--- /dev/null
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnauthorizedController.java
@@ -0,0 +1,324 @@
+package cz.muni.ics.oidc.web.controllers;
+
+import cz.muni.ics.oauth2.model.ClientDetailsEntity;
+import cz.muni.ics.oidc.PerunConstants;
+import cz.muni.ics.oidc.RedirectUtils;
+import cz.muni.ics.oidc.models.Facility;
+import cz.muni.ics.oidc.models.Group;
+import cz.muni.ics.oidc.models.PerunAttributeValue;
+import cz.muni.ics.oidc.models.PerunUser;
+import cz.muni.ics.oidc.models.Vo;
+import cz.muni.ics.oidc.server.adapters.PerunAdapter;
+import cz.muni.ics.oidc.server.configurations.FacilityAttrsConfig;
+import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
+import cz.muni.ics.oidc.web.WebHtmlClasses;
+import cz.muni.ics.openid.connect.view.HttpCodeView;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * Ctonroller for the unauthorized page.
+ *
+ * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
+ */
+@Controller
+@Slf4j
+public class PerunUnauthorizedController {
+
+    public static final String UNAUTHORIZED_MAPPING = "/unauthorized";
+    public static final String UNAUTHORIZED_SPECIFIC_MAPPING = "/unauthorizedSpec";
+    public static final String UNAUTHORIZED_IS_ELIGIBLE_MAPPING = "/unauthorizedIsEligible";
+    public static final String UNAUTHORIZED_ENSURE_VO_MAPPING = "/unauthorizedEnsureVo";
+    public static final String UNAUTHORIZED_ENSURE_VO_REDIRECT_NOTIFY_MAPPING = "/unauthorizedEnsureVoRedirectNotify";
+    public static final String UNAUTHORIZED_AUTHORIZATION_MAPPING = "/unauthorizedAuthorization";
+    public static final String UNAUTHORIZED_NOT_IN_ENV_VOS_GROUPS_MAPPING = "/unauthorizedEnvVosGroups";
+    public static final String UNAUTHORIZED_NOT_LOGGED_IN_MAPPING = "/unauthorizedNotLoggedIn";
+
+    public static final String UNAUTHORIZED_REGISTER_CHOOSE_VO_GROUP_MAPPING = "/unauthorizedRegisterChooseVoGroup";
+    public static final String UNAUTHORIZED_REGISTER_NOTIFY_ACTION_REQUIRED_MAPPING = "/unauthorizedRegisterNotifyActionRequired";
+
+    // SESSION ATTRIBUTES FOR PASSING DATA
+
+    public static final String SESS_ATTR_REGISTRATION_UNITS = "registrationUnits";
+    public static final String SESS_ATTR_CLIENT = "client";
+
+    public static final String SESS_ATTR_TARGET = "target";
+
+    public static final String SESS_ATTR_USER = "logged_user";
+    public static final String SESS_ATTR_FACILITY = "client_facility";
+
+    public static final String SESS_ATTR_HEADER_TRANSLATION = "header_translation";
+    public static final String SESS_ATTR_TEXT_TRANSLATION = "text_translation";
+    public static final String SESS_ATTR_BUTTON_TRANSLATION = "button_translation";
+    public static final String SESS_ATTR_CONTACT_TRANSLATION = "contact_translation";
+    public static final String SESS_ATTR_HEADER = "header";
+    public static final String SESS_ATTR_MESSAGE = "message";
+
+    // MODEL ATTRIBUTES
+    private static final String MODEL_REG_URLS = "registrationUrls";
+    private static final String MODEL_ATTR_CLIENT = "client";
+    private static final String MODEL_ATTR_GROUPS_FOR_REGISTRATION = "groupsForRegistration";
+    private static final String MODEL_ATTR_PERUN_REGISTRAR_URL = "registrarUrl";
+    private static final String MODEL_ATTR_TRANSL_HEADER = "outHeader";
+    private static final String MODEL_ATTR_TRANSL_MESSAGE = "outMessage";
+    private static final String MODEL_ATTR_TANSL_SUBMIT_BTN = "outButton";
+    private static final String MODEL_ATTR_TRANSL_CONTACT_P = "outContactP";
+    private static final String MODEL_ATTR_TARGET = "target";
+    private static final String MODEL_ATTR_AAI_CONTACT_ADDRESS = "contactMail";
+
+    // TRANSLATION KEYS
+
+    private static final String AUTHORIZATION_HDR = "403_authorization_hdr";
+    private static final String AUTHORIZATION_MSG = "403_authorization_msg";
+
+    private static final String NOT_LOGGED_IN_HDR = "403_not_logged_in_hdr";
+    private static final String NOT_LOGGED_IN_MSG = "403_not_logged_in_msg";
+
+    // VIEWS
+    public static final String VIEW_UNAUTHORIZED_SPEC = "unauthorized_spec";
+
+    private final PerunOidcConfig perunOidcConfig;
+    private final WebHtmlClasses htmlClasses;
+    private final PerunAdapter perunAdapter;
+    private final FacilityAttrsConfig facilityAttrsConfig;
+
+    public PerunUnauthorizedController(PerunOidcConfig perunOidcConfig,
+                                       WebHtmlClasses htmlClasses,
+                                       PerunAdapter perunAdapter,
+                                       FacilityAttrsConfig facilityAttrsConfig)
+    {
+        this.perunOidcConfig = perunOidcConfig;
+        this.htmlClasses = htmlClasses;
+        this.perunAdapter = perunAdapter;
+        this.facilityAttrsConfig = facilityAttrsConfig;
+    }
+
+    @GetMapping(value = UNAUTHORIZED_MAPPING)
+    public String showUnauthorized(HttpServletRequest req, Map<String, Object> model) {
+        HttpSession sess = req.getSession();
+
+        ClientDetailsEntity client = (ClientDetailsEntity) sess.getAttribute(SESS_ATTR_CLIENT);
+
+        if (client == null) {
+            model.put(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
+            return HttpCodeView.VIEWNAME;
+        }
+
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+        model.put(MODEL_ATTR_CLIENT, client);
+        return "unauthorized";
+    }
+
+    @GetMapping(value = UNAUTHORIZED_SPECIFIC_MAPPING)
+    public String showUnauthorizedSpec(HttpServletRequest req, Map<String, Object> model)
+    {
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+
+        HttpSession sess = req.getSession();
+        String header = (String) sess.getAttribute(SESS_ATTR_HEADER);
+        String message = (String) sess.getAttribute(SESS_ATTR_MESSAGE);
+
+        model.put(MODEL_ATTR_TRANSL_HEADER, header);
+        model.put(MODEL_ATTR_TRANSL_MESSAGE, message);
+        model.put(MODEL_ATTR_AAI_CONTACT_ADDRESS, perunOidcConfig.getEmailContact());
+        return VIEW_UNAUTHORIZED_SPEC;
+    }
+
+    @GetMapping(value = UNAUTHORIZED_IS_ELIGIBLE_MAPPING)
+    public String showUnauthorizedIsEligible(HttpServletRequest req, HttpSession sess, Map<String, Object> model) {
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+
+        String header = (String) sess.getAttribute(SESS_ATTR_HEADER_TRANSLATION);
+        String message = (String) sess.getAttribute(SESS_ATTR_TEXT_TRANSLATION);
+        String button = (String) sess.getAttribute(SESS_ATTR_BUTTON_TRANSLATION);
+        String contactP = (String) sess.getAttribute(SESS_ATTR_CONTACT_TRANSLATION);
+        String targetUrl = (String) sess.getAttribute(SESS_ATTR_TARGET);
+        ClientDetailsEntity client = (ClientDetailsEntity) sess.getAttribute(SESS_ATTR_CLIENT);
+
+        model.put(MODEL_ATTR_TRANSL_HEADER, header);
+        model.put(MODEL_ATTR_TRANSL_MESSAGE, message);
+        model.put(MODEL_ATTR_TANSL_SUBMIT_BTN, button);
+        model.put(MODEL_ATTR_TRANSL_CONTACT_P, contactP);
+        model.put(MODEL_ATTR_AAI_CONTACT_ADDRESS, perunOidcConfig.getEmailContact());
+        model.put(MODEL_ATTR_CLIENT, client);
+        model.put(MODEL_ATTR_TARGET, targetUrl);
+        return "unauthorized_is_eligible";
+    }
+
+    @GetMapping(value = UNAUTHORIZED_ENSURE_VO_MAPPING)
+    public String showUnauthorizedEnsureVo(HttpServletRequest req, HttpSession sess, Map<String, Object> model) {
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+
+        model.put(MODEL_ATTR_CLIENT, sess.getAttribute(SESS_ATTR_CLIENT));
+        model.put(MODEL_ATTR_AAI_CONTACT_ADDRESS, perunOidcConfig.getEmailContact());
+
+        return VIEW_UNAUTHORIZED_SPEC;
+    }
+
+    @GetMapping(value = UNAUTHORIZED_ENSURE_VO_REDIRECT_NOTIFY_MAPPING)
+    public String showUnauthorizedEnsureVoRedirectNotify(HttpServletRequest req,
+                                                         HttpSession sess,
+                                                         Map<String, Object> model)
+    {
+        String target = (String) sess.getAttribute(SESS_ATTR_TARGET);
+        model.put(MODEL_ATTR_TARGET, target);
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+        return "unauthorized_ensure_vo_member_notify_redirect";
+    }
+
+    @GetMapping(value = UNAUTHORIZED_AUTHORIZATION_MAPPING)
+    public String showUnauthorizedAuthorization(HttpServletRequest req, HttpSession sess, Map<String, Object> model) {
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+        ClientDetailsEntity client = (ClientDetailsEntity) sess.getAttribute(SESS_ATTR_CLIENT);
+
+        model.put(MODEL_ATTR_CLIENT, client);
+        model.put(MODEL_ATTR_TRANSL_HEADER, AUTHORIZATION_HDR);
+        model.put(MODEL_ATTR_TRANSL_MESSAGE, AUTHORIZATION_MSG);
+        model.put(MODEL_ATTR_AAI_CONTACT_ADDRESS, perunOidcConfig.getEmailContact());
+        return VIEW_UNAUTHORIZED_SPEC;
+    }
+
+    @GetMapping(value = UNAUTHORIZED_NOT_IN_ENV_VOS_GROUPS_MAPPING)
+    public String showUnauthorizedNotInEnvUnits(HttpServletRequest req, Map<String, Object> model) {
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+
+        HttpSession sess = req.getSession();
+
+        Map<Vo, Set<Group>> registrationUnits = (Map<Vo, Set<Group>>) sess.getAttribute(SESS_ATTR_REGISTRATION_UNITS);
+        ClientDetailsEntity client = (ClientDetailsEntity) sess.getAttribute(SESS_ATTR_CLIENT);
+
+        Map<String, String> urls = new HashMap<>();
+        for (Map.Entry<Vo, Set<Group>> entry : registrationUnits.entrySet()) {
+            Vo vo = entry.getKey();
+            if (entry.getValue().isEmpty()) {
+                String url = ControllerUtils.createUrl(
+                        perunOidcConfig.getRegistrarUrl(), Map.of("vo", vo.getShortName())
+                );
+                urls.put(url, vo.getName());
+            } else {
+                for (Group group : entry.getValue()) {
+                    String url = ControllerUtils.createUrl(
+                            perunOidcConfig.getRegistrarUrl(),
+                            Map.of("vo", vo.getShortName(), "group", group.getName())
+                    );
+                    urls.put(url, group.getName() + "(" + group.getDescription() + ")");
+                }
+            }
+        }
+
+        model.put(MODEL_REG_URLS, urls);
+        model.put(MODEL_ATTR_CLIENT, client);
+        return "unauthorized_not_in_env_units";
+    }
+
+    @GetMapping(value = UNAUTHORIZED_NOT_LOGGED_IN_MAPPING)
+    public String showUnauthorizedNotLoggedIn(HttpServletRequest req, Map<String, Object> model) {
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+
+        model.put(MODEL_ATTR_TRANSL_HEADER, NOT_LOGGED_IN_HDR);
+        model.put(MODEL_ATTR_TRANSL_MESSAGE, NOT_LOGGED_IN_MSG);
+        model.put(MODEL_ATTR_AAI_CONTACT_ADDRESS, perunOidcConfig.getEmailContact());
+        return VIEW_UNAUTHORIZED_SPEC;
+    }
+
+    @GetMapping(value = UNAUTHORIZED_REGISTER_CHOOSE_VO_GROUP_MAPPING)
+    public String unauthorizedRegisterChooseVoGroup(HttpServletRequest req,
+                                                    HttpServletResponse res,
+                                                    HttpSession sess,
+                                                    Map<String, Object> model)
+    {
+        ClientDetailsEntity client = (ClientDetailsEntity) sess.getAttribute(SESS_ATTR_CLIENT);
+        Facility facility = (Facility) sess.getAttribute(SESS_ATTR_FACILITY);
+        PerunUser user = (PerunUser) sess.getAttribute(SESS_ATTR_USER);
+
+        if (client == null) {
+            log.error("confirmAccess: could not find client in session");
+            return error(model);
+        } else if (facility == null) {
+            log.error("confirmAccess: could not find facility in session");
+            return error(model);
+        } else if (user == null) {
+            log.error("confirmAccess: could not find user in session");
+            return error(model);
+        }
+
+        Map<String, PerunAttributeValue> facilityAttributes = perunAdapter.getFacilityAttributeValues(facility,
+                facilityAttrsConfig.getMembershipAttrNames());
+
+        List<String> voShortNames = facilityAttributes.get(facilityAttrsConfig.getVoShortNamesAttr()).valueAsList();
+        Map<Vo, List<Group>> groupsForRegistration = perunAdapter.getAdapterRpc()
+                .getGroupsForRegistration(facility, user.getId(), voShortNames);
+
+        if (groupsForRegistration.isEmpty()) {
+            log.debug("Redirecting user '{}' to the internal page informing about unauthorized access", user);
+            Map<String, Object> sessAttributes = new HashMap<>();
+            sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_CLIENT, client);
+            RedirectUtils.redirectInternal(req, res, perunOidcConfig, PerunUnauthorizedController.UNAUTHORIZED_AUTHORIZATION_MAPPING, sessAttributes);
+            return null;
+        } else if (groupsForRegistration.keySet().size() == 1) {
+            for (Map.Entry<Vo, List<Group>> entry: groupsForRegistration.entrySet()) {
+                // no other way how to extract the first item (as it is the only)
+                List<Group> groupList = groupsForRegistration.get(entry.getKey());
+                if (groupList.size() == 1) {
+                    Group group = groupList.get(0);
+                    String redirectUrl = createRegistrarUrl(entry.getKey(), group.getName());
+                    RedirectUtils.redirectExternal(res, redirectUrl);
+                    return null;
+                }
+            }
+        }
+
+        model.put(MODEL_ATTR_CLIENT, client);
+        model.put(MODEL_ATTR_GROUPS_FOR_REGISTRATION, groupsForRegistration);
+        model.put(MODEL_ATTR_PERUN_REGISTRAR_URL, perunOidcConfig.getRegistrarUrl());
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig, "unauthorized_register_choose_vo_group");
+        return "unauthorized_register_choose_vo_group";
+    }
+
+    @GetMapping(value = UNAUTHORIZED_REGISTER_NOTIFY_ACTION_REQUIRED_MAPPING)
+    public String unauthorizedRegisterNotifyActionRequired(HttpServletRequest req,
+                                                           HttpSession sess,
+                                                           Map<String, Object> model)
+    {
+        ClientDetailsEntity client = (ClientDetailsEntity) sess.getAttribute(SESS_ATTR_CLIENT);
+        if (client == null) {
+            log.error("confirmAccess: could not find client in session");
+            return error(model);
+        }
+
+        model.put(MODEL_ATTR_CLIENT, client);
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig, "unauthorized_register_notify_action_required");
+        return "unauthorized_register_notify_action_required";
+    }
+
+    private String createRegistrarUrl(Vo vo, String groupName) {
+        return createRegistrarUrl(vo.getShortName(), groupName);
+    }
+
+    private String createRegistrarUrl(String voShortName, String groupName) {
+        Map<String, String> params = new HashMap<>();
+        params.put(PerunConstants.REGISTRAR_PARAM_VO, voShortName);
+        if (groupName != null && !groupName.isEmpty() && !groupName.equalsIgnoreCase(PerunConstants.GROUP_NAME_MEMBERS)) {
+            params.put(PerunConstants.REGISTRAR_PARAM_GROUP, groupName);
+        }
+
+        return ControllerUtils.createUrl(perunOidcConfig.getRegistrarUrl(), params);
+    }
+
+    private String error(Map<String, Object> model) {
+        model.put(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
+        return HttpCodeView.VIEWNAME;
+    }
+
+}
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/RegistrationController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/RegistrationController.java
deleted file mode 100644
index 68bedde97..000000000
--- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/RegistrationController.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package cz.muni.ics.oidc.web.controllers;
-
-import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
-import cz.muni.ics.oidc.web.WebHtmlClasses;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-
-import javax.servlet.http.HttpServletRequest;
-import java.util.Map;
-
-/**
- * Controller for the unapproved page which offers registration.
- *
- * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
- */
-@Controller
-@Slf4j
-public class RegistrationController {
-
-    public static final String PARAM_TARGET = "target";
-
-    public static final String CONTINUE_DIRECT_MAPPING = "/continueDirect";
-
-    @Autowired
-    private PerunOidcConfig perunOidcConfig;
-
-    @Autowired
-    private WebHtmlClasses htmlClasses;
-
-    @GetMapping(value = CONTINUE_DIRECT_MAPPING, params = { PARAM_TARGET })
-    public String showRegistrationForm(HttpServletRequest req, Map<String, Object> model,
-                                       @RequestParam(PARAM_TARGET) String target)
-    {
-        model.put(PARAM_TARGET, target);
-        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
-        return "registrationFormContinue";
-    }
-
-}
diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/TestSpWarningController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/TestSpWarningController.java
new file mode 100644
index 000000000..db83230c5
--- /dev/null
+++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/TestSpWarningController.java
@@ -0,0 +1,63 @@
+package cz.muni.ics.oidc.web.controllers;
+
+import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
+import cz.muni.ics.oidc.web.WebHtmlClasses;
+import cz.muni.ics.openid.connect.view.HttpCodeView;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import java.util.Map;
+
+/**
+ *  Controller for IS TEST SP pages.
+ *
+ * @author Pavol Pluta <pavol.pluta1@gmail.com>
+ */
+@Controller
+@Slf4j
+public class TestSpWarningController {
+
+    public static final String MAPPING = "/testSp";
+    public static final String SESS_ATTR_TARGET = "target";
+    public static final String SESS_ATTR_IS_TEST_SP_APPROVED = "isTestSpApprovedSession";
+
+    private final WebHtmlClasses htmlClasses;
+    private final PerunOidcConfig perunOidcConfig;
+
+    @Autowired
+    public TestSpWarningController(WebHtmlClasses htmlClasses, PerunOidcConfig perunOidcConfig) {
+        this.htmlClasses = htmlClasses;
+        this.perunOidcConfig = perunOidcConfig;
+    }
+
+    @GetMapping(value = MAPPING)
+    public String showTestSpWarning(HttpServletRequest req,
+                                    Map<String, Object> model)
+    {
+        ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig);
+        return "test_rp_warning";
+    }
+
+    @PostMapping(value = MAPPING)
+    public String showTestSpWarningSubmit(HttpSession sess, Map<String, Object> model)
+    {
+        if (sess != null) {
+            sess.setAttribute(SESS_ATTR_IS_TEST_SP_APPROVED, true);
+            String target = (String) sess.getAttribute(SESS_ATTR_TARGET);
+            if (target != null) {
+                return "redirect:" + target;
+            }
+        }
+        log.debug("showTestSpWarningSubmit: missing session attribute TARGET, cannot redirect to original req");
+        model.put(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
+        return HttpCodeView.VIEWNAME;
+
+    }
+
+}
-- 
GitLab