Overview
Recently I have had to do a small SAML 2.0 integration project for one of our clients to enable single sign on. Here are some thoughts on the issue.
Saml 2.0 is pretty easy
Much can be said about SOAP and insane and inane web service API’s, but SAML 2.0 is pretty straight forward. The basic flow can be summed;
System A - Identity holder
This system is the authority of authentication for our users.
System B - Third party
This system is the requester of access. This system asks, “Is this user authenticated?”
How do the systems talk?
- User clicks on a link to System B from System A. (i.e. we want to launch System B, we are already logged into System A)
- System B creates a request and sends it to System A. (this is done through the User via redirection of their browser to System A with a token from System B)
- System A sends a POST to System B which contains a ‘success’ or ‘failure’ message. System B then logs the user in and redirects to the appropriate and originally requested resource.
That’s essentially it…
The nitty-gritty
You can go a few ways… Probably your best bet would be to go the OpenSAML route. However, I don’t like to add jars to my java projects and given that we’re using Groovy and Spring, XML parsing / creation + HTTP handlers are pretty easy.
I should mention that I have done merely the System B approach to SAML 2.0, so we will not be creating an assertion. Though, I’ve used one to test my ‘verifyAssertion’ handler.
A Spring based SAML Controller (part 1)
package demo.saml2Notes:
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
class SAMLController {
@Autowired SAMLService samlService
static def RedirectUrl = “https://www.wookets.com/sso/saml2/handleresponse"
@RequestMapping(value = “/sso/saml2/createrequest”)
def createSamlRequest(HttpServletResponse response, @RequestParam String redirectUrl) {
log.debug “Creating a new AuthnRequest”
// write a request out
String authn = ‘<?xml version=“1.0” encoding=“UTF-8”?&rt;’ // adding this even though it might not be needed
authn += samlService.createAuthnRequest()
// url encode
Deflater deflater = new Deflater(Deflater.DEFLATED, true);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater);
deflaterOutputStream.write(authn.getBytes());
deflaterOutputStream.close();
String samlResponse = byteArrayOutputStream.toByteArray().encodeBase64().toString()
def encodedAuthn = URLEncoder.encode(samlResponse, “UTF-8”)
// send a redirect
response.sendRedirect(RedirectUrl + “?SAMLRequest=” + encodedAuthn)
log.debug “${RedirectUrl}?SAMLRequest=${encodedAuthn}”
}
}
- We are using a basic Spring @Controller to accept an HTTPRequest and set the response
- redirectUrl is not part of SAML 2.0, but is a helper method for your partner. Instead of you hardcoding their URL, they simply pass the URL to you.
- The response needs to be; 1) zipped, 2) base64 encoded, 3) url encoded. And in that order.
- Notice that we are using a GET redirect variable. This is because redirect wipes the body out when we send it to the client (the client actually wipes it, but we can only pass variables through the URL)
A Spring based SAML Service (part 1)
package demo.saml2
import groovy.xml.MarkupBuilder;
import org.springframework.stereotype.Service;
@Service
class SAMLService {
def createAuthnRequest = {
log.debug “Creating an AuthnRequest”
// define variables to create saml AuthnRequst
def requestId = “ThisShouldBeRandomlySecurelyGenerated”
def timestamp = Calendar.instance.getTime().format(“yyyy-MM-dd’T’HH:mm:ss’Z’“) //String.format(‘%tFT%tTZ’, cal)
def providerName = “https://www.partnerurl.com" // this is just whatever your partner has it set to
def verifyAssertionUrl = “https://www.wookets/sso/saml/verifyassertion" // this is the callback url
// save requestId to db for later use…
// create saml token
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.doubleQuotes = true
xml.“samlp:AuthnRequest”(“xmlns:samlp”: “urn:oasis:names:tc:SAML:2.0:protocol”,
AssertionConsumerServiceIndex: “0”, AssertionConsumerServiceURL: verifyAssertionUrl,
ForceAuthn: “true”, ID: requestId, IsPassive: “false”, IssueInstant: timestamp,
ProtocolBinding: “urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect”,
ProviderName: providerName, VERSION: “2.0”) {
“saml:Issuer”(“xmlns:saml”:“urn:oasis:names:tc:SAML:2.0:assertion”, providerName)
“samlp:NameIDPolicy”(AllowCreate: “true”)
}
log.debug “returning: ${writer.toString()}”
return writer.toString()
}
}
Notes:
- We could have just put this all in the Controller, but I created a Service, because of separation of concerns… You may not have concerns which need separating…
- This groovy closure generates a unique secured id and uses that and a timestamp to create the AuthnRequest. Note, I’m not using OpenSAML or any jars here. Just groovy putting together the XML according to the spec and what the partner is expecting. Your mileage may vary.
Resources
https://wiki.shibboleth.net/confluence/display/OpenSAML/Home
http://en.wikipedia.org/wiki/SAML_2.0