There are many ways of authenticating to your backend APIs and one of them are JWT or JSON Web Tokens. To make sure your token is genuine, you need to verify its signature. Let's do it in Swift!

A JSON Web Token looks like this:

HEADERS.PAYLOAD.SIGNATURE

Notice the three parts separated by 2 dots.

All three parts are Base64-URL-encoded JSON strings. This is the first tricky part as there is no native way of processing Base64-URL-encoded strings on Apple platforms.

Fortunately this issue is easy to resolve because the only differences between Base64-URL-encoded strings and Base64-encoded strings (that we can work with directly are:

  • + in Base64 becomes -  in Base64-URL
  • / in Base64 becomes _ in Base64-URL
  • = end padding in Base64 is omitted in Base64-URL

With this knowledge we can write our own Base64-URL to Base64 function.

extension String {
    var base64FromBase64Url: String {
        var base64 = self
            .replacingOccurrences(of: "-", with: "+")
            .replacingOccurrences(of: "_", with: "/")
        
        base64 += String(repeating: "=", count: base64.count % 4)
        
        return base64
    }
}

Verifying the signature

Verifying the signature is as simple as computing the signature using the headers and payload together with a certificate.

RSASHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), certificate)

First we parse our token...

let token = "HEADERS.PAYLOAD.SIGNATURE"

let tokenParts = token.split(separator: ".")

let tokenHeadersAndPayload = "\(tokenParts[0]).\(tokenParts[1])"
let tokenSignature = String(tokenParts[2])

Then we parse the headers, payload and signature to get their CFData representations...

let tokenHeadersAndPayloadData = tokenHeadersAndPayload.data(using: .ascii)! as CFData
let tokenSignatureData = Data(base64Encoded: tokenSignature.base64FromBase64Url)! as CFData

Lastly we parse our certificate to get its public key as a SecKey representation...

let certificateBase64 = "BASE64 ENCODED CERTIFICATE"

let certificateData = Data(base64Encoded: certificateBase64)! as CFData
let certificate = SecCertificateCreateWithData(nil, certificateData)!

let publicKey: SecKey!

if #available(iOS 12, *) {
    publicKey = SecCertificateCopyKey(certificate)
}
else {
    publicKey = SecCertificateCopyPublicKey(certificate)
}

Notice that the certificate usually comes in a Base64-encoded form, not Base64-URL-Encoded form. This is what we need and so there is no need to convert it as we did with tokenSignatureData above.

The last thing that is remaining is to verify the signature...

let result = SecKeyVerifySignature(
    publicKey,
    .rsaSignatureMessagePKCS1v15SHA256,
    tokenHeadersAndPayloadData, tokenSignatureData,
    nil
)

Notice the .rsaSignatureMessagePKCS1v15SHA256 parameter. By changing it, you can verify signatures created using various algorithms. You can find the list of supported algorithm at the SecKeyAlgorithm documentation page.

That's it! Using built-in functionality provided by Apple we managed to verify the signature of our JSON Web Token.