Using JSON Web Tokens with CUWebAuth
The Internet is a bunch of small, loosely joined services. That makes it easy to construct and debug useful applications, and re-use the same code in different situations.
In the Cornell Vet School, like many other organizations, we’re moving to a MicroService architecture. We build REST API’s that communicate with databases, files and external services. Testing the REST API’s separately from the application is straightforward, and we can make the API’s simple and modular. This makes our entire application stack easier to navigate.
But how do you allow the “right” people to use your microservices and keep the unauthorized people out? In Server-Side HTML architectures (Rails, J2EE, ColdFusion, .NET, etc.) you can directly write files or read database on behalf from clients, keeping the details secret. But in Single-Page Application architectures (Angular, React, Ember, etc.) API information is clear-as-day in a browser’s View Source.
For Cornell web pages, the authentication standard is CUWebAuth. CUWebAuth is an Apache or IIS module that allows single-sign-on, and is heavily battle-tested. One could, in theory, lock down API’s with CUWebAuth as well. The problem is CUWebAuth needs to be built from source, and only runs on manually-configured Apache and IIS machines. In the Amazon world this means EC2 … and EC2 is the lowest-level, highest-complexity service you could possibly use. It’s overkill for serving static web pages since Amazon has many alternatives — Elastic Beanstalk, Lambda, API Gateway — that are much simpler.
In our case, we were OK with one EC2-based CUWebAuth server hosting our Single Page application. But we didn’t want CUWebAuth dictating every server decision. If the user logs in successfully to the one CUWebAuth server, we can trust who they are on that server. Can we simply propagate that trust elsewhere? What you’d like is for all API servers to recognize that CUWebAuth-issued trusted assertion “I am bg666”. The API should be able to verify the assertion “I am bg666” is legitimate and not just some student-in-their-dorm-room-posing-as-bg666.
One answer is to use JSON Web Tokens, or JWT. These tokens are lightweight and verifiable, and are the envelope for assertions like “I am bg666.” You can build and use a JWT infrastructure with off-the-shelf components and a few lines of server code. We’ll look at the three loosely-joined pieces to make this work:
* A script on the CUWebAuth server to issue the JWT
* A piece of code that relays the JWT to an API server
* Middleware on the API server that verifies the JWT
A Little on JWT
Token-based architectures are nothing new. Kerberos, Active Directory, even the SSL certificate infrastructure use a variant of token passing. CUWebAuth itself uses a homebrew token passing scheme based on Kerberos. The idea is some battle-hardened secure server vouches for your identity. This prevents you from having to store different, possibly diverging, credentials on all the servers you access.
Modern token-based architectures like SAML and OAuth can be pretty difficult to navigate. In contrast, JWT provides a simple, easy to debug standard for passing tokens. The following token:
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "bg666",
"name": "Bennie Goetz",
"iss": "vetapp.vmit.cucloud.net"
}
SIGNATURE
… is technically not legal JSON, but its components are legal JSON. The first portion is the header, which describes the encryption algorithm used in the signature. The second is the interesting part, naming the subject of the token. Sub and iss are standard fields for the subject and the issuer. This is in effect saying “This person is username bg666 — and the server vetapp.vmit.cucloud.net said so”. The signature is a binary encrypted digital signature that can be verified by any party having the secret key. In symmetric encryption, which we will use in these examples, both the JWT issuer and the verifier share the same secret key.
The format of this token is nice for humans, but not so much for URL’s or HTTP. So usually a JWT is passed Base 64 encoded:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiZzY2NiIsIm5hbWUiOiJCZW5uaWUgR29ldHoiLCJhZG1pbiI6dHJ1ZX0.t6XEMu0wt6CwmaxJQuiL-W6YofbF3sRFmzbGQsikOl4
Each of the “.” separates the header from the payload from the signature. It’s important to note that this data is not encrypted. Someone can easily use a Base64 encoder, like http://jwt.io to read its contents. If you do all of your HTTP traffic over SSL, as we do in the Vet School, that’s not a problem. But we don’t even bother passing secret information in tokens — we only care about the username. The important thing is the signature can be verified by an API server, as long as it has the shared key. So some arbitrary person cannot simply forge a token and add it to an API request. Such a token would not pass the signature check, and would be rejected.
Another plus: the client doesn’t need the secret key. That’s a good thing because we don’t need to pass it to the client, and thus expose it to View Source or the cookie store. The client simply hands the token unchanged to the API server.
The CUWebAuth Server
So let’s start building! Here’s a high-level view of how a JWT session looks:
So let’s do the Apache/CUWebAuth part first. Server applications that live on the CUWebAuth server can trust the REMOTE_USER environment variable or request header. If you run a Server-Side HTML architecture (Rails, etc.) you can use its filtering mechanism to inject cookies or response headers with the JWT in it.
In the Vet School’s case, we want the Apache server just to serve static content — we don’t want the overhead of a Server-Side HTML stack like Rails. What can serve in its place? An Apache module can. Apache modules generally add special processing to requests, like authentication, or to responses, like adding headers. Some popular Apache modules are mod_rewrite which handles old-to-new-URL mapping, and CUWebAuth itself (at least at Cornell).
You can write arbitrary Apache modules for your own purposes. Generally they are written in C for speed, but … bluggh, that’s too much work. Instead we opt for MRuby, which is a subset of Ruby that can be compiled down to machine code. The Apache module mod_mruby allows MRuby code to hook in and call the Apache API’s directly just like a C-based Apache module can. And it’s much easier to write.
The hard part is encoding the JWT. Fortunately, there’s an MRuby Gem that does that called mruby-jwt. We build mod_mruby using this procedure in Ansible:
- name: install all packages required to compile mod_mruby
yum:
name:
- libyaml-devel
- libffi-devel
- readline-devel
- zlib-devel
- gdbm-devel
- ncurses-devel
- ruby-devel
- gcc-c++
- jq
- git- name: install gpg keys
shell: gpg2 --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB- name: install RVM locally
shell: curl -sSL https://get.rvm.io | grep -v __rvm_print_headline | bash -s stable --ruby
args:
creates: /usr/local/rvm# Will be done automatically on subsequent logins, but doing it manually is required the first
# time and won't hurt anyway.
- name: load RVM config
shell: source /etc/profile.d/rvm.sh- name: get latest version of mod_mruby
shell: git clone git://github.com/matsumoto-r/mod_mruby.git
args:
chdir: /usr/local/src
creates: /usr/local/src/mod_mruby- name: install mod_mruby build script with JWT gem added
copy: src=build_config.rb dest=/usr/local/src/mod_mruby/build_config.rb- name: build mod_mruby
shell: sh build.sh
args:
chdir: /usr/local/src/mod_mruby
creates: /usr/local/src/mod_mruby/src/.libs/mod_mruby.so
environment:
APXS_PATH_ENV: '--with-apxs=/usr/bin/apxs'
APACHECTL_PATH_ENV: '--with-apachectl=/usr/sbin/apachectl'- name: install mod_mruby
shell: make install
args:
chdir: /usr/local/src/mod_mruby
creates: /usr/lib64/httpd/modules/mod_mruby.so
environment:
APXS_PATH_ENV: '--with-apxs=/usr/bin/apxs'
APACHECTL_PATH_ENV: '--with-apachectl=/usr/sbin/apachectl'- name: Add mod_mruby tester
copy: src=test.rb dest=/var/www/cgi-bin/test.rb- name: Add mod_mruby filter
copy: src=add_jwt.rb dest=/var/www/cgi-bin/add_jwt.rb- name: Always restart httpd.
service: name=httpd state=restarted
And add the following line to mod_mruby’s build_config.rb:
# CU VMIT Custom
conf.gem :github => 'ainoya/mruby-jwt'
Once mod_mruby is installed and mruby-jwt is compiled in, our actual module is quite simple:
#
# add_jwt.rb
# Add JWT token, bottling up authenticated user name, in response for each request that needs it
# Module for mod_ruby
# August, 2017
#module ModMruby
class AddJwt
def add_it
env = Apache::Env.new()
secret_key = “this is a pretty secret thing”
creds = {
“iss” => env[“SERVER_NAME”],
“sub” => env[“REMOTE_USER”]
}
jwt_token = JWT.encode(creds, secret_key)
env[“CUVMIT_JWT”] = jwt_token
end
end
endt = ModMruby::AddJwt.new
t.add_it
This module simply reads environment variables that CUWebAuth has set, creates a standard JWT token, and puts this JWT back into the environment where Apache can use it. Then in the httpd.conf file we hook it into every request like so:
LoadModule mruby_module /usr/lib64/httpd/modules/mod_mruby.so<Location />
AuthName Cornell
AuthType all
Require valid-user mrubyFixupsMiddle /var/www/cgi-bin/add_jwt.rb # The “e” looks weird, but it grabs environment variables.
Header set Set-Cookie “cuvmit-jwt-token=%{CUVMIT_JWT}e”
</Location>
This sets a cookie called cuvmit-jwt-token, which goes back to the client. Because this location is also being handled by CUWebAuth, it’s guaranteed to be SSL and encrypted so the token will not be visible except on the client.
JavaScript Code to Relay The Token
Now that you have a JWT, the world is your oyster! (Or at least as much of an oyster as you’re allowed.) Now this JWT must be passed to API’s. You can do this in any way you wish — request headers, data in the API call itself, etc. — just as long as the API knows where to find it. But the de facto standard is to send it in an Authentication header like this:
Authenticate: Bearer 734587jhdfn…
Adding this header to every request manually would be a chore. So there’s an Angular module that does it for us called angular-jwt. We configure it in the standard Angular configuration block:
function Config ($routeProvider, jwtOptionsProvider, $httpProvider) {
…
// We need to inject cookies manually because config
// doesn’t usually have access to it.
var $cookies;
angular
.injector([‘ngCookies’])
.invoke([‘$cookies’, (_$cookies_) => {
$cookies = _$cookies_;
}]); jwtOptionsProvider.config({
tokenGetter: () => {
return $cookies.get(“cuvmit-jwt-token”);
},
whiteListedDomains: [“vmit.cucloud.net”,”localhost”]
});
$httpProvider.interceptors.push(‘jwtInterceptor’);
}
This is nice because now all XMLHttpRequests to the whitelisted domains (in our case, anything in the *.vmit.cucloud.net domain and localhost) will get an Authenticate header with the JWT. Because config runs when the Angular app is loaded, and the app comes from our CUWebAuth domain, it is guaranteed to have the cuvmit-jwt-token cookie.
NodeJS Express Middleware
Finally we need the piece on the API server. In our case we use NodeJS and Express for all of our microservice API’s. We don’t want to have to verify the token in each API request code. Fortunately, there’s a NodeJS package called express-jwt which handles that for us. All we have to do is plug it into our route table.
import * as jwt from "express-jwt";
…
this.router.get("/employee/:id",
jwt({secret: process.env.CUVMIT_JWT_SECRET}), (req, res) => {
const worker = new EmployeesController();
worker.get_one(req, res);
}
);
The jwt call verifies the signature is valid, and therefore that the JWT accurately reflects who is using the API. In our case, we use symmetric encryption — so the CUVMIT_JWT_SECRET environment variable must
match the one on our CUWebAuth server. But you can also configure asymmetric encryption, so the private key and public keys do not have to be shared. (This is a little slower than symmetric.)
Other JWT Options
So that’s the gist of it. Because JWT is the common glue between the pieces, you can replace any of the three components with your own:
Use Other Authentication Sources
CUWebAuth is great if your principals are Cornell-affiliated. But what if you want a different authentication scheme?
- If you have your own external customer database with passwords and a login page, you can issue JWT tokens directly from there.
- If you’d like to use OAuth sources like Google+ or Facebook, you can use a third-party JWT issuer like Auth Zero.
Use Other Application Frameworks
We use AngularJS, but other client-side JavaScript frameworks handle JWT:
- jwt-react-tools plugs into React
- ember-simple-auth-token plugs into Ember
- The ruby gem jwt plugins into the popular Devise library for Rails
Use Other API Frameworks
We use NodeJS and Express, but you can also write your API’s in:
- Sinatra with the jwt Gem in Ruby
- jwt.net for .NET servers
- JAX-RS with JWT decoding for J2EE Servers
Use Other Assertions
Finally, you are not limited to just the username in your JWT. Though not shown here, we also send an expiration date with each token so users cannot simply copy the token and use an API indefinitely. You can also add extra authorization helps that CUWebAuth provides like CUWA_GROUPS.
JWT is not a universal security solution. If you want to pass secrets through browsers that not even clients can decode, you must use something stronger. And though tokens are very difficult to procure — one must physically have access to the machine — once someone has a token, they can use it to wreak havoc for a little while.
But security is always a balancing act between ease-of-use and reasonable control. In our case, we found JWT to be just the right mix.