JSON Web Tokens With Nginx and Shibboleth

Craig Riecke
CUC4
Published in
4 min readMay 12, 2020

In the first part of this article, I went through the steps of installing Nginx and Shibboleth. Now you have a web server serving up front ends securely. The next part is handing JSON Web Tokens, or JWT’s, with the content so the client can relay these to API’s. The API makes the decision “I can allow person X to do this thing.”

I won’t go over the relative virtues of JWT’s — you can read about them in the original article. Suffice it say, at the Vet School we’ve found them to be immensely useful, easy to debug, and secure enough for our purposes.

Extending Nginx with NJS

So Nginx doesn’t serve up JWT’s on its own. That’s OK — we can add a custom filter to do that. There are several programming language options for writing filters: LUA, Perl, or C. We chose NJS, which I believe stands for Nginx JavaScript Service (the reference is unclear). NJS is definitely JavaScript. We like JavaScript. ‘Nuff said.

Interestingly, NJS is not NodeJS so you can’t just throw NPM modules at it. But it’s a subset of ECMAScript 5.1, and has SHA256 cryptographic libraries available. That’s pretty much everything we need to make a JWT.

First we install the NJS module binaries, which are available as a package. Assuming you have the Nginx repos defined as we did in the previous article, you can just do:

$ sudo apt-get install nginx-module-njs

Then add the following line to /etc/nginx/nginx.conf

load_module modules/ngx_http_js_module.so;

Restart Nginx:

$ sudo systemctl nginx restart

And you now have NJS support at your fingertips.

The Script

So here’s the JavaScript file that does most of the magic, which we put in /etc/nginx/conf.d/hs_jwt.js This code is a variant on some sample code specified in the NJS manual

function generate_hs256_jwt(claims, key, valid) {
var header = { typ: 'JWT', alg: 'HS256' };
Object.assign(claims,
{exp: Math.floor(Date.now()/1000) + valid}
);
var s = [header, claims]
.map(JSON.stringify)
.map((v) => v.toUTF8())
.map((v) => v.toString('base64url'))
.join('.');
var h = require('crypto').createHmac('sha256', key); return s + '.' + h.update(s).digest().toString('base64url');
}
function jwt(r) {
var s = r.variables;
var claims = {
iss: r.variables.server_name,
sub: r.variables.shib_remote_user,
groups: r.variables.shib_remote_groups.split(";")
};
var generatedJwt = generate_hs256_jwt(
claims, "SECRET!!", 600
);
return generatedJwt;
}
export default {jwt};

None of this should be mysterious to JavaScript programmers. It creates a token with the most interesting part looking like:

{
"iss": "myserver.cornell.edu",
"sub": "cr396",
"groups": [
"employees",
"VM-InformationTech",
],
"exp": 98732495873
}

Then signs it with a SHA256 digest using password “SECRET!!” and encodes the entire thing in Base64. Like all JWT’s, the principal assertion is unencrypted, so any application can read that part. The signature, however, is what the API reads, decrypts by using the same underlying password, and therefore verifies the user. The client passing around the JWT’s does not know “SECRET!!” and thus cannot do any verification. That’s fine. As long as the API trusts the token, that’s the main thing since it does all the dangerous stuff.

Now, to hook it into the content pipeline, do this:

# Add these two lines for JWT Involvement
js_import /etc/nginx/conf.d/hs_jwt.js;
js_set $jwt hs_jwt.jwt;
server {
... the usual stuff
location / {
root /usr/share/nginx/html;
index index.html;
shib_request /shibauthorizer;
shib_request_set
$shib_remote_user $upstream_http_variable_uid;
# For optional groups attribute
shib_request_set
$shib_remote_groups $upstream_http_variable_groups;

# Add this line
add_header Set-Cookie
"cuvmit-jwt-token=$jwt; Domain=vet.cornell.edu; Path=/;";
}

The js_set line sets an Nginx variable $jwt to the JavaScript function jwt that we exported from the script (the function itself, not the output of a function call — at least not yet). Then the add_header directive calls the jwt function with the usual parameters — in this case r . r contains all the interesting contextual stuff like the URL, the server name, and a property called variables containing all the Nginx variables set in that request. For us, the interesting ones are shib_remote_user and shib_remote_groups which come to us courtesy of the NGinx Shibboleth Module.

And that’s it! The cookie goes back with the content, and the client program can send it with all API calls.

And The World Is Your Oyster

The NJS world is really easy to navigate in. Need to add more claims to your JWT? Just add properties to the token. Need to use Asymmetric encryption for your JWT? Just use a different crypto method.

The client, now possessing the JWT in their session cookie stash, can attach it to every API request. In Angular, we can do that with an HTTP interceptor that looks like this:

import { Injectable } from "@angular/core"; 
import { HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
} from "@angular/common/http";
import { CookieService } from "ngx-cookie-service";
import { Observable } from "rxjs";
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
jwt: string;
constructor(private cookieService: CookieService) {} intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// Only attach JWT to our own API requests
if (req.url.match(/\.vmit\.cucloud\.net/)) {

if (!this.jwt) {
this.jwt = this.cookieService.get("cuvmit-jwt-token");
}
const jwtRequest = req.clone({
setHeaders: {
Authorization: `Bearer ${this.jwt}`,
},
});

return next.handle(jwtRequest);
} else {
return next.handle(req);
}
}
}

In React and VueJS there are similar mechanisms. The good news is you don’t have to include them manually with each API call.

That’s all there is to it! We found the entire mechanism to be easily traceable and debuggable. All of the services — Nginx, Shibboleth and Supervisor — all have verbose logging options. In the NJS, you can add r.log statements like you normally would.

Best of all, the entire chain is built on freely available, Internet-scoped components.

--

--