Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

If you use CXF JAX-RS client API to experiment with SAML then all you need to do is to register an appropriate out interceptor as shown in the above code fragments. The interceptor will ensure that a SAML assertion is created and added inside the XML envelope, as a form or HTTP header value.
All of the SAML output interceptors depend on a "security.saml-callback-handler" property linking to a custom javax.security.auth.callback.Callback implementation which in its handle(Callbacks) method provides the information which is needed to create a SAML assertion to a org.apache.ws.security.saml.ext.SAMLCallback Callback instance, for example, see this custom implementation.

More involved cases with SAML assertions being created by identity providers will be supported, with the help of CXF (WS) STSClient when needed.

...

At the moment these methods can not be properly validated when the assertion is provided in a header or in the form, the additional signature signing the encoded SAML token will be needed - this will be supported in due time. Use "bearer" in those cases.

Validating SAML

...

SAML assertions may contain so-called claims which are represented by a sequence of SAML AttributeStatements containing one or more Attributes, for example:

...

<saml2:Assertion>
 <!-- ... -->
 <saml2:AttributeStatement>
    <saml2:Attribute NameFormat="http://schemas.xmlsoap.org/ws/2005/05/identity/claims"
                 Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role" 
                 FriendlyName="subject-role">
       <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">user</saml2:AttributeValue>
    </saml2:Attribute>
    <saml2:Attribute NameFormat="http://claims/authentication"
                     Name="http://claims/authentication-format">
        <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">password</saml2:AttributeValue>
    </saml2:Attribute>
 </saml2:AttributeStatement>
 <!-- ... -->
</saml2:Assertion>

An individual claim is scoped by NameFormat and Name attribute. NameFormat is similar to a namespace, while Name identifies what the value of this claim represents, for example, in the above fragment two claims are provided, one has a value "user" which represents a role of the assertion's Subject, another one has a value of "password" which identifies the way Subject authenticated itself, i.e, Subject provided its password (presumably to IDP).

Now, what is interesting is to see if it is possible to use these claims with Role-Based Access-Control (for example, with endpoints relying on @RolesAllowed annotations) as well as with the more complex authorization logic (for example, let this resource be invoked only if Subject used a password to get authenticated at IDP).

Claims Based Access Control

CXF JAX-RS offers an extension letting users to enforce a new fine-grained Claims Based Access Control (CBAC) based on Claim and Claims annotations as well as ClaimMode enum class.

Note a package for Claim, Claims and ClaimMode annotations has changed from "org.apache.cxf.rs.security.saml.authorization" to "org.apache.cxf.security.claims.authorization". Starting from CXF 2.7.1, the default name format for claims is "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified" instead of "http://schemas.xmlsoap.org/ws/2005/05/identity/claims".

Here is a simple code fragment:

...

import org.apache.cxf.rs.security.saml.authorization.Claim;
import org.apache.cxf.rs.security.saml.authorization.Claims;

@Path("/bookstore")
public class SecureClaimBookStore {
    
    @POST
    @Path("/books")
    @Produces("application/xml")
    @Consumes("application/xml")
    @Claims({ 
        @Claim({"admin" }),
        @Claim(name = "http://claims/authentication-format", 
               format = "http://claims/authentication", 
               value = {"fingertip", "smartcard" })
    })
    public Book addBook(Book book) {
        return book;
    }
    
}

SecureClaimBookStore.addBook(Book) can only be invoked if Subject meets the following requirement: it needs to have a Claim with a value "admin" and another Claim confirming that it got authenticated using either a 'fingertip' or 'smartcard' method. Note that @Claim({"admin"}) has no name and format classifiers set - it relies on default name and format values, namely "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role" and "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified" ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims" before CXF 2.7.1) respectively. These default values may change in the future depending on which claims are found to be used most often - but as you can see you can always provide name and format values which will scope a given claim value.

Note that in the above example, a Claim with the name "http://claims/authentication-format" has two values, 'fingertip' and 'smartcard'. By default, in order to meet this Claim, Subject needs to have a Claim which has either a 'fingertip' or 'smartcard' value. If it is expected that Subject needs to have a Claim which has both 'fingertip' and 'smartcard' values, then the following change needs to be done:

...

import org.apache.cxf.security.claims.authorization.Claim;
import org.apache.cxf.security.claims.authorization.Claims;

@Path("/bookstore")
public class SecureClaimBookStore {
    
    @POST
    @Path("/books")
    @Produces("application/xml")
    @Consumes("application/xml")
    @Claims({ 
        @Claim({"admin" }),
        @Claim(name = "http://claims/authentication-format", 
               format = "http://claims/authentication", 
               value = {"fingertip", "smartcard" },
               matchAll = true)
    })
    public Book addBook(Book book) {
        return book;
    }
    
}

Claims can be specified using individual @Claim annotation, they can be set at the class level and overridden at the method level and finally a lax mode of check can be specified:

...

import org.apache.cxf.security.claims.authorization.Claim;
import org.apache.cxf.security.claims.authorization.Claims;

@Path("/bookstore")
@Claim({"user"})
public class SecureClaimBookStore {
    
    @POST
    @Path("/books")
    @Produces("application/xml")
    @Consumes("application/xml")
    @Claims({ 
        @Claim({"admin" }),
        @Claim(name = "http://claims/authentication-format", 
               format = "http://claims/authentication", 
               value = {"fingertip", "smartcard" },
               matchAll = true)
    })
    public Book addBook(Book book) {
        return book;
    }

    @GET
    @Claim(name = "http://claims/authentication-format", 
               format = "http://claims/authentication", 
               value = {"password" },
               mode = ClaimMode.LAX)
    public Book getBook() {
        //...
    }

    @GET
    public BookList getBookList() {
        //...
    }
    
    
}

In the above example, getBookList() can be invoked if Subject has a Claim with the value "user"; addBook() has it overridden - "admin" is expected and the authentication format Claim too; getBook() can be invoked if Subject has a Claim with the value "user" and it also must have the authentication format Claim with the value "password" - or no such Claim at all.

org.apache.cxf.rs.security.saml.authorization.ClaimsAuthorizingInterceptor enforces the CBAC rules. This filter can be overridden and configured with the rules directly which can be useful if no Claim-related annotations are expected in the code. Map nameAliases and formatAliases properties are supported to make @Claim annotations look a bit simpler, for example:

...

@Claim(name = "auth-format", format = "authentication", value = {"password" })

where "auth-format" and "authentication" are aliases for "http://claims/authentication-format" and "http://claims/authentication" respectively.

Given the above example, the question is how to extract the information available in a SAML Assertion for the current request to succeed in passing through the security filter enforcing the CBAC rules.

The first and most important thing which needs to be done is to verify that an assertion Subject can be mapped to a recognized identity instance.

There is a number of ways a Subject can be validated.

If STS is asked to validate the assertion then a successful response from IDP will likely be good enough for CXF to trust the identity of the provider.
If the assertion signature is verified locally using the public key of IDP then it could a good enough confirmation too.

Alternatively, a custom validator, extending either org.apache.ws.security.validate.SamlAssertionValidator or CXF SAML SecurityContextProvider implementation can be registered with the server side SAML handler.

The latter option is preferred because not only one can validate Subject - but also ensure that a resulting SecurityContext will return a user Principal with a proper name - given that the actual Subject name available in the assertion may need to be translated to a name recognized by the local security stores or application. A combination of the assertion's Subject and AttributeStatement elements may need to be checked to establish a real name.

In cases like this you may want to register a custom SecurityContextProvider even if you have STS validating the assertion. Yet another reason is to retrieve the information about roles for a given Subject or map the assertion claims to roles for working with the RBAC to succeed, see the next section for more information.

Have a look please at this server configuration example:

...

<bean id="serviceBeanClaims" class="org.apache.cxf.systest.jaxrs.security.saml.SecureClaimBookStore"/>
<bean id="samlEnvHandler" class="org.apache.cxf.rs.security.saml.SamlEnvelopedInHandler">
 <property name="securityContextProvider">
    <bean class="org.apache.cxf.systest.jaxrs.security.saml.CustomSecurityContextProvider"/>
 </property>
</bean>
    
<bean id="claimsHandler" 
     class="org.apache.cxf.rs.security.saml.authorization.ClaimsAuthorizingFilter">
    <property name="securedObject" ref="serviceBeanClaims"/>   
</bean>

<jaxrs:server address="/saml-claims"> 
       <jaxrs:serviceBeans>
          <ref bean="serviceBeanClaims"/>
       </jaxrs:serviceBeans>
       <jaxrs:providers>
          <ref bean="samlEnvHandler"/>
          <ref bean="claimsHandler"/>
       </jaxrs:providers>
</jaxrs:server>

An instance of org.apache.cxf.rs.security.saml.authorization.ClaimsAuthorizingFilter is used to enforce CBAC. It's a simple JAX-RS filter wrapper around ClaimsAuthorizingInterceptor. SamlEnvelopedInHandler processes and validates SAML assertions and it also relies on a simple CustomSecurityContextProvider to help it to figure out what the actual Subject name is. A more involved implementation can do some additional validation as well as override few more super class methods, more on it next. The claims themselves have already been parsed and will be made available to a resulting SecurityContext which ClaimsAuthorizingFilter will rely upon.

Role Based Access Control

If you have an existing RBAC system (based on javax.annotation.security.RolesAllowed or even org.springframework.security.annotation.Secured annotations) in place and have SAML assertions with claims that are known to represent roles, then making those claims work with the RBAC system can be achieved easily.

For example, given this code:

...

import org.springframework.security.annotation.Secured;

@Path("/bookstore")
@Claim({"user"})
public class SecureBookStore {
    
    @POST
    @Secured("admin")
    public Book addBook(Book book) {
        return book;
    }
}

Subjects

The first and most important thing which needs to be done is to verify that an assertion Subject can be mapped to a recognized identity instance.

There is a number of ways a Subject can be validated.

If STS is asked to validate the assertion then a successful response from IDP will likely be good enough for CXF to trust the identity of the provider.
If the assertion signature is verified locally using the public key of IDP then it could a good enough confirmation too.

Alternatively, a custom validator, extending either org.apache.ws.security.validate.SamlAssertionValidator or CXF SAML SecurityContextProvider implementation can be registered with the server side SAML handler.

The latter option is preferred because not only one can validate Subject - but also ensure that a resulting SecurityContext will return a user Principal with a proper name - given that the actual Subject name available in the assertion may need to be translated to a name recognized by the local security stores or application. A combination of the assertion's Subject and AttributeStatement elements may need to be checked to establish a real name.

In cases like this you may want to register a custom SecurityContextProvider even if you have STS validating the assertion. Yet another reason is to retrieve the information about roles for a given Subject or map the assertion claims to roles for working with the RBAC to succeed, see the next section for more information.

Have a look please at this server configuration examplewhere @Secured can be replaced with @RoledAllowed if needed, the following configuration will do it:

Code Block
xml
xml
<bean id="serviceBeanRolesserviceBeanClaims" class="org.apache.cxf.systest.jaxrs.security.saml.SecureBookStoreSecureClaimBookStore"/>
<bean id="samlEnvHandler" class="org.apache.cxf.rs.security.saml.SamlEnvelopedInHandler">
 <property name="securityContextProvider">
    <bean class="org.apache.cxf.systest.jaxrs.security.saml.CustomSecurityContextProvider"/>
 </property>
</bean>

<bean id="authorizationInterceptor" class="org.apache.cxf.interceptor.security.SecureAnnotationsInterceptor">
    <property name="securedObject" ref="serviceBean"/><jaxrs:server address="/saml-claims"> 
    <property  name="annotationClassName"  <jaxrs:serviceBeans>
          <ref    valuebean="org.springframework.security.annotation.SecuredserviceBeanClaims"/>
       </bean>
jaxrs:serviceBeans>
       
<bean id="rolesHandler" class="org.apache.cxf.jaxrs.security.SimpleAuthorizingFilter">
<jaxrs:providers>
         <property name="interceptor" ref<ref bean="authorizationInterceptorsamlEnvHandler"/>
</bean>
       
<jaxrs:server address="/saml-roles"> 
  <jaxrs:serviceBeans></jaxrs:providers>
</jaxrs:server>

SAML Authorization

SAML assertions may contain so-called claims which are represented by a sequence of SAML AttributeStatements containing one or more Attributes, for example:

Code Block
xml
xml
<saml2:Assertion>
 <!-- ... -->
 <saml2:AttributeStatement>
     <ref bean="serviceBeanRoles"/>
  </jaxrs:serviceBeans>
  <jaxrs:providers>
 <saml2:Attribute NameFormat="http://schemas.xmlsoap.org/ws/2005/05/identity/claims"
         <ref bean="samlEnvHandler"/>
      <ref beanName="rolesHandler"/>
  </jaxrs:providers>
  
  <!-- If default role qualifier and format are not supported: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role" 
                 FriendlyName="subject-role">
       <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">user</saml2:AttributeValue>
   <jaxrs:properties> </saml2:Attribute>
     <entry key="org.apache.cxf.saml.claims.role.nameformat" 
<saml2:Attribute NameFormat="http://claims/authentication"
                     valueName="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/>
http://claims/authentication-format">
        <entry key="org.apache.cxf.saml.claims.role.qualifier" 
                value="urn:oid:1.3.6.1.4.1.5923.1.1.1.1"/>
  </jaxrs:properties>
 <saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">password</saml2:AttributeValue>
    </saml2:Attribute>
 </saml2:AttributeStatement>
 <!-- ... -->
</jaxrssaml2:server>Assertion>

That is all what is needed. Note that in order to help the default SAML SecurityContextProvider figure out which claims are roles, one can set the two properties as shown above - this not needed if it's known that claims identifying roles have NameFormat and Name values with the default values, which are "http://schemas.xmlsoap.org/ws/2005/05/identity/claims" and "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role" respectively at the moment.

Note that you can have RBAC and CBAC combined for a more sophisticated access control rules be enforced while still keeping the existing code relying on @RolesAllowed or @Secured intact. Override ClaimsAuthorizingFilter and configure it with the Claims rules directly and register it alongside SimpleAuthorizingFilter and here you go.

An individual claim is scoped by NameFormat and Name attribute. NameFormat is similar to a namespace, while Name identifies what the value of this claim represents, for example, in the above fragment two claims are provided, one has a value "user" which represents a role of the assertion's Subject, another one has a value of "password" which identifies the way Subject authenticated itself, i.e, Subject provided its password (presumably to IDP).

Now, what is interesting is to see if it is possible to use these claims with Role-Based Access-Control (for example, with endpoints relying on @RolesAllowed annotations) as well as with the more complex authorization logic (for example, let this resource be invoked only if Subject used a password to get authenticated at IDP).

Claims Based Access Control

CXF JAX-RS supports Claims Based Access Control (CBAC) based on Claim and Claims annotations using claims extracted from SAML Assertions. Please see the JAX-RS Token Authorization page for more information.

Role Based Access Control

CXF JAX-RS also supports Role Based Access Control (RBAC) based on role claims extracted from SAML Assertions. Please see the JAX-RS Token Authorization page for more informationAlso note how SecureAnnotationsInterceptor can handle different types of role annotations, with @RoledAllowed being supported by default.

SAML Web SSO Profile

Please see this page for more information