diff --git a/perun-oidc-server-webapp/src/main/resources/db/hsql/hsql_database_tables.sql b/perun-oidc-server-webapp/src/main/resources/db/hsql/hsql_database_tables.sql index 697322c73bd3c5f52be64663391aa7d4e90326be..38828753834d8e9c5a901d6b7423ed7cd7adcfe0 100644 --- a/perun-oidc-server-webapp/src/main/resources/db/hsql/hsql_database_tables.sql +++ b/perun-oidc-server-webapp/src/main/resources/db/hsql/hsql_database_tables.sql @@ -302,3 +302,13 @@ CREATE TABLE IF NOT EXISTS device_code_request_parameter ( param VARCHAR(2048), val VARCHAR(2048) ); + +CREATE TABLE IF NOT EXISTS client_only_allowed_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +CREATE TABLE IF NOT EXISTS client_blocked_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); diff --git a/perun-oidc-server-webapp/src/main/resources/db/hsql/v18.0.0.sql b/perun-oidc-server-webapp/src/main/resources/db/hsql/v18.0.0.sql new file mode 100644 index 0000000000000000000000000000000000000000..d7634d020757d1b37bc3973f542f2a1c36ca00c6 --- /dev/null +++ b/perun-oidc-server-webapp/src/main/resources/db/hsql/v18.0.0.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS client_only_allowed_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +CREATE TABLE IF NOT EXISTS client_blocked_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +alter table client_only_allowed_idps + add constraint client_only_allowed_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; + +alter table client_blocked_idps + add constraint client_blocked_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; \ No newline at end of file diff --git a/perun-oidc-server-webapp/src/main/resources/db/mysql/mysql_database_tables.sql b/perun-oidc-server-webapp/src/main/resources/db/mysql/mysql_database_tables.sql index 2b0b07728e9beb5e24f8646bcb792025b29a7ad5..5c0774dbf0dd837674412969a8166ee1a976bf20 100644 --- a/perun-oidc-server-webapp/src/main/resources/db/mysql/mysql_database_tables.sql +++ b/perun-oidc-server-webapp/src/main/resources/db/mysql/mysql_database_tables.sql @@ -205,6 +205,16 @@ CREATE TABLE IF NOT EXISTS client_claims_redirect_uri ( redirect_uri VARCHAR(2048) ); +CREATE TABLE IF NOT EXISTS client_only_allowed_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +CREATE TABLE IF NOT EXISTS client_blocked_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + CREATE TABLE IF NOT EXISTS refresh_token ( id BIGINT AUTO_INCREMENT PRIMARY KEY, token_value VARCHAR(4096), @@ -471,3 +481,13 @@ alter table whitelisted_site_scope add constraint whitelisted_site_scope_whitelisted_site_id_fk foreign key (owner_id) references whitelisted_site (id) on update cascade on delete cascade; + +alter table client_only_allowed_idps + add constraint client_only_allowed_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; + +alter table client_blocked_idps + add constraint client_blocked_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; \ No newline at end of file diff --git a/perun-oidc-server-webapp/src/main/resources/db/mysql/v18.0.0.sql b/perun-oidc-server-webapp/src/main/resources/db/mysql/v18.0.0.sql new file mode 100644 index 0000000000000000000000000000000000000000..d7634d020757d1b37bc3973f542f2a1c36ca00c6 --- /dev/null +++ b/perun-oidc-server-webapp/src/main/resources/db/mysql/v18.0.0.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS client_only_allowed_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +CREATE TABLE IF NOT EXISTS client_blocked_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +alter table client_only_allowed_idps + add constraint client_only_allowed_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; + +alter table client_blocked_idps + add constraint client_blocked_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; \ No newline at end of file diff --git a/perun-oidc-server-webapp/src/main/resources/db/psql/psql_database_tables.sql b/perun-oidc-server-webapp/src/main/resources/db/psql/psql_database_tables.sql index 44f4e85d25c0afd6645978deec654d8b20db2c1c..f3a0e5ee3ceb6b53bd945d45d3627b92b861dba8 100644 --- a/perun-oidc-server-webapp/src/main/resources/db/psql/psql_database_tables.sql +++ b/perun-oidc-server-webapp/src/main/resources/db/psql/psql_database_tables.sql @@ -209,6 +209,16 @@ CREATE TABLE IF NOT EXISTS client_claims_redirect_uri ( redirect_uri VARCHAR(2048) ); +CREATE TABLE IF NOT EXISTS client_only_allowed_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +CREATE TABLE IF NOT EXISTS client_blocked_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + CREATE TABLE IF NOT EXISTS refresh_token ( id BIGSERIAL PRIMARY KEY, token_value VARCHAR(4096), @@ -435,6 +445,16 @@ alter table client_scope foreign key (owner_id) references client_details (id) on update cascade on delete cascade; +alter table client_only_allowed_idps + add constraint client_only_allowed_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; + +alter table client_blocked_idps + add constraint client_blocked_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; + alter table device_code add constraint device_code_client_details_id_fk foreign key (client_id) references client_details (client_id) diff --git a/perun-oidc-server-webapp/src/main/resources/db/psql/v18.0.0.sql b/perun-oidc-server-webapp/src/main/resources/db/psql/v18.0.0.sql new file mode 100644 index 0000000000000000000000000000000000000000..d7634d020757d1b37bc3973f542f2a1c36ca00c6 --- /dev/null +++ b/perun-oidc-server-webapp/src/main/resources/db/psql/v18.0.0.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS client_only_allowed_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +CREATE TABLE IF NOT EXISTS client_blocked_idps ( + owner_id BIGINT, + idp_entity_id VARCHAR(512) +); + +alter table client_only_allowed_idps + add constraint client_only_allowed_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; + +alter table client_blocked_idps + add constraint client_blocked_idps_client_details_id_fk + foreign key (owner_id) references client_details (id) + on update cascade on delete cascade; \ No newline at end of file diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/user-context.xml b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/user-context.xml index 0e7e1ef7c63fb5e5f8e26d6119a5dc4df400cfd7..7862afc1cade81dc7212c2f37c35a229476534d7 100644 --- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/user-context.xml +++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/user-context.xml @@ -98,6 +98,8 @@ <prop key="proxy.extSource.name"/> <prop key="proxy.base.url"/> <prop key="proxy.add_client_id_to_acrs">false</prop> + <prop key="proxy.only_allowed_idps_enabled">false</prop> + <prop key="proxy.blocked_idps_enabled">false</prop> <!-- OIDC STUFF --> <prop key="jwk">file:///etc/perun/perun-oidc-keystore.jwks</prop> <prop key="id_token.scopes">openid,profile,email,phone,address</prop> @@ -128,6 +130,7 @@ <prop key="filter.stats.spIdColumnName">spId</prop> <prop key="sentry.config.location"/> <prop key="ga4gh.tokenExchange.brokerUrl"/> + </props> </property> </bean> @@ -476,6 +479,8 @@ <property name="krbTokenExchangeRequiredScopes" value="#{'${token-exchange.kerberos.requiredScopes}'.split('\s*,\s*')}"/> <property name="requesterIdPrefix" value="${saml.requester-id.prefix}"/> <property name="logRequestsEnabled" value="${logRequestsEnabled}"/> + <property name="onlyAllowedIdpsEnabled" value="${proxy.only_allowed_idps_enabled}"/> + <property name="blockedIdpsEnabled" value="${proxy.blocked_idps_enabled}"/> </bean> <bean id="facilityAttrsConfig" class="cz.muni.ics.oidc.server.configurations.FacilityAttrsConfig"> diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/ClientDetailsEntity.java b/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/ClientDetailsEntity.java index 06be4156d5bb2a5bf80dec032f1743bc162b2778..8d26683e31957b4766539497090738dfc840bfa5 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/ClientDetailsEntity.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/ClientDetailsEntity.java @@ -338,6 +338,18 @@ public class ClientDetailsEntity implements ClientDetails { @Column(name = "parent_client_id") private Long parentClientId; + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "client_only_allowed_idps", joinColumns = @JoinColumn(name = "owner_id")) + @Column(name = "idp_entity_id") + @CascadeOnDelete + private Set<String> onlyAllowedIdps; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "client_blocked_idps", joinColumns = @JoinColumn(name = "owner_id")) + @Column(name = "idp_entity_id") + @CascadeOnDelete + private Set<String> blockedIdps; + @Transient private Map<String, Object> additionalInformation = new HashMap<>(); diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/saml/PerunSamlEntryPoint.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/saml/PerunSamlEntryPoint.java index 4fd96fe19884440641e21702d1af4b30f88c7fce..013cd6e6519bd15e825baa49375d83fb26beece1 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/saml/PerunSamlEntryPoint.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/saml/PerunSamlEntryPoint.java @@ -1,7 +1,9 @@ package cz.muni.ics.oidc.saml; +import cz.muni.ics.oauth2.model.ClientDetailsEntity; import cz.muni.ics.oauth2.model.DeviceCode; import cz.muni.ics.oauth2.repository.impl.DeviceCodeRepository; +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.server.adapters.PerunAdapter; @@ -39,10 +41,12 @@ import java.util.Set; import static cz.muni.ics.oauth2.web.endpoint.DeviceEndpoint.PATH_DEVICE_AUTHORIZE; import static cz.muni.ics.oauth2.web.endpoint.DeviceEndpoint.USER_CODE; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.AARC_IDP_HINT; +import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.BLOCKED_IDPS_ACR_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.CLIENT_ID_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.EFILTER_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.FILTER_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.IDP_ENTITY_ID_PREFIX; +import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.ONLY_ALLOWED_IDPS_ACR_PREFIX; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_CLIENT_ID; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_MAX_AGE; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_PROMPT; @@ -55,18 +59,21 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { private final FacilityAttrsConfig facilityAttrsConfig; private final SamlProperties samlProperties; private final DeviceCodeRepository deviceCodeRepository; + private final ClientDetailsEntityService clientDetailsEntityService; public PerunSamlEntryPoint(PerunAdapter perunAdapter, PerunOidcConfig config, FacilityAttrsConfig facilityAttrsConfig, SamlProperties samlProperties, - DeviceCodeRepository deviceCodeRepository) - { + DeviceCodeRepository deviceCodeRepository, + ClientDetailsEntityService clientDetailsEntityService + ) { this.perunAdapter = perunAdapter; this.config = config; this.facilityAttrsConfig = facilityAttrsConfig; this.samlProperties = samlProperties; this.deviceCodeRepository = deviceCodeRepository; + this.clientDetailsEntityService = clientDetailsEntityService; } @Override @@ -176,12 +183,12 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { } private void processPrompt(Map<String, String> requestParameters, WebSSOProfileOptions options) { - if (PerunSamlUtils.needsReAuthByPrompt(requestParameters.getOrDefault(PARAM_PROMPT, null))) { - log.debug("Transformed prompt parameter ({}) to SAML forceAuthn=true", - requestParameters.get(PARAM_PROMPT)); + String prompt = requestParameters.getOrDefault(PARAM_PROMPT, ""); + if (PerunSamlUtils.needsReAuthByPrompt(prompt)) { + log.debug("Transformed prompt parameter ({}) to SAML forceAuthn=true", prompt); options.setForceAuthN(true); } - if ("none".equalsIgnoreCase(requestParameters.getOrDefault(PARAM_PROMPT, ""))) { + if ("none".equalsIgnoreCase(prompt)) { log.debug("Detected prompt=none, translating to 'isPassive=true' in SAML"); options.setPassive(true); } @@ -203,25 +210,48 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { acrs = convertAcrValuesToList(acrValues); } - if (!hasAcrForcingIdp(acrs)) { - String clientId = requestParameters.getOrDefault(AuthProcFilterConstants.PARAM_CLIENT_ID, null); - if (clientId != null) { + String clientId = requestParameters.getOrDefault(AuthProcFilterConstants.PARAM_CLIENT_ID, null); + if (StringUtils.hasText(clientId)) { + // ADD FILTER AND E-FILTER + if (config.isAskPerunForIdpFiltersEnabled() && !hasAcrForcingIdp(acrs)) { String idpFilter = extractIdpFilterForRp(clientId); if (idpFilter != null) { log.debug("Added IdP filter as SAML AuthnContextClassRef ({})", idpFilter); acrs.add(idpFilter); } } - } - if (StringUtils.hasText(requestParameters.getOrDefault(PARAM_CLIENT_ID, "")) && config.isAddClientIdToAcrs()) { - String clientIdAcr = CLIENT_ID_PREFIX + requestParameters.get(PARAM_CLIENT_ID); - log.debug("Adding client_id ACR ({}) to list of AuthnContextClassRefs for purposes" + - " of displaying service name on the wayf", clientIdAcr); - acrs.add(clientIdAcr); + ClientDetailsEntity client = clientDetailsEntityService.loadClientByClientId(clientId); + if (client != null) { + // ADD BLOCKED IdPs + String blockedIdps = getBlockedIdpsAcr(client); + log.debug("blockedIdps ({})", blockedIdps); + if (StringUtils.hasText(blockedIdps)) { + String acr = BLOCKED_IDPS_ACR_PREFIX + blockedIdps; + log.debug("Added blockedIdps as SAML AuthnContextClassRef ({})", acr); + acrs.add(acr); + } + + // ADD ONLY ALLOWED IdPs + String onlyAllowedIdps = getOnlyAllowedIdpsAcr(client); + log.debug("allowedIdps ({})", onlyAllowedIdps); + if (StringUtils.hasText(onlyAllowedIdps)) { + String acr = ONLY_ALLOWED_IDPS_ACR_PREFIX + onlyAllowedIdps; + log.debug("Added onlyAllowedIdps as SAML AuthnContextClassRef ({})", acr); + acrs.add(acr); + } + } + + // ADD CLIENT_ID + if (config.isAddClientIdToAcrs()) { + String clientIdAcr = CLIENT_ID_PREFIX + requestParameters.get(PARAM_CLIENT_ID); + log.debug("Adding client_id ACR ({}) to list of AuthnContextClassRefs for purposes" + + " of displaying service name on the wayf", clientIdAcr); + acrs.add(clientIdAcr); + } } - if (acrs.size() > 0) { + if (!acrs.isEmpty()) { processAcrs(acrs); options.setAuthnContexts(acrs); log.debug("Transformed acr_values ({}) to SAML AuthnContextClassRef ({})", @@ -237,7 +267,7 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { } String clientId = requestParameters.getOrDefault(PARAM_CLIENT_ID, null); if (StringUtils.hasText(clientId)) { - log.debug("Adding ClientID ({}) to SAML RequesterIDs", requestParameters.get(PARAM_CLIENT_ID)); + log.debug("Adding ClientID ({}) to SAML RequesterIDs", clientId); Set<String> requesterIds = options.getRequesterIds(); if (requesterIds == null) { requesterIds = new HashSet<>(); @@ -356,4 +386,26 @@ public class PerunSamlEntryPoint extends SAMLEntryPoint { return result; } + private String getOnlyAllowedIdpsAcr(ClientDetailsEntity client) { + String result = null; + if (config.isOnlyAllowedIdpsEnabled()) { + Set<String> idps = client.getOnlyAllowedIdps(); + if (idps != null && !idps.isEmpty()) { + result = String.join(";", idps); + } + } + return result; + } + + private String getBlockedIdpsAcr(ClientDetailsEntity client) { + String result = null; + if (config.isBlockedIdpsEnabled()) { + Set<String> idps = client.getBlockedIdps(); + if (idps != null && !idps.isEmpty()) { + result = String.join(";", idps); + } + } + return result; + } + } diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/configurations/FacilityAttrsConfig.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/configurations/FacilityAttrsConfig.java index 1af124f9d03b35144856f5541dc0bbd65b67627e..d0d826185d462271cf3c4dbbc416d2c56ee52191 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/configurations/FacilityAttrsConfig.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/configurations/FacilityAttrsConfig.java @@ -1,5 +1,7 @@ package cz.muni.ics.oidc.server.configurations; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/configurations/PerunOidcConfig.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/configurations/PerunOidcConfig.java index d4f072cd8219510e0efeee2bcb464b56398b24ce..7051aa73683c988e6e6764187fc816911f9565b8 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/configurations/PerunOidcConfig.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/configurations/PerunOidcConfig.java @@ -86,6 +86,10 @@ public class PerunOidcConfig implements InitializingBean { private Set<String> krbTokenExchangeRequiredScopes; + private boolean onlyAllowedIdpsEnabled = false; + + private boolean blockedIdpsEnabled = false; + @Autowired private ServletContext servletContext; @@ -169,6 +173,8 @@ public class PerunOidcConfig implements InitializingBean { log.info("Localization files path: {}", localizationFilesPath); log.info("Email contact: {}", emailContact); log.info("Sentry enabled: {}", StringUtils.hasText(sentryConfigFileLocation)); + log.info("OnlyAllowedIdPs ACR enabled: {}", onlyAllowedIdpsEnabled); + log.info("BlockedIdPs ACR enabled: {}", blockedIdpsEnabled); log.info("Perun OIDC version: {}", getPerunOIDCVersion()); } } diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/AuthProcFilterConstants.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/AuthProcFilterConstants.java index 63f6eae94a8768d5293427ba3bb838cc1496ae8c..2baf755d74f6551ec2233284a155d2c75775fbbc 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/AuthProcFilterConstants.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/AuthProcFilterConstants.java @@ -32,6 +32,9 @@ public interface AuthProcFilterConstants { String FILTER_PREFIX = "urn:cesnet:proxyidp:filter:"; String EFILTER_PREFIX = "urn:cesnet:proxyidp:efilter:"; + String ONLY_ALLOWED_IDPS_ACR_PREFIX = "urn:cesnet:proxyidp:only_allowed_idps:"; + String BLOCKED_IDPS_ACR_PREFIX = "urn:cesnet:proxyidp:blocked_idps:"; + String SAML_EPUID = "urn:oid:1.3.6.1.4.1.5923.1.1.1.13"; String SAML_EPPN = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6"; String SAML_EPTID = "urn:oid:1.3.6.1.4.1.5923.1.1.1.10";