]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
DRYD-1243: Add SAML support. (#366)
authorRay Lee <ray.lee@lyrasis.org>
Fri, 22 Sep 2023 00:48:31 +0000 (20:48 -0400)
committerGitHub <noreply@github.com>
Fri, 22 Sep 2023 00:48:31 +0000 (20:48 -0400)
28 files changed:
build.properties
services/account/jaxb/src/main/resources/accounts_common.xsd
services/account/jaxb/src/main/resources/accounts_common_list.xsd
services/account/pstore/src/main/resources/db/postgresql/account.sql
services/account/service/src/main/java/org/collectionspace/services/account/AccountResource.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountDocumentHandler.java
services/account/service/src/main/java/org/collectionspace/services/account/storage/AccountValidatorHandler.java
services/authentication/service/pom.xml
services/authentication/service/src/main/java/org/collectionspace/authentication/CSpaceUser.java
services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/CSpaceUserDeserializer.java
services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/Saml2AuthenticatedCSpaceUserDeserializer.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/realm/CSpaceRealm.java
services/authentication/service/src/main/java/org/collectionspace/authentication/realm/db/CSpaceDbRealm.java
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceDaoAuthenticationProvider.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceLogoutSuccessHandler.java
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2Authentication.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2LogoutRequestRepository.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceUserDetailsService.java
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SSORequiredException.java [new file with mode: 0644]
services/authentication/service/src/main/java/org/collectionspace/authentication/spring/Saml2AuthenticatedCSpaceUser.java [new file with mode: 0644]
services/authorization/service/src/main/java/org/collectionspace/services/authorization/AuthZ.java
services/common/pom.xml
services/common/src/main/cspace/config/services/service-config-security.xml
services/common/src/main/java/org/collectionspace/services/common/authorization_mgt/AuthorizationCommon.java
services/common/src/main/java/org/collectionspace/services/common/security/SecurityConfig.java
services/config/src/main/java/org/collectionspace/services/common/config/ConfigUtils.java
services/config/src/main/resources/service-config.xsd
services/login/service/src/main/java/org/collectionspace/services/login/LoginResource.java

index 5d1206ce2a25fe57999279ceac0e21e5e9d48136..27cbdd1d717b4084c1e5ecf1981955645c30c688 100644 (file)
@@ -22,7 +22,7 @@ domain.nuxeo=nuxeo-server
 # UI settings
 cspace.ui.package.name=cspace-ui
 cspace.ui.library.name=cspaceUI
-cspace.ui.version=9.0.0-dev.1
+cspace.ui.version=9.0.0-dev.2
 cspace.ui.build.branch=master
 cspace.ui.build.node.ver=14
 service.ui.library.name=${cspace.ui.library.name}-service
index 3ac4545b93cff738a94c28a99e91747969b9c3a5..7caba18ecc9b74c315a94814268b886361ffbf2c 100644 (file)
                         </xs:appinfo>
                     </xs:annotation>
                 </xs:element>
+                <xs:element name="requireSSO" type="xs:boolean" minOccurs="0" maxOccurs="1">
+                    <xs:annotation>
+                        <xs:documentation>
+                            If true, login through an SSO identity provider is required.
+                        </xs:documentation>
+                        <xs:appinfo>
+                            <hj:basic>
+                                <orm:column name="require_sso" />
+                            </hj:basic>
+                        </xs:appinfo>
+                    </xs:annotation>
+                </xs:element>
                 <xs:element name="tenants" type="account_tenant" minOccurs="1" maxOccurs="unbounded">
                     <xs:annotation>
                         <xs:documentation>
index 6c88ecdc74e891931affb26f6d7c1c5c97775b61..02131abe138d0ae53b804f6257119f1bb2b674a9 100644 (file)
@@ -67,6 +67,7 @@
                                     </xs:element>
                                     <xs:element name="personRefName" type="xs:string" minOccurs="1" />
                                     <xs:element name="email" type="xs:string" minOccurs="1" />
+                                    <xs:element name="requireSSO" type="xs:boolean" minOccurs="0" />
                                     <xs:element name="status" type="status" minOccurs="1" />
                                     <!-- uri to retrive collection object details -->
                                     <xs:element name="uri" type="xs:anyURI" minOccurs="1" />
index 6289f74edea1bb31d0b3286141b6ba5a367b8939..64fc15c34a74f36a58e6920dc3b0b3530f1fcf81 100644 (file)
@@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS accounts_common (
        mobile VARCHAR(255),
        person_ref_name VARCHAR(255),
        phone VARCHAR(255),
+       require_sso BOOLEAN,
        screen_name VARCHAR(128) NOT NULL,
        status VARCHAR(15) NOT NULL,
        updated_at TIMESTAMP,
@@ -13,6 +14,10 @@ CREATE TABLE IF NOT EXISTS accounts_common (
        roles_protection VARCHAR(255)
 );
 
+-- Upgrade older accounts_common tables to 8.0
+
+ALTER TABLE accounts_common ADD COLUMN IF NOT EXISTS require_sso BOOLEAN;
+
 CREATE TABLE IF NOT EXISTS accounts_tenants (
        hjid INT8 NOT NULL PRIMARY KEY,
        tenant_id VARCHAR(128) NOT NULL,
index cc21837f7744514c1df46c6fe369ccae15c90267..2bcf39e01ef532c4ee856cef14ea0cf25b356e85 100644 (file)
@@ -556,6 +556,12 @@ public class AccountResource extends SecurityResourceBase<AccountsCommon, Accoun
             return Response.status(Response.Status.NOT_FOUND).entity(msg).type("text/plain").build();
         }
 
+        ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
+
+        if (ConfigUtils.isSsoAvailable(serviceConfig) && accountListItem.isRequireSSO() != null && accountListItem.isRequireSSO()) {
+               return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("The account requires single sign-on.").type("text/plain").build();
+        }
+
         // If no tenant ID was supplied, use the account's first associated tenant ID for purposes
         // of password reset. This is the same way that a tenant is selected for the account when
         // logging in. In practice, accounts are only associated with one tenant anyway.
index 9539b143a4ba2f4b6b570edc1fa63cd62fff78af..938a7629a1ea4af563c632f13a5e78bfe19ac37c 100644 (file)
@@ -72,6 +72,20 @@ public class AccountDocumentHandler
 
         setTenant(account);
 
+        if (account.getPassword() == null) {
+            // The password is allowed to be null when the user is created with requireSSO == true.
+            // Generate a random password to ensure that it won't be blank if the requireSSO flag
+            // is changed.
+
+            RandomStringGenerator generator = new RandomStringGenerator.Builder()
+                .withinRange(34, 126)
+                .build();
+
+            String randomPassword = generator.generate(24);
+
+            account.setPassword(randomPassword.getBytes());
+        }
+
         if (account.getStatus() == null) {
             account.setStatus(Status.ACTIVE);
         }
@@ -147,6 +161,9 @@ public class AccountDocumentHandler
         if (from.getPersonRefName() != null) {
             to.setPersonRefName(from.getPersonRefName());
         }
+        if (from.isRequireSSO() != null) {
+            to.setRequireSSO(from.isRequireSSO());
+        }
 
         // Note that we do not allow update of locks
         //fixme update for tenant association
@@ -254,6 +271,7 @@ public class AccountDocumentHandler
 
             accListItem.setTenants(account.getTenants());
             accListItem.setEmail(account.getEmail());
+            accListItem.setRequireSSO(account.isRequireSSO());
             accListItem.setStatus(account.getStatus());
             String id = account.getCsid();
             accListItem.setUri(getServiceContextPath() + id);
index 43462797e32b158b6752cabbd039877ab234adca..626dfa251ffed07d5ac10463ad4cf68b7b48ca35 100644 (file)
@@ -102,7 +102,10 @@ public class AccountValidatorHandler implements ValidatorHandler {
                     invalid = true;
                     msgBldr.append("\nuserId : missing");
                 }
-                if (account.getPassword() == null || account.getPassword().length == 0) {
+                if (
+                    (account.isRequireSSO() == null || !account.isRequireSSO())
+                    && (account.getPassword() == null || account.getPassword().length == 0)
+                ) {
                     invalid = true;
                     msgBldr.append("\npassword : missing");
                 }
index d9770168a015ffc60a5006c8e5fc95496649d6b2..6606c1b547f226cbb336441efe2f1eef869df40d 100644 (file)
             <version>${spring.security.authorization.server.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-saml2-service-provider</artifactId>
+            <version>${spring.security.version}</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
                        <groupId>org.postgresql</groupId>
                        <artifactId>postgresql</artifactId>
index 7f3ff714d4348bb08ed4ab81206c8dbe037628d2..43b9c2def51aa0a7c7f610efae60dc1a755a85fc 100644 (file)
@@ -34,6 +34,7 @@ public class CSpaceUser extends User {
 
     private Set<CSpaceTenant> tenants;
     private CSpaceTenant primaryTenant;
+    private boolean requireSSO;
     private String salt;
 
     /**
@@ -46,6 +47,7 @@ public class CSpaceUser extends User {
      * @param authorities the authorities that have been granted to the user
      */
     public CSpaceUser(String username, String password, String salt,
+            boolean requireSSO,
             Set<CSpaceTenant> tenants,
             Set<? extends GrantedAuthority> authorities) {
 
@@ -57,6 +59,7 @@ public class CSpaceUser extends User {
                 authorities);
 
         this.tenants = tenants;
+        this.requireSSO = requireSSO;
         this.salt = salt;
 
         if (!tenants.isEmpty()) {
@@ -89,4 +92,12 @@ public class CSpaceUser extends User {
     public String getSalt() {
        return salt != null ? salt : "";
     }
+
+    /**
+     * Determines if the user is required to log in using single sign-on.
+     * @return true if SSO is required, false otherwise
+     */
+    public boolean isRequireSSO() {
+        return requireSSO;
+    }
 }
index acaa97b82a08b44b08b306f3d8d71e5eba1a71b8..70061cc3964fd6bc4b5f5beb84eb1b1e5d11446b 100644 (file)
@@ -35,9 +35,10 @@ public class CSpaceUserDeserializer extends JsonDeserializer<CSpaceUser> {
                JsonNode passwordNode = readJsonNode(jsonNode, "password");
                String username = readJsonNode(jsonNode, "username").asText();
                String password = passwordNode.asText("");
+               boolean requireSSO = readJsonNode(jsonNode, "requireSSO").asBoolean();
                String salt = readJsonNode(jsonNode, "salt").asText();
 
-               CSpaceUser result = new CSpaceUser(username, password, salt, tenants,   authorities);
+               CSpaceUser result = new CSpaceUser(username, password, salt, requireSSO, tenants,       authorities);
 
                if (passwordNode.asText(null) == null) {
                        result.eraseCredentials();
diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/Saml2AuthenticatedCSpaceUserDeserializer.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/jackson2/Saml2AuthenticatedCSpaceUserDeserializer.java
new file mode 100644 (file)
index 0000000..473838b
--- /dev/null
@@ -0,0 +1,55 @@
+package org.collectionspace.authentication.jackson2;
+
+import java.io.IOException;
+import java.util.Set;
+
+import org.collectionspace.authentication.CSpaceTenant;
+import org.collectionspace.authentication.spring.Saml2AuthenticatedCSpaceUser;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.MissingNode;
+
+public class Saml2AuthenticatedCSpaceUserDeserializer extends JsonDeserializer<Saml2AuthenticatedCSpaceUser> {
+       private static final TypeReference<Set<SimpleGrantedAuthority>> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference<Set<SimpleGrantedAuthority>>() {
+       };
+
+  private static final TypeReference<Set<CSpaceTenant>> CSPACE_TENANT_SET = new TypeReference<Set<CSpaceTenant>>() {
+       };
+
+  @Override
+       public Saml2AuthenticatedCSpaceUser deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
+               ObjectMapper mapper = (ObjectMapper) parser.getCodec();
+               JsonNode jsonNode = mapper.readTree(parser);
+
+               Set<? extends GrantedAuthority> authorities = mapper.convertValue(jsonNode.get("authorities"), SIMPLE_GRANTED_AUTHORITY_SET);
+               Set<CSpaceTenant> tenants = mapper.convertValue(jsonNode.get("tenants"), CSPACE_TENANT_SET);
+
+    Saml2AuthenticatedPrincipal principal = mapper.convertValue(readJsonNode(jsonNode, "principal"), Saml2AuthenticatedPrincipal.class);
+               JsonNode passwordNode = readJsonNode(jsonNode, "password");
+               String username = readJsonNode(jsonNode, "username").asText();
+               String password = passwordNode.asText("");
+               boolean requireSSO = readJsonNode(jsonNode, "requireSSO").asBoolean();
+               String salt = readJsonNode(jsonNode, "salt").asText();
+
+               Saml2AuthenticatedCSpaceUser result = new Saml2AuthenticatedCSpaceUser(principal, username, password, salt, requireSSO, tenants, authorities);
+
+               if (passwordNode.asText(null) == null) {
+                       result.eraseCredentials();
+               }
+
+               return result;
+       }
+
+       private JsonNode readJsonNode(JsonNode jsonNode, String field) {
+               return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
+       }
+}
index bfbd207551ef4d6933018e6b7bafb34399db46a3..36078ad185074ac5472d59ddd8ef4b26d03cf04e 100644 (file)
@@ -38,7 +38,7 @@ import org.collectionspace.authentication.CSpaceTenant;
  * Interface for the CollectionSpace realm.
  */
 public interface CSpaceRealm {
-
+       
        /**
         * Retrieves the "salt" used to encrypt the user's password
         * @param username
@@ -49,7 +49,7 @@ public interface CSpaceRealm {
 
     /**
      * Retrieves the hashed password used to authenticate a user.
-     *
+     * 
      * @param username
      * @return the password
      * @throws AccountNotFoundException if the user is not found
@@ -59,7 +59,7 @@ public interface CSpaceRealm {
 
     /**
      * Retrieves the roles for a user.
-     *
+     * 
      * @param username
      * @return a collection of roles
      * @throws AccountException if the roles could not be retrieved
@@ -68,7 +68,7 @@ public interface CSpaceRealm {
 
     /**
      * Retrieves the enabled tenants associated with a user.
-     *
+     * 
      * @param username
      * @return a collection of tenants
      * @throws AccountException if the tenants could not be retrieved
@@ -77,11 +77,20 @@ public interface CSpaceRealm {
 
     /**
      * Retrieves the tenants associated with a user, optionally including disabled tenants.
-     *
+     * 
      * @param username
      * @param includeDisabledTenants if true, include disabled tenants
      * @return a collection of tenants
      * @throws AccountException if the tenants could not be retrieved
      */
     public Set<CSpaceTenant> getTenants(String username, boolean includeDisabledTenants) throws AccountException;
+
+    /**
+     * Determines if the user is required to login using single sign-on.
+     * 
+     * @param username
+     * @return true if SSO is required, false otherwise
+     * @throws AccountException
+     */
+    public boolean isRequireSSO(String username) throws AccountException;
 }
index 09942bd3021f636219f4c579bfe202e51707a175..2fef70989449d4f3f6c02f7822c9c1d4680f6df2 100644 (file)
@@ -74,16 +74,17 @@ import org.slf4j.LoggerFactory;
 
 /**
  * CSpaceDbRealm provides access to user, password, role, tenant database
- * @author
+ * @author 
  */
 public class CSpaceDbRealm implements CSpaceRealm {
        public static String DEFAULT_DATASOURCE_NAME = "CspaceDS";
-
+       
     private Logger logger = LoggerFactory.getLogger(CSpaceDbRealm.class);
-
+    
     private String datasourceName;
     private String principalsQuery;
     private String saltQuery;
+    private String requireSSOQuery;
     private String rolesQuery;
     private String tenantsQueryNoDisabled;
     private String tenantsQueryWithDisabled;
@@ -96,7 +97,7 @@ public class CSpaceDbRealm implements CSpaceRealm {
        private long delayBetweenAttemptsMillis = DELAY_BETWEEN_ATTEMPTS_MILLISECONDS;
     private static final String DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR = "delayBetweenAttemptsMillis";
        private static final long DELAY_BETWEEN_ATTEMPTS_MILLISECONDS = 200;
-
+       
        protected void setMaxRetrySeconds(Map<String, ?> options) {
                Object optionsObj = options.get(MAX_RETRY_SECONDS_STR);
                if (optionsObj != null) {
@@ -109,11 +110,11 @@ public class CSpaceDbRealm implements CSpaceRealm {
                        }
                }
        }
-
+       
        protected long getMaxRetrySeconds() {
                return this.maxRetrySeconds;
        }
-
+       
        protected void setDelayBetweenAttemptsMillis(Map<String, ?> options) {
                Object optionsObj = options.get(DELAY_BETWEEN_ATTEMPTS_MILLISECONDS_STR);
                if (optionsObj != null) {
@@ -126,15 +127,15 @@ public class CSpaceDbRealm implements CSpaceRealm {
                        }
                }
        }
-
+       
        protected long getDelayBetweenAttemptsMillis() {
                return this.delayBetweenAttemptsMillis;
        }
-
+       
        public CSpaceDbRealm() {
         datasourceName = DEFAULT_DATASOURCE_NAME;
        }
-
+    
     /**
      * CSpace Database Realm
      * @param datasourceName datasource name
@@ -152,6 +153,10 @@ public class CSpaceDbRealm implements CSpaceRealm {
         if (tmp != null) {
                saltQuery = tmp.toString();
         }
+        tmp = options.get("requireSSOQuery");
+        if (tmp != null) {
+               requireSSOQuery = tmp.toString();
+        }
         tmp = options.get("rolesQuery");
         if (tmp != null) {
             rolesQuery = tmp.toString();
@@ -168,10 +173,10 @@ public class CSpaceDbRealm implements CSpaceRealm {
         if (tmp != null) {
             suspendResume = Boolean.valueOf(tmp.toString()).booleanValue();
         }
-
+        
         this.setMaxRetrySeconds(options);
         this.setDelayBetweenAttemptsMillis(options);
-
+        
         if (logger.isTraceEnabled()) {
             logger.trace("DatabaseServerLoginModule, dsJndiName=" + datasourceName);
             logger.trace("principalsQuery=" + principalsQuery);
@@ -270,14 +275,14 @@ public class CSpaceDbRealm implements CSpaceRealm {
                 if (logger.isDebugEnabled()) {
                     logger.debug("No roles found");
                 }
-
+                
                 return roles;
             }
 
             do {
                 String roleName = rs.getString(1);
                 roles.add(roleName);
-
+                
             } while (rs.next());
         } catch (SQLException ex) {
             AccountException ae = new AccountException("Query failed");
@@ -316,7 +321,7 @@ public class CSpaceDbRealm implements CSpaceRealm {
     public Set<CSpaceTenant> getTenants(String username) throws AccountException {
         return getTenants(username, false);
     }
-
+    
     private boolean userIsTenantManager(Connection conn, String username) {
         String acctQuery = "SELECT csid FROM accounts_common WHERE userid=?";
         PreparedStatement ps = null;
@@ -356,7 +361,7 @@ public class CSpaceDbRealm implements CSpaceRealm {
         }
         return accountIsTenantManager;
     }
-
+    
     /**
      * Execute the tenantsQuery against the datasourceName to obtain the tenants for
      * the authenticated user.
@@ -366,13 +371,13 @@ public class CSpaceDbRealm implements CSpaceRealm {
     public Set<CSpaceTenant> getTenants(String username, boolean includeDisabledTenants) throws AccountException {
 
        String tenantsQuery = getTenantQuery(includeDisabledTenants);
-
+       
         if (logger.isDebugEnabled()) {
             logger.debug("getTenants using tenantsQuery: " + tenantsQuery + ", username: " + username);
         }
 
         Set<CSpaceTenant> tenants = new LinkedHashSet<CSpaceTenant>();
-
+        
         Connection conn = null;
         PreparedStatement ps = null;
         ResultSet rs = null;
@@ -393,7 +398,7 @@ public class CSpaceDbRealm implements CSpaceRealm {
                     if (logger.isDebugEnabled()) {
                         logger.debug("GetTenants called with tenantManager - synthesizing the pseudo-tenant");
                     }
-
+                    
                     tenants.add(new CSpaceTenant(AuthN.TENANT_MANAGER_ACCT_ID, "PseudoTenant"));
                 } else {
                     if (logger.isDebugEnabled()) {
@@ -403,7 +408,7 @@ public class CSpaceDbRealm implements CSpaceRealm {
                     // empty Tenants set.
                     // FIXME  should this be allowed?
                 }
-
+                
                 return tenants;
             }
 
@@ -461,7 +466,7 @@ public class CSpaceDbRealm implements CSpaceRealm {
                        if (requestAttempts > 0) {
                                Thread.sleep(getDelayBetweenAttemptsMillis()); // Wait a little time between reattempts.
                        }
-
+                       
                        try {
                                // proceed to the original request by calling doFilter()
                                result = this.getConnection(getDataSourceName());
@@ -482,7 +487,7 @@ public class CSpaceDbRealm implements CSpaceRealm {
                                requestAttempts++; // keep track of how many times we've tried the request
                        }
                } while (System.currentTimeMillis() < quittingTime);  // keep trying until we run out of time
-
+               
                //
                // Add a warning to the logs if we encountered *any* failures on our re-attempts.  Only add the warning
                // if we were eventually successful.
@@ -498,10 +503,10 @@ public class CSpaceDbRealm implements CSpaceRealm {
                        // If we get here, it means all of our attempts to get a successful call to chain.doFilter() have failed.
                        throw lastException;
                }
-
+               
                return result;
        }
-
+    
        /*
         * Don't call this method directly.  Instead, use the getConnection() method that take no arguments.
         */
@@ -509,52 +514,52 @@ public class CSpaceDbRealm implements CSpaceRealm {
         InitialContext ctx = null;
         Connection conn = null;
         DataSource ds = null;
-
+        
         try {
             ctx = new InitialContext();
             try {
                ds = (DataSource) ctx.lookup(dataSourceName);
             } catch (Exception e) {}
-
+            
                try {
                        Context envCtx = (Context) ctx.lookup("java:comp/env");
                        ds = (DataSource) envCtx.lookup(dataSourceName);
                } catch (Exception e) {}
-
+               
                try {
                        Context envCtx = (Context) ctx.lookup("java:comp");
                        ds = (DataSource) envCtx.lookup(dataSourceName);
                } catch (Exception e) {}
-
+               
                try {
                        Context envCtx = (Context) ctx.lookup("java:");
                        ds = (DataSource) envCtx.lookup(dataSourceName);
                } catch (Exception e) {}
-
+               
                try {
                        Context envCtx = (Context) ctx.lookup("java");
                        ds = (DataSource) envCtx.lookup(dataSourceName);
                } catch (Exception e) {}
-
+               
                try {
                        ds = (DataSource) ctx.lookup("java:/" + dataSourceName);
-               } catch (Exception e) {}
+               } catch (Exception e) {}  
 
                if (ds == null) {
                ds = AuthN.getDataSource();
                }
-
+               
             if (ds == null) {
                 throw new IllegalArgumentException("datasource not found: " + dataSourceName);
             }
-
+            
             conn = ds.getConnection();
             if (conn == null) {
                conn = AuthN.getDataSource().getConnection();  //FIXME:REM - This is the result of some type of JNDI mess.  Should try to solve this problem and clean up this code.
             }
-
+            
             return conn;
-
+            
         } catch (NamingException ex) {
             AccountException ae = new AccountException("Error looking up DataSource from: " + dataSourceName);
             ae.initCause(ex);
@@ -619,7 +624,7 @@ public class CSpaceDbRealm implements CSpaceRealm {
         this.tenantsQueryNoDisabled = tenantQuery;
     }
      */
-
+    
     /*
      * This method crawls the exception chain looking for network related exceptions and
      * returns 'true' if it finds one.
@@ -633,13 +638,13 @@ public class CSpaceDbRealm implements CSpaceRealm {
                                result = true;
                                break;
                        }
-
+                       
                        cause = cause.getCause();
                }
 
                return result;
        }
-
+       
        /*
         * Return 'true' if the exception is in the "java.net" package.
         */
@@ -713,7 +718,80 @@ public class CSpaceDbRealm implements CSpaceRealm {
                 }
             }
         }
-
+        
         return salt;
     }
+
+    @Override
+    public boolean isRequireSSO(String username) throws AccountException {
+        Boolean requireSSO = null;
+        Connection conn = null;
+        PreparedStatement ps = null;
+        ResultSet rs = null;
+
+        try {
+            conn = getConnection();
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("Executing query: " + requireSSOQuery + ", with username: " + username);
+            }
+
+            ps = conn.prepareStatement(requireSSOQuery);
+
+            ps.setString(1, username);
+
+            rs = ps.executeQuery();
+
+            if (rs.next() == false) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug(requireSSOQuery + " returned no matches from db");
+                }
+
+                throw new AccountNotFoundException("No matching username found");
+            }
+
+            requireSSO = rs.getBoolean(1);
+        } catch (SQLException ex) {
+            if (logger.isTraceEnabled() == true) {
+                logger.error("Could not open database to read AuthN tables.", ex);
+            }
+
+            AccountException ae = new AccountException("Authentication query failed: " + ex.getLocalizedMessage());
+
+            ae.initCause(ex);
+            
+            throw ae;
+        } catch (AccountNotFoundException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            AccountException ae = new AccountException("Unknown Exception");
+
+            ae.initCause(ex);
+            
+            throw ae;
+        } finally {
+            if (rs != null) {
+                try {
+                    rs.close();
+                } catch (SQLException e) {
+                }
+            }
+
+            if (ps != null) {
+                try {
+                    ps.close();
+                } catch (SQLException e) {
+                }
+            }
+
+            if (conn != null) {
+                try {
+                    conn.close();
+                } catch (SQLException ex) {
+                }
+            }
+        }
+
+        return requireSSO;
+    }
 }
diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceDaoAuthenticationProvider.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceDaoAuthenticationProvider.java
new file mode 100644 (file)
index 0000000..cc60325
--- /dev/null
@@ -0,0 +1,37 @@
+package org.collectionspace.authentication.spring;
+
+import org.collectionspace.authentication.CSpaceUser;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/**
+ * A DaoAuthenticationProvider that checks if the user being authenticated is required to log in
+ * via single sign-on.
+ */
+public class CSpaceDaoAuthenticationProvider extends DaoAuthenticationProvider {
+  private boolean isSsoAvailable = false;
+
+  /**
+   * Checks if the user is required to log in using SSO. If so, SSORequiredException is thrown.
+   */
+  @Override
+  protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
+    CSpaceUser user = (CSpaceUser) userDetails;
+
+    if (this.isSsoAvailable() && user.isRequireSSO()) {
+      throw new SSORequiredException("Single sign-on is required for " + user.getUsername() + ". Please sign in through an SSO provider.");
+    }
+
+    super.additionalAuthenticationChecks(userDetails, authentication);
+  }
+
+  public boolean isSsoAvailable() {
+    return this.isSsoAvailable;
+  }
+
+  public void setSsoAvailable(boolean isSsoAvailable) {
+    this.isSsoAvailable = isSsoAvailable;
+  }
+}
index d0e42afc7e884266fd665d56bbd56d3148c1b630..786d4a7931765479a5334965daca3d976e8f5041 100644 (file)
@@ -13,6 +13,9 @@ import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuc
  * A LogoutSuccessHandler that reads the post-logout redirect URL from a parameter in the logout
  * request. As an anti-phishing security measure, the URL is checked against a list of permitted
  * redirect URLs (originating from tenant binding configuration or OAuth client configuration).
+ *
+ * For SAML logouts, the redirect URL is saved to a request attribute, which is also checked, if
+ * the redirect parameter is not present.
  */
 public class CSpaceLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
        final Logger logger = LoggerFactory.getLogger(CSpaceLogoutSuccessHandler.class);
@@ -33,6 +36,10 @@ public class CSpaceLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
   protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
     String redirectUrl = request.getParameter(REDIRECT_PARAMETER_NAME);
 
+    if (redirectUrl == null) {
+      redirectUrl = (String) request.getSession().getAttribute(CSpaceSaml2LogoutRequestRepository.REDIRECT_ATTRIBUTE_NAME);
+    }
+
     if (redirectUrl != null && !isPermitted(redirectUrl)) {
       logger.warn("Logout redirect url not permitted: {}", redirectUrl);
 
diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2Authentication.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2Authentication.java
new file mode 100644 (file)
index 0000000..60488ac
--- /dev/null
@@ -0,0 +1,76 @@
+package org.collectionspace.authentication.spring;
+
+import java.util.Collection;
+
+import org.collectionspace.authentication.CSpaceUser;
+import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * A Saml2Authentication whose principal is a CSpaceUser.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonAutoDetect(
+  fieldVisibility = JsonAutoDetect.Visibility.ANY,
+  getterVisibility = JsonAutoDetect.Visibility.NONE,
+       isGetterVisibility = JsonAutoDetect.Visibility.NONE
+)
+@JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true)
+public class CSpaceSaml2Authentication extends Saml2Authentication {
+  private final CSpaceUser user;
+
+  public CSpaceSaml2Authentication(CSpaceUser user, Saml2Authentication authentication) {
+    this(
+      user,
+      (Saml2AuthenticatedPrincipal) authentication.getPrincipal(),
+      authentication.getSaml2Response(),
+      authentication.getAuthorities()
+    );
+  }
+
+  public CSpaceSaml2Authentication(
+    CSpaceUser user,
+    AuthenticatedPrincipal principal,
+    java.lang.String saml2Response,
+    java.util.Collection<? extends GrantedAuthority> authorities
+  ) {
+    this(
+      new Saml2AuthenticatedCSpaceUser((Saml2AuthenticatedPrincipal) principal, user),
+      principal,
+      saml2Response,
+      authorities
+    );
+  }
+
+  @JsonCreator
+  public CSpaceSaml2Authentication(
+    @JsonProperty("user") Saml2AuthenticatedCSpaceUser user,
+    @JsonProperty("principal") AuthenticatedPrincipal principal,
+    @JsonProperty("saml2Response") java.lang.String saml2Response,
+    @JsonProperty("authorities") java.util.Collection<? extends GrantedAuthority> authorities
+  ) {
+    super(principal, saml2Response, authorities);
+
+    this.user = user;
+
+    this.setAuthenticated(true);
+  }
+
+  @Override
+  public Object getPrincipal() {
+    return user;
+  }
+
+  @Override
+  public Collection<GrantedAuthority> getAuthorities() {
+    return user.getAuthorities();
+  }
+}
diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2LogoutRequestRepository.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/CSpaceSaml2LogoutRequestRepository.java
new file mode 100644 (file)
index 0000000..703fadc
--- /dev/null
@@ -0,0 +1,42 @@
+package org.collectionspace.authentication.spring;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
+import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
+
+/**
+ * A Saml2LogoutRequestRepository that saves the redirect paramaeter from the logout request to a
+ * request attribute. This allows CSpaceLogoutSuccessHandler to have access to the parameter value
+ * following the logout request to the IdP.
+ */
+public class CSpaceSaml2LogoutRequestRepository implements Saml2LogoutRequestRepository {
+  public static final String REDIRECT_ATTRIBUTE_NAME = "org.collectionspace.authentication.logout.redirect";
+
+  private HttpSessionLogoutRequestRepository repository = new HttpSessionLogoutRequestRepository();
+
+  @Override
+  public Saml2LogoutRequest loadLogoutRequest(HttpServletRequest request) {
+    return repository.loadLogoutRequest(request);
+  }
+
+  @Override
+  public void saveLogoutRequest(
+    Saml2LogoutRequest logoutRequest,
+    HttpServletRequest request,
+    HttpServletResponse response)
+  {
+    repository.saveLogoutRequest(logoutRequest, request, response);
+
+    String redirect = request.getParameter("redirect");
+
+    request.getSession().setAttribute(REDIRECT_ATTRIBUTE_NAME, redirect);
+  }
+
+  @Override
+  public Saml2LogoutRequest removeLogoutRequest(HttpServletRequest request, HttpServletResponse response) {
+    return repository.removeLogoutRequest(request, response);
+  }
+}
index 74901a35e3429aae87f267f326ebac100b147d12..754c588f8842d0f6bd1a8b632acdc6c223a4c74a 100644 (file)
@@ -75,12 +75,14 @@ public class CSpaceUserDetailsService implements UserDetailsService {
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
         String password = null;
         String salt = null;
+        Boolean requireSSO = null;
         Set<CSpaceTenant> tenants = null;
         Set<GrantedAuthority> grantedAuthorities = null;
-
+        
         try {
             password = realm.getPassword(username);
             salt = realm.getSalt(username);
+            requireSSO = realm.isRequireSSO(username);
             tenants = getTenants(username);
             grantedAuthorities = getAuthorities(username);
         }
@@ -90,32 +92,33 @@ public class CSpaceUserDetailsService implements UserDetailsService {
         catch (AccountException e) {
             throw new AuthenticationServiceException(e.getMessage(), e);
         }
-
-        CSpaceUser cspaceUser =
+        
+        CSpaceUser cspaceUser = 
             new CSpaceUser(
                 username,
                 password,
                 salt,
+                requireSSO,
                 tenants,
                 grantedAuthorities);
-
+                
         return cspaceUser;
     }
-
+    
     protected Set<GrantedAuthority> getAuthorities(String username) throws AccountException {
         Set<String> roles = realm.getRoles(username);
         Set<GrantedAuthority> authorities = new LinkedHashSet<GrantedAuthority>(roles.size());
-
+        
         for (String role : roles) {
             authorities.add(new SimpleGrantedAuthority(role));
         }
-
+        
         return authorities;
     }
-
+    
     protected Set<CSpaceTenant> getTenants(String username) throws AccountException {
         Set<CSpaceTenant> tenants = realm.getTenants(username);
-
+        
         return tenants;
     }
 }
diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SSORequiredException.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/SSORequiredException.java
new file mode 100644 (file)
index 0000000..5dbcd4a
--- /dev/null
@@ -0,0 +1,10 @@
+package org.collectionspace.authentication.spring;
+
+import org.springframework.security.core.AuthenticationException;
+
+public class SSORequiredException extends AuthenticationException {
+
+  public SSORequiredException(String msg) {
+    super(msg);
+  } 
+}
diff --git a/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/Saml2AuthenticatedCSpaceUser.java b/services/authentication/service/src/main/java/org/collectionspace/authentication/spring/Saml2AuthenticatedCSpaceUser.java
new file mode 100644 (file)
index 0000000..bfe2635
--- /dev/null
@@ -0,0 +1,89 @@
+package org.collectionspace.authentication.spring;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.collectionspace.authentication.CSpaceTenant;
+import org.collectionspace.authentication.CSpaceUser;
+import org.collectionspace.authentication.jackson2.Saml2AuthenticatedCSpaceUserDeserializer;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * A CSpaceUser that is also a Saml2AuthenticatedPrincipal. This is needed because various parts of
+ * Spring Security use instanceof Saml2AuthenticatedPrincipal to determine if the currently
+ * authenticated user logged in via SAML.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
+@JsonDeserialize(using = Saml2AuthenticatedCSpaceUserDeserializer.class)
+@JsonAutoDetect(
+       fieldVisibility = JsonAutoDetect.Visibility.ANY,
+       getterVisibility = JsonAutoDetect.Visibility.NONE,
+       isGetterVisibility = JsonAutoDetect.Visibility.NONE
+)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Saml2AuthenticatedCSpaceUser extends CSpaceUser implements Saml2AuthenticatedPrincipal {
+       private Saml2AuthenticatedPrincipal principal;
+
+       public Saml2AuthenticatedCSpaceUser(Saml2AuthenticatedPrincipal principal, CSpaceUser user) {
+               this(
+                       principal,
+                       user.getUsername(),
+                       user.getPassword(),
+                       user.getSalt(),
+                       user.isRequireSSO(),
+                       user.getTenants(),
+                       (Set<GrantedAuthority>) user.getAuthorities()
+               );
+       }
+
+       public Saml2AuthenticatedCSpaceUser(
+               Saml2AuthenticatedPrincipal principal,
+               String username,
+               String password,
+               String salt,
+               boolean requireSSO,
+               Set<CSpaceTenant> tenants,
+               Set<? extends GrantedAuthority> authorities
+       ) {
+               super(username, password, salt, requireSSO, tenants, authorities);
+
+               this.principal = principal;
+       }
+
+       @Override
+       public String getName() {
+               return principal.getName();
+       }
+
+       @Override
+       public <A> A getFirstAttribute(String name) {
+               return principal.getFirstAttribute(name);
+       }
+
+       @Override
+       public <A> List<A> getAttribute(String name) {
+               return principal.getAttribute(name);
+       }
+
+       @Override
+       public Map<String, List<Object>> getAttributes() {
+               return principal.getAttributes();
+       }
+
+       @Override
+       public String getRelyingPartyRegistrationId() {
+               return principal.getRelyingPartyRegistrationId();
+       }
+
+       @Override
+       public List<String> getSessionIndexes() {
+               return principal.getSessionIndexes();
+       }
+}
index 662d07494a011e533200f3b7535a00923c849e18..4e15399f50276fd98b080d193f7fcef73131bbda 100644 (file)
@@ -289,7 +289,7 @@ public class AuthZ {
 
        HashSet<CSpaceTenant> tenantSet = new HashSet<CSpaceTenant>();
        tenantSet.add(tenant);
-       CSpaceUser principal = new CSpaceUser(user, password, null, tenantSet, grantedAuthorities);
+       CSpaceUser principal = new CSpaceUser(user, password, null, false, tenantSet, grantedAuthorities);
 
         Authentication authRequest = new UsernamePasswordAuthenticationToken(principal, password, grantedAuthorities);
         SecurityContextHolder.getContext().setAuthentication(authRequest);
index e0ac45539c813cc1c1990c1ce57cacd606eeb6fb..5e46126d115f21f5c2a4c489a2911c4112ae0d63 100644 (file)
                        <version>${spring.security.version}</version>
                        <scope>provided</scope>
                </dependency>
+               <dependency>
+                       <groupId>org.springframework.security</groupId>
+                       <artifactId>spring-security-saml2-service-provider</artifactId>
+                       <version>${spring.security.version}</version>
+                       <scope>provided</scope>
+               </dependency>
 
                <dependency>
                        <groupId>com.fasterxml.jackson.core</groupId>
index 34ef8c46af180683e4cf71c083835c8847155811..1983247193b3f2df54e737344572e9b6b23f94f2 100644 (file)
                 </client>
             </client-registrations>
         </oauth>
+
+        <!--
+            Example SSO config.
+        -->
+        <!--
+        <sso>
+            <saml>
+                <single-logout />
+
+                <relying-party-registrations>
+                    <relying-party id="mocksaml">
+                        <name>Mock SAML</name>
+                        <icon location="https://mocksaml.com/favicon.ico" />
+                        <metadata location="https://mocksaml.com/api/saml/metadata" />
+
+                        <signing-x509-credentials>
+                            <x509-credential>
+                                <private-key location="file:///home/collectionspace/tomcat/cspace/services/credentials/private.key" />
+                                <x509-certificate location="file:///home/collectionspace/tomcat/cspace/services/credentials/certificate.crt" />
+                            </x509-credential>
+                        </signing-x509-credentials>
+                    </relying-party>
+                </relying-party-registrations>
+            </saml>
+        </sso>
+        -->
     </security>
 </svc:service-config>
index 2dd9ce10b69c69bb67e87c008ad2436135361d08..09545a4dd8cf0edeb30ac69e341f53c1e4a70319 100644 (file)
@@ -148,8 +148,8 @@ public class AuthorizationCommon {
                        "INSERT INTO users (username,passwd,created_at) VALUES (?,?,now())";
        final private static String INSERT_ACCOUNT_SQL =
                        "INSERT INTO accounts_common "
-                                       + "(csid, email, userid, status, screen_name, metadata_protection, roles_protection, created_at) "
-                                       + "VALUES (?,?,?,'ACTIVE',?, 'immutable', 'immutable', now())";
+                                       + "(csid, email, userid, require_sso, status, screen_name, metadata_protection, roles_protection, created_at) "
+                                       + "VALUES (?,?,?,false,'ACTIVE',?, 'immutable', 'immutable', now())";
 
        // TENANT MANAGER specific SQL
        final private static String QUERY_TENANT_MGR_USER_SQL =
index 8e13db12cb9f401b6d49b8734d7d452da5fc8ca9..9056f5140c9d3c1c909611511b59862453ee6e2b 100644 (file)
@@ -1,32 +1,50 @@
 package org.collectionspace.services.common.security;
 
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.net.MalformedURLException;
+import java.security.cert.X509Certificate;
+import java.security.KeyFactory;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
 import java.security.interfaces.RSAPrivateKey;
 import java.security.interfaces.RSAPublicKey;
+import java.security.spec.PKCS8EncodedKeySpec;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
+import java.util.function.Consumer;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.sql.DataSource;
 
+import org.apache.commons.io.IOUtils;
 import org.collectionspace.authentication.CSpaceUser;
+import org.collectionspace.authentication.spring.CSpaceDaoAuthenticationProvider;
 import org.collectionspace.authentication.spring.CSpaceJwtAuthenticationToken;
 import org.collectionspace.authentication.spring.CSpaceLogoutSuccessHandler;
 import org.collectionspace.authentication.spring.CSpacePasswordEncoderFactory;
+import org.collectionspace.authentication.spring.CSpaceSaml2Authentication;
+import org.collectionspace.authentication.spring.CSpaceSaml2LogoutRequestRepository;
 import org.collectionspace.authentication.spring.CSpaceUserAttributeFilter;
 import org.collectionspace.authentication.spring.CSpaceUserDetailsService;
 import org.collectionspace.services.client.AccountClient;
 import org.collectionspace.services.common.ServiceMain;
 import org.collectionspace.services.common.config.ConfigUtils;
 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
+import org.collectionspace.services.config.AssertingPartyDetailsType;
 import org.collectionspace.services.config.OAuthAuthorizationGrantTypeEnum;
 import org.collectionspace.services.config.OAuthClientAuthenticationMethodEnum;
 import org.collectionspace.services.config.OAuthClientSettingsType;
@@ -34,22 +52,31 @@ import org.collectionspace.services.config.OAuthClientType;
 import org.collectionspace.services.config.OAuthScopeEnum;
 import org.collectionspace.services.config.OAuthTokenSettingsType;
 import org.collectionspace.services.config.OAuthType;
+import org.collectionspace.services.config.SAMLRelyingPartyType;
+import org.collectionspace.services.config.SAMLType;
 import org.collectionspace.services.config.ServiceConfig;
+import org.collectionspace.services.config.X509CertificateType;
+import org.collectionspace.services.config.X509CredentialType;
 import org.collectionspace.services.config.tenant.TenantBindingType;
 import org.collectionspace.authentication.realm.db.CSpaceDbRealm;
+import org.opensaml.saml.saml2.core.Assertion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.Ordered;
 import org.springframework.core.annotation.Order;
 import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
 import org.springframework.http.HttpMethod;
 import org.springframework.jdbc.core.JdbcOperations;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.lang.Nullable;
 import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
 import org.springframework.security.authentication.ProviderManager;
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.Customizer;
@@ -64,6 +91,8 @@ import org.springframework.security.config.annotation.web.configurers.FormLoginC
 import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
 import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
 import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
+import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
+import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -79,14 +108,31 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
 import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+import org.springframework.security.saml2.core.Saml2X509Credential;
+import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.ResponseToken;
+import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
+import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails;
+import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
+import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
+import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
+import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
 import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.security.web.context.SecurityContextPersistenceFilter;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.CorsConfigurationSource;
 
+import com.google.common.io.CharStreams;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
@@ -219,9 +265,13 @@ public class SecurityConfig {
                final AuthenticationManager authenticationManager,
                final UserDetailsService userDetailsService,
                final RegisteredClientRepository registeredClientRepository,
-               final ApplicationEventPublisher appEventPublisher
+               final ApplicationEventPublisher appEventPublisher,
+               final Optional<RelyingPartyRegistrationRepository> optionalRelyingPartyRegistrationRepository
        ) throws Exception {
 
+               ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
+               SAMLType saml = ConfigUtils.getSAML(serviceConfig);
+
                this.initializeCorsConfigurations();
 
                http
@@ -353,15 +403,88 @@ public class SecurityConfig {
                        // Insert the username from the security context into a request attribute for logging.
                        .addFilterBefore(new CSpaceUserAttributeFilter(), LogoutFilter.class);
 
+               RelyingPartyRegistrationRepository relyingPartyRegistrationRepository = optionalRelyingPartyRegistrationRepository.orElse(null);
+
+               if (relyingPartyRegistrationRepository != null) {
+                       RelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
+                               new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository);
+
+                       // TODO: Use OpenSaml4AuthenticationProvider (requires Java 11) instead of deprecated OpenSamlAuthenticationProvider.
+                       final OpenSamlAuthenticationProvider samlAuthenticationProvider = new OpenSamlAuthenticationProvider();
+
+                       samlAuthenticationProvider.setResponseAuthenticationConverter(new Converter<ResponseToken, CSpaceSaml2Authentication>() {
+                               @Override
+                               public CSpaceSaml2Authentication convert(ResponseToken responseToken) {
+                                       Saml2Authentication authentication = OpenSamlAuthenticationProvider
+                                               .createDefaultResponseAuthenticationConverter()
+                                               .convert(responseToken);
+
+                                       Assertion assertion = responseToken.getResponse().getAssertions().get(0);
+                                       String username = assertion.getSubject().getNameID().getValue();
+
+                                       try {
+                                               CSpaceUser user = (CSpaceUser) userDetailsService.loadUserByUsername(username);
+
+                                               return new CSpaceSaml2Authentication(user, authentication);
+                                       }
+                                       catch(UsernameNotFoundException e) {
+                                               String errorMessage = "No CollectionSpace account was found for " + username + ".";
+
+                                               throw(new UsernameNotFoundException(errorMessage, e));
+                                       }
+                               }
+                       });
+
+                       http
+                               .saml2Login(new Customizer<Saml2LoginConfigurer<HttpSecurity>>() {
+                                       @Override
+                                       public void customize(Saml2LoginConfigurer<HttpSecurity> configurer) {
+                                               ProviderManager providerManager = new ProviderManager(samlAuthenticationProvider);
+
+                                               providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(appEventPublisher));
+
+                                               configurer
+                                                       .authenticationManager(providerManager)
+                                                       .loginPage(LOGIN_FORM_URL)
+                                                       .defaultSuccessUrl(DEFAULT_LOGIN_SUCCESS_URL);
+                                       }
+                               })
+                               // Produce relying party metadata @ /cspace-services/saml2/service-provider-metadata/{id}.
+                               .addFilterBefore(
+                                       new Saml2MetadataFilter(
+                                               relyingPartyRegistrationResolver,
+                                               new OpenSamlMetadataResolver()
+                                       ),
+                                       Saml2WebSsoAuthenticationFilter.class
+                               );
+
+                       if (saml != null && saml.getSingleLogout() != null) {
+                               http
+                                       .saml2Logout(new Customizer<Saml2LogoutConfigurer<HttpSecurity>>() {
+                                               @Override
+                                               public void customize(Saml2LogoutConfigurer<HttpSecurity> configurer) {
+                                                       configurer.logoutRequest(new Customizer<Saml2LogoutConfigurer<HttpSecurity>.LogoutRequestConfigurer>() {
+                                                               @Override
+                                                               public void customize(Saml2LogoutConfigurer<HttpSecurity>.LogoutRequestConfigurer configurer) {
+                                                                       configurer.logoutRequestRepository(new CSpaceSaml2LogoutRequestRepository());
+                                                               }
+                                                       });
+                                               }
+                                       });
+                       }
+               }
+
                return http.build();
        }
 
        @Bean
        public DaoAuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService) {
-               DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+               ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
+               CSpaceDaoAuthenticationProvider provider = new CSpaceDaoAuthenticationProvider();
 
                provider.setUserDetailsService(userDetailsService);
                provider.setPasswordEncoder(CSpacePasswordEncoderFactory.createDefaultPasswordEncoder());
+               provider.setSsoAvailable(ConfigUtils.isSsoAvailable(serviceConfig));
 
                return provider;
        }
@@ -525,6 +648,125 @@ public class SecurityConfig {
                return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
        }
 
+       @Bean
+       public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+               List<RelyingPartyRegistration> registrations = new ArrayList<RelyingPartyRegistration>();
+               ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
+               List<SAMLRelyingPartyType> relyingPartiesConfig = ConfigUtils.getSAMLRelyingPartyRegistrations(serviceConfig);
+
+               if (relyingPartiesConfig != null) {
+                       for (final SAMLRelyingPartyType relyingPartyConfig : relyingPartiesConfig) {
+                               RelyingPartyRegistration.Builder registrationBuilder;
+
+                               if (relyingPartyConfig.getMetadata() != null) {
+                                       registrationBuilder = RelyingPartyRegistrations
+                                               .fromMetadataLocation(relyingPartyConfig.getMetadata().getLocation())
+                                               .registrationId(relyingPartyConfig.getId());
+                               } else {
+                                       final AssertingPartyDetailsType assertingPartyDetails = relyingPartyConfig.getAssertingPartyDetails();
+
+                                       registrationBuilder = RelyingPartyRegistration
+                                               .withRegistrationId(relyingPartyConfig.getId())
+                                               .assertingPartyDetails(new Consumer<AssertingPartyDetails.Builder>() {
+                                                       @Override
+                                                       public void accept(AssertingPartyDetails.Builder builder) {
+                                                               builder.entityId(assertingPartyDetails.getEntityId());
+
+                                                               if (assertingPartyDetails.isWantAuthnRequestsSigned() != null) {
+                                                                       builder.wantAuthnRequestsSigned(assertingPartyDetails.isWantAuthnRequestsSigned());
+                                                               }
+
+                                                               if (assertingPartyDetails.getSigningAlgorithms() != null) {
+                                                                       builder.signingAlgorithms(new Consumer<List<String>>() {
+                                                                               @Override
+                                                                               public void accept(List<String> algorithms) {
+                                                                                       algorithms.addAll(assertingPartyDetails.getSigningAlgorithms().getSigningAlgorithm());
+                                                                               }
+                                                                       });
+                                                               }
+
+                                                               if (assertingPartyDetails.getSingleSignOnServiceBinding() != null) {
+                                                                       builder.singleSignOnServiceBinding(Saml2MessageBinding.valueOf(assertingPartyDetails.getSingleSignOnServiceBinding().value()));
+                                                               }
+
+                                                               if (assertingPartyDetails.getSingleSignOnServiceLocation() != null) {
+                                                                       builder.singleSignOnServiceLocation(assertingPartyDetails.getSingleSignOnServiceLocation());
+                                                               }
+
+                                                               if (assertingPartyDetails.getSingleLogoutServiceBinding() != null) {
+                                                                       builder.singleLogoutServiceBinding(Saml2MessageBinding.valueOf(assertingPartyDetails.getSingleLogoutServiceBinding().value()));
+                                                               }
+
+                                                               if (assertingPartyDetails.getSingleLogoutServiceLocation() != null) {
+                                                                       builder.singleLogoutServiceLocation(assertingPartyDetails.getSingleLogoutServiceLocation());
+                                                               }
+
+                                                               if (assertingPartyDetails.getSingleLogoutServiceResponseLocation() != null) {
+                                                                       builder.singleLogoutServiceResponseLocation(assertingPartyDetails.getSingleLogoutServiceResponseLocation());
+                                                               }
+
+                                                               if (assertingPartyDetails.getEncryptionX509Credentials() != null) {
+                                                                       builder.encryptionX509Credentials(new Consumer<Collection<Saml2X509Credential>>() {
+                                                                               @Override
+                                                                               public void accept(Collection<Saml2X509Credential> credentials) {
+                                                                                       for (X509CredentialType credentialConfig : assertingPartyDetails.getEncryptionX509Credentials().getX509Credential()) {
+                                                                                               X509Certificate certificate = certificateFromConfig(credentialConfig.getX509Certificate());
+
+                                                                                               if (certificate != null) {
+                                                                                                       credentials.add(Saml2X509Credential.encryption(certificate));
+                                                                                               }
+                                                                                       }
+                                                                               }
+                                                                       });
+                                                               }
+
+                                                               if (assertingPartyDetails.getVerificationX509Credentials() != null) {
+                                                                       builder.verificationX509Credentials(new Consumer<Collection<Saml2X509Credential>>() {
+                                                                               @Override
+                                                                               public void accept(Collection<Saml2X509Credential> credentials) {
+                                                                                       for (X509CredentialType credentialConfig : assertingPartyDetails.getVerificationX509Credentials().getX509Credential()) {
+                                                                                               X509Certificate certificate = certificateFromConfig(credentialConfig.getX509Certificate());
+
+                                                                                               if (certificate != null) {
+                                                                                                       credentials.add(Saml2X509Credential.verification(certificate));
+                                                                                               }
+                                                                                       }
+                                                                               }
+                                                                       });
+                                                               }
+                                                       }
+                                               });
+                               }
+
+                               if (relyingPartyConfig.getSigningX509Credentials() != null) {
+                                       registrationBuilder.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo");
+
+                                       registrationBuilder.signingX509Credentials(new Consumer<Collection<Saml2X509Credential>>() {
+                                               @Override
+                                               public void accept(Collection<Saml2X509Credential> credentials) {
+                                                       for (X509CredentialType credentialConfig : relyingPartyConfig.getSigningX509Credentials().getX509Credential()) {
+                                                               PrivateKey privateKey = privateKeyFromUrl(credentialConfig.getPrivateKey().getLocation());
+                                                               X509Certificate certificate = certificateFromConfig(credentialConfig.getX509Certificate());
+
+                                                               if (certificate != null) {
+                                                                       credentials.add(Saml2X509Credential.signing(privateKey, certificate));
+                                                               }
+                                                       }
+                                               }
+                                       });
+                               }
+
+                               registrations.add(registrationBuilder.build());
+                       }
+               }
+
+               if (registrations.size() > 0) {
+                       return new InMemoryRelyingPartyRegistrationRepository(registrations);
+               }
+
+               return null;
+       }
+
        @Bean
        public UserDetailsService userDetailsService() {
                Map<String, Object> options = new HashMap<String, Object>();
@@ -532,6 +774,7 @@ public class SecurityConfig {
                options.put("dsJndiName", "CspaceDS");
                options.put("principalsQuery", "select passwd from users where username=?");
                options.put("saltQuery", "select salt from users where username=?");
+               options.put("requireSSOQuery", "select require_sso from accounts_common where userid=?");
                options.put("rolesQuery", "select r.rolename from roles as r, accounts_roles as ar where ar.user_id=? and ar.role_id=r.csid");
                options.put("tenantsQueryWithDisabled", "select t.id, t.name from accounts_common as a, accounts_tenants as at, tenants as t where a.userid=? and a.csid = at.TENANTS_ACCOUNTS_COMMON_CSID and at.tenant_id = t.id order by t.id");
                options.put("tenantsQueryNoDisabled", "select t.id, t.name from accounts_common as a, accounts_tenants as at, tenants as t where a.userid=? and a.csid = at.TENANTS_ACCOUNTS_COMMON_CSID and at.tenant_id = t.id and NOT t.disabled order by t.id");
@@ -540,4 +783,87 @@ public class SecurityConfig {
 
                return new CSpaceUserDetailsService(new CSpaceDbRealm(options));
        }
+
+       public PrivateKey privateKeyFromUrl(String url) {
+               Resource resource;
+
+               try {
+                       resource = new UrlResource(url);
+               } catch (MalformedURLException ex) {
+                       throw new UnsupportedOperationException(ex);
+               }
+
+               if (!resource.exists()) {
+                       return null;
+               }
+
+               try (Reader reader = new InputStreamReader(resource.getInputStream())) {
+                       String key = CharStreams.toString(reader);
+
+                       String privateKeyPEM = key
+                               .replace("-----BEGIN PRIVATE KEY-----", "")
+                               .replaceAll(System.lineSeparator(), "")
+                               .replace("-----END PRIVATE KEY-----", "");
+
+                       byte[] encoded = Base64.getDecoder().decode(privateKeyPEM);
+
+                       KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+                       PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
+
+                       return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
+               }
+               catch (Exception ex) {
+                       throw new UnsupportedOperationException(ex);
+               }
+       }
+
+       private X509Certificate certificateFromConfig(X509CertificateType certificate) {
+               String value = certificate.getValue();
+
+               if (value != null && value.length() > 0) {
+                       if (!value.startsWith("-----BEGIN CERTIFICATE-----")) {
+                               value = "-----BEGIN CERTIFICATE-----\n" + value + "-----END CERTIFICATE-----\n";
+                       }
+
+                       return certificateFromString(value);
+               }
+
+               String location = certificate.getLocation();
+
+               if (location != null) {
+                       return certificateFromUrl(location);
+               }
+
+               return null;
+       }
+
+       private X509Certificate certificateFromUrl(String url) {
+               Resource resource;
+
+               try {
+                       resource = new UrlResource(url);
+               } catch (MalformedURLException ex) {
+                       throw new UnsupportedOperationException(ex);
+               }
+
+               if (!resource.exists()) {
+                       return null;
+               }
+
+               try (InputStream is = resource.getInputStream()) {
+                       return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
+               }
+               catch (Exception ex) {
+                       throw new UnsupportedOperationException(ex);
+               }
+       }
+
+       private X509Certificate certificateFromString(String source) {
+               try (InputStream is = IOUtils.toInputStream(source, "utf-8")) {
+                       return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
+               }
+               catch (Exception ex) {
+                       throw new UnsupportedOperationException(ex);
+               }
+       }
 }
index ab9ad75d40645b0c6e8c324ba2748797b8a40f9a..e3ec282b4351731175a6c94b5e7ae0605a7c5f66 100644 (file)
@@ -9,6 +9,10 @@ import org.collectionspace.services.config.CORSType;
 import org.collectionspace.services.config.OAuthClientRegistrationsType;
 import org.collectionspace.services.config.OAuthClientType;
 import org.collectionspace.services.config.OAuthType;
+import org.collectionspace.services.config.SAMLRelyingPartyRegistrationsType;
+import org.collectionspace.services.config.SAMLRelyingPartyType;
+import org.collectionspace.services.config.SAMLType;
+import org.collectionspace.services.config.SSOType;
 import org.collectionspace.services.config.SecurityType;
 import org.collectionspace.services.config.ServiceConfig;
 import org.collectionspace.services.config.tenant.RepositoryDomainType;
@@ -163,6 +167,46 @@ public class ConfigUtils {
                return null;
        }
 
+       public static SSOType getSSO(ServiceConfig serviceConfig) {
+               SecurityType security = serviceConfig.getSecurity();
+
+               if (security != null) {
+                       return security.getSso();
+               }
+
+               return null;
+       }
+
+       public static SAMLType getSAML(ServiceConfig serviceConfig) {
+               SSOType sso = getSSO(serviceConfig);
+
+               if (sso != null) {
+                       return sso.getSaml();
+               }
+
+               return null;
+       }
+
+       public static List<SAMLRelyingPartyType> getSAMLRelyingPartyRegistrations(ServiceConfig serviceConfig) {
+               SAMLType saml = getSAML(serviceConfig);
+
+               if (saml != null) {
+                       SAMLRelyingPartyRegistrationsType registrations = saml.getRelyingPartyRegistrations();
+
+                       if (registrations != null) {
+                               return registrations.getRelyingParty();
+                       }
+               }
+
+               return null;
+       }
+
+       public static boolean isSsoAvailable(ServiceConfig serviceConfig) {
+               List<SAMLRelyingPartyType> samlRegistrations = getSAMLRelyingPartyRegistrations(serviceConfig);
+
+               return (samlRegistrations != null && samlRegistrations.size() > 0);
+       }
+
        public static String getUILoginSuccessUrl(TenantBindingType tenantBinding) throws MalformedURLException {
                UIConfig uiConfig = tenantBinding.getUiConfig();
 
index c3e57baf4f8e5f98489285fe07876d69aa9efd95..8bdf0b6b3c0efcf80d890db96fbd176af4a8b1e0 100644 (file)
@@ -87,6 +87,7 @@
         <xs:sequence>
             <xs:element name="cors" type="CORSType" minOccurs="0" maxOccurs="1" />
             <xs:element name="oauth" type="OAuthType" minOccurs="0" maxOccurs="1" />
+            <xs:element name="sso" type="SSOType" minOccurs="0" maxOccurs="1" />
         </xs:sequence>
     </xs:complexType>
 
             <xs:element name="access-token-time-to-live" type="xs:string" minOccurs="0" maxOccurs="1" />
         </xs:sequence>
     </xs:complexType>
+
+    <xs:complexType name="SSOType">
+        <xs:annotation>
+            <xs:documentation>Configures single sign-on.</xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="saml" type="SAMLType" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="SAMLType">
+        <xs:annotation>
+            <xs:documentation>Configures SAML single sign-on.</xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="single-logout" type="SAMLSingleLogoutType" minOccurs="0" maxOccurs="1" />
+            <xs:element name="relying-party-registrations" type="SAMLRelyingPartyRegistrationsType" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="SAMLSingleLogoutType">
+        <xs:annotation>
+            <xs:documentation>Configures SAML single logout. Single logout is enabled if this element is present.</xs:documentation>
+        </xs:annotation>
+    </xs:complexType>
+
+    <xs:complexType name="SAMLRelyingPartyRegistrationsType">
+        <xs:annotation>
+            <xs:documentation>Configures connections to SAML identity providers.</xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="relying-party" type="SAMLRelyingPartyType" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="SAMLRelyingPartyType">
+        <xs:annotation>
+            <xs:documentation>Configures a connection to a SAML identity provider.</xs:documentation>
+        </xs:annotation>
+
+        <xs:sequence>
+            <xs:element name="name" type="xs:string" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>
+                        A user-facing name for the IdP. This appears in the login UI, so it should
+                        be human-readable, using the terminology/branding that users of the IdP
+                        recognize. If no name is supplied, the registration ID is used in the user
+                        interface.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:element>
+
+            <xs:element name="icon" type="IconType" minOccurs="0" maxOccurs="1" >
+                <xs:annotation>
+                    <xs:documentation>
+                        An icon for the IdP, used in the login UI. If no icon is supplied, a
+                        default icon is used.
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:element>
+
+            <xs:choice minOccurs="1" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>
+                        Configures the details of the IdP. Provide either metadata for automatic
+                        configuration, or asserting-party-details to manually specify the settings.
+                    </xs:documentation>
+                </xs:annotation>
+
+                <xs:element name="metadata" type="SAMLMetadataType" />
+                <xs:element name="asserting-party-details" type="AssertingPartyDetailsType" />
+            </xs:choice>
+
+            <xs:element name="signing-x509-credentials" type="X509CredentialsType" minOccurs="0" maxOccurs="1">
+                <xs:annotation>
+                    <xs:documentation>
+                        The credentials used to sign requests to the IdP. Required if the IdP
+                        wants login requests to be signed (some do, some don't), or if single
+                        logout is enabled (since logout requests must always be signed).
+                    </xs:documentation>
+                </xs:annotation>
+            </xs:element>
+        </xs:sequence>
+
+        <xs:attribute name="id" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>
+                    A registration ID that must be unique among all SAML IdPs. This ID appears in
+                    URLs, so it's preferable to use only URL-friendly characters.
+                </xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="IconType">
+        <xs:annotation>
+            <xs:documentation>
+                Configures an icon.
+            </xs:documentation>
+        </xs:annotation>
+
+        <xs:attribute name="location" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>
+                    The URL from which to retrieve the icon. This may be a file:// URL if the icon
+                    is stored in a local file.
+                </xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="SAMLMetadataType">
+        <xs:annotation>
+            <xs:documentation>
+                Configures metadata retrieval for a SAML relying party.
+            </xs:documentation>
+        </xs:annotation>
+
+        <xs:attribute name="location" type="xs:string" use="required">
+            <xs:annotation>
+                <xs:documentation>
+                    The URL from which to retrieve the metadata. This may be a file:// URL if the
+                    metadata is stored in a local file.
+                </xs:documentation>
+            </xs:annotation>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="X509CredentialsType">
+        <xs:sequence>
+            <xs:element name="x509-credential" type="X509CredentialType" minOccurs="1" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="X509CredentialType">
+        <xs:sequence>
+            <xs:element name="private-key" type="PrivateKeyType" minOccurs="0" maxOccurs="1" />
+            <xs:element name="x509-certificate" type="X509CertificateType" minOccurs="1" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="PrivateKeyType">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute name="location" type="xs:string" />
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+
+    <xs:complexType name="X509CertificateType">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute name="location" type="xs:string" />
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+
+    <xs:simpleType name="MessageBindingEnum">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="post"/>
+            <xs:enumeration value="redirect"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <xs:complexType name="AssertingPartyDetailsType">
+        <xs:sequence>
+            <xs:element name="entity-id" type="xs:string" minOccurs="1" maxOccurs="1" />
+            <xs:element name="want-authn-requests-signed" type="xs:boolean" minOccurs="0" maxOccurs="1" />
+            <xs:element name="signing-algorithms" type="SigningAlgorithmsType" minOccurs="0" maxOccurs="1" />
+
+            <xs:element name="single-sign-on-service-binding" type="MessageBindingEnum" minOccurs="0" maxOccurs="1" />
+            <xs:element name="single-sign-on-service-location" type="xs:string" minOccurs="0" maxOccurs="1" />
+
+            <xs:element name="single-logout-service-binding" type="MessageBindingEnum" minOccurs="0" maxOccurs="1" />
+            <xs:element name="single-logout-service-location" type="xs:string" minOccurs="0" maxOccurs="1" />
+            <xs:element name="single-logout-service-response-location" type="xs:string" minOccurs="0" maxOccurs="1" />
+
+            <xs:element name="encryption-x509-credentials" type="X509CredentialsType" minOccurs="0" maxOccurs="1" />
+            <xs:element name="verification-x509-credentials" type="X509CredentialsType" minOccurs="0" maxOccurs="1" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="SigningAlgorithmsType">
+        <!-- https://litsec.github.io/opensaml-javadoc-mirror/org/opensaml/opensaml-xmlsec-api/3.4.2/constant-values.html#org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS -->
+        <xs:sequence>
+            <xs:element name="signing-algorithm" type="xs:string" minOccurs="1" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
 </xs:schema>
index cdb2d22a9dbaee293265cb04949f4d11d0f13efe..d60b00501e8f9a1ef2f801ec8a9fd5a9c28112f1 100644 (file)
@@ -24,6 +24,7 @@ import org.collectionspace.authentication.AuthN;
 import org.collectionspace.services.common.ServiceMain;
 import org.collectionspace.services.common.config.ConfigUtils;
 import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl;
+import org.collectionspace.services.config.SAMLRelyingPartyType;
 import org.collectionspace.services.config.ServiceConfig;
 import org.collectionspace.services.config.tenant.TenantBindingType;
 import org.slf4j.Logger;
@@ -63,8 +64,36 @@ public class LoginResource {
     @Produces(MediaType.TEXT_HTML)
     public String getHtml(@Context HttpServletRequest request) throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException, TemplateException {
         ServiceConfig serviceConfig = ServiceMain.getInstance().getServiceConfig();
+        List<SAMLRelyingPartyType> samlRegistrations = ConfigUtils.getSAMLRelyingPartyRegistrations(serviceConfig);
 
         Map<String, Object> uiConfig = new HashMap<>();
+        Map<String, Object> ssoConfig = new HashMap<>();
+
+        if (samlRegistrations != null) {
+            for (SAMLRelyingPartyType samlRegistration : samlRegistrations) {
+                Map<String, String> registrationConfig = new HashMap<>();
+                String name = samlRegistration.getName();
+
+                if (name == null || name.length() == 0) {
+                    name = samlRegistration.getId();
+                }
+
+                registrationConfig.put("name", name);
+
+                if (samlRegistration.getIcon() != null) {
+                    registrationConfig.put("icon", samlRegistration.getIcon().getLocation());
+                }
+
+                String url = "/cspace-services/saml2/authenticate/" + samlRegistration.getId();
+
+                ssoConfig.put(url, registrationConfig);
+            }
+        }
+
+        if (!ssoConfig.isEmpty()) {
+            uiConfig.put("sso", ssoConfig);
+        }
+
         CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
 
         if (csrfToken != null) {