Matt RaibleMatt Raible is a Web Developer and Java Champion. Connect with him on LinkedIn.

The Angular Mini-Book The Angular Mini-Book is a guide to getting started with Angular. You'll learn how to develop a bare-bones application, test it, and deploy it. Then you'll move on to adding Bootstrap, Angular Material, continuous integration, and authentication.

Spring Boot is a popular framework for building REST APIs. You'll learn how to integrate Angular with Spring Boot and use security best practices like HTTPS and a content security policy.

For book updates, follow @angular_book on Twitter.

The JHipster Mini-Book The JHipster Mini-Book is a guide to getting started with hip technologies today: Angular, Bootstrap, and Spring Boot. All of these frameworks are wrapped up in an easy-to-use project called JHipster.

This book shows you how to build an app with JHipster, and guides you through the plethora of tools, techniques and options you can use. Furthermore, it explains the UI and API building blocks so you understand the underpinnings of your great application.

For book updates, follow @jhipster-book on Twitter.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

Integrating Node.js, Ruby and Spring with Okta's SAML Support

Okta Security has always piqued my interest, ever since I first developed AppFuse and figured out how to make J2EE security work back in 2004. I hacked AppFuse to have Remember Me functionality, then moved onto Acegi/Spring Security. Spring Security had the features I needed, even if it did require almost 100 lines of XML to configure it. These days, it's much better and its JavaConfig - combined with Spring Boot - is pretty slick.

That was the first part of my security life. The second phase began the night I met Trish, and learned she sold security products. She knew of OWASP and their top 10 rules. It was Trish that inspired me to write my Java Web Application Security presentation. I really enjoyed writing that presentation, comparing Apache Shiro, Spring Security and Java EE's security frameworks. I followed up the first time I presented it with a number of blog posts and screencasts. Hmmmm, maybe I should update the presentation/screencasts to use Java configuration only (#NoXML) and submit it to a couple conferences this year? I digress.

I had to do a security-related spike over the last couple weeks. I was trying to get SAML authentication working with Okta and my client's Active Directory server. Luckily, someone setup the AD integration so all I had to do was try a few different languages/frameworks. I searched and found ThoughtWorks' okta-samples, which includes examples using Node.js and Sinatra (Ruby + JRuby). I also found a Spring SAML example that includes one of my favorite things in JavaLand: Java-based configuration.

I'm happy to report I was able to get all of these applications working with my client's Okta setup. This article will tell you how I did it. For each application, I created a new application on Okta using its "Template SAML 2.0 Application" and added myself in the application's "People" tab. Each section below contains the configuration I used for Okta. The instructions below assume you're similar to me, a developer that has Java 8, Node and Ruby installed, but none of the specific frameworks. As I write this, I have everything working on my Mac with Yosemite, but I wrote the instructions below using one of my old laptops, fresh after a Yosemite upgrade.

The first thing I did was checkout ThoughtWorks samples.

git clone https://github.com/ThoughtWorksInc/okta-samples.git

Node.js

I started by getting the Node.js sample working. For Okta's configuration, I used:

Setting Value
Application label Okta Node.js Example
Force Authentication false
Post Back URL http://localhost:3000/login/callback
Name ID Format EmailAddress
Recipient http://localhost:3000/
Audience Restriction http://localhost:3000/
authnContextClassRef PasswordProtectedTransport
Response Signed
Assertion Signed
Request Compressed
Destination http://localhost:3000/login/callback
Attribute Statements email|${user.email},firstName|${user.firstName}

The Node.js sample uses express, as well as passport and passport-saml. The passport packages are used to handle the SAML authentication and connect is used to compress the requests from your local server.

The only thing I needed to do to make the Node.js app work was to paste the X509 cert string and target URL into its config.json from the Okta app. In Okta's Admin interface, I clicked on the "Sign On" tab and clicked its "View Setup Instructions" button. I copied the "Redirect Login URL" value and copied it into config.json's entryPoint value. I then downloaded the certificate and opened it in vi. I ran the following two commands to remove ^M and line endings (more details here).

:%s/<Ctrl-V><Ctrl-M>//g
:%s/\n//g

Next, I copied everything between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- and pasted it into the cert value of config.json. I had to remove the comments from config.json for everything to work.

After making these changes, I was able to run "npm install" and "npm start" and successfully login at http://localhost:3000.

Ruby

The Ruby sample uses Sinatra, omniauth and omniauth-saml. To run the okta-ruby-sinatra application, I had to start by installing Bundler.

sudo gem install bundler

Then I installed all the required gems for this project using the following command.

bundle install

This resulted in the following error:

An error occurred while installing nokogiri (1.6.1), and Bundler cannot continue.
Make sure that `gem install nokogiri -v '1.6.1'` succeeds before bundling

I tried Bundler's suggestion, but it failed:

Gem::Installer::ExtensionBuildError: ERROR: Failed to build gem native extension.

    /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby extconf.rb
mkmf.rb can't find header files for ruby at /System/Library/Frameworks/Ruby.
framework/Versions/2.0/usr/lib/ruby/include/ruby.h

I then tried upgrading to Xcode 6.1.1. I received the same error and running "bundle update sinatra" and "sudo gem update --system" didn't help anything. I found an old Stack Overflow answer that suggested running "xcode-select --install" to install Xcode's Command Line Developer Tools. After doing so, I ran "sudo gcc" to accept to all Apple's licensing agreements. I ran "bundle install" again and this time it failed with the following error:

-----
libxml2 is missing.  please visit http://nokogiri.org/tutorials/installing_nokogiri.html for help with installing dependencies.
-----
...
An error occurred while installing nokogiri (1.6.1), and Bundler cannot continue.
Make sure that `gem install nokogiri -v '1.6.1'` succeeds before bundling.

I tried Bundler's suggested again: "sudo gem install nokogiri -v '1.6.1'". This didn't work, so I tried "bundle update" and it finally worked. I ran "bundle install" for the final time, followed by "ruby app.rb". WEBrick started and I created a "Okta Ruby Example" application on Okta with the following settings.

Setting Value
Application label Okta Ruby Example
Force Authentication false
Post Back URL http://localhost:4567/auth/saml/callback
Name ID Format EmailAddress
Recipient http://localhost:4567
Audience Restriction http://localhost:4567
authnContextClassRef PasswordProtectedTransport
Response Signed
Assertion Signed
Request Compressed
Destination http://localhost:4567/auth/saml/callback
Attribute Statements email|${user.email},firstName|${user.firstName}

To configure Sinatra with Otka's settings, I started by renaming the config.yml.sample file:

mv config.yml.sample config.yml
In Otka's Admin UI for the application, I clicked on the "Sign On" tab and clicked its "View Setup Instructions" button. I copied the "Redirect Login URL" value and copied it into config.yml's target_url value. I then downloaded the certificate and ran the the following command in the directory I downloaded it to.

openssl x509 -noout -fingerprint -in "okta.cert"

I copied the fingerprint into config.yml's fingerprint value and restarted the app. I opened http://localhost:4567 in my browser and was able to successfully login.

JRuby

To start with JRuby, I first read the project's README. It mentioned issues with "nokogiri" and explains the project contains a patched release of nokogiri 1.6.0. Since I knew there was a later release, I modified Gemfile and removed the version and path information from the last line. I copied the config.yml from the Ruby project and ran the following commands to install Bundler, the project's dependencies and start the app.

jruby -S gem install bundler
jruby -S bundle install

Running the second command resulted in the following error:

Your jruby version is 1.7.18, but your Gemfile specified jruby 1.7.4

I modified Gemfile to specify "1.7.18" and tried again. This time it worked. I started the application using the following command:

jruby app.rb
NOTE: If you see the the following in your browser window, it means you forgot to copy config.yml from the Ruby project.
undefined method `auth' for Sinatra::Application:Class

When I tried to login at http://localhost:4567, I saw an infinite redirect and the following error in my console.

W, [2015-01-08T08:53:22.514000 #56144]  WARN -- : attack prevented by Rack::Protection::SessionHijacking
0:0:0:0:0:0:0:1 - - [08/Jan/2015 08:53:22] "GET / HTTP/1.1" 302 - 0.0190
0:0:0:0:0:0:0:1 - - [08/Jan/2015:08:53:22 MST] "GET / HTTP/1.1" 302 0

Stack Overflow indicated this is a problem caused by an old version of rack-protection. Running "jruby -S bundle update rack-protection" updated the project to use rack-protection 1.5.3 (was 1.5.1). After restarting and trying again, I received the following error:

I, [2015-01-08T08:59:32.679000 #56176]  INFO -- omniauth: (saml) Callback phase initiated.
E, [2015-01-08T08:59:34.747000 #56176] ERROR -- omniauth: (saml) Authentication failure! invalid_ticket: Onelogin::Saml::ValidationError, Digest mismatch
0:0:0:0:0:0:0:1 - - [08/Jan/2015:08:59:34 -0700] "POST /auth/saml/callback HTTP/1.1" 302 9 2.0760
0:0:0:0:0:0:0:1 - - [08/Jan/2015:08:59:34 -0700] "GET /auth/failure?message=invalid_ticket&strategy=saml HTTP/1.1" 404 449 0.0080
0:0:0:0:0:0:0:1 - - [08/Jan/2015:08:59:34 MST] "GET /auth/failure?message=invalid_ticket&strategy=saml HTTP/1.1" 404 449
- -> /auth/failure?message=invalid_ticket&strategy=saml
0:0:0:0:0:0:0:1 - - [08/Jan/2015:08:59:34 -0700] "GET /__sinatra__/404.png HTTP/1.1" 200 18893 0.0200
0:0:0:0:0:0:0:1 - - [08/Jan/2015:08:59:32 MST] "POST /auth/saml/callback HTTP/1.1" 302 9
- -> /auth/saml/callback
0:0:0:0:0:0:0:1 - - [08/Jan/2015:08:59:34 MST] "GET /__sinatra__/404.png HTTP/1.1" 200 18893
http://localhost:4567/auth/failure?message=invalid_ticket&strategy=saml -> /__sinatra__/404.png

At this point, the only thing different from my working version and my old laptop was the version of Java. My old laptop had "build 1.8.0_05-b13", so I upgraded to the latest version of Java 8 (update 25). This didn't help, so I tried updating all bundles with "jruby -S bundle update". This failed too, so I figured I'd try to use the version of JRuby that was on my working laptop (version 1.7.16.1). I installed Homebrew, ran "brew install jruby", removed the newer version from my path and downgraded the version in Gemfile. I had to re-install Bundler and the projects dependencies with the following commands.

jruby -S gem install bundler
jruby -S bundle install

Same error again. I reverted Gemfile.lock and ran the only bundle update command I'd run on my working laptop:

$ jruby -S bundle update sinatra

Unfortunately, this still didn't fix the issue. I copied the project from my working laptop and tried running that project. It failed, proving that it was an environment issue, not a project/code issue. I tried rebooting and when that didn't work, I gave up. It's pretty strange this didn't work on a fresh Yosemite install - it took me less than 10 minutes to get it working originally.

Spring

The Spring sample I got working with Okta was Vincenzo De Notaris' spring-boot-security-saml-sample. This project uses Spring Boot and Spring Security SAML. I created a "Okta Spring Example" application on Okta with the following settings.

Setting Value
Application label Okta Spring Example
Force Authentication false
Post Back URL http://localhost:8080/saml/SSO
Name ID Format EmailAddress
Recipient http://localhost:8080/saml/SSO
Audience Restriction com:vdenotaris:spring:sp
authnContextClassRef PasswordProtectedTransport
Response Signed
Assertion Signed
Request Uncompressed
Destination http://localhost:8080/saml/SSO
Attribute Statements email|${user.email},firstName|${user.firstName}

The biggest thing I learned while trying to get these values correct was that Request needs to be set to Uncompressed.

After cloning the GitHub project to my hard drive, I added a new SSO provider by adding a new bean to WebSecurityConfig.java. The URL I got from Okta's Admin UI: Sign On > View Setup Instructions > Public Link (near the bottom of the page).

@Bean(name = "idp-okta")
public ExtendedMetadataDelegate ssoOktaExtendedMetadataProvider()
      throws MetadataProviderException {
    @SuppressWarnings({ "deprecation"})
    HTTPMetadataProvider httpMetadataProvider
          = new HTTPMetadataProvider("https://client.okta.com/app/random-key-here/sso/saml/metadata", 5000);
    httpMetadataProvider.setParserPool(parserPool());
    ExtendedMetadataDelegate extendedMetadataDelegate =
          new ExtendedMetadataDelegate(httpMetadataProvider, extendedMetadata());
    extendedMetadataDelegate.setMetadataTrustCheck(false);
    extendedMetadataDelegate.setMetadataRequireSignature(false);
    return extendedMetadataDelegate;
}

For the SSL connection to work, I had to download the certificate and import it into the application's keystore. To do this in Chrome, I went to https://client.okta.com, clicked on the lock icon in the address bar, then dragged/dropped the certificate image to my desktop. This resulted in a *.okta.com.cer file on my desktop. I added it to the keystore using the following commands (thanks Stack Overflow).

keytool -importcert -file ~/Desktop/\*.okta.com.cer -keystore src/main/resources/saml/samlKeystore.jks

When prompted for the password, I entered "nalle123". This value is specified in WebSecurityConfig.java's keyManager bean. I then added this provider to the list of providers in the metadata bean.

@Bean
@Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException {
        List<MetadataProvider> providers = new ArrayList<MetadataProvider>();
    providers.add(ssoOktaExtendedMetadataProvider());
    providers.add(ssoCircleExtendedMetadataProvider());
    return new CachingMetadataManager(providers);
}

After making these changes, I started the application using "mvn spring-boot:run". I navigated to http://localhost:8080, chose Okta as my Idp and logged in successfully!

Summary

This article shows you how I got Node.js, Ruby and Spring applications working with Okta's SAML support. My experience with this when I first tried it: Node was super-easy, Ruby was a bit more difficult, JRuby was a cinch and Spring took several days. As you can tell from this article, Ruby/JRuby were the most difficult to make work on a clean machine.

All in all, working with Okta has been a pleasant experience so far. Hopefully this article helps make it a good experience for you as well.

Posted in Java at Jan 08 2015, 11:43:47 AM MST 6 Comments
Comments:

Hi Matt -
Thanks for the summary on spring-boot and Okta integration. I'm getting stuck trying to connect to Okta through the use of the above documented extended metadata delegate.

I keep getting this error:

Caused by: javax.net.ssl.SSLPeerUnverifiedException: SSL peer failed hostname validation for name: null
	at org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory.verifyHostname(TLSProtocolSocketFactory.java:233) ~[openws-1.5.1.jar:na]
	at org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory.createSocket(TLSProtocolSocketFactory.java:186) ~[openws-1.5.1.jar:na]
	at org.springframework.security.saml.trust.httpclient.TLSProtocolSocketFactory.createSocket(TLSProtocolSocketFactory.java:97) ~[spring-security-saml2-core-1.0.1.RELEASE.jar:1.0.1.RELEASE]
	at org.apache.commons.httpclient.HttpConnection.open(HttpConnection.java:707) ~[commons-httpclient-3.1.jar:na]
	at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$HttpConnectionAdapter.open(MultiThreadedHttpConnectionManager.java:1361) ~[commons-httpclient-3.1.jar:na]
	at org.apache.commons.httpclient.HttpMethodDirector.executeWithRetry(HttpMethodDirector.java:387) ~[commons-httpclient-3.1.jar:na]
	at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:171) ~[commons-httpclient-3.1.jar:na]
	at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397) ~[commons-httpclient-3.1.jar:na]
	at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323) ~[commons-httpclient-3.1.jar:na]
	at org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.fetchMetadata(HTTPMetadataProvider.java:250) ~[opensaml-2.6.1.jar:na]

I found your gist posted below which essentially uses a pre-downloaded metadata.xml document on the classpath. I got that to work, but I thought id reach out since it seems like you ran into the same issue and appear to have gotten around it somehow.

https://gist.github.com/mraible/c8b52972f76e6f5e30d5

Posted by Mark on February 09, 2016 at 03:02 PM MST #

Hey Mark - this looks like an SSL certificate trust issue. If you look through my instructions, you'll notice an openssl command to get a fingerprint and a keytool command to add the Okta certificate to my keystore. You might try ensuring you completed these steps.

Posted by Matt Raible on February 09, 2016 at 04:52 PM MST #

Thanks Matt -- I got it to work. Turns out I was trusting the wrong cert from Okta. I ended up using these instructions on Spring's site to generate the SSL cert from okta's website. "Loading SSL/TLS certificates"

http://docs.spring.io/autorepo/docs/spring-security-saml/1.0.x-SNAPSHOT/reference/htmlsingle/#configuration-key-management-ssl-keys

The other issue I had was that I using Okta's developer guide instructions by putting the metadata url into the "entity Id" field in the Okta Admin App setup page. Your code example helped me again by showing how you would just enter the string value of the unique entityId that is configured in our spring app.

Nice job, very helpful!

Posted by Mark on February 10, 2016 at 07:28 AM MST #

Hello Matt,
Thanks for the details Okta integration. I get stuck trying to connect to Okta

the error log:

ERROR [http-nio-8080-exec-2] (DirectJDKLog.java:182) - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception java.lang.RuntimeException: Key for
alias apollo not found at org.springframework.security.saml.metadata.MetadataGenerator.getServerKeyInfo(MetadataGenerator.java:207) at
org.springframework.security.saml.metadata.MetadataGenerator.buildSPSSODescriptor(MetadataGenerator.java:329) at
org.springframework.security.saml.metadata.MetadataGenerator.generateMetadata(MetadataGenerator.java:189) at
org.springframework.security.saml.metadata.MetadataGeneratorFilter.processMetadataInitialization(MetadataGeneratorFilter.java:127) at
org.springframework.security.saml.metadata.MetadataGeneratorFilter.doFilter(MetadataGeneratorFilter.java:86) at
org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) at
org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213) at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) at
org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) at
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) at
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at
org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) at
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) at
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at
org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87) at
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) at
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at
org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) at
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) at
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at
org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121) at
org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) at
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212) at
org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) at
org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) at
org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:521) at
org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1096) at
org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:674) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1500)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1456) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at
java.lang.Thread.run(Thread.java:745)

Posted by Dhanayan on March 08, 2016 at 09:29 AM MST #

Dhanayan: I'd recommend asking this question on Stack Overflow where the Okta folks might see it.

Posted by Matt Raible on March 08, 2016 at 10:18 AM MST #

For Spring Boot integration with SAML I recently released this plugin https://github.com/ulisesbocchio/spring-boot-security-saml that allows to configure Single Sign On through a DSL or configuration properties.

Posted by Uli on May 22, 2016 at 08:30 PM MDT #

Post a Comment:
  • HTML Syntax: Allowed