- 1. Overview
- 2. Installation
- 2.1. From the Marketplace
- 2.2. By Coordinates
- 3. Configuration
- 4. Implementation
- 5. General Application Properties
This component provides a readily available instrument of authentication in any CUBA-based application using SAML open standard. That allows identity provider (IdP) to pass authorization credentials to your applications - service providers (SP).
The add-on enables Single Sign-On in your application. You log in once with the IdP and this set of credentials will be used to log in your CUBA applications.
Key features:
- simplified authorization procedure for users and service providers;
- separately existing of an identity provider and service providers, which centralizes user management;
- user interface to set and configure SAML connections.
See sample project using this add-on.
The add-on can be added to your project in one of the ways described below. Installation from the Marketplace is the simplest way. The last version of the add-on compatible with the used version of the platform will be installed. Also, you can install the add-on by coordinates choosing the required version of the add-on from the table.
In case you want to install the add-on by manual editing or by building from sources see the complete add-ons installation guide in CUBA Platform documentation.
- Open your application in CUBA Studio. Check the latest version of CUBA Studio on the CUBA Platform site.
- Go to CUBA -> Marketplace in the main menu.
- Find the SAML add-on there.
- Click Install and apply the changes. The add-on corresponding to the used platform version will be installed.
- Open your application in CUBA Studio. Check the latest version of CUBA Studio on the CUBA Platform site.
- Go to CUBA -> Marketplace in the main menu.
- Click the icon in the upper-right corner.
- Paste the add-on coordinates in the corresponding field as follows:
com.haulmont.addon.saml:saml-addon-global:<add-on version>
where <add-on version>
is compatible with the used version of the CUBA platform.
Platform Version | Add-on Version |
---|---|
7.2.x | 0.4.1 |
7.1.x | 0.3.0 |
7.0.x | 0.2.2 |
6.10.x | 0.1.0 |
- Click Install and apply the changes. The add-on will be installed to your project.
To use your own key for keystore passwords encryption specify encryption.key
and encryption.iv
properties in app.properties.xml
in the core
module. Otherwise, the default keys declared in the app-component.xml
file will be used.
The further configuration consists of creating keystore and setting SAML connection.
Before setting SAML connection you need to create keystore containing a username, password, description, and JKS (Java Key Store) file. Your service provider application must have a unique public/private key pair.
Firstly, you need to generate a public/private key pair. Use the following links to instructions:
You will get JKS file as the result.
Create a keystore using your application UI:
- Go to Administration -> SAML screen.
- Click the KeyStore button.
- Click the Create button.
- Fill in the Login field - login that was used for JKS file generation.
- Fill in the Password field - password that was used for JKS file generation.
- (Optional) Fill in the Description field - will be used with the login as keystore representation in SAML Connection editor screen.
- Upload
.jks
keystore file. - Click OK to create the keystore with entered settings.
You can not delete keystore if it is linked at least to one connection. Firstly, you need to unselect keystore in SAML Connection editor screen.
To configure SAML connection to identity provider do the following steps:
- Go to Administration -> SAML screen.
- Click the Create button.
- Fill in the Name field - it will be shown to users in the login screen.
- Fill in the SSO Path field - it will be used for tenant login.
- Select the required keystore in the drop-down list of Keystore field.
- Choose Default access group that will be set to new users logged in with this IdP.
- Choose Processing service to process new users logged in with this IdP.
- Fill in the Service provider identity. This field will be used by IdP to identify your application. For example:
cuba-saml-demo
. Then click the Refresh button. Copy the generated XML from the field below and register it in the IdP. - Fill in the Identity Provider metadata URL field provided by this IdP. Example:
http://idp.ssocircle.com/idp-meta.xml
. Then click the Refresh button. If the URL is correct and IdP works OK - you will see some XML content below. Another way to specify IdP metadata is to upload an XML file using the corresponding button. - Click User Creation checkbox, if you want to create a user from information received from IdP in case the user does not exist in the application.
- Click Active checkbox. After that, the IdP will be shown in the login screen.
- Click OK to save settings.
Using a specific tenant URL is a simple way to log in. For example,
http://localhost:8080/app/saml/login?tenant=ssoPath
, where ssoPath
is the value of the field with the same name in SAML Connection entity. When you use such URL, the system automatically redirects you to the specific IdP.
By default, the component provides BaseSamlProcessor
which fills in the following attributes for the new user from the SAML session:
- FirstName
- LastName
- MiddleName
- EmailAddress
However, you can define your own implementation of the interface com.haulmont.addon.saml.core.SamlProcessor
which will handle the SAML data using your own logic.
The getName()
method should return a user-friendly name, to show it in the lookup field on the SAML Connection editor screen.
- saml-admin - grants full access to SAML configuration.
To extend the standard login screen:
- Open your project in CUBA Studio.
- Expand the Generic UI in the CUBA project tree.
- Right-click Screens and go to New -> Screen.
- Go to the Legacy Screen Templates tab and select the Login window.
- Click Next -> Finish.
Then add a lookup field with the list of IdP providers in the screen controller. When you choose one of providers SAML request will be initiated.
Here is an example of the implementation of the whole controller:
- Screen controller
ext-loginWindow.xml
:
Click to expand the code
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
class="com.haulmont.sd.web.screens.ExtAppLoginWindow"
extends="/com/haulmont/cuba/web/app/loginwindow/loginwindow.xml"
xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
messagesPack="com.haulmont.sd.web.screens">
<dialogMode height="600"
width="800"/>
<layout>
<vbox id="loginWrapper">
<vbox id="loginMainBox">
<grid id="loginFormLayout">
<columns>
<column id="loginFormCaptionColumn"/>
<column id="loginFormFieldColumn"/>
</columns>
<rows>
<row id="ssoRow" ext:index="0">
<label id="ssoLookupFieldLabel" value="msg://captions.loginBy" align="MIDDLE_CENTER"/>
<lookupField id="ssoLookupField" nullOptionVisible="true" align="MIDDLE_CENTER"/>
</row>
</rows>
</grid>
</vbox>
</vbox>
</layout>
</window>
- Java class
ExtAppLoginWindow.java
Click to expand the example for 6.10
import com.haulmont.addon.saml.entity.SamlConnection;
import com.haulmont.addon.saml.security.SamlSession;
import com.haulmont.addon.saml.security.config.SamlConfig;
import com.haulmont.addon.saml.service.SamlService;
import com.haulmont.addon.saml.web.security.saml.SamlSessionPrincipal;
import com.haulmont.cuba.core.global.DataManager;
import com.haulmont.cuba.core.global.LoadContext;
import com.haulmont.cuba.core.global.View;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.core.sys.SecurityContext;
import com.haulmont.cuba.gui.components.Label;
import com.haulmont.cuba.gui.components.LookupField;
import com.haulmont.cuba.gui.executors.BackgroundWorker;
import com.haulmont.cuba.gui.executors.UIAccessor;
import com.haulmont.cuba.security.app.TrustedClientService;
import com.haulmont.cuba.security.auth.Credentials;
import com.haulmont.cuba.security.entity.User;
import com.haulmont.cuba.security.global.LoginException;
import com.haulmont.cuba.security.global.UserSession;
import com.haulmont.cuba.web.app.loginwindow.AppLoginWindow;
import com.haulmont.cuba.web.auth.WebAuthConfig;
import com.haulmont.cuba.web.security.ExternalUserCredentials;
import com.vaadin.server.*;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class ExtAppLoginWindow extends AppLoginWindow {
private static final Logger log = LoggerFactory.getLogger(ExtAppLoginWindow.class);
@Inject
protected SamlService samlService;
@Inject
protected TrustedClientService trustedClientService;
@Inject
protected DataManager dataManager;
@Inject
protected BackgroundWorker backgroundWorker;
@Inject
protected SamlConfig samlConfig;
@Inject
protected WebAuthConfig webAuthConfig;
@Inject
protected Label ssoLookupFieldLabel;
@Inject
protected LookupField ssoLookupField;
protected RequestHandler samlCallbackRequestHandler = this::handleSamlCallBackRequest;
protected UIAccessor uiAccessor;
@Override
public void init(Map<String, Object> params) {
super.init(params);
uiAccessor = backgroundWorker.getUIAccessor();
ssoLookupField.setOptionsList(getActiveConnections());
ssoLookupFieldLabel.setVisible(!CollectionUtils.isEmpty(ssoLookupField.getOptionsList()));
ssoLookupField.setVisible(!CollectionUtils.isEmpty(ssoLookupField.getOptionsList()));
ssoLookupField.addValueChangeListener(e -> {
if (e.getValue() != null) {
SamlConnection connection = (SamlConnection) e.getValue();
VaadinSession.getCurrent().getSession().setAttribute(SamlSessionPrincipal.SAML_CONNECTION_CODE, connection.getCode());
Page.getCurrent().setLocation(getLoginUrl());
}
ssoLookupField.setValue(null);
});
}
@Override
public void ready() {
super.ready();
VaadinSession.getCurrent().addRequestHandler(samlCallbackRequestHandler);
try {
samlCallbackRequestHandler.handleRequest(VaadinSession.getCurrent(), null, null);
} catch (IOException e) {
log.error("Failed to check SAML login", e);
}
}
protected boolean handleSamlCallBackRequest(VaadinSession session, @Nullable VaadinRequest request,
@Nullable VaadinResponse response) throws IOException {
Principal principal = VaadinService.getCurrentRequest().getUserPrincipal();
if (principal instanceof SamlSessionPrincipal) {
SamlSessionPrincipal samlPrincipal = (SamlSessionPrincipal) principal;
if (samlPrincipal.isActive()) {
final SamlSession samlSession = samlPrincipal.getSamlSession();
uiAccessor.accessSynchronously(() -> {
try {
User user = samlService.getUser(samlSession);
ExternalUserCredentials credentials = new ExternalUserCredentials(user.getLogin());
doLogin(credentials);
} catch (LoginException e) {
log.info("Login by SAML failed", e);
showLoginException(String.format(getMessage("errors.message.samlLoginFailed"), samlSession.getPrincipal()));
} catch (Exception e) {
log.warn("Login by SAML failed. Internal error.", e);
showUnhandledExceptionOnLogin(e);
}
});
}
}
return false;
}
@Override
protected void doLogin(Credentials credentials) throws LoginException {
super.doLogin(credentials);
VaadinSession.getCurrent().removeRequestHandler(samlCallbackRequestHandler);
}
protected List<SamlConnection> getActiveConnections() {
UserSession systemSession;
try {
systemSession = trustedClientService.getSystemSession(webAuthConfig.getTrustedClientPassword());
} catch (LoginException e) {
log.error("Unable to obtain system session", e);
return Collections.emptyList();
}
return AppContext.withSecurityContext(new SecurityContext(systemSession), () -> {
List<SamlConnection> items = dataManager.loadList(LoadContext.create(SamlConnection.class)
.setQuery(new LoadContext.Query("select e from samladdon$SamlConnection e where e.active = true order by e.code"))
.setView(View.MINIMAL));
return items;
});
}
protected String getLoginUrl() {
return (samlConfig.getProxyEnabled() ? samlConfig.getProxyServerUrl() : globalConfig.getWebAppUrl())
+ samlConfig.getSamlBasePath() + samlConfig.getSamlLoginPath();
}
}
Click to expand the example for 7.0
import com.haulmont.addon.saml.entity.SamlConnection;
import com.haulmont.addon.saml.security.SamlSession;
import com.haulmont.addon.saml.security.config.SamlConfig;
import com.haulmont.addon.saml.service.SamlService;
import com.haulmont.addon.saml.web.security.saml.SamlSessionPrincipal;
import com.haulmont.cuba.core.global.DataManager;
import com.haulmont.cuba.core.global.LoadContext;
import com.haulmont.cuba.core.global.View;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.core.sys.SecurityContext;
import com.haulmont.cuba.gui.components.Label;
import com.haulmont.cuba.gui.components.LookupField;
import com.haulmont.cuba.gui.executors.BackgroundWorker;
import com.haulmont.cuba.gui.executors.UIAccessor;
import com.haulmont.cuba.security.app.TrustedClientService;
import com.haulmont.cuba.security.auth.Credentials;
import com.haulmont.cuba.security.entity.User;
import com.haulmont.cuba.security.global.LoginException;
import com.haulmont.cuba.security.global.UserSession;
import com.haulmont.cuba.web.app.loginwindow.AppLoginWindow;
import com.haulmont.cuba.web.auth.WebAuthConfig;
import com.haulmont.cuba.web.security.ExternalUserCredentials;
import com.vaadin.server.*;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static java.util.Objects.isNull;
public class ExtAppLoginWindow extends AppLoginWindow {
private static final Logger log = LoggerFactory.getLogger(ExtAppLoginWindow.class);
@Inject
protected SamlService samlService;
@Inject
protected TrustedClientService trustedClientService;
@Inject
protected DataManager dataManager;
@Inject
protected BackgroundWorker backgroundWorker;
@Inject
protected SamlConfig samlConfig;
@Inject
protected WebAuthConfig webAuthConfig;
@Inject
protected Label<String> ssoLookupFieldLabel;
@Inject
protected LookupField<SamlConnection> ssoLookupField;
protected RequestHandler samlCallbackRequestHandler = this::handleSamlCallBackRequest;
protected UIAccessor uiAccessor;
@Override
public void init(Map<String, Object> params) {
super.init(params);
uiAccessor = backgroundWorker.getUIAccessor();
ssoLookupField.setOptionsList(getActiveConnections());
ssoLookupFieldLabel.setVisible(!CollectionUtils.isEmpty(ssoLookupField.getOptionsList()));
ssoLookupField.setVisible(!CollectionUtils.isEmpty(ssoLookupField.getOptionsList()));
ssoLookupField.addValueChangeListener(e -> {
if (e.getValue() != null) {
SamlConnection connection = e.getValue();
VaadinSession.getCurrent().getSession().setAttribute(SamlSessionPrincipal.SAML_CONNECTION_CODE, connection.getSsoPath());
Page.getCurrent().setLocation(getLoginUrl());
}
ssoLookupField.setValue(null);
});
}
@Override
public void ready() {
super.ready();
VaadinSession.getCurrent().addRequestHandler(samlCallbackRequestHandler);
try {
samlCallbackRequestHandler.handleRequest(VaadinSession.getCurrent(), null, null);
} catch (IOException e) {
log.error("Failed to check SAML login", e);
}
}
protected boolean handleSamlCallBackRequest(VaadinSession session, @Nullable VaadinRequest request,
@Nullable VaadinResponse response) throws IOException {
Principal principal = VaadinService.getCurrentRequest().getUserPrincipal();
if (principal instanceof SamlSessionPrincipal) {
SamlSessionPrincipal samlPrincipal = (SamlSessionPrincipal) principal;
if (samlPrincipal.isActive()) {
final SamlSession samlSession = samlPrincipal.getSamlSession();
uiAccessor.accessSynchronously(() -> {
try {
User user = samlService.getUser(samlSession);
if (isNull(user)) {
throw new LoginException("User does not exists");
}
ExternalUserCredentials credentials = new ExternalUserCredentials(user.getLogin());
doLogin(credentials);
} catch (LoginException e) {
log.info("Login by SAML failed", e);
showLoginException(String.format(getMessage("errors.message.samlLoginFailed"), samlSession.getPrincipal()));
} catch (Exception e) {
log.warn("Login by SAML failed. Internal error.", e);
showUnhandledExceptionOnLogin(e);
}
});
}
}
//check the error
Object error = VaadinService.getCurrentRequest().getWrappedSession()
.getAttribute(SamlSessionPrincipal.SAML_ERROR_ATTRIBUTE);
if (error != null) {
uiAccessor.accessSynchronously(() -> {
showUnhandledExceptionOnLogin((Exception) error);
});
}
return false;
}
@Override
protected void doLogin(Credentials credentials) throws LoginException {
super.doLogin(credentials);
VaadinSession.getCurrent().removeRequestHandler(samlCallbackRequestHandler);
}
protected List<SamlConnection> getActiveConnections() {
UserSession systemSession;
try {
systemSession = trustedClientService.getSystemSession(webAuthConfig.getTrustedClientPassword());
} catch (LoginException e) {
log.error("Unable to obtain system session", e);
return Collections.emptyList();
}
return AppContext.withSecurityContext(new SecurityContext(systemSession), () -> {
List<SamlConnection> items = dataManager.loadList(LoadContext.create(SamlConnection.class)
.setQuery(new LoadContext.Query("select e from samladdon$SamlConnection e where e.active = true order by e.ssoPath"))
.setView(View.MINIMAL));
return items;
});
}
protected String getLoginUrl() {
return (samlConfig.getProxyEnabled() ? samlConfig.getProxyServerUrl() : globalConfig.getWebAppUrl())
+ samlConfig.getSamlBasePath() + samlConfig.getSamlLoginPath();
}
}
- The
messages.properties
file should contain the following strings:
captions.loginBy = Login by
errors.message.samlLoginFailed = User '%s' hasn't been logged by SAML.
- The
web-app.properties
file should contain the following strings:
cuba.addon.saml.basePath = /saml
cuba.addon.saml.logoutPath = /logout
cuba.addon.saml.loginPath = /login
cuba.addon.saml.metadataPath = /metadata
cuba.addon.saml.responseSkewSec = 60
cuba.addon.saml.maxAuthenticationAgeSec = 7200
cuba.addon.saml.maxAssertionTimeSec = 3000
cuba.addon.saml.logAllSamlMessages = true
Also, you can observe the details of the implementation in the corresponding demo project.
By default, OpenSAML component uses SHA1 digest algorithm for signing SAML messages. The most convenient way to use different signing messages
is to create a class in the web
module with additional changes in SecurityContext
.
Click to expand the example
import org.opensaml.xml.Configuration;
import org.opensaml.xml.security.BasicSecurityConfiguration;
import org.opensaml.xml.signature.SignatureConstants;
public class SecurityConfiguration {
public void initialize() {
BasicSecurityConfiguration configuration = (BasicSecurityConfiguration) Configuration.getGlobalSecurityConfiguration();
// Asymmetric key algorithms
configuration.registerSignatureAlgorithmURI("RSA", SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
configuration.registerSignatureAlgorithmURI("DSA", SignatureConstants.ALGO_ID_SIGNATURE_DSA);
configuration.registerSignatureAlgorithmURI("EC", SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA256);
// HMAC algorithms
configuration.registerSignatureAlgorithmURI("AES", SignatureConstants.ALGO_ID_MAC_HMAC_SHA256);
configuration.registerSignatureAlgorithmURI("DESede", SignatureConstants.ALGO_ID_MAC_HMAC_SHA256);
// Other signature-related params
configuration.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
configuration.setSignatureHMACOutputLength(null);
configuration.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256);
}
}
Create a file in the web
module for additional configuration of SAML servlet and declare the SecurityConfiguration
class as a bean. For example, you can name this file as saml-dispatcher-spring.xml
.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<bean class="com.haulmont.demo.saml.web.SecurityConfiguration" init-method="initialize" depends-on="samlBootstrap"/>
</beans>
Basic configuration is initialized in the org.springframework.security.saml.SAMLBootstrap
class.
To make sure that security config is initialized and does not override your changes set depends-on attribute with the value of the bean id of the org.springframework.security.saml.SAMLBootstrap
class (related bean is declared in the saml-dispatcher-spring.xml
file of the addon).
Then add the saml.springContextConfig
property to the web-app.properties
file and set the value with the path of your additional configuration file.
(The plus
sign is necessary, see documentation).
saml.springContextConfig = +com/haulmont/demo/saml/saml-dispatcher-spring.xml
Pay attention that the signing method declared in your configuration will be used for all created SAML connections!
All supported signing methods are declared in the org.opensaml.xml.signature.SignatureConstants
class.
- Description: URL SAML context path, e.g.
/saml
- Interface: SamlConfig
Used in the Web Client.
- Description: SAML login path part, e.g.
/login
, and with the base path the result will be/saml/logout
- Interface: SamlConfig
Used in the Web Client.
- Description: SAML logout path part, e.g.
/logout
and with the base path the result will be/saml/logout
- Interface: SamlConfig
Used in the Web Client.
- Description: SAML metadata display path part, e.g.
/metadata
and with the base path the result will be/saml/metadata?tenant=code
where code isSAMLConnection.code
- Interface: SamlConfig
Used in the Web Client.
- Description: Maximum difference between local time and time of the assertion creation which still allows message to be processed. Basically determines maximum difference between clocks of the IdP and SP machines (in seconds).
- Default value:
60
- Interface: SamlConfig
Used in the Web Client.
- Description: Maximum time between users authentication and processing of the AuthNResponse message (in seconds).
- Default value:
7200
- Interface: SamlConfig
Used in the Web Client.
- Description: Maximum time between assertion creation and current time when the assertion is usable (in seconds).
- Default value:
3000
- Interface: SamlConfig
Used in the Web Client.
- Description: Defines whether the logout action will be also performed on the IdP when user performs logout in the CUBA application (SP)
- Default value:
false
- Interface: SamlConfig
Used in the Web Client.
- Description: Defines is a application use a proxy server or not
- Default value:
false
- Interface: SamlConfig
Used in the Web Client.
- Description: Defines the address of remote proxy server if a proxy server is using, e.g.
https://myhost.com
- Default value: **
- Interface: SamlConfig
Used in the Web Client.
- Description: Determines if all SAML messages should be logged
- Default value:
true
- Interface: SamlConfig
Used in the Web Client.