From 8ac8f9588c1acbd6d13330997b6266e2d34b5ea8 Mon Sep 17 00:00:00 2001 From: Aldo Mihasi Date: Thu, 28 Apr 2022 12:03:01 +0300 Subject: [PATCH] #7680: Support SAML Authentication --- dmp-backend/web/pom.xml | 51 +++ .../CustomAuthenticationProvider.java | 7 +- .../ConfigurableProviderCustomProvider.java | 4 +- ...onfigurableProviderCustomProviderImpl.java | 4 +- .../ConfigurableProviderUser.java | 4 +- .../entities/ConfigurableProvider.java | 100 ++---- .../oauth2/Oauth2ConfigurableProvider.java | 88 +++++ .../Oauth2ConfigurableProviderToken.java} | 4 +- ...uth2ConfigurableProviderUserSettings.java} | 4 +- .../saml2/Saml2ConfigurableProvider.java | 147 ++++++++ .../models/ConfigurableProviderModel.java | 59 +--- .../models/ConfigurableProvidersModel.java | 14 +- .../Oauth2ConfigurableProviderModel.java | 65 ++++ .../saml2/Saml2ConfigurableProviderModel.java | 56 +++ .../security/validators/TokenValidator.java | 4 +- .../ConfigurableProviderTokenValidator.java | 96 +++++- .../configurableProvider/Saml2SSOUtils.java | 322 ++++++++++++++++++ .../resources/configurableLoginProviders.json | 30 +- dmp-frontend/package.json | 6 +- .../common/enum/configurable-provider-type.ts | 4 + .../configurableProvider.ts | 7 +- .../oauth2ConfigurableProvider.ts | 9 + .../saml2ConfigurableProvider.ts | 7 + .../app/core/services/saml-login.service.ts | 51 +++ .../configurable-login.component.ts | 28 +- .../app/ui/auth/login/login.component.html | 27 +- .../app/ui/auth/login/login.component.scss | 5 +- .../src/app/ui/auth/login/login.module.ts | 7 +- .../src/app/ui/auth/login/login.routing.ts | 4 +- .../saml-login-response.component.ts | 63 ++++ 30 files changed, 1102 insertions(+), 175 deletions(-) create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProvider.java rename dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/{ConfigurableProviderToken.java => oauth2/Oauth2ConfigurableProviderToken.java} (85%) rename dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/{ConfigurableProviderUserSettings.java => oauth2/Oauth2ConfigurableProviderUserSettings.java} (89%) create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/saml2/Saml2ConfigurableProvider.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/oauth2/Oauth2ConfigurableProviderModel.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/saml2/Saml2ConfigurableProviderModel.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/Saml2SSOUtils.java create mode 100644 dmp-frontend/src/app/core/common/enum/configurable-provider-type.ts create mode 100644 dmp-frontend/src/app/core/model/configurable-provider/oauth2ConfigurableProvider.ts create mode 100644 dmp-frontend/src/app/core/model/configurable-provider/saml2ConfigurableProvider.ts create mode 100644 dmp-frontend/src/app/core/services/saml-login.service.ts create mode 100644 dmp-frontend/src/app/ui/auth/login/saml/saml-login-response/saml-login-response.component.ts diff --git a/dmp-backend/web/pom.xml b/dmp-backend/web/pom.xml index 6798b6022..37c2258fa 100644 --- a/dmp-backend/web/pom.xml +++ b/dmp-backend/web/pom.xml @@ -120,6 +120,56 @@ org.springframework.boot spring-boot-starter-tomcat + + + org.opensaml + opensaml-core + ${opensaml.version} + compile + + + org.opensaml + opensaml-saml-api + ${opensaml.version} + compile + + + org.opensaml + opensaml-saml-impl + ${opensaml.version} + compile + + + org.opensaml + opensaml-soap-api + ${opensaml.version} + compile + + + org.opensaml + opensaml-xmlsec-api + ${opensaml.version} + + + org.opensaml + opensaml-security-api + ${opensaml.version} + + + org.opensaml + opensaml-security-impl + ${opensaml.version} + + + org.opensaml + opensaml-profile-api + ${opensaml.version} + + + org.opensaml + xmltooling + 1.4.4 + @@ -155,5 +205,6 @@ eu.eudat.EuDatApplication + 4.0.1 \ No newline at end of file diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/CustomAuthenticationProvider.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/CustomAuthenticationProvider.java index a068a5dcb..f6f7b8be9 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/CustomAuthenticationProvider.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/CustomAuthenticationProvider.java @@ -7,6 +7,8 @@ import eu.eudat.models.data.login.LoginInfo; import eu.eudat.models.data.principal.PrincipalModel; import eu.eudat.models.data.security.Principal; import eu.eudat.logic.security.validators.TokenValidatorFactory; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.resolver.ResolverException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -27,7 +29,7 @@ public class CustomAuthenticationProvider { String token = credentials.getTicket(); try { Principal principal = this.tokenValidatorFactory.getProvider(credentials.getProvider()).validateToken(credentials); - return PrincipalModel.fromEntity(principal); + return (principal != null) ? PrincipalModel.fromEntity(principal) : null; } catch (NonValidTokenException e) { logger.error("Could not validate a user by his token! Reason: " + e.getMessage(), e); throw new UnauthorisedException("Token validation failed - Not a valid token"); @@ -37,6 +39,9 @@ public class CustomAuthenticationProvider { } catch (NullEmailException e) { logger.error(e.getMessage(), e); throw new NullEmailException(); + } catch (ResolverException | ComponentInitializationException e){ + logger.error(e.getMessage(), e); + throw new GeneralSecurityException(); } } } \ No newline at end of file diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderCustomProvider.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderCustomProvider.java index 0f29de3fa..4474703ec 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderCustomProvider.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderCustomProvider.java @@ -1,11 +1,11 @@ package eu.eudat.logic.security.customproviders.ConfigurableProvider; -import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProviderUserSettings; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2.Oauth2ConfigurableProviderUserSettings; import eu.eudat.logic.security.validators.configurableProvider.helpers.ConfigurableProviderResponseToken; public interface ConfigurableProviderCustomProvider { ConfigurableProviderResponseToken getAccessToken(String code, String redirectUri, String clientId, String clientSecret, String accessTokenUrl, String grantType, String access_token, String expires_in); - ConfigurableProviderUser getUser(String accessToken, ConfigurableProviderUserSettings user); + ConfigurableProviderUser getUser(String accessToken, Oauth2ConfigurableProviderUserSettings user); } diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderCustomProviderImpl.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderCustomProviderImpl.java index 1ebf8c336..eec2b4141 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderCustomProviderImpl.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderCustomProviderImpl.java @@ -1,6 +1,6 @@ package eu.eudat.logic.security.customproviders.ConfigurableProvider; -import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProviderUserSettings; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2.Oauth2ConfigurableProviderUserSettings; import eu.eudat.logic.security.validators.configurableProvider.helpers.ConfigurableProviderResponseToken; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -43,7 +43,7 @@ public class ConfigurableProviderCustomProviderImpl implements ConfigurableProvi } @Override - public ConfigurableProviderUser getUser(String accessToken, ConfigurableProviderUserSettings user) { + public ConfigurableProviderUser getUser(String accessToken, Oauth2ConfigurableProviderUserSettings user) { RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = this.createBearerAuthHeaders(accessToken); HttpEntity entity = new HttpEntity<>(headers); diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderUser.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderUser.java index 111abad14..199fe2485 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderUser.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/ConfigurableProviderUser.java @@ -1,6 +1,6 @@ package eu.eudat.logic.security.customproviders.ConfigurableProvider; -import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProviderUserSettings; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2.Oauth2ConfigurableProviderUserSettings; import java.util.Map; @@ -30,7 +30,7 @@ public class ConfigurableProviderUser { this.email = email; } - ConfigurableProviderUser getConfigurableProviderUser(Map data, ConfigurableProviderUserSettings user) { + ConfigurableProviderUser getConfigurableProviderUser(Map data, Oauth2ConfigurableProviderUserSettings user) { if (user.getId() != null && !user.getId().isEmpty()) this.id = (String) data.get(user.getId()); if (user.getName() != null && !user.getName().isEmpty()) diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProvider.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProvider.java index 2e62f7007..7599de3e5 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProvider.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProvider.java @@ -1,22 +1,23 @@ package eu.eudat.logic.security.customproviders.ConfigurableProvider.entities; +import com.fasterxml.jackson.annotation.*; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2.Oauth2ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2.Saml2ConfigurableProvider; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = Oauth2ConfigurableProvider.class, name = "oauth2"), + @JsonSubTypes.Type(value = Saml2ConfigurableProvider.class, name = "saml2") +}) public class ConfigurableProvider { private boolean enabled; private String configurableLoginId; + private String type; private String name; - private String clientId; - private String clientSecret; - private String redirect_uri; - private String access_token_url; - private String grant_type; - private ConfigurableProviderToken token; - private ConfigurableProviderUserSettings user; - private String oauthUrl; - private String scope; - private String state; + private String logoUrl; - public boolean getEnabled() { + public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { @@ -30,6 +31,13 @@ public class ConfigurableProvider { this.configurableLoginId = configurableLoginId; } + public String getType() { + return type; + } + public void setType(String type) { + this.type = type; + } + public String getName() { return name; } @@ -37,73 +45,11 @@ public class ConfigurableProvider { this.name = name; } - public String getClientId() { - return clientId; + public String getLogoUrl() { + return logoUrl; } - public void setClientId(String clientId) { - this.clientId = clientId; + public void setLogoUrl(String logoUrl) { + this.logoUrl = logoUrl; } - public String getClientSecret() { - return clientSecret; - } - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } - - public String getRedirect_uri() { - return redirect_uri; - } - public void setRedirect_uri(String redirect_uri) { - this.redirect_uri = redirect_uri; - } - - public String getAccess_token_url() { - return access_token_url; - } - public void setAccess_token_url(String access_token_url) { - this.access_token_url = access_token_url; - } - - public String getGrant_type() { - return grant_type; - } - public void setGrant_type(String grant_type) { - this.grant_type = grant_type; - } - - public ConfigurableProviderToken getToken() { - return token; - } - public void setToken(ConfigurableProviderToken token) { - this.token = token; - } - - public ConfigurableProviderUserSettings getUser() { - return user; - } - public void setUser(ConfigurableProviderUserSettings user) { - this.user = user; - } - - public String getOauthUrl() { - return oauthUrl; - } - public void setOauthUrl(String oauthUrl) { - this.oauthUrl = oauthUrl; - } - - public String getScope() { - return scope; - } - public void setScope(String scope) { - this.scope = scope; - } - - public String getState() { - return state; - } - public void setState(String state) { - this.state = state; - } } diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProvider.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProvider.java new file mode 100644 index 000000000..0e620d556 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProvider.java @@ -0,0 +1,88 @@ +package eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2; + +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProvider; + +public class Oauth2ConfigurableProvider extends ConfigurableProvider { + + private String clientId; + private String clientSecret; + private String redirect_uri; + private String access_token_url; + private String grant_type; + private Oauth2ConfigurableProviderToken token; + private Oauth2ConfigurableProviderUserSettings user; + private String oauthUrl; + private String scope; + private String state; + + public String getClientId() { + return clientId; + } + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getRedirect_uri() { + return redirect_uri; + } + public void setRedirect_uri(String redirect_uri) { + this.redirect_uri = redirect_uri; + } + + public String getAccess_token_url() { + return access_token_url; + } + public void setAccess_token_url(String access_token_url) { + this.access_token_url = access_token_url; + } + + public String getGrant_type() { + return grant_type; + } + public void setGrant_type(String grant_type) { + this.grant_type = grant_type; + } + + public Oauth2ConfigurableProviderToken getToken() { + return token; + } + public void setToken(Oauth2ConfigurableProviderToken token) { + this.token = token; + } + + public Oauth2ConfigurableProviderUserSettings getUser() { + return user; + } + public void setUser(Oauth2ConfigurableProviderUserSettings user) { + this.user = user; + } + + public String getOauthUrl() { + return oauthUrl; + } + public void setOauthUrl(String oauthUrl) { + this.oauthUrl = oauthUrl; + } + + public String getScope() { + return scope; + } + public void setScope(String scope) { + this.scope = scope; + } + + public String getState() { + return state; + } + public void setState(String state) { + this.state = state; + } + +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProviderToken.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProviderToken.java similarity index 85% rename from dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProviderToken.java rename to dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProviderToken.java index ac2021a82..f03235263 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProviderToken.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProviderToken.java @@ -1,6 +1,6 @@ -package eu.eudat.logic.security.customproviders.ConfigurableProvider.entities; +package eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2; -public class ConfigurableProviderToken { +public class Oauth2ConfigurableProviderToken { private String access_token; private String expires_in; diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProviderUserSettings.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProviderUserSettings.java similarity index 89% rename from dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProviderUserSettings.java rename to dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProviderUserSettings.java index 693957ca3..cf639c1bc 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/ConfigurableProviderUserSettings.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/oauth2/Oauth2ConfigurableProviderUserSettings.java @@ -1,6 +1,6 @@ -package eu.eudat.logic.security.customproviders.ConfigurableProvider.entities; +package eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2; -public class ConfigurableProviderUserSettings { +public class Oauth2ConfigurableProviderUserSettings { private String id; private String name; private String email; diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/saml2/Saml2ConfigurableProvider.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/saml2/Saml2ConfigurableProvider.java new file mode 100644 index 000000000..d1e930075 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/entities/saml2/Saml2ConfigurableProvider.java @@ -0,0 +1,147 @@ +package eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2; + +import com.fasterxml.jackson.annotation.JsonValue; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProvider; + +import java.util.Map; + +public class Saml2ConfigurableProvider extends ConfigurableProvider { + + public enum SAML2UsingFormat { + NAME("name"), FRIENDLY_NAME("friendly_name"); + + private String name; + SAML2UsingFormat(String name) { + this.name = name; + } + @JsonValue + public String getName() { return name; } + + public static SAML2UsingFormat fromName(String name) { + for (SAML2UsingFormat type: SAML2UsingFormat.values()) { + if (name.equals(type.getName())) { + return type; + } + } + throw new IllegalArgumentException("Unsupported SAML2 Attribute " + name); + } + } + + public enum SAML2AttributeType { + XSSTRING("XSString"), XSINTEGER("XSInteger"), XSDATETIME("XSDateTime"), XSBOOLEAN("XSBoolean"), XSBASE64BINARY("XSBase64Binary"), XSURI("XSURI"), XSQNAME("XSQName"), XSANY("XSAny"); + + private String type; + SAML2AttributeType(String type) { + this.type = type; + } + @JsonValue + public String getType() { return type; } + + public static SAML2AttributeType fromType(String type) { + for (SAML2AttributeType t: SAML2AttributeType.values()) { + if (type.equals(t.getType())) { + return t; + } + } + throw new IllegalArgumentException("Unsupported SAML2 Attribute Type " + type); + } + } + + private String spEntityId; + private String idpEntityId; + private String idpUrl; + private String idpMetadataUrl; + private boolean responseSigned; + private String signedResponseCredential; + private boolean assertionSigned; + private SAML2UsingFormat usingFormat; + private Map attributeTypes; + private Map configurableUserFromAttributes; + private String binding; + //private String assertionConsumerServiceUrl; + + public String getSpEntityId() { + return spEntityId; + } + public void setSpEntityId(String spEntityId) { + this.spEntityId = spEntityId; + } + + public String getIdpEntityId() { + return idpEntityId; + } + public void setIdpEntityId(String idpEntityId) { + this.idpEntityId = idpEntityId; + } + + public String getIdpUrl() { + return idpUrl; + } + public void setIdpUrl(String idpUrl) { + this.idpUrl = idpUrl; + } + + public String getIdpMetadataUrl() { + return idpMetadataUrl; + } + public void setIdpMetadataUrl(String idpMetadataUrl) { + this.idpMetadataUrl = idpMetadataUrl; + } + + public boolean isResponseSigned() { + return responseSigned; + } + public void setResponseSigned(boolean responseSigned) { + this.responseSigned = responseSigned; + } + + public String getSignedResponseCredential() { + return signedResponseCredential; + } + public void setSignedResponseCredential(String signedResponseCredential) { + this.signedResponseCredential = signedResponseCredential; + } + + public boolean isAssertionSigned() { + return assertionSigned; + } + public void setAssertionSigned(boolean assertionSigned) { + this.assertionSigned = assertionSigned; + } + + public SAML2UsingFormat getUsingFormat() { + return usingFormat; + } + public void setUsingFormat(SAML2UsingFormat usingFormat) { + this.usingFormat = usingFormat; + } + + public Map getConfigurableUserFromAttributes() { + return configurableUserFromAttributes; + } + public void setConfigurableUserFromAttributes(Map configurableUserFromAttributes) { + this.configurableUserFromAttributes = configurableUserFromAttributes; + } + + public Map getAttributeTypes() { + return attributeTypes; + } + public void setAttributeTypes(Map attributeTypes) { + this.attributeTypes = attributeTypes; + } + + public String getBinding() { + return binding; + } + public void setBinding(String binding) { + this.binding = binding; + } + +// public String getAssertionConsumerServiceUrl() { +// return assertionConsumerServiceUrl; +// } +// public void setAssertionConsumerServiceUrl(String assertionConsumerServiceUrl) { +// this.assertionConsumerServiceUrl = assertionConsumerServiceUrl; +// } + +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/ConfigurableProviderModel.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/ConfigurableProviderModel.java index b09b002d2..4df28c445 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/ConfigurableProviderModel.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/ConfigurableProviderModel.java @@ -5,12 +5,9 @@ import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.Con public class ConfigurableProviderModel { private String configurableLoginId; + private String type; private String name; - private String clientId; - private String redirect_uri; - private String oauthUrl; - private String scope; - private String state; + private String logoUrl; public String getConfigurableLoginId() { return configurableLoginId; @@ -19,6 +16,13 @@ public class ConfigurableProviderModel { this.configurableLoginId = configurableLoginId; } + public String getType() { + return type; + } + public void setType(String type) { + this.type = type; + } + public String getName() { return name; } @@ -26,51 +30,20 @@ public class ConfigurableProviderModel { this.name = name; } - public String getClientId() { - return clientId; + public String getLogoUrl() { + return logoUrl; } - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getRedirect_uri() { - return redirect_uri; - } - public void setRedirect_uri(String redirect_uri) { - this.redirect_uri = redirect_uri; - } - - public String getOauthUrl() { - return oauthUrl; - } - public void setOauthUrl(String oauthUrl) { - this.oauthUrl = oauthUrl; - } - - public String getScope() { - return scope; - } - public void setScope(String scope) { - this.scope = scope; - } - - public String getState() { - return state; - } - public void setState(String state) { - this.state = state; + public void setLogoUrl(String logoUrl) { + this.logoUrl = logoUrl; } public ConfigurableProviderModel fromDataModel(ConfigurableProvider entity) { ConfigurableProviderModel model = new ConfigurableProviderModel(); model.setConfigurableLoginId(entity.getConfigurableLoginId()); + model.setType(entity.getType()); model.setName(entity.getName()); - model.setClientId(entity.getClientId()); - model.setRedirect_uri(entity.getRedirect_uri()); - model.setOauthUrl(entity.getOauthUrl()); - model.setScope(entity.getScope()); - model.setState(entity.getState()); - + model.setLogoUrl(entity.getLogoUrl()); return model; } + } diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/ConfigurableProvidersModel.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/ConfigurableProvidersModel.java index 28514d6d4..0ddc12088 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/ConfigurableProvidersModel.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/ConfigurableProvidersModel.java @@ -2,6 +2,10 @@ package eu.eudat.logic.security.customproviders.ConfigurableProvider.models; import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProvider; import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProviders; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2.Oauth2ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2.Saml2ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.models.oauth2.Oauth2ConfigurableProviderModel; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.models.saml2.Saml2ConfigurableProviderModel; import java.util.LinkedList; import java.util.List; @@ -21,8 +25,14 @@ public class ConfigurableProvidersModel { List providerModelList = new LinkedList<>(); if (entity != null) { for (ConfigurableProvider entityProvider : entity.getProviders()) { - if (entityProvider.getEnabled()) - providerModelList.add(new ConfigurableProviderModel().fromDataModel(entityProvider)); + if (entityProvider.isEnabled()){ + if(entityProvider instanceof Oauth2ConfigurableProvider) + providerModelList.add(new Oauth2ConfigurableProviderModel().fromDataModel(entityProvider)); + else if(entityProvider instanceof Saml2ConfigurableProvider) + providerModelList.add(new Saml2ConfigurableProviderModel().fromDataModel(entityProvider)); + else + providerModelList.add(new ConfigurableProviderModel().fromDataModel(entityProvider)); + } } } model.setProviders(providerModelList); diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/oauth2/Oauth2ConfigurableProviderModel.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/oauth2/Oauth2ConfigurableProviderModel.java new file mode 100644 index 000000000..26c6aeb10 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/oauth2/Oauth2ConfigurableProviderModel.java @@ -0,0 +1,65 @@ +package eu.eudat.logic.security.customproviders.ConfigurableProvider.models.oauth2; + +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2.Oauth2ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.models.ConfigurableProviderModel; + +public class Oauth2ConfigurableProviderModel extends ConfigurableProviderModel { + + private String clientId; + private String redirect_uri; + private String oauthUrl; + private String scope; + private String state; + + public String getClientId() { + return clientId; + } + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getRedirect_uri() { + return redirect_uri; + } + public void setRedirect_uri(String redirect_uri) { + this.redirect_uri = redirect_uri; + } + + public String getOauthUrl() { + return oauthUrl; + } + public void setOauthUrl(String oauthUrl) { + this.oauthUrl = oauthUrl; + } + + public String getScope() { + return scope; + } + public void setScope(String scope) { + this.scope = scope; + } + + public String getState() { + return state; + } + public void setState(String state) { + this.state = state; + } + + @Override + public Oauth2ConfigurableProviderModel fromDataModel(ConfigurableProvider entity) { + Oauth2ConfigurableProviderModel model = new Oauth2ConfigurableProviderModel(); + model.setConfigurableLoginId(entity.getConfigurableLoginId()); + model.setType(entity.getType()); + model.setName(entity.getName()); + model.setLogoUrl(entity.getLogoUrl()); + model.setClientId(((Oauth2ConfigurableProvider)entity).getClientId()); + model.setRedirect_uri(((Oauth2ConfigurableProvider)entity).getRedirect_uri()); + model.setOauthUrl(((Oauth2ConfigurableProvider)entity).getOauthUrl()); + model.setScope(((Oauth2ConfigurableProvider)entity).getScope()); + model.setState(((Oauth2ConfigurableProvider)entity).getState()); + return model; + } + +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/saml2/Saml2ConfigurableProviderModel.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/saml2/Saml2ConfigurableProviderModel.java new file mode 100644 index 000000000..d4a1e606b --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/customproviders/ConfigurableProvider/models/saml2/Saml2ConfigurableProviderModel.java @@ -0,0 +1,56 @@ +package eu.eudat.logic.security.customproviders.ConfigurableProvider.models.saml2; + +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2.Saml2ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.models.ConfigurableProviderModel; + +public class Saml2ConfigurableProviderModel extends ConfigurableProviderModel { + + private String spEntityId; + private String idpUrl; + private String binding; + //private String assertionConsumerServiceUrl; + + public String getSpEntityId() { + return spEntityId; + } + public void setSpEntityId(String spEntityId) { + this.spEntityId = spEntityId; + } + + public String getIdpUrl() { + return idpUrl; + } + public void setIdpUrl(String idpUrl) { + this.idpUrl = idpUrl; + } + + public String getBinding() { + return binding; + } + public void setBinding(String binding) { + this.binding = binding; + } + +// public String getAssertionConsumerServiceUrl() { +// return assertionConsumerServiceUrl; +// } +// public void setAssertionConsumerServiceUrl(String assertionConsumerServiceUrl) { +// this.assertionConsumerServiceUrl = assertionConsumerServiceUrl; +// } + + @Override + public Saml2ConfigurableProviderModel fromDataModel(ConfigurableProvider entity) { + Saml2ConfigurableProviderModel model = new Saml2ConfigurableProviderModel(); + model.setConfigurableLoginId(entity.getConfigurableLoginId()); + model.setType(entity.getType()); + model.setName(entity.getName()); + model.setLogoUrl(entity.getLogoUrl()); + model.setSpEntityId(((Saml2ConfigurableProvider)entity).getSpEntityId()); + model.setIdpUrl(((Saml2ConfigurableProvider)entity).getIdpUrl()); + model.setBinding(((Saml2ConfigurableProvider)entity).getBinding()); + //model.setAssertionConsumerServiceUrl(((Saml2ConfigurableProvider)entity).getAssertionConsumerServiceUrl()); + return model; + } + +} diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/TokenValidator.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/TokenValidator.java index f03ca04c8..01323f1e0 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/TokenValidator.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/TokenValidator.java @@ -4,12 +4,14 @@ import eu.eudat.exceptions.security.NonValidTokenException; import eu.eudat.exceptions.security.NullEmailException; import eu.eudat.models.data.login.LoginInfo; import eu.eudat.models.data.security.Principal; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.resolver.ResolverException; import java.io.IOException; import java.security.GeneralSecurityException; public interface TokenValidator { - Principal validateToken(LoginInfo credentials) throws NonValidTokenException, IOException, GeneralSecurityException, NullEmailException; + Principal validateToken(LoginInfo credentials) throws NonValidTokenException, IOException, GeneralSecurityException, NullEmailException, ResolverException, ComponentInitializationException; } diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/ConfigurableProviderTokenValidator.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/ConfigurableProviderTokenValidator.java index bf426f96e..e9fda70e9 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/ConfigurableProviderTokenValidator.java +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/ConfigurableProviderTokenValidator.java @@ -6,6 +6,8 @@ import eu.eudat.logic.proxy.config.configloaders.ConfigLoader; import eu.eudat.logic.security.customproviders.ConfigurableProvider.ConfigurableProviderCustomProvider; import eu.eudat.logic.security.customproviders.ConfigurableProvider.ConfigurableProviderUser; import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.oauth2.Oauth2ConfigurableProvider; +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2.Saml2ConfigurableProvider; import eu.eudat.logic.security.validators.TokenValidator; import eu.eudat.logic.security.validators.configurableProvider.helpers.ConfigurableProviderRequest; import eu.eudat.logic.security.validators.configurableProvider.helpers.ConfigurableProviderResponseToken; @@ -13,14 +15,22 @@ import eu.eudat.logic.services.operations.authentication.AuthenticationService; import eu.eudat.models.data.login.LoginInfo; import eu.eudat.models.data.loginprovider.LoginProviderUser; import eu.eudat.models.data.security.Principal; + +import org.opensaml.saml.saml2.core.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Component("configurableProviderTokenValidator") public class ConfigurableProviderTokenValidator implements TokenValidator { + private static final Logger logger = LoggerFactory.getLogger(ConfigurableProviderTokenValidator.class); + private ConfigurableProviderCustomProvider configurableProvider; private AuthenticationService nonVerifiedUserAuthenticationService; private ConfigLoader configLoader; @@ -32,7 +42,7 @@ public class ConfigurableProviderTokenValidator implements TokenValidator { } public ConfigurableProviderResponseToken getAccessToken(ConfigurableProviderRequest configurableProviderRequest) { - ConfigurableProvider provider = getConfigurableProviderFromId(configurableProviderRequest.getConfigurableLoginId()); + Oauth2ConfigurableProvider provider = (Oauth2ConfigurableProvider)getConfigurableProviderFromId(configurableProviderRequest.getConfigurableLoginId()); return this.configurableProvider.getAccessToken(configurableProviderRequest.getCode(), provider.getRedirect_uri(), provider.getClientId(), provider.getClientSecret(), provider.getAccess_token_url(), provider.getGrant_type(), provider.getToken().getAccess_token(), provider.getToken().getExpires_in()); @@ -41,15 +51,83 @@ public class ConfigurableProviderTokenValidator implements TokenValidator { public Principal validateToken(LoginInfo credentials) throws NullEmailException { String configurableLoginId = ((Map) credentials.getData()).get("configurableLoginId").toString(); ConfigurableProvider configurableProvider = getConfigurableProviderFromId(configurableLoginId); - ConfigurableProviderUser configurableUser = this.configurableProvider.getUser(credentials.getTicket(), configurableProvider.getUser()); - LoginProviderUser user = new LoginProviderUser(); - user.setId(configurableUser.getId()); - user.setEmail(configurableUser.getEmail()); - user.setName(configurableUser.getName()); - user.setProvider(credentials.getProvider()); - user.setSecret(credentials.getTicket()); - return this.nonVerifiedUserAuthenticationService.Touch(user); + LoginProviderUser user = new LoginProviderUser(); + if (configurableProvider.getType().equals("oauth2")) { + ConfigurableProviderUser configurableUser = this.configurableProvider.getUser(credentials.getTicket(), ((Oauth2ConfigurableProvider)configurableProvider).getUser()); + user.setId(configurableUser.getId()); + user.setEmail(configurableUser.getEmail()); + user.setName(configurableUser.getName()); + user.setProvider(credentials.getProvider()); + user.setSecret(credentials.getTicket()); + return this.nonVerifiedUserAuthenticationService.Touch(user); + } + else if (configurableProvider.getType().equals("saml2")) { + + Response saml2Response = null; + try { + Saml2ConfigurableProvider saml2ConfigurableProvider = (Saml2ConfigurableProvider)configurableProvider; + saml2Response = Saml2SSOUtils.processResponse(credentials.getTicket(), saml2ConfigurableProvider.getIdpEntityId(), saml2ConfigurableProvider.getSpEntityId(), + saml2ConfigurableProvider.getIdpMetadataUrl(), saml2ConfigurableProvider.isResponseSigned(), saml2ConfigurableProvider.isAssertionSigned()); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + + List assertions = saml2Response.getAssertions(); + if(assertions != null && !assertions.isEmpty()){ + + List attributeStatements = assertions.get(0).getAttributeStatements(); + if(attributeStatements != null && !attributeStatements.isEmpty()){ + + List attributes = attributeStatements.get(0).getAttributes(); + if(attributes != null && !attributes.isEmpty()){ + + Saml2ConfigurableProvider.SAML2UsingFormat usingFormat = ((Saml2ConfigurableProvider)configurableProvider).getUsingFormat(); + Map attributeMapping = ((Saml2ConfigurableProvider)configurableProvider).getConfigurableUserFromAttributes(); + Map attributeType = ((Saml2ConfigurableProvider)configurableProvider).getAttributeTypes(); + Map saml2User = new HashMap<>(); + for(Attribute attribute: attributes){ + + String attributeName = Saml2SSOUtils.getAttributeName(attribute, usingFormat); + if(attributeName != null && attributeMapping.containsValue(attributeName)){ + + Saml2ConfigurableProvider.SAML2AttributeType attrType = attributeType.get(attributeName); + if(attribute.getAttributeValues() != null && !attribute.getAttributeValues().isEmpty() && attrType != null){ + Object attributeValue = Saml2SSOUtils.getAttributeType(attribute.getAttributeValues().get(0), attrType); + if(attributeValue != null) { + saml2User.put(attributeName, attributeValue); + } + } + + } + + } + + try{ + String subjectNameId = assertions.get(0).getSubject().getNameID().getValue(); + String userId = configurableLoginId + ": " + subjectNameId; + user.setId(userId); + } catch(NullPointerException e){ + logger.error("Could not get Subject NameID value of assertion"); + return null; + } + user.setEmail((String)saml2User.get(attributeMapping.get("email"))); + user.setName((String)saml2User.get(attributeMapping.get("name"))); + user.setProvider(credentials.getProvider()); + user.setSecret(credentials.getTicket()); + + } + + } + + } + else + return null; + + return this.nonVerifiedUserAuthenticationService.Touch(user); + + } + return null; } private ConfigurableProvider getConfigurableProviderFromId(String configurableId) { diff --git a/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/Saml2SSOUtils.java b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/Saml2SSOUtils.java new file mode 100644 index 000000000..112c85b38 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/logic/security/validators/configurableProvider/Saml2SSOUtils.java @@ -0,0 +1,322 @@ +package eu.eudat.logic.security.validators.configurableProvider; + +import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2.Saml2ConfigurableProvider; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.BasicParserPool; +import org.apache.http.impl.client.HttpClientBuilder; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.config.InitializationException; +import org.opensaml.core.config.InitializationService; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.core.xml.io.UnmarshallingException; +import org.opensaml.core.xml.schema.*; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.criterion.EntityRoleCriterion; +import org.opensaml.saml.criterion.ProtocolCriterion; +import org.opensaml.saml.metadata.resolver.impl.HTTPMetadataResolver; +import org.opensaml.saml.metadata.resolver.impl.PredicateRoleDescriptorResolver; +import org.opensaml.saml.saml2.core.*; +import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.saml.security.impl.MetadataCredentialResolver; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.criteria.UsageCriterion; +import org.opensaml.security.x509.X509Credential; +import org.opensaml.xml.util.Base64; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; +import org.opensaml.xmlsec.signature.support.SignatureValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +public class Saml2SSOUtils { + + private static final Logger logger = LoggerFactory.getLogger(Saml2SSOUtils.class); + private static boolean isBootStrapped = false; + private static BasicParserPool parserPool; + private static XMLObjectProviderRegistry registry; + + private Saml2SSOUtils() { + } + + private static void doBootstrap() throws Exception { + if (!isBootStrapped) { + try { + boostrap(); + isBootStrapped = true; + } catch (Exception e) { + throw new Exception("Error in bootstrapping the OpenSAML2 library", e); + } + } + } + + private static void boostrap(){ + parserPool = new BasicParserPool(); + parserPool.setMaxPoolSize(100); + parserPool.setCoalescing(true); + parserPool.setIgnoreComments(true); + parserPool.setIgnoreElementContentWhitespace(true); + parserPool.setNamespaceAware(true); + parserPool.setExpandEntityReferences(false); + parserPool.setXincludeAware(false); + + final Map features = new HashMap(); + features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE); + features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE); + features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE); + features.put("http://apache.org/xml/features/validation/schema/normalized-value", Boolean.FALSE); + features.put("http://javax.xml.XMLConstants/feature/secure-processing", Boolean.TRUE); + + parserPool.setBuilderFeatures(features); + + parserPool.setBuilderAttributes(new HashMap()); + + try { + parserPool.initialize(); + } catch (ComponentInitializationException e) { + logger.error(e.getMessage(), e); + } + + registry = new XMLObjectProviderRegistry(); + ConfigurationService.register(XMLObjectProviderRegistry.class, registry); + registry.setParserPool(parserPool); + + try { + InitializationService.initialize(); + } catch (InitializationException e) { + logger.error(e.getMessage(), e); + } + } + + public static String getAttributeName(Attribute attribute, Saml2ConfigurableProvider.SAML2UsingFormat usingFormat){ + String friendlyName = attribute.getFriendlyName(); + String name = attribute.getName(); + if(usingFormat.getName().equals(Saml2ConfigurableProvider.SAML2UsingFormat.FRIENDLY_NAME.getName())){ + return (friendlyName != null) ? friendlyName : name; + } + else{ + return (name != null) ? name : friendlyName; + } + } + + public static Object getAttributeType(XMLObject attribute, Saml2ConfigurableProvider.SAML2AttributeType attributeType){ + + if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSSTRING.getType())){ + return ((XSString)attribute).getValue(); + } + else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSINTEGER.getType())){ + return ((XSInteger)attribute).getValue(); + } + else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSDATETIME.getType())){ + return ((XSDateTime)attribute).getValue(); + } + else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSBOOLEAN.getType())){ + return ((XSBoolean)attribute).getValue(); + } + else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSBASE64BINARY.getType())){ + return ((XSBase64Binary)attribute).getValue(); + } + else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSURI.getType())){ + return ((XSURI)attribute).getURI(); + } + else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSQNAME.getType())){ + return ((XSQName)attribute).getValue(); + } + else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSANY.getType())){ + return ((XSAny)attribute).getTextContent(); + } + else { + return null; + } + + } + + private static XMLObject unmarshall(String saml2SSOString) throws Exception { + + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + //documentBuilderFactory.setExpandEntityReferences(false); + documentBuilderFactory.setNamespaceAware(true); + try { + DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); + ByteArrayInputStream is = new ByteArrayInputStream(saml2SSOString.getBytes(StandardCharsets.UTF_8)); + Document document = docBuilder.parse(is); + Element element = document.getDocumentElement(); + + UnmarshallerFactory unmarshallerFactory = registry.getUnmarshallerFactory(); + Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); + return unmarshaller.unmarshall(element); + } catch (ParserConfigurationException | UnmarshallingException | SAXException | IOException e) { + throw new Exception("Error in unmarshalling SAML2SSO Request from the encoded String", e); + } + + } + + public static Response processResponse(String saml2SSOResponse, String idPEntityId, String spEntityId, String metadataUrl, boolean responseSigned, boolean assertionSigned) throws Exception { + + doBootstrap(); + if (saml2SSOResponse != null) { + byte[] decodedResponse = Base64.decode(saml2SSOResponse); + ByteArrayInputStream bytesIn = new ByteArrayInputStream(decodedResponse); + InflaterInputStream inflater = new InflaterInputStream(bytesIn, new Inflater(true)); + String response = new BufferedReader(new InputStreamReader(inflater, StandardCharsets.UTF_8)) + .lines().collect(Collectors.joining("\n")); + return processSSOResponse(response, idPEntityId, spEntityId, metadataUrl, responseSigned, assertionSigned); + + } else { + throw new Exception("Invalid SAML2 Response. SAML2 Response can not be null."); + } + } + + private static Response processSSOResponse(String saml2SSOResponse, String idPEntityId, String spEntityId, String metadataUrl, boolean responseSigned, boolean assertionSigned) throws Exception { + + Response saml2Response = (Response) Saml2SSOUtils.unmarshall(saml2SSOResponse); + + Assertion assertion = null; +// if (config.isAssertionEncrypted()) { +// List encryptedAssertions = saml2Response.getEncryptedAssertions(); +// EncryptedAssertion encryptedAssertion = null; +// if (!CollectionUtils.isEmpty(encryptedAssertions)) { +// encryptedAssertion = encryptedAssertions.get(0); +// try { +// assertion = getDecryptedAssertion(encryptedAssertion); +// } catch (Exception e) { +// if (log.isDebugEnabled()) { +// log.debug("Assertion decryption failure : ", e); +// } +// throw new Exception("Unable to decrypt the SAML2 Assertion"); +// } +// } +// } else { + List assertions = saml2Response.getAssertions(); + if (assertions != null && !assertions.isEmpty()) { + assertion = assertions.get(0); + } +// } + if (assertion == null) { + throw new Exception("SAML2 Assertion not found in the Response"); + } + + String idPEntityIdValue = assertion.getIssuer().getValue(); + if (idPEntityIdValue == null || idPEntityIdValue.isEmpty()) { + throw new Exception("SAML2 Response does not contain an Issuer value"); + } else if (!idPEntityIdValue.equals(idPEntityId)) { + throw new Exception("SAML2 Response Issuer verification failed"); + } + + String subject = null; + if (assertion.getSubject() != null && assertion.getSubject().getNameID() != null) { + subject = assertion.getSubject().getNameID().getValue(); + } + + if (subject == null) { + throw new Exception("SAML2 Response does not contain the name of the subject"); + } + + validateAudienceRestriction(assertion, spEntityId); + + final HTTPMetadataResolver metadataResolver = new HTTPMetadataResolver(HttpClientBuilder.create().build(), metadataUrl); + metadataResolver.setId(metadataResolver.getClass().getCanonicalName()); + metadataResolver.setParserPool(parserPool); + metadataResolver.initialize(); + + final MetadataCredentialResolver metadataCredentialResolver = new MetadataCredentialResolver(); + final PredicateRoleDescriptorResolver roleResolver = new PredicateRoleDescriptorResolver(metadataResolver); + final KeyInfoCredentialResolver keyResolver = DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver(); + metadataCredentialResolver.setKeyInfoCredentialResolver(keyResolver); + metadataCredentialResolver.setRoleDescriptorResolver(roleResolver); + metadataCredentialResolver.initialize(); + roleResolver.initialize(); + + CriteriaSet criteriaSet = new CriteriaSet(); + criteriaSet.add(new UsageCriterion(UsageType.SIGNING)); + criteriaSet.add(new EntityRoleCriterion(IDPSSODescriptor.DEFAULT_ELEMENT_NAME)); + criteriaSet.add(new ProtocolCriterion(SAMLConstants.SAML20P_NS)); + criteriaSet.add(new EntityIdCriterion(idPEntityId)); + + Credential credential = metadataCredentialResolver.resolveSingle(criteriaSet); + + validateSignature(saml2Response, assertion, responseSigned, null, assertionSigned, credential); + + return saml2Response; + + } + + private static void validateAudienceRestriction(Assertion assertion, String requiredSPEntityId) throws Exception { + + if (assertion != null) { + Conditions conditions = assertion.getConditions(); + if (conditions != null) { + List audienceRestrictions = conditions.getAudienceRestrictions(); + if (audienceRestrictions != null && !audienceRestrictions.isEmpty()) { + boolean audienceFound = false; + for (AudienceRestriction audienceRestriction : audienceRestrictions) { + if (audienceRestriction.getAudiences() != null && !audienceRestriction.getAudiences().isEmpty() + ) { + for (Audience audience : audienceRestriction.getAudiences()) { + if (requiredSPEntityId.equals(audience.getURI())) { + audienceFound = true; + break; + } + } + } + if (audienceFound) { + break; + } + } + if (!audienceFound) { + throw new Exception("SAML2 Assertion Audience Restriction validation failed"); + } + } else { + throw new Exception("SAML2 Response doesn't contain AudienceRestrictions"); + } + } else { + throw new Exception("SAML2 Response doesn't contain Conditions"); + } + } + } + + private static void validateSignature(Response response, Assertion assertion, Boolean isResponseSigned, X509Credential signedResponseCredential, Boolean isAssertionSigned, Credential assertionCredential) throws Exception { + + if (isResponseSigned) { + if (response.getSignature() == null) { + throw new Exception("SAML2 Response signing is enabled, but signature element not found in SAML2 Response element"); + } else { + try { + SignatureValidator.validate(response.getSignature(), signedResponseCredential); + } catch (Exception e) { + throw new Exception("Signature validation failed for SAML2 Response"); + } + } + } + if (isAssertionSigned) { + if (assertion.getSignature() == null) { + throw new Exception("SAML2 Assertion signing is enabled, but signature element not found in SAML2 Assertion element"); + } else { + try { + SignatureValidator.validate(assertion.getSignature(), assertionCredential); + } catch (Exception e) { + throw new Exception("Signature validation failed for SAML2 Assertion"); + } + } + } + } + +} \ No newline at end of file diff --git a/dmp-backend/web/src/main/resources/configurableLoginProviders.json b/dmp-backend/web/src/main/resources/configurableLoginProviders.json index ae519164b..7cc036138 100644 --- a/dmp-backend/web/src/main/resources/configurableLoginProviders.json +++ b/dmp-backend/web/src/main/resources/configurableLoginProviders.json @@ -2,7 +2,8 @@ "providers": [ { "enabled": false, - "configurableLoginId": "myId", + "type": "oauth2", + "configurableLoginId": "oauth2-localhost", "name": "myApp", "clientId": "", "clientSecret": "", @@ -21,7 +22,32 @@ }, "oauthUrl": "/authorize", "scope": "email", - "state": "123562" + "state": "123562", + "logoUrl": null + }, + { + "enabled": true, + "type": "saml2", + "configurableLoginId": "keycloak-saml2", + "name": "keycloak", + "spEntityId": "argos", + "idpEntityId": "http://localhost:8080/auth/realms/master", + "idpUrl": "http://localhost:8080/auth/realms/master/protocol/saml", + "idpMetadataUrl": "http://localhost:8080/auth/realms/master/protocol/saml/descriptor", + "responseSigned": false, + "signedResponseCredential": null, + "assertionSigned": true, + "usingFormat": "friendly_name", + "configurableUserFromAttributes": { + "email": "email", + "name": "givenName" + }, + "attributeTypes": { + "email": "XSString", + "givenName": "XSString" + }, + "binding": "Redirect", + "logoUrl": "https://upload.wikimedia.org/wikipedia/commons/2/29/Keycloak_Logo.png" } ] } diff --git a/dmp-frontend/package.json b/dmp-frontend/package.json index 12b85564e..3838fb831 100644 --- a/dmp-frontend/package.json +++ b/dmp-frontend/package.json @@ -38,6 +38,7 @@ "ngx-dropzone": "^3.0.0", "ngx-guided-tour": "^1.1.11", "ngx-matomo": "^0.1.4", + "pako": "^1.0.11", "rxjs": "^6.3.2", "tinymce": "^5.9.2", "tslib": "^2.0.0", @@ -47,12 +48,12 @@ "devDependencies": { "@angular-devkit/build-angular": "~12.2.7", "@angular/cdk": "^12.2.7", - "@angular/material": "^12.2.7", "@angular/cli": "12.2.7", "@angular/compiler-cli": "^12.2.7", + "@angular/language-service": "^12.2.7", + "@angular/material": "^12.2.7", "@angular/platform-browser-dynamic": "^12.2.7", "@angular/router": "^12.2.7", - "@angular/language-service": "^12.2.7", "@types/facebook-js-sdk": "^3.3.5", "@types/file-saver": "^2.0.3", "@types/gapi": "^0.0.41", @@ -61,6 +62,7 @@ "@types/jasminewd2": "~2.0.10", "@types/moment-timezone": "^0.5.13", "@types/node": "^12.11.1", + "@types/pako": "^1.0.3", "codelyzer": "^6.0.2", "ts-node": "~10.2.1", "tslint": "~6.1.0", diff --git a/dmp-frontend/src/app/core/common/enum/configurable-provider-type.ts b/dmp-frontend/src/app/core/common/enum/configurable-provider-type.ts new file mode 100644 index 000000000..8f1a01e8b --- /dev/null +++ b/dmp-frontend/src/app/core/common/enum/configurable-provider-type.ts @@ -0,0 +1,4 @@ +export enum ConfigurableProviderType { + Oauth2 = "oauth2", + Saml2 = "saml2" +} \ No newline at end of file diff --git a/dmp-frontend/src/app/core/model/configurable-provider/configurableProvider.ts b/dmp-frontend/src/app/core/model/configurable-provider/configurableProvider.ts index c8098f618..d69525c1c 100644 --- a/dmp-frontend/src/app/core/model/configurable-provider/configurableProvider.ts +++ b/dmp-frontend/src/app/core/model/configurable-provider/configurableProvider.ts @@ -1,9 +1,6 @@ export class ConfigurableProvider { configurableLoginId: string; + type: string; name: string; - clientId: string; - redirect_uri: string; - oauthUrl: string; - scope: string; - state: string; + logoUrl: string; } diff --git a/dmp-frontend/src/app/core/model/configurable-provider/oauth2ConfigurableProvider.ts b/dmp-frontend/src/app/core/model/configurable-provider/oauth2ConfigurableProvider.ts new file mode 100644 index 000000000..7080cbccc --- /dev/null +++ b/dmp-frontend/src/app/core/model/configurable-provider/oauth2ConfigurableProvider.ts @@ -0,0 +1,9 @@ +import { ConfigurableProvider } from "./configurableProvider"; + +export class Oauth2ConfigurableProvider extends ConfigurableProvider{ + clientId: string; + redirect_uri: string; + oauthUrl: string; + scope: string; + state: string; +} \ No newline at end of file diff --git a/dmp-frontend/src/app/core/model/configurable-provider/saml2ConfigurableProvider.ts b/dmp-frontend/src/app/core/model/configurable-provider/saml2ConfigurableProvider.ts new file mode 100644 index 000000000..72d7e5f1c --- /dev/null +++ b/dmp-frontend/src/app/core/model/configurable-provider/saml2ConfigurableProvider.ts @@ -0,0 +1,7 @@ +import { ConfigurableProvider } from "./configurableProvider"; + +export class Saml2ConfigurableProvider extends ConfigurableProvider{ + spEntityId: string; + idpUrl: string; + binding: string; +} \ No newline at end of file diff --git a/dmp-frontend/src/app/core/services/saml-login.service.ts b/dmp-frontend/src/app/core/services/saml-login.service.ts new file mode 100644 index 000000000..0664780fc --- /dev/null +++ b/dmp-frontend/src/app/core/services/saml-login.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@angular/core'; +import { Guid } from '@common/types/guid'; +import * as pk from 'pako'; + +@Injectable() +export class SamlLoginService { + + constructor() {} + + buildRelayState(spId: string, configurableLoginId: string): string { + let uri = 'spId=' + spId; + uri += '&configurableLoginId=' + configurableLoginId; + return encodeURIComponent(uri); + } + + resolveConfigurableLoginId(relayState: string): string { + const decoded = decodeURIComponent(relayState); + const routeParams = new URLSearchParams(decoded); + return routeParams.has('configurableLoginId') ? routeParams.get('configurableLoginId') : undefined; + } + resolveSpId(relayState: string): string { + const decoded = decodeURIComponent(relayState); + const routeParams = new URLSearchParams(decoded); + return routeParams.has('spId') ? routeParams.get('spId') : ''; + } + + getSamlLoginUrl(spEntityID: string, idpUrl: string, binding: string, configurableLoginId: string) { + const now = new Date(); + let protocolBinding = ''; + switch (binding) { + case "Redirect": protocolBinding = 'ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" '; break; + case "Artifact": protocolBinding = 'ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" '; break; + case "Post": protocolBinding = 'ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Post" '; break; + } + const authenticationRequest = '' + + '' + spEntityID + '' + + ''; + const uint = new Uint8Array(authenticationRequest.length); + for (let i = 0, j = authenticationRequest.length; i < j; ++i) { + uint[i] = authenticationRequest.charCodeAt(i); + } + const base64String = btoa(pk.deflateRaw(uint, { to: 'string' })); + const relayState = this.buildRelayState(spEntityID, configurableLoginId); + const url = idpUrl + '?RelayState=' + relayState + '&SAMLRequest=' + encodeURIComponent(base64String); + return url; + } + +} \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/auth/login/configurable-login/configurable-login.component.ts b/dmp-frontend/src/app/ui/auth/login/configurable-login/configurable-login.component.ts index 69d35a0d4..649a2a40f 100644 --- a/dmp-frontend/src/app/ui/auth/login/configurable-login/configurable-login.component.ts +++ b/dmp-frontend/src/app/ui/auth/login/configurable-login/configurable-login.component.ts @@ -10,6 +10,10 @@ import { BaseComponent } from '@common/base/base.component'; import { environment } from 'environments/environment'; import { takeUntil } from 'rxjs/operators'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; +import { SamlLoginService } from '@app/core/services/saml-login.service'; +import { Oauth2ConfigurableProvider } from '@app/core/model/configurable-provider/oauth2ConfigurableProvider'; +import { Saml2ConfigurableProvider } from '@app/core/model/configurable-provider/saml2ConfigurableProvider'; +import { ConfigurableProviderType } from '@app/core/common/enum/configurable-provider-type'; @Component({ selector: 'app-configurable-login', @@ -35,7 +39,8 @@ export class ConfigurableLoginComponent extends BaseComponent implements OnInit private router: Router, private httpClient: HttpClient, private providers: ConfigurableProvidersService, - private configurationService: ConfigurationService + private configurationService: ConfigurationService, + private samlLoginService: SamlLoginService ) { super(); } @@ -70,16 +75,23 @@ export class ConfigurableLoginComponent extends BaseComponent implements OnInit } public configurableAuthorize() { - let authUrl = this.provider.oauthUrl - + '?response_type=code&client_id=' + this.provider.clientId - + '&redirect_uri=' + this.provider.redirect_uri - + '&scope=' + this.provider.scope; - if (this.provider.state.length > 0) authUrl = authUrl + '&state=' + this.provider.state - window.location.href = authUrl; + if(this.provider.type === ConfigurableProviderType.Oauth2){ + let provider = this.provider as Oauth2ConfigurableProvider; + let authUrl = provider.oauthUrl + + '?response_type=code&client_id=' + provider.clientId + + '&redirect_uri=' + provider.redirect_uri + + '&scope=' + provider.scope; + if (provider.state.length > 0) authUrl = authUrl + '&state=' + provider.state + window.location.href = authUrl; + } + else if(this.provider.type === ConfigurableProviderType.Saml2){ + let provider = this.provider as Saml2ConfigurableProvider; + window.location.href = this.samlLoginService.getSamlLoginUrl(provider.spEntityId, provider.idpUrl, provider.binding, provider.configurableLoginId); + } } public configurableLoginUser(code: string, state: string) { - if (state !== this.provider.state) { + if (state !== (this.provider).state) { this.router.navigate(['/login']) } this.httpClient.post(this.configurationService.server + 'auth/configurableProviderRequestToken', { code: code, provider: AuthProvider.Configurable, configurableLoginId: this.providerId }) diff --git a/dmp-frontend/src/app/ui/auth/login/login.component.html b/dmp-frontend/src/app/ui/auth/login/login.component.html index 13ef44581..bfb4b9f13 100644 --- a/dmp-frontend/src/app/ui/auth/login/login.component.html +++ b/dmp-frontend/src/app/ui/auth/login/login.component.html @@ -50,17 +50,24 @@ -
-
-