How-tos

Spring, Liberty and Single Sign On

Share this post:

Have you ever wondered how you could protect your Spring app with the Bluemix SSO service?

In this article, we’ll cover how you can convert a Spring application running on Liberty from using a manually configured Open ID Connect (OIDC) Server, to using the Bluemix Single Sign On service.

A short while ago I wrote a short article on how to propagate identity using JSON Web Tokens (JWT’s) within a microservice architecture. That article used a Liberty server configured to act as the OAuth provider acting as the source of identity. That kept the article code free of external dependencies (such as configuring popular social media services to perform the same role), but also meant the article didn’t really get a chance to talk about other sources of identity, and how easy it can be to use a service such as the Bluemix Single Sign On (SSO) service, when hosting as a Cloud Foundry (CF) App in Bluemix.

The Sample Application

This is covered in more depth over in the original article, but in case you don’t fancy reading through
the entire thing and coming back, here’s a quick overview of the sample code we’re talking about.

JWT Original Application Architecture Diagram with Liberty OP

Here we have 2 Relying Parties (“RP”s) that each talk to two Relying Services (“RS”s), passing on the identity they obtained from an OpenID Provider (“OP”).

In this post, we’ll cover how to convert the sample to stop using the supplied OP (Running on Liberty), to using the Bluemix SSO service instead. When we’re done, we’ll end up with an application that looks a little like this..

JWT Updated Application Architecture Diagram with Bluemix SSO as OP

Bluemix Single Sign On

The Single Sign On service documentation describes how the Bluemix buildpack for Liberty will detect that the CF application has a binding to an SSO service instance, automatically configuring the Liberty server to talk to the service. It also explains that it will use the Liberty OIDC client support to make that connection.

This is great news for us, because our two RP servers already use the OIDC client to talk to the OP we supply. This means our app is already built knowing how to read the identity that results from the OIDC client/server exchange, and all we have to do is remove our OP configuration from our RPs, and allow our RPs to be configured by Bluemix to talk to the SSO service instead.

The original sample code is available in the sample.microservices.security.jwt project in GitHub. If you want to skip straight to a version with all the changes described here, there’s a  branch in GitHub prepared for you. If you use that branch, then skip ahead to the Running in Bluemix section.

Removing the Old OP Configuration

First, we find the sections in our RPs `server.xml` files that configure for the OP. Each RP has it’s own server.xml, you can view the full files in GitHub at JEE Server.xml, and Spring Server.xml.

In each server, locate the following configuration elements:


<!-- oidc trust store -->
<keyStore id="oidctruststore"
password="trust-pass"
location="${server.config.dir}/truststore.jks" />

<authFilter id="jwtapp">
<requestUrl id="appurl" urlPattern="/Test" matchType="contains" />
</authFilter>

<openidConnectClient id="RP"
clientId="jee-rp"
clientSecret="fish"
signatureAlgorithm="RS256"
trustAliasName="jwtsampleapp"
trustStoreRef="oidctruststore"
scope="openid jee-api spring-api jee"
authorizationEndpointUrl="${env.AUTH_ENDPOINT_URL}"
tokenEndpointUrl="${env.TOKEN_ENDPOINT_URL}"
issuerIdentifier="https://thesampleop/" />

Remove the keyStore with the id oidctruststore, (previously used to confirm signatures from the OP), and remove any authFilter blocks (no longer required), and remove the openidConnectClient block (will be replaced by the one added by the buildpack).

We need to remove these parts, because otherwise the ones the buildpack will add will conflict.

With those gone, we can also remove the declaration for the JwtConsumer, it was being used to process the JWT access token from the OP, and the Bluemix SSO service does not use a JWT format access token.

Locate and remove the jwtConsumer element.


<jwtConsumer id="oidcConsumer"
signatureAlgorithm="RS256"
issuer="https://thesampleop/"
trustStoreRef="defaultTrustStore"
trustedAlias="jwtsampleapp" />

In theory, that’s all it takes to convert the sample application from using a Liberty OP to using Bluemix SSO. If you were to rebuild & deploy that RP to Bluemix, and linked an instance of an SSO service to the app, and restaged it, then you’d find the RP is now up and using Bluemix SSO for its authentication.

Sadly it’s not quite that simple, and our job is not yet complete because of some choices the original sample made. Lets work through updating the sample so it can work with Bluemix SSO…

Removing the JWT access tokens

The original sample configured Liberty to act as the OP, and told it to use JWT formatted access tokens, this allowed the RP’s to trivially process the access token, and retrieve additional information, such as the scopes granted by the OP.

Bluemix SSO doesn’t use JWT formatted access tokens, and we really don’t care so much for scopes anyways, so we have a little work to do to remove the code that used to handle the access token processing.

Note:Remember, if you have no interest in following the changes made to the original sample, and just want to use the updated code, it’s all in a GitHub branch for you.

Updating the JEE RP

The JEE RP demonstrates two approaches, with a Servlet Filter, and without. This gives us 2 places to edit; the Filter itself, and the Servlet that doesn’t use the Filter.

Updating the filterless Approach

Starting with the Servlet TestServletWithoutUsingFilter remove this block of code…


// lets extract the scopes from the access token, we can send them along to the rs.
ArrayList<String> scopes=new ArrayList<>();
out.println("<HR>Access Token Introspection<p>");
out.println("AccessToken Type : "+PropagationHelper.getAccessTokenType()+"<br>");
// process the access_token as a jwt to obtain scopes.
try {
JwtConsumer jwtConsumer = JwtConsumer.create("oidcConsumer");
JwtToken access_Token = jwtConsumer.createJwt(PropagationHelper.getAccessToken());
out.println("AccessToken: "+"<br>");
for(Entry<String, Object> e : access_Token.getClaims().entrySet()){
out.println(" - "+e.getKey()+" :: "+e.getValue()+"<br>");
}
scopes = access_Token.getClaims().getClaim("scope", ArrayList.class);
} catch (InvalidConsumerException | InvalidTokenException e1) {
e1.printStackTrace();
e1.printStackTrace(out);
}

And then later, remove the line that added the scopes to the JWT.


jwtBuilder.claim("scopes", scopes);

Let’s add something to the JWT that we get from the id token instead. The Bluemix SSO documentation lists quite a few claims we could use. For this walkthrough lets stick to emailAddress.

To add the email to the JWT, we just use this line where previously the scopes were being added:


jwtBuilder.claim("email", id_token.getClaim("emailAddress"));

We’re adding it to the JWT with a new claim name of email to demonstrate the names of the claims in the JWT don’t have to match those from the ID Token.

Updating the Filter Approach.

Now lets deal with the JEE RP filter implementation, where the JWT creation is handled by the class JwtJaxRSClientFilter
which lives at liberty-jee-rp/src/main/java/net/wasdev/securemicroservices/JwtJaxRSClientFilter.java

We want to alter this filter to stop extracting the scopes from the access token.

Locate and remove this section of code:


// lets extract the scopes from the access token, we can send them along to the rs.
ArrayList<String> scopes=new ArrayList<>();
// process the access_token as a jwt to obtain scopes.
try {
JwtConsumer jwtConsumer = JwtConsumer.create("oidcConsumer");
JwtToken access_Token = jwtConsumer.createJwt(PropagationHelper.getAccessToken());
scopes = access_Token.getClaims().getClaim("scope", ArrayList.class);
} catch (InvalidConsumerException | InvalidTokenException e1) {
e1.printStackTrace();
throw new WebApplicationException(e1);
}

As with the “Filterless” approach, we also remove the line,


jwtBuilder.claim("scopes", scopes);

and replace it with the line…


jwtBuilder.claim("email", id_token.getClaim("emailAddress"));

That’s it for the RP changes.. but we also need to look at the RS…

Updating the JEE RS

Great! We’ve updated the JEE RP to send email in the JWT instead of scopes so we’d better update the RS and tell it to expect that instead!

This change is thankfully quite simple, as the RS really didn’t do anything beyond print a message for the RP to display.

The two lines to be removed that deal with the scopes are in the TestServlet class, look like this:


ArrayList<String> scopes = jwt_Token.getClaims().getClaim("scopes", ArrayList.class);
out.println("JEE RS JWT Had scopes "+scopes.toString());

Lets replace them with lines that will print the email instead…


String email = jwt_Token.getClaims().getClaim("email", String.class);
out.println("JEE RS JWT had email "+email);

Updating the Spring RP

Over in the Spring RP, we’ve got a similar issue to deal with, the JWT construction code there is expecting to be able to read a JWT formatted access token, the code this time is in JWTAuthenticationInterceptor the code is identical to the code from the JEE RP filter, and looks like this:


// lets extract the scopes from the access token, we can send them along to the rs.
ArrayList<String> scopes=new ArrayList<>();
// process the access_token as a jwt to obtain scopes.
try {
JwtConsumer jwtConsumer = JwtConsumer.create("oidcConsumer");
JwtToken access_Token = jwtConsumer.createJwt(PropagationHelper.getAccessToken());
scopes = access_Token.getClaims().getClaim("scope", ArrayList.class);
} catch (InvalidConsumerException | InvalidTokenException e1) {
e1.printStackTrace();
return null;
}

We need to remove this code, and the corresponding line that adds the scopes to the JWT;


jwtBuilder.claim("scopes", scopes);

And once again, we just replace that last line with one that adds the email:


jwtBuilder.claim("email", id_token.getClaim("emailAddress"));

Updating the Spring RS

The Spring RS is a little more complex, as it processes the scopes claim from the JWT in two places.

First, is the JwtUserDetails object, that represents the JWT information as a Spring UserDetails object.

The constructor was taking the scopes and using them to create Authority instances for Spring. It doesn’t have to do that, it was just a nice way to show how you might use scopes. But since we do not have any scope information, we can just remove this code:


//we'll use the scopes claim to init our authorities,
//this is not required, but just shows one way to link the jwt
//into spring.
authorities = new HashSet<>();
String scopes = jwt.getClaims().getClaim("scope", String.class);
if(scopes!=null && !scopes.isEmpty()){
for(String scope : scopes.split(",")){
final boolean add = authorities.add(new SimpleGrantedAuthority(scope));
}
}

Secondly, back over in the GreetingController we need to stop reading the scopes claim to print to the RS reply message, and tell it to use the new email claim instead.

Swap:


ArrayList<String> scopes = userDetails.getJwt().getClaims().getClaim("scopes", ArrayList.class);
return "Spring RS App invoked with valid JWT "+name+
"<br>You authenticated to me as "+userDetails.getUsername()+
" and have scopes of "+scopes.toString();

for…


String email = userDetails.getJwt().getClaims().getClaim("email", String.class);
return "Spring RS App invoked with valid JWT "+name+
"<br>You authenticated to me as "+userDetails.getUsername()+
" and had an email of "+email;

Running in Bluemix

In Bluemix we’re going to need an SSO Service instance, and then we’ve got a few minor changes to make to our pom.xml’s so that we can deploy our RP’s bound to the service instance.

Start by heading over to the Bluemix Single Sign On Catalog Page and provision an instance of Bluemix Single Sign On (SSO).
At the time of writing, the first 60 days are free, but be aware there’s no ‘free tier’ for this service type.

Name your SSO service something simple and memorable, for the rest of this example, we’re going to use the name ssoservice

Add a Cloud Directory identity source, and configure a user with a password, email etc for testing.

Initial deploy and Service Binding.

Now things get a little odd, the maven CF plugin isn’t able to bind a new CF app instance to an existing SSO service instance. It’s able to redeploy a CF app that’s already bound, but that first bind has to be done by something other than the plugin. (And if you totally delete the app, you have to repeat it). A little awkward, but once the initial deploy is done, subsequent deploys work fine with the binding already present.

So before we go change our pom.xml to add the binding, let’s first push the updated apps without the binding up to Bluemix, where we can then add the service binding either via the UI, or the command line.

Note: if you are following along using the already modified code from GitHub, you’ll need to undo the pom.xml changes that bind the service to perform the initial bind (see below for the binding changes in the pom files)

Run the following to build the code, and push it out to Bluemix as CF apps.


mvn generate-resources
mvn -P bluemix -Dcf.username=your@bluemix.userid.com -Dcf.password=yourBluemixPassword -Dcf.org=yourBluemixOrg package

Note: cf.org is used as part of the hostname, if your org name is mixed case, or not suitable for use as a hostname, you’ll want to edit the pom.xml’s to pick an alternative. The ‘already modified’ code in GitHub has a property called host.prefix that can be altered to do just that

Now, bind the SSO service to the RP applications, and restage them. Swap yourCfOrgOrCustomHostPrefix for your cf.org value, or custom host.prefix if you are using one. Or use the Bluemix web console to perform the binding.


cf bs yourCfOrgOrCustomHostPrefix-jwtsample-liberty-jee-rp ssoservice
cf restage yourCfOrgOrCustomHostPrefix-jwtsample-liberty-jee-rp
cf bs yourCfOrgOrCustomHostPrefix-jwtsample-liberty-spring-rp ssoservice
cf restage yourCfOrgOrCustomHostPrefix-jwtsample-liberty-spring-rp

You can confirm the service is bound using cf env appname, you’ll see the `ssoservice` information within the VCAP_SERVICES block.

Updating the pom.xml’s

Once we have the bindings established in Bluemix, we can update our pom.xmls so it will stay bound if we redeploy.

Note: this probably should work for an initial binding too, but the cf maven plugin is kinda old and based on an unmaintained branch of the cf command line tools, which is the direction they’ve chosen to go, but it’s still really handy for a lot of stuff, so we’re still using it here!

In the liberty-jee-rp/pom.xml and liberty-spring-rp/pom.xml files, find the plugin section for the cf-maven-plugin, (it can be found easily by looking for the env element) and just after the env element, add a new services element, like this..


<service>
<name>ssoservice</name>
<label>SingleSignOn</label>
<plan>professional</plan>
</service>

If you named your service something other than ssoservice you’ll need to update that, and the same if your selected plan wasn’t called professional.

Push changes and maintain service binding.

With the pom files updated to maintain the service binding, now when we run…


mvn -P bluemix -Dcf.username=your@bluemix.userid.com -Dcf.password=yourBluemixPassword -Dcf.org=yourBluemixOrg package

The apps will be compiled and pushed, and bound to the ssoservice as requested.

Testing the app.

The JEE RP can be driven and tested at the following URLs.
– https://yourCfOrgOrCustomHostPrefix-jwtsample-liberty-jee-rp.mybluemix.net/signed-jwt-jee-rp-application/Test
– https://yourCfOrgOrCustomHostPrefix-jwtsample-liberty-jee-rp.mybluemix.net/signed-jwt-jee-rp-application/Test2

And the Spring RP can be invoked at the following URL.
– https://yourCfOrgOrCustomHostPrefix-jwtsample-liberty-spring-rp.mybluemix.net/signed-jwt-spring-rp-application/Test

In each case, you’ll be forwarded to the Bluemix SSO login page, where you can sign in with the user you
created back at the start of the `Running In Bluemix` section.

Note: if you are trying to test from the same browser that you have logged into the Bluemix Console from, you may experience odd behavior, test with a different browser to be safe

More How-tos stories
May 6, 2019

Use IBM Cloud Certificate Manager to Obtain Let’s Encrypt TLS Certificates for Your Public Domains

IBM Cloud Certificate Manager now lets you obtain TLS certificates signed by Let’s Encrypt. Let’s Encrypt is an automated, ACME-protocol-based CA that issues free certificates valid for 90 days.

Continue reading

April 9, 2019

Track Your Cloud Activities Using IBM Cloud Activity Tracker with LogDNA

With IBM Cloud Activity Tracker with LogDNA, you can improve the security monitoring of your application by setting alerts for user access patterns and gain greater trackability for how your Cloud Service and Cloud Account is being used, configured, and accessed.

Continue reading

April 5, 2019

IBM Cloud App ID: Updated Runtime APIs Provide Tighter Interoperability for Your Cloud-Native Apps

As part of our efforts to tighten interoperability and broaden the frameworks that are able to use IBM Cloud App ID, we've updated the runtime APIs.

Continue reading