Integrating Node.js, Ruby and Spring with Okta's SAML Support
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.ymlIn 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
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.
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:
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 09:02 PM MST #
openssl
command to get a fingerprint and akeytool
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 10: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 01:28 PM MST #
Hello Matt,
Thanks for the details Okta integration. I get stuck trying to connect to Okta
the error log:
Posted by Dhanayan on March 08, 2016 at 03:29 PM MST #
Posted by Matt Raible on March 08, 2016 at 04:18 PM MST #
Posted by Uli on May 23, 2016 at 02:30 AM MDT #