Spring SAML - Reading and refreshing IdP metadata

2020-02-29 06:53发布

I am using WSO2 and SSOCircle with the Spring-SAML extension. We are testing configurations at this time and have defined 2 IdP's and 2 SP's within our applicationContext. So, currently, we have 2 statically defined IdP's within our spring xml config and this is working. For testing purpose we are using the combination of CachingMetadataManager and ResourceBackedMetadataProvider so the IdP metadata is built inside of our WAR archive. Sample:

<bean id="metadata" class="org.springframework.security.saml.metadata.CachingMetadataManager">
<constructor-arg>
  <list>
    <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
      <constructor-arg>
        <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
          <constructor-arg>
            <bean class="java.util.Timer"/>
          </constructor-arg>
          <constructor-arg>
            <bean class="org.opensaml.util.resource.ClasspathResource">
              <constructor-arg value="/metadata/wso2idp_metadata.xml"/>
            </bean>
          </constructor-arg>
          <property name="parserPool" ref="parserPool"/>
        </bean>
      </constructor-arg>
      <constructor-arg>
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
        </bean>
      </constructor-arg>
    </bean>
    <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
      <constructor-arg>
        <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
          <constructor-arg>
            <bean class="java.util.Timer"/>
          </constructor-arg>
          <constructor-arg>
            <bean class="org.opensaml.util.resource.ClasspathResource">
              <constructor-arg value="/metadata/ssocircleidp_metadata.xml"/>
            </bean>
          </constructor-arg>
          <property name="parserPool" ref="parserPool"/>
        </bean>
      </constructor-arg>
      <constructor-arg>
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
        </bean>
      </constructor-arg>
    </bean>
  </list>
</constructor-arg>

For production, we want to be able to store our IdP metadata in a database (centrally located). I want to be able to add, remove and modify the metadata without redeploying the WAR or restarting the server(s). Initially, I thought I could override the CachingMetadataManager and define a noarg constructor that could load all Metadata providers dynamically but this is not possible because the CachingMetadataManager only defines a single constructor that must take in a MetadataProvider List. I ended up doing the following:

<bean id="metadataList" class="org.arbfile.util.security.saml.DBMetadataProviderList">
  <constructor-arg ref="parserPool" />
  <constructor-arg>
    <bean class="java.util.Timer"/>
  </constructor-arg>
</bean>

<bean id="metadata" class="org.springframework.security.saml.metadata.CachingMetadataManager">
   <constructor-arg ref="metadataList" />
</bean>

Bean metadataList can be defined simply as:

public final class DBMetadataProviderList extends ArrayList<MetadataProvider>
{
  private final static Logger log = LoggerFactory.getLogger(DBMetadataProviderList.class);
  private ParserPool parser;

  public DBMetadataProviderList(ParserPool _parser, Timer _timer) throws MetadataProviderException
  {
    this.parser = _parser;
// Lookup metadata from DB
  }

}

This does allow me to read in the IdP metadata dynamically. My logic falls down when it comes to refreshing though. I found this post on the spring forum, however it is 3 to 4 years old. What is the best way to dynamically read, add and update IdP metadata, cache it and have the cache refresh at some interval? In my case 1 row in a DB table would equate to a single IdP metadata definition.

1条回答
欢心
2楼-- · 2020-02-29 07:21

After reading several more posts and scanning source code, I discovered the answer to this question is more complex than I thought. There are really 3 different scenarios to address.

  1. The initial reading of all IdP metadata providers from a database table
  2. Expiring and re-reading the IdP metadata XML data
  3. Dynamically adding and removing a provider without config changes or server restart

I'll take on each of these one at a time. Item 1: There are probably several ways to solve this but review the DBMetadataProviderList class (in my OP) above as a quick and dirty solution. Here is more complete constructor code:

//This constructor allows us to read in metadata stored in a database. 
public DBMetadataProviderList(ParserPool _parser, Timer _timer) throws MetadataProviderException
{
    this.parser = _parser;
    List<String> metadataProviderIds = getUniqueEntityIdListFromDB();
    for (final String mdprovId : metadataProviderIds)
    {
        DBMetadataProvider metadataProvider = new DBMetadataProvider(_timer, mdprovId);
        metadataProvider.setParserPool(this.parser);
        metadataProvider.setMaxRefreshDelay(480000); // 8 mins (set low for testing)
        metadataProvider.setMinRefreshDelay(120000); // 2 mins
        ExtendedMetadataDelegate md = new ExtendedMetadataDelegate(metadataProvider,  new ExtendedMetadata());
        add(md);
    }
}

To solve item #2 I used the FilesystemMetadataProvider as a guide and created a DBMetadataProvider class. By extending the AbstractReloadingMetadataProvider class and implementing the fetchMetadata() method we have built-in cache refreshing thanks to opensaml. Below are the important parts (example code only):

public class DBMetadataProvider extends AbstractReloadingMetadataProvider
{
  private String metaDataEntityId;  // unique Id for DB lookups

 /**
  * Constructor.
  * @param entityId the entity Id of the metadata.  Use as key to identify a database row.
  */
 public DBMetadataProvider(String entityId)
 {
    super();
    setMetaDataEntityId(entityId);
 }

 /**
  * Constructor.
  * @param backgroundTaskTimer timer used to refresh metadata in the background
  * @param entityId the entity Id of the metadata.  Use as key to identify a database row.
  */
 public DBMetadataProvider(Timer backgroundTaskTimer, String entityId)
 {
    super(backgroundTaskTimer);
    setMetaDataEntityId(entityId);
 }

 public String getMetaDataEntityId() { return metaDataEntityId;  }

 public void setMetaDataEntityId(String metaDataEntityId){ this.metaDataEntityId = metaDataEntityId; }

 @Override
 protected String getMetadataIdentifier() { return getMetaDataEntityId(); }

// This example code simply does straight JDBC
 @Override
 protected byte[] fetchMetadata() throws MetadataProviderException
 {
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;
    try
    {
        conn = JDBCUtility.getConnection();
        ps = conn.prepareStatement("select  bla bla bla ENTITY_ID = ?");
        ps.setString(1, getMetaDataEntityId());
        rs = ps.executeQuery();
        if (rs.next())
        {
            // include a modified date column in schema so that we know if row has changed
            Timestamp sqldt = rs.getTimestamp("MOD_DATE"); // use TimeStamp here to get full datetime
            DateTime metadataUpdateTime = new DateTime(sqldt.getTime(), ISOChronology.getInstanceUTC());
            if (getLastRefresh() == null || getLastUpdate() == null || metadataUpdateTime.isAfter(getLastRefresh()))
            {
                log.info("Reading IdP metadata from database with entityId = " + getMetaDataEntityId());
                Clob clob = rs.getClob("XML_IDP_METADATA");
                return clob2Bytes(clob);
            }
            return null;
        }
        else
        {
            // row was not found
            throw new MetadataProviderException("Metadata with entityId = '" + getMetaDataEntityId() + "' does not exist");
        }
    }
    catch (Exception e)
    {
        String msg = "Unable to query metadata from database with entityId = " + getMetaDataEntityId();
        log.error(msg, e);
        throw new MetadataProviderException(msg, e);
    }
    finally
    {
        // clean up connections
    }
  }

 }

This resource helped me get to the right technique for a cache reloading metadata provider class. Finally, item #3 can be solved by implementing this post.

查看更多
登录 后发表回答