Tech Blog

Working with JWT Authentication Tokens

We’ve mentioned JWT tokens in several previous blog posts. According to jwt.io, JSON Web Tokens are “an open, industry standard method for representing claims securely between two parties.” A quick Google search produces many good websites and videos that describe how these tokens work. And because JWTs are an open Internet standard, there are libraries in nearly every language and development stack that can be used to create and validate them.

A Brief JWT Primer

For our purposes, we’ll provide the most basic explanation of JWTs. JSON Web tokens provide a list of claims about the identity and authorization of a user. To ensure its authenticity, the token is signed using a symmetric key (shared by both sides) or an asymmetric (public/private) key using one of many standard algorithms. JWTs are comprised of three parts, header, payload (data) and signature, each base64 encoded and concatenated into a single string with each part separated by ‘.’. This simple structure allows the simple creation and decoding/validation of tokens.

For example, using the PyJWT library in Python, we can install the package with:

$ pip install PyJWT

And then create and decode a token as follows:

$ python
>>> token = jwt.encode({"user":"me","inst_code":"01MY_INST"}, "MyVerySecretKey", algorithm="HS256")
>>> print(token)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibWUiLCJpbnN0X2NvZGUiOiIwMU1ZX0lOU1QifQ.NBlOJPGNJ-klJfrOljrutjYLRqSHuve2RqpkPh_Ijww
>>> jwt.decode(token, "MyVerySecretKey", algorithms=["HS256"])
{'user': 'me', 'inst_code': '01MY_INST'}

The JWT standard defines several 3-letter claims, such as aud (audience), iss (issuer), sub (subject). Other private claims can be added as needed. A good introduction to JWTs is available on the JWT.io website.

JWTs in Alma

Alma uses JWTs to authenticate with other systems. As described in the documentation, Alma authentication tokens are signed with RS256, which is an asymmetric algorithm which uses an RSA Signature with SHA-256. This allows any client to verify the authenticity of the tokens with the published public key.

The Cloud App framework can be used to retrieve a signed authentication token, which in turn can be used to authenticate with external systems and services. There is also a Cloud App tutorial which describes how to retrieve the token from Alma, send it in the HTTP request to a web service, and validate the token in the service.

Using JWTs

We can use additional standards to further leverage interoperability features. JSON Web Key Sets (JWKS) is a standard way of providing the public keys required to validate tokens. JWKSs can be used on their own, or in coordination with other standards such as OpenID Connect (OIDC), an identity layer build on top of the OAuth 2.0 standard.

Validating Tokens

JWT libraries are available in many languages (see a curated list at jwt.io). Some natively support referencing a JWKS to retrieve the public key. For example, we can use Node.js to validate a token provided by Alma Cloud Apps. First we install the required libraries:

$ npm install jsonwebtoken jwks-rsa

Then we tell the jwks-rsa library where to find the JWKS (https://apps01.ext.exlibrisgroup.com/auth/jwks.json) and create a function which extracts the relevant public key from the key set based on the key ID provided in the token. Finally, we provide the function to the verify function in the jsonwebtoken library to validate the decode the token.

const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const verify = require('util').promisify(jwt.verify);
const client = jwksClient({
  jwksUri: 'https://apps01.ext.exlibrisgroup.com/auth/jwks.json'
});
const getKey = (header, callback) => {
  client.getSigningKey(header.kid, function(err, key) {
    if (err) return callback(err, null);
    callback(null, key.getPublicKey());
  });
}

const token = 'eyJhbGciOiJSUzI...';
(async () => {
  try {
    const decoded = await verify(token, getKey, { algorithm: 'RS256' });
    console.log('decoded', decoded);  
  } catch(e) {
    console.error(e.message);
  }
})()

 

AWS API Proxy Authentication

In a previous blog post, we used the AWS public cloud to build an API. AWS HTTP APIs support JWT authorizers . When defining a new JWT authorizer, we need to provide the following details:

  • Identity Source: where the authorizer should take the token from, in our case from the authorization HTTP header ($request.header.Authorization)
  • Issuer URL: the root of the OIDC discovery location. In the case of the Cloud Apps token, that’s https://apps01.ext.exlibrisgroup.com/auth. The authorizer retrieves the OIDC configuration (including the JWKS) using that URL, and from there is able to retrieve the public keys to be used to verify the token at runtime
  • Audience: A list of valid values for the aud field in the token. For Cloud Apps, this field will contain the string ExlCloudApp: followed by the Cloud App ID (the Github user and repository, e.g. for “jweisman/my-cloud-app”, the value would be ExlCloudApp:jweisman/my-cloud-app)

Once the authorizer is defined, we can test the API by placing a token in the Authorization field in a REST client, such as Postman.

Claims in the verified token are forwarded from the API Gateway to the Lambda function which answers the request. The claims are available in the event.requestContext.authorizer.jwt.claims property. For example, I can retrieve the user and institution as follows:

// Optional chaining would be nice; wait for Node 14
// const { inst_code, sub, urls } = event.requestContext?.authorizer?.jwt?.claims;
const { inst_code, sub } = event.requestContext && event.requestContext.authorizer && event.requestContext.authorizer.jwt && event.requestContext.authorizer.jwt.claims;
console.log(`API called by ${sub} from ${inst_code}`);

The details are available in the log after my request is succesful:

562a398    INFO    API called by joshw from TR_INTEGRATION_INST

Cloud App Example

I can adapt my Angular component to retrieve the authentication token from Alma (in ngOnInit)  and add it to the headers in my HTTP request. Now my API is secured, and only calls which are authenticated with a valid Cloud App authorization token will be processed.

Below is the code for my sample component. Happy secure-coding!

 

import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { CloudAppEventsService } from '@exlibris/exl-cloudapp-angular-lib';
import { of } from 'rxjs';
import { catchError, debounceTime, map, switchMap } from 'rxjs/operators';

const APIURL = 'https://********.execute-api.eu-central-1.amazonaws.com/pinyin';

@Component({
  selector: 'app-pinyin',
  templateUrl: './pinyin.component.html',
  styleUrls: ['./pinyin.component.css']
})
export class PinyinComponent implements OnInit {
  text = new FormControl();
  options: { headers: HttpHeaders };
  pinyin$ = this.text.valueChanges.pipe(
    debounceTime(500),
    switchMap(value=>this.http.get<PinyinResponse>(`${APIURL}/${value}`, this.options)),
    catchError((e: HttpErrorResponse)=>of({text: e.message})),
    map(result=>result.text),
  );

  constructor(
    private http: HttpClient,
    private events: CloudAppEventsService,
  ) { }

  ngOnInit() {
    this.events.getAuthToken().subscribe(token=>{
      this.options = { 
        headers: new HttpHeaders( { Authorization: `Bearer ${token}` })
      }
    });
  }
}

export interface PinyinResponse {
  original: string,
  text: string,
  data: string[],
}
<mat-form-field>
  <mat-label>Chinese Text</mat-label>
  <input matInput [formControl]="text">
</mat-form-field>
<mat-spinner *ngIf="loading" diameter="25"></mat-spinner>
<p *ngIf="!loading">
  {{pinyin$ | async}}
</p>