Introduction
This document describes how to integrate the Spring-Security-oAuth2 project with Spring-Security-SAML.
I assume the reader is familiar with both oAuth and its components, and SAML and its components.
Motivation
Suppose you want your system to support oAuth2. I would recommend using the Spring-Security-oAuth project. When you use Spring, you enjoy the many benefits of this open-source package: it is widely used, there is responsive support (in the forum), it is open source, and much more. This package allows the developer to write an oAuth-client, an oAuth resource server, or an oAuth authorization server.
Let us discuss SAML. If you want to implement your own SAML SP (Service Provider), I recommend using Spring-Security-SAML, for the same reasons I recommended Spring-security-oAuth, above.
Now, consider an application that authenticates its users with oAuth, meaning the application is an "oAuth resource server", and its clients implement the oAuth protocol, meaning they are "oAuth clients". I was asked to enable this application to connect SAML IdPs (identity providers) and authenticate users in front of them. This means the application must support not only oAuth, but SAML as well. Note, however, that if the application supports SAML, changes would have to be made in all clients, not only in the application itself. Currently the clients are "oAuth clients", (i.e., they fulfill the oAuth protocol). If the application supports SAML as well, the clients will also have to support it on their side. In SAML, the redirects are implemented differently, and the requests are different. So, the question is, how can we make this application support SAML without changing all clients?
The solution is to create an application ("the bridge") that will be a bridge between oAuth and SAML. When a non-authorized client tries to access the protected resource, it is redirected to the authorization server (this is how oAuth works). But here is the trick: from the client’s point of view– and from the application itself – this bridge functions as a valid "oAuth authorization server". Therefore, there is no need to change anything, not in the client code and not in the application code. On the other hand, instead of opening a popup dialog with username and password, this server functions as an SP and redirects the user to authenticate in front of a pre-configured IdP.
Get to Business
How oAuth works
Let us start with a brief description of how oAuth works. OAuth supports several flows (called "Authorization Grants"); in this document I will discuss the grant called "authorization code". The other grants can be implemented similarly.
From the spec:
In the "authorization code" flow, the authorization code is obtained by using an authorization server as an intermediary between the client and resource owner. Instead of requesting authorization directly from the resource owner, the client directs the resource owner to an authorization server (via its user-agent = the browser), which in turn directs the resource owner back to the client with the authorization code.
Before directing the resource owner back to the client with the authorization code, the authorization server authenticates the resource owner and obtains authorization. Because the resource owner only authenticates with the authorization server, the resource owner's credentials are never shared with the client.
The authorization code provides a few important security benefits such as the ability to authenticate the client, and the transmission of the access token directly to the client without passing it through the resource owner's user-agent, potentially exposing it to others, including the resource owner.
How SAML works
When the user wants to get to a protected resource (=service provider, or SP) using a web browser, he navigates to that webpage. The SP will not ask for a username and password to log in, but instead redirect the browser to the IDP for authentication.
Embedded within this redirect message (as the SAMLRequest
parameter) is a SAML authentication request message. As SAML is XML-based the complete authentication request message is compressed (to save space in the URL) and encoded (because many characters are not allowed in URLs).
When the IDP receives this message and decides to grant the SP’s request, it will authenticate the user by asking him to enter his credentials (unless he already did – for example when having logged in at another service earlier – in which case single sign-on is triggered by simply skipping authentication). After successful authentication, the user’s browser is sent back to the SP at the so called AssertionConsumerService URL. As before, a SAML protocol message is piggybacking along – this time carrying a SAML authentication response message.
Spring configurations
As mentioned earlier, amongst Spring’s benefits are its flexibility and configurability. One can develop oAuth or SAML projects with minimal POJO classes; most of the work is done via the beans configuration. It is up to the developer to decide whether the basic implementation of Spring suits him, or whether he should configure the application differently, or even implement his own classes.
The Spring configurations (XML beans file) for the oAuth authorization server looks something like this (I’ve pasted only the relevant beans):
<security:http pattern="/oauth/token"
authentication-manager-ref="clientAuthenticationManager">
<security:intercept-url pattern="/oauth/token"
access="ROLE_CLIENT" requires-channel="https"/>
<security:anonymous enabled="false" />
<security:http-basic />
<security:custom-filter
ref="clientCredentialsTokenEndpointFilter"
before="BASIC_AUTH_FILTER" />
</security:http>
<security:http auto-config="true"
authentication-manager-ref="usersAuthManager">
<security:intercept-url pattern="/oauth/**" access="ROLE_USER" />
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
<security:anonymous enabled="false"/>
</security:http>
<security:authentication-manager alias="usersAuthManager">
<security:authentication-provider user-service-ref="userDetailsService"/>
</security:authentication-manager>
<security:user-service id="userDetailsService">
<security:user name="demo1@company.com"
password="pass" authorities="ROLE_USER" />
<security:user name="demo2@company.com"
password="demo" authorities="ROLE_USER" />
</security:user-service>
<bean id="clientCredentialsTokenEndpointFilter"
class="org.springframework.security.oauth2.provider.
client.ClientCredentialsTokenEndpointFilter">
<property name="authenticationManager" ref="clientAuthenticationManager" />
</bean>
<oauth:authorization-server
client-details-service-ref="clientDetails"
token-services-ref="watchdoxAuthorizationServerTokenServices"
user-approval-handler-ref="automaticUserApprovalHandler">
<oauth:authorization-code />
<oauth:implicit />
<oauth:refresh-token />
<oauth:client-credentials />
<oauth:password />
</oauth:authorization-server>
<security:authentication-manager id="clientAuthenticationManager">
<security:authentication-provider user-service-ref="clientDetailsUserService" />
</security:authentication-manager>
<bean id="clientDetailsUserService"
class="org.springframework.security.oauth2...ClientDetailsUserDetailsService">
<constructor-arg ref="clientDetails" />
</bean>
<bean id="clientDetails"
class="org.springframework.security.oauth2...JdbcClientDetailsService">
<constructor-arg ref="dataSource" />
</bean>
<bean id="dataSource" ...>
… DataSource configurations here
</bean>
The Spring configurations for SAML SP can be found in the Spring-Security-SAML-sample project, so I will not include it here.
Now, we have to understand how to integrate these two projects.
Integrating Spring’s oAuth and SAML
As mentioned earlier, our application is an oAuth authorization server. We would like to configure this application so it will also be a SAML SP. By doing this, every time a user is redirected to our application in order to authenticate, we will redirect him again to the SAML IdP. Technically, redirecting the user to the IdP means creating a SAML request. So, our application must have the capability to process SAML responses that come from that IdP.
Importing the saml-securityContext.xml
From the beans.xml file of our application, we have to add all the SAML beans that are declared in the saml-securityContext.xml:
<import resource="classpath:saml-securityContext.xml"/>
SAML Entry point
The first thing to note in SAML properties is that in the main http block there is a declaration of the entry-point:
entry-point-ref="samlEntryPoint"
Generally, it means that whenever a relevant http call tries to access this app, this is the entry-point that handles the request. In the SAML case, samlEntryPoint
(class SAMLEntryPoint
) checks if the IdP is known; if not, it starts a discovery process, otherwise it initializes the SSO process – this generates the SAML request to the IdP.
Obviously, we need this functionality. So we will declare our web application as a "protected resource", so that every time the user tries to authenticate, the SAML process will occur. We achieve this by changing the http block that handles the /oAuth/authorize call, and setting its endpoint filter to point to "samlEntryPoint
".
<security:http entry-point-ref="samlEntryPoint">
User’s Authentication Manager
As you can see in the code above, the same http block that we discussed also points to an Authentication Manager:
...authentication-manager-ref="usersAuthManager">
This Authentication Manager is the one that is responsible for the user’s authentication. Within it, there is a declaration of the provider, which holds the users and passwords. Of course, in real world implementations, this provider directs to a JDBC, where all users are stored with their encrypted passwords. The good news here is, we do not have to handle any users and passwords, since we delegate this to the IdP. So, we change the declaration of our server to point to the authentication manager of the SAML SP: this manager has a provider (SAMLAuthenticationProvider
or some other class that extends it). This class implements the authenticate()
method, that attempts to perform authentication of an Authentication
object.
MetadataGeneratorFilter
This is a filter that automatically generates default SP metadata. To include it in our filter chain, it has to be declared as the first element in the chain of the http block we saw above:
<security:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
SAML Filter Chain
Working with Spring-SAML requires several filters to be added to the chain, such as logout filter, metadata display filter and a few more. Spring-SAML creates a dedicated chain for this, that should be run right after the BASIC_AUTH_FILTER
. So, in the http block (yes, the same one…) we add the following:
<security:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>
To Sum Up
We end up with these changes only; all other beans remain unchanged:
<import resource="classpath:saml-securityContext.xml"/>
<security:http authentication-manager-ref="authenticationManager"
entry-point-ref="samlEntryPoint">
<security:intercept-url pattern="/oauth/**" access="ROLE_USER" />
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
<security:anonymous enabled="false"/>
<security:custom-filter before="FIRST" ref="metadataGeneratorFilter"/>
<security:custom-filter after="BASIC_AUTH_FILTER" ref="samlFilter"/>
</security:http>
The Token
Some of you may be asking – hey, what about the token?
Ah!
Until now, everything works great; Spring SAML makes sure we
have a valid Authentication object in the SecurityContextHolder
(actually, it
is of type ExpiringUsernameAuthenticationToken
); thanks to this, we
are able to get the authorization code and then the access token. The user is
happy, and we are all happy. But what happens when we want to get some
information from the SAML token (e.g. the user’s name or email) and create an
oAuth token that contains them? In this case (not a rare one, actually) we need
to become a bit more familiar with how SAMLAuthenticationProvider
works.
SAMLAuthenticationProvider
is autowired to the
authentication manager thanks to the SAML-securityContext.xml (as discussed above). The authentication
manager calls the SAMLAuthenticationProvider.authenticate()
method and passes
it the Authentication object, which is of type SAMLAuthenticationToken
.
This method validates the token, parses it and makes all necessary checks to
make sure it corresponds to the SAMLRequest
. Then – and this is the interesting
part– it tries to load the user’s details from the SAML credentials (parsed
earlier from the response). How does this happen? It checks if there is a SAMLUserDetailsService
object available; if there is none, it returns null (as it did in our case till
now) and this is fine, but it does not use the details from the SAML token. If,
however, there is a SAMLUserDetailsService
object, it calls its
loadUserBySaml()
method.
Now, we have to ensure that there is a SAMLUserDetailsService
object attached. We do this by implementing one by ourselves, and annotating it
as "Component" so it will be autowired to the SAMLAuthenticationProvider
.
@Component
public class SAMLUserDetailsServiceImpl implements SAMLUserDetailsService
{
@Override
public Object loadUserBySAML(SAMLCredential credential)
throws UsernameNotFoundException
{
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
String email = credential.getNameID().getValue();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
authorities.add(authority);
UserDetails userDetails = new User(
email, "password", true, true, true, true, authorities);
return userDetails;
}
}
The provider then takes the UserDetails
object and sets it
to the Authentication object that it creates. Now we have access to these
details so we can call the SecurityContextHolder.getAuthentication()
and get
them. When we do this? Spring’s TokenEndpoint.getAccessToken()
calls implicitly
to the tokenServices bean, to create the
access token. Spring’s default token-services is DefaultTokenServices
, which
does not use the ‘name’ field of the given Authentication object. (Actually, it
uses a random UUID). To overcome this, we create our own token-service that
will do this work. Note that its bean name has to be attached properly in the
XML file:
<!-- OAuth2 Configuration -->
<oauth:authorization-server
token-services-ref="myAuthorizationServerTokenServices"
...
Below is an example for such token service:
@Component("myAuthorizationServerTokenServices")
public class OAuth2TokenServices implements AuthorizationServerTokenServices, InitializingBean
{
@Override
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException
{
String email = authentication.getName();
String generatedToken = generate the Token with the 'email'
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken( generatedToken );
result.setExpiration( expiration );
result.setTokenType("Bearer");
...
return result;
}
}
Summary
We have seen how to integrate two different Spring projects, each handling a different authentication mechanism, and by integrating them have achieved a bridge that, from the clients’ side, remains an oAuth authentication server, but allows the application to connect and authenticate in front of SAML IdPs.
Many thanks to David Goldhar for his help with this document!
References
- OAuth spec
- SAML diagram and flow
- oAuth diagram