May 21, 2013

Logging in Apache CXF STS enhanced

This extension will be available in CXF release 2.7.6 which is not yet available. But you can run tests with the SNAPSHOT build till this version is released. Please provide feedback to the CXF mailing list.

Different logging frameworks (SLF4J, Log4J, Logback, JUL) can be used to log events for Apache CXF STS. The configuration allows to define which logger should log messages till to which log level. That works fine to drill down generic issues but it doesn't help too much to know whether a certain user could successfully log in or had any specific issues. Further, the WS-Trust interface is very generic. Therefore, the same user can request tokens but for different applications using different credential types. If a log in error occurs some context information is required to easily drill down user specific issues.

Based on the experience of a customer deployment, the following information is helpful to figure out how often a user requested a token and under which circumstances:

  • AppliesTo
    For which application did the user request a token
  • Source IP
    From which machine did the application request a token for a user
  • Claims
    Which claims did the user request for an application
  • Security Header
    How did the user try to log in (Username/password, Kerberos, X509, ...)
  • Realm
    For which security domain did the user request a token
  • etc.
All this information are available within the core classes of the STS and thus not customizable without patching these classes. The next release of CXF will provide a customizable logging/auditing functionality to fulfill various requirements.

Spring Eventing

The Spring framework provides an eventing mechanism which is designed for simple communication between Spring beans. Instead of introducing a new eventing mechanism to push data to a class which processes the data and writes it to a log file the new feature leverages the usage of the Spring framework in the CXF STS. How Spring eventing works is described on the following blog. If you don't want to delay STS related processing you can publish the events asynchronously which is described here.

CXF STS custom Application Events

Depending on the STS operation called, a different object with context information is created in the CXF STS. The following table summarizes the defined bindings in the WS-Trust specification, the CXF related context object as well a link with more information about this binding:

BindingContext objcectSpring EventDocumentation
IssueTokenProviderParametersSTSIssueSuccessEvent
STSIssueFailureEvent
blog
ValidateTokenValidatorParametersSTSValidateSuccessEvent
STSValidateFailureEvent
blog
CancelTokenCancellerParametersSTSCancelSuccessEvent
STSCancelFailureEvent
blog
RenewTokenRenewerParametersSTSRenewSuccessEvent
STSRenewFailureEvent
blog

The different binding implementations support the interface ApplicationEventPublisherAware thus they can publish events about a successful or failed request. You have to provide an implementation of ApplicationListener to listen to Spring Application Events. Due to the usage of generics you can specify which events you want to listen to. All the above STS specific events inherit the abstract class AbstractSTSEvent. If you want to listen to all STS events then you must provide an implementation like this:

public class AllSTSEventsListener implements ApplicationListener {
    @Override
    public void onApplicationEvent(AbstractSTSEvent event) {
        // do whatever you want here
    }
}
If you want to listen to all successful issue events you must use the generic STSIssueSuccessEvent. The STS provides the LoggerListener which listens to all STS events and uses the CXF Logging API to write the log message. All you have to do is configure the following bean in the STS application context configuration (ex. cxf-transport.xml):
<bean id="loggerListener" class="org.apache.cxf.sts.event.LoggerListener" />

If you want to configure another Application listeners, just add a bean configuration and you're done.

The LoggerListener is able to log the following context information:

  • TIME
    Creation time of the event
  • OPERATION
    STS binding/operation
  • WS_SEC_PRINCIPAL
    Principal in WS-Security token
  • STATUS
    Successful/failed request
  • DURATION
    Processing time
  • TOKENTYPE
    Token type requested (SAML 1.1, SAML 2.0, etc)
  • REALM
    Security domain
  • APPLIESTO
    Application for which token is requested
  • CLAIMS
    Claims requested
  • ACTAS_PRINCIPAL
    Principal of ActAs token
  • ONBEHALFOF_PRINCIPAL
    Principal of On-Behalf-Of token
  • VALIDATE_PRINCIPAL
    Principal of Validate token
  • CANCEL_PRINCIPAL
    Principal of Cancel token
  • RENEW_PRINCIPAL
    Principal of Renew token
  • REMOTE_HOST
    Hostname/IP which requested the token
  • REMOTE_PORT
    Source Port which requested the token
  • URL
    STS URL used to request token
The LoggerListener provides the following properties to customize its behaviour:

NameTypeMandatoryDefaultDescription
logFieldnameBooleanNoNoShould the fieldname be logged, ex. OPERATION=issue
dateFormatStringNogetDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)Format of the date
logLevelStringNoFINEWhich log level should be used?
logStacktraceBooleanNoNoIn case of an error, shall the stacktrace be logged?
fieldOrderList<String>NoTIME
STATUS
DURATION
REMOTE_HOST
REMOTE_PORT
OPERATION
URL
REALM
WS_SEC_PRINCIPAL
ONBEHALFOF_PRINCIPAL
ACTAS_PRINCIPAL
VALIDATE_PRINCIPAL
CANCEL_PRINCIPAL
RENEW_PRINCIPAL
TOKENTYPE
APPLIESTO
CLAIMS
EXCEPTION
Order of context fields to be logged

If you want that all LoggerListener related log messages are written into a different file (ex. audit.log) I highly recommend to not use Java Util Logging as it's not so easy to configure a dedicated handler/appender for one logger.

  1. Configure Log4J as the logging framework in CXF (see here)
  2. Add the log4j dependency to your POM
  3. Configure the logger and appender
    log4j.rootLogger=INFO, CONSOLE, LOGFILE
    log4j.logger.org.apache.cxf.sts.event.LoggerListener=DEBUG, AUDIT
    
    # CONSOLE is set to be a ConsoleAppender using a PatternLayout.
    log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
    log4j.appender.CONSOLE.Threshold=INFO
    log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
    log4j.appender.CONSOLE.layout.ConversionPattern=%d [%t] %-5p %c %x - %m%n
    
    # AUDIT is set to be a File appender using a PatternLayout.
    log4j.appender.AUDIT=org.apache.log4j.FileAppender
    log4j.appender.AUDIT.File=${catalina.base}/logs/audit.log
    log4j.appender.AUDIT.Append=true
    log4j.appender.AUDIT.Threshold=DEBUG
    log4j.appender.AUDIT.layout=org.apache.log4j.PatternLayout
    log4j.appender.AUDIT.layout.ConversionPattern=%m%n
    
The audit log file looks like this if configured as above:
5/10/13 8:59:59 AM;SUCCESS;2839ms;127.0.0.1;57378;Issue;https://localhost:9443/fediz-idp-sts/STSService;null;alice;null;null;http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0;https://localhost:8081/doubleit/services/doubleittransportsaml1claims;null;null;

Enjoy.

May 14, 2013

LDAP support enhanced for CXF STS 2.7.5

I described in a previous blog how to configure the CXF STS for an LDAP directory for authentication and to retrieve user claims (attributes). The new release 2.7.5 of CXF provides extended support for roles managed in a LDAP directory. In previous versions, the LdapClaimsHandler added groups as roles if the groups were assigned to a multi-value attribute of the user. The new release provides an LdapGroupClaimsHandler which supports the case where an attribute of the groups lists the users who belong to this group. Further, it introduces the semantic of an application role. A user might have the role "User" for application X and role "Manager" and "User" for application Y.

The STS provides the semantic of an application with the AppliesTo parameter which is a URI. If you request a SAML token which includes the roles for a specific application (ex. MyApp), you get User and Manager back. A mapping is required in the STS to map the AppliesTo URI (URL or URN) to a String value like MyApp.

The sub-project Fediz provides in 1.1 (not released yet) a Maven profile to build the STS with an LDAP backend (instead of managing users/claims in a file). You can have a look at the ldap.xmlhere. The following configuration configures the LdapClaimsHandler and LdapGroupClaimsHandler. There is nothing special for the LdapClaimsHandler. The LdapGroupClaimsHandler also uses the Spring LdapContextSource and LdapTemplate.

    <util:list id="claimHandlerList">
        <ref bean="userClaimsHandler" />
        <ref bean="groupClaimsHandler" />
    </util:list>

    <bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
        <property name="url" value="ldap://localhost:389/" />
        <property name="userDn" value="uid=admin,ou=system" />
        <property name="password" value="secret" />
    </bean>

    <bean id="ldapTemplate" class="org.springframework.ldap.core.LdapTemplate">
        <constructor-arg ref="contextSource" />
    </bean>

    <util:map id="claimsToLdapAttributeMapping">
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
            value="givenName" />
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
            value="sn" />
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
            value="mail" />
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country"
            value="c" />
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode"
            value="postalCode" />
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress"
            value="postalAddress" />                        
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality"
            value="town" />
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince"
            value="st" />
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender"
            value="gender" />
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth"
            value="dateofbirth" />                                                
        <entry key="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role"
            value="member" />
    </util:map>

    <bean id="userClaimsHandler" class="org.apache.cxf.sts.claims.LdapClaimsHandler">
        <property name="ldapTemplate" ref="ldapTemplate" />
        <property name="claimsLdapAttributeMapping" ref="claimsToLdapAttributeMapping" />
        <property name="userBaseDN" value="ou=users,dc=fediz,dc=org" />
        <property name="userNameAttribute" value="uid" />
    </bean>
    
    <util:map id="appliesToScopeMapping">
        <entry key="urn:org:apache:cxf:fediz:fedizhelloworld"
            value="Example" />
    </util:map>
    
    <bean id="groupClaimsHandler" class="org.apache.cxf.sts.claims.LdapGroupClaimsHandler">
        <property name="ldapTemplate" ref="ldapTemplate" />
        <property name="userBaseDN" value="ou=users,dc=fediz,dc=org" />
        <property name="userNameAttribute" value="uid" />
        <property name="groupBaseDN" value="ou=groups,dc=fediz,dc=org" />
        <property name="appliesToScopeMapping" ref="appliesToScopeMapping" />
    </bean>
    
    <jaxws:endpoint id="transportSTS1" implementor="#transportSTSProviderBean"
        address="/STSService" wsdlLocation="/WEB-INF/wsdl/ws-trust-1.4-service.wsdl"
        xmlns:ns1="http://docs.oasis-open.org/ws-sx/ws-trust/200512/"
        serviceName="ns1:SecurityTokenService" endpointName="ns1:TransportUT_Port">
        <jaxws:properties>
            <entry key="ws-security.ut.validator">
                <bean class="org.apache.ws.security.validate.JAASUsernameTokenValidator">
                    <property name="contextName" value="LDAP" />
                </bean>
            </entry>
        </jaxws:properties>
    </jaxws:endpoint>
I've highlighted the important beans to support the mapping of groups to (application) roles. The bean LdapGroupClaimsHandler has got the following attributes:

NameMandatoryDefaultDescription
ldapTemplateYesN.A.The Spring LDAP template
groupBaseDNYesN.A.The base group context where the search starts
groupObjectClassNogroupOfNamesObject class for groups. Used for search filter.
groupMemeberAttributeNomemberThe group attribute where the list of users are stored
groupURINohttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/roleThe SAML attribute name where the roles should be stored
groupNameGlobalFilterNoROLEDefault uses the CN of the group as role name
groupNameScopedFilterNoSCOPE_ROLEDefault cuts the SCOPE and the underscore of the CN of the group
appliesToScopeMappingNoN.A.The mapping is required if application specific roles must be supported
userNameAttributeNocnUser id attribute. Only required if LDAP is not used for authentication and thus the DN of the user must be resolved first. Used for search filter.
userObjectClassNopersonObject class for users. Only required if LDAP is not used for authentication and thus the DN of the user must be resolved first. Used for search filter.

The bean appliesToScopeMapping defines the mapping of the URI in the AppliesTo variable to a Name as URI's are not valid within a CN of an LDAP group.

One example for the usage of groupNameScopedFilter. One more example. Let's assume you use the same LDAP directory for the application environemnt development and pre-production and defines the following naming convention for application roles:
DEV_<Application>_<ROLE>_Group and UAT_<Application>_<ROLE>_Group The groupNameScopedFilter will look like this DEV_SCOPE_ROLE_Group (assumption: Different STS instances are deployed for development and pre-production).

The following table lists a few group examples and how the role value will look like in the SAML attribute. The assumption is that the AppliesTo element is urn:org:apache:cxf:fediz:fedizhelloworld which maps to the scope Example (see configuration example above) and the groupNameScopedFilter is configured like DEV_SCOPE_ROLE_Group:

Group CNRole name
DEV_Example_User_GroupUser
DEV_Example_Admin_GroupAdmin
DEV_Example2_User_Groupignored
UAT_Example_User_Groupignored
INFR_Citrix_Accessignored

Last but not least I'd like to comment the default value of userNameAttribute which is CN. As per recommendation (5.4) the CN is typically the person's fullname and therefore doesn't fit for the user id (login name). Due to the reason that the LdapClaimsHandler had the cn as default value I wanted to keep that in sync and change it in the next non-patch release of CXF.

If you face issues or like more functionality send a message to the CXF mailing list or open a JIRA issue.