Shibboleth’s IDP can store audit logs that indicate when people authenticate against the IDP web application. These files are written to disk by default using the settings in the logging.xml configuration file. This tutorial will show how audit logs can be placed in a MS SQL database and also include the IP addresses of the connecting clients.

Shibboleth uses the SLF4J, a logging library that is a front end for a variety of backend loggers. By default, it uses logback to handle process audit logs. SLF4J supports a DBAppender. Unlike the RollingFileAppender, the DBAppender doesn’t support encoder/Pattern configuration. It places all logging messages in a standard database schema which can be found in the logback-0.9.28\logback-classic\src\main\java\ch\qos\logback\classic\db\dialect directory of the logback source code.

Note that in version 0.9.28 of logback, there is a bug in the MS SQL schema where all the event_id fields have an invalid type of DECIMAL(40). These must be changed to DECIMAL(38). This tutorial assume the use of an MS SQL database. Other databases will work, but the triggers and schema will need to be adjusted for those dialects accordingly.

In the Shibboleth logging.xml configuration file, start by adding the following appender:

<appender name="IDP_DB_APPENDER" class="ch.qos.logback.classic.db.DBAppender">
      <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
        <dataSource class="com.jolbox.bonecp.BoneCPDataSource">
          <driverClass>com.microsoft.sqlserver.jdbc.SQLServerDriver</driverClass>
          <jdbcUrl>jdbc:sqlserver://dbserver.example.edu:5555;databaseName=ShibAudit</jdbcUrl>
          <username>someUsername</username>
          <password>somePassword</password>
        </dataSource>
      </connectionSource>
    </appender>

Replace the database name, server, usernames and passwords respectively.

For the above example, the BoneCP connectionSource is used for connection pooling. The BoneCP libraries will be available if uApprove has been installed. Additional configuration options can be specified for tweaking connection counts and partitions within the pool.

An alternative to BoneCP is the c3p0 connection pool library. This library comes with Shibboleth and requires no extra jar files:

<appender name="IDP_DB_APPENDER" class="ch.qos.logback.classic.db.DBAppender">
    <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
      <dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <driverClass>com.microsoft.sqlserver.jdbc.SQLServerDriver</driverClass>
        <jdbcUrl>jdbc:sqlserver://dbserver.example.edu:5555;databaseName=ShibAudit</jdbcUrl>
        <user>someUser</user>
        <password>somePassword</password>
      </dataSource>
    </connectionSource>
  </appender>

Again, replace the database name, server, usernames and passwords respectively. Next, the new appender needs to be attached to the audit logger:

<logger name="Shibboleth-Audit" level="ALL">
        <appender-ref ref="IDP_AUDIT" />
        <appender-ref ref="IDP_DB_APPENDER" />
    </logger>

At this point, Shibboleth IDP can be restarted and attempts to authenticate with the IDP should result in log entries in the database. Make sure this is working. If not, check the idp-process.log as well as Tomcat’s cataline.out log file to determine if there were errors creating the DBAppender object.

Shibboleth 2.2.1 places its audit logs in a pipe delimited format that can easily be parsed. Rather than let the logger store this long pipe delimited string in our database, it would be beneficial to convert these logs into structure that’s easier to query. Start by creating the following table in the same database the DBAppender writes to:

CREATE TABLE audit_log (
   logtime datetime,
   remoteAddr VARCHAR(255),  
   requestbinding VARCHAR(100),
   requestId VARCHAR(50),
   relayingPartyId VARCHAR(255),
   messageProfileId VARCHAR(100),
   assertingPartyId VARCHAR(255),
   responseBinding VARCHAR(100),
   responseId VARCHAR(50),
   principalName VARCHAR(8),
   authNMethod VARCHAR(100),
   releasedAttributeIds VARCHAR(255),
   nameIdentifier VARCHAR(100),
   assertionIDs VARCHAR(100)
);

Now, a trigger will be added to the logging_event table so that whenever a row is inserted, it will be parsed and placed into the new tables.

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE TRIGGER [dbo].[parseLog]
   ON  [dbo].[logging_event]
   AFTER INSERT
AS 
BEGIN

	SET NOCOUNT ON;

  DECLARE @data VARCHAR(4000)
  DECLARE @xml XML
  DECLARE @unixTimestamp datetime
  
  --Convert UNIX timestamp, with ms, to mssql datetime
  SET @unixTimestamp = (SELECT dateadd(ms,CAST(RIGHT(timestmp,3) AS INT),dateadd(ss,CAST(timestmp AS BIGINT)/1000,'01/01/1970')) FROM INSERTED)
       
  --Data as XML for easy parsing
  SET @data = (SELECT formatted_message FROM INSERTED)
  SET @xml = '<Cols><Col>' + REPLACE(@data,'|','</Col><Col>') + '</Col></Cols>'    
  
  --Different Tables for different logger types
  DECLARE @type AS VARCHAR(254)
  SET @type = (SELECT logger_name FROM INSERTED)
  
  IF @type = 'Shibboleth-Audit'
  BEGIN 
	  --Fields to parse 
	   DECLARE @requestbinding AS VARCHAR(100)
	   DECLARE @requestId AS VARCHAR(50)
	   DECLARE @relayingPartyId AS VARCHAR(255)
	   DECLARE @messageProfileId AS VARCHAR(100)
	   DECLARE @assertingPartyId AS VARCHAR(255)
	   DECLARE @responseBinding AS VARCHAR(100)
	   DECLARE @responseId AS VARCHAR(50)
	   DECLARE @principalName AS VARCHAR(8)
	   DECLARE @authNMethod AS VARCHAR(100)
	   DECLARE @releasedAttributeIds AS VARCHAR(255)
	   DECLARE @nameIdentifier AS VARCHAR(100)
	   DECLARE @assertionIDs AS VARCHAR(100)
	   
	   --Store the event_id in the IP address. The event_property trigger
	   --  will replace this with the real IP
	   DECLARE @eventId AS DECIMAL(38,0)
	   SET @eventId = (SELECT event_id FROM INSERTED)

	  SELECT 
		@requestBinding = x.d.value('Col[2]', 'VARCHAR(100)'),
		@requestId = x.d.value('Col[3]', 'VARCHAR(50)'),
		@relayingPartyId = x.d.value('Col[4]', 'VARCHAR(255)'),
		@messageProfileId = x.d.value('Col[5]', 'VARCHAR(100)'),
		@assertingPartyId = x.d.value('Col[6]', 'VARCHAR(255)'),
		@responseBinding = x.d.value('Col[7]', 'VARCHAR(100)'),
		@responseId = x.d.value('Col[8]', 'VARCHAR(50)'),
		@principalName = x.d.value('Col[9]', 'VARCHAR(8)'),
		@authNMethod = x.d.value('Col[10]', 'VARCHAR(100)'),
		@releasedAttributeIds = x.d.value('Col[11]', 'VARCHAR(255)'),
		@nameIdentifier = x.d.value('Col[12]', 'VARCHAR(100)'),
		@assertionIDs = x.d.value('Col[13]', 'VARCHAR(100)')
	  FROM  @xml.nodes('/Cols') x(d)

	  INSERT INTO audit_log (logtime, remoteAddr,requestbinding,requestId,relayingPartyId,messageProfileId,assertingPArtyId,
		responseBinding,responseId,principalName,authNMethod,releasedAttributeIds, nameIdentifier, assertionIds)
		VALUES(@unixTimeStamp,@eventId, @requestBinding,@requestId,@relayingPartyId,
		@messageProfileId,@assertingPartyId,@responseBinding,@responseId,@principalName,
		@authNMethod,@releasedAttributeIds,@nameIdentifier,@assertionIDs)
  END
END
GO 

Since MS SQL has no built-in function for splitting fields on a delimiter, the above function replaces the pipe symbols with opening and closing XML tags so that MS SQL’s built-in XPath engine can be used to parse the field. A conversion must also be done to translate the UNIX timestamp to an MS SQL datetime type. Finally, the event_id is used as a placeholder in the field containing the client’s IP address. This is because by default, Shibboleth’s audit logs do not contain IP information. This can be added however using a custom servlet filter with SLF4J.

SLF4J supports a MDC which allows for values to be mapped to a logger for a given thread instance. Since only one thread in a Java servlet container is used per connection at any given time, this can be used to hold items such as the connecting client’s IP address for logging. The DBAppender inserts these properties in the logging_event_property table. A custom filter can be written to create these properties, but SLF4J comes with the MDCInsertingServletFilter which injects certain common attributes into the MDC such as user-agent, remote host, query string, etc.

In the idp.war, add the following filter to the web.xml.

<filter>
    <filter-name>MDCInsertingServletFilter</filter-name>
    <filter-class>
      ch.qos.logback.classic.helpers.MDCInsertingServletFilter
    </filter-class>
  </filter>
  <filter-mapping>
    <filter-name>MDCInsertingServletFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

The SLF4J documentation recommended adding the MDC as the first filter, but unless attributes are needed for logging within other filters from what the MDC injects, this isn’t always necessary. For the database audit logging, the filter can be placed at the end of the idp.war‘s web.xml file.

Finally, a trigger must be added to handle the properties that are inserted, namely the remote IP address, and update the record of the original log entry.

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE TRIGGER [dbo].[setLogIP]
   ON  [dbo].[logging_event_property]
   AFTER INSERT
AS 
BEGIN   
   IF (SELECT mapped_key FROM INSERTED) = 'req.remoteHost'
   BEGIN   
      UPDATE audit_log  SET remoteAddr = (SELECT mapped_value FROM INSERTED)
        WHERE remoteAddr = CONVERT(varchar(255),(SELECT event_id FROM INSERTED))
   END   
END
GO

The DBAppender is transaction based, so if any triggers or query statements fail (e.g. if permissions are not setup correctly for the database user), the entire logging transaction will be rolled back and no results will appear in the database table. It is best to implement this is stages, starting with getting the DBAppender working and then adding the new table and triggers.

Since the standard text based log files do not include an IP address, it is best to include this new attribute in the original files as well. This can be done by modifying the RollingFileAppender for the audit log. The following example adds the IP address and also sets the rollover for the log file to 1 year (365 days). This should be adjusted to the retention requirements of the institution’s legal department.

<appender name="IDP_AUDIT" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>/opt/shibboleth-idp/logs/idp-audit.log</File>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>/opt/shibboleth-idp/logs/idp-audit-%d{yyyy-MM-dd}.log</FileNamePattern>
            <maxHistory>365</maxHistory>
        </rollingPolicy>

        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <charset>UTF-8</charset>
            <Pattern>%X{req.remoteHost}|%msg%n</Pattern>
        </encoder>
    </appender>

Having audit logs in a database makes it convenient to aggregate Shibboleth authentication requests for reports, as well as retrieve information quickly for legal and security requests. It is recommended to also keep the RollingFileAppender to use as a backup in case of problems with the database connection.