JSON Web Tokens with Swift-JWT

By Kye Maloy

Created on 2018-09-02

Authorisation is an important part of any web service, and JSON Web Tokens, or JWTs, have risen in popularity in recent years and serve as an alternative to cookies and OAuth tokens. Swift-JWT is a new, powerful Swift library for creating, signing, and verifying JWTs, and it works seamlessly with Kitura.

Introduction to JSON Web Tokens

As a brief introduction to JWTs for those who have never come across them before, they are a small JSON payload consisting of a Header object, a Claims object and a signature.

The Header object describes the JWT type and the algorithm used. It is then Base64 Encoded.

The Claims object has fields that are either Registered, Public or Private. A full list of the Registered claims can be found here. Registered claims tend to be interoperable and predefined, whereas Public and Private claims are created by the developer implementing the JWT. Public claims should be used according to the IANA JWT Registry to avoid namespace collisions. Finally, Private claims would be those specific to your application, and are not always needed. An example could be a field marking JWT bearer as an administrator. This is then also Base64Encoded and appended to the Header using a single period (.).

Lastly, a signature is generated by using a combination of the encoded header, claims, a secret and is then signed using the algorithm specified in the Headers ‘alg’ field. This is appended to the end of the JWT, forming three parts separated by periods. The signature can be left blank if the JWT requires no authentication. For more on JWTs, I recommend the JWT.io website.

Now may be a good time to point out that this blog will focus on signed JWTs, not encrypted ones. JSON Web Signature (JWS) and JSON Web Encryption (JWE) are similar but differ in their use cases and implementations. Swift-JWT supports JWS only. The JWTs created with it cannot be tampered with, because changing the Header or Claims will change the signature of the JWT, invalidating it. However, as the Claims are only Base64 Encoded, they are readable to anyone, but because the Signature can only be validated with public key associated with the private key that signed it, you can trust the token.

The main benefit of using a JWT is that they are self contained. There is no overhead required, no database lookups, and they can be sent in URLs and HTTP headers. They are also lightweight and human parsable, as they come in JSON format. This means they can be handled easily by applications dealing with JSON. Now we have some priory surrounding JWTs, we can take a look at the API for Swift-JWT.


Using Swift-JWT

First add the Swift-JWT library to your Package.swift and import it into your project. The library works on Swift 4 and later.

In my example, I will be using a public/private key pair created on the macOS command line, using OpenSSL and ssh-keygen utilities. The command I used is:

ssh-keygen -t rsa -b 4096 -f jwtRS256.key
# Don't add a passphrase
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub

The reason for not adding a passphrase is because this encrypts the key, meaning Swift-JWT can’t read its contents. I then place the files generated into a subdirectory of my Swift project, in the root directory, and call this new directory JWT.

The data from the file must be parsed into a local variable in Swift so we can use it to instantiate a new JWT object. To do this, we create a URL object pointing to the file and then then create a Data object with the contents of the URL.

let myKeyPath = URL.init(fileURLWithPath: getAbsolutePath(relativePath: "/jwt/jwtRS256.key")!)
var key: Data = try Data(contentsOf: myKeyPath, options: .alwaysMapped)

Now the JWT is ready to be created. To do this, you can do the whole thing in one call or create the components separately and then make the JWT at the end. If you are adding a lot of claims to your JWT I would recommend the latter approach for readability.

var jwt = JWT(header: Header([.typ:"JWT", .alg:"rsa256"]),
claims: Claims([.name:"Kitura", .jti:”probableRandomString", .iss:”websiteName",
.aud:"anyone", .sub:”emailOfCLIENT", .iat:"03/15/2018", .exp:"03/15/2019",
.nbf:03/14/2018, “customClaim”:”admin=YES]))

There’s quite a lot going on here so to break it up, we will look at the individual parts of the JWT. First, we create the Header, which takes a .typ parameter of JWT to tell whatever application this is passed into that the contents are a signed JWT (JWS) and not JWE. The .alg field then shows that this is JWS was signed using the RSA256 algorithm.

The Claims are then created. There are several library provided claims used here, which are defined in the RFC specification, and many are optional. Keep the JWT lightweight and only implement the ones that make sense to your application. In my example, we define a name as “Kitura”, a unique ID for this JWT as “probableRandomString” and so on. For a full list of the available registered claims included, see the file Claims.swift. You can also define your own Claims, known as private claims, which would be application specific using a String as the name of the Claim, as shown above with the customClaim claim.

We now need to sign the JWT using the private key we made in the terminal. To do this, we run:

// Note: You can use a public/private key pair or a certificate to sign a JWT.
let signedJWT = try jwt.sign(using: .rs256(key, .privateKey))

We create a new JWT that is composed of the three components: a Base64 Encoded Header, Claims object and signature made of the combined two and the key. Someone with access to the public key would then be able to reverse the encoding and verify the signature. The header and claims would be decipherable without the key, as they are only encoded, not encrypted. The signature on the end verifies the validity of the JWT object and allows it to be implicitly trusted.

A server can send this signed JWT in Kitura with:

response.headers["Set-Cookie"] = "jwt=\(signedJWT)"

A common use case would have a server issuing signed JWTs that a client saves to their local storage. At sometime in the future, the client makes a request and sends the JWT in the Authorisation Header of their HTTP request. The server can use the public key that goes with it’s private key to verify it did make the token using the following syntax:

// Inside a route handler for a raw route that requires authentication
let receivedJWT = request.headers["Cookie"]?.split(separator: ";")
var jwt = String()
for item in receivedJWT {
    if item.contains("jwt") {
        let separator = item.split(seperator: "=")
        jwt = String(describing: newArray[1])
    }
}

This logic receives a full list of the cookies saved on the client’s system, but we are only interested in the the JWT so we parse it and end with cookie storing the value of the received, signed JWT. Now we can verify the JWT was signed by us, and access its Claims. To start, we need to store the public key as a variable we can access in Swift:

let myPublicKeyPath = URL.init(fileURLWithPath: getAbsolutePath(relativePath: "/jwt/jwtRS256.key.public")!)
var publicKey: Data = try Data(contentsOf: myPublicKeyPath, options: .alwaysMapped)

Now we have it in memory, we can use it to verify the JWT was signed by us, and then use its Claims.

let verifyJWT = try JWT.verify(jwt, using: .rs256(publicKey, .publicKey))

if verifyJWT {
    if let decodedJWT = try JWT.decode(jwt) {
        // Library provided method for checking expiration, audience etc.
        let claim = decodedJWT.validateClaim()
        switch claim {
        case .success:
            // Do something if the JWT is valid and trusted.
        case .failure:
            // Do something if the JWT is invalid but trusted (i.e. expired)
        }
    } else {
        print("Error decoding JWT")
    }
} else {
    // Do something as JWT was tampered with or not signed by us  
}

In just a few lines of code, we have received a JWT from the client, checked that we signed it, and checked the validity of the Claims. You can pass an issuer: and audience: parameter into the validateClaims method to check against them. The other claims are only assessed if they are present, if the JWT did not contain them they are skipped.

Inside the success case is where the logic for your application would go for a successful login. The failure case let’s you deal with cases where the token may be expired or for the wrong audience or issuer and the initial verification lets you be sure you issued the original JWT and it has not been tampered with.


Conclusion

The Swift-JWT library will be adding new signing algorithms and use cases as time goes on, and we have added some Claims specific to certain specifications such as MicroProfile. We will also be adding a demo of Swift-JWT to Kitura-Sample, with an example of a simple login and logged-in-user only route and handling on unauthorised access attempts.