Generate JWTs with Swift on AWS Lambda

Matheus C.
Matheus C.
Published March 11, 2021 Updated March 12, 2021

Authorization is one of the essential parts of any iOS application. Once a user is logged in, it's your authorization scheme that will make sure users can't interact with your app in ways they're not allowed to. Without a robust authorization scheme, hackers could easily access sensitive user data and engage in other damaging activities such as scamming.

Thankfully, the widespread use and standardization of JWT (JSON Web Tokens) have made robust and cryptographically secure authorization more straightforward to achieve.

In this article, we'll use Stream Chat as an example service to authorize our users for using Swift Lambda to generate JWTs. You can then use the JWT for interacting with the Stream Chat service via the REST API or by configuring the iOS Chat SDK.

What is JSON Web Token?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

How does it work?

After authenticating with a login and password or another method, the user receives a JWT. After authentication, the user will attach this JWT to every request as a way of proving their identity and permission to access a particular resource. This JWT was signed by your server during authentication using a secret key and is verified in subsequent requests.

Set Up Swift Lambda

Since JWTs should be generated and verified server-side, we can set up an AWS Lambda written in Swift to do that job. You can choose an alternative method and language, but the next steps will be different. By the end, we'll have an HTTP endpoint that outputs a JWT for a specific user id to access Stream's chat service.

Install Kitura's Swift-JWT

To generate a JWT, we'll use Kitura's Swift-JWT package, which takes care of the heavy lifting of building a JWT.

To install it, open your Swift Lambda's Package.swift in its root folder and add .package(name: "SwiftJWT", url: "https://github.com/Kitura/Swift-JWT.git", from: "3.6.2") to the package's dependency list. Also, add "SwiftJWT" to the Lambda target's dependency list. By the end, your Package.swift will look similar to the one below.

import PackageDescription

let package = Package(
    name: "Lambda",
    platforms: [
        .macOS(.v10_13),
    ],
    products: [
        .executable(name: "Lambda", targets: ["Lambda"]),
    ],
    dependencies: [
        .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.2.0"),
        .package(name: "SwiftJWT", url: "https://github.com/Kitura/Swift-JWT.git", from: "3.6.2")
    ],
    targets: [
        .target(name: "Lambda", dependencies: [
            .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
            .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
            "SwiftJWT"
        ])
    ]
)

Install OpenSSL

Swift-JWT depends on the OpenSSL library to perform cryptographic operations. To install it, add RUN yum -y install openssl-devel to your Swift Lambda's Dockerfile which can be found in its root folder. The Dockerfile should look similar to the one below.

FROM swift:5.3.1-amazonlinux2

RUN yum -y install zip
RUN yum -y install openssl-devel

Configure Endpoint

Finally, we'll need our endpoint to accept POST requests, since we'll send credentials in the request body. To do that, change the Swift Lambda's serverless.yml line from method: get to method: post. It will look similar to the snippet below.

service: swift-lambda

provider:
  name: aws
  runtime: provided

package:
  artifact: .build/lambda/Lambda/lambda.zip

functions:
  lambda:
    handler: handler.lambda
    events:
      - http:
          path: /
          method: post

Parse Request

After you're done configuring your Swift Lambda, we can start writing code in Sources/Lambda/main.swift. The code we need will parse a POST request and extract the user_id field from its JSON body. After that, it will generate a JWT for that user id and return it.

import AWSLambdaEvents
import AWSLambdaRuntime
import SwiftJWT
import Foundation

Lambda.run { (context: Lambda.Context, event: APIGateway.Request, callback: @escaping (Result<APIGateway.Response, Error>) -> Void) in
    guard
        let bodyData = event.body?.data(using: .utf8),
        let json = try? JSONSerialization.jsonObject(with: bodyData, options: []) as? [String: Any],
        let userId = json["user_id"] as? String
    else {
        callback(.success(APIGateway.Response(statusCode: .badRequest)))
        return
    }
    
    if let jwt = try? generateJWT(for: userId) {
        callback(.success(APIGateway.Response(statusCode: .ok, body: jwt)))
    } else {
        callback(.success(.init(statusCode: .internalServerError)))
    }
}

func generateJWT(for userId: String) throws -> String {
    return "" // TODO: Generate JWT
}

We'll implement the generateJWT(for userId: String) function in the next steps.

Sign Up to Stream

Before we can generate a JWT, we need a secret to sign it. You can get one by signing up to Stream or by accessing the dashboard.

Image shows secret in Stream dashboard

Generate JWT

To generate a JWT, we'll use SwiftJWT. First, we need to declare a struct conforming to the Claims protocol with a user_id property of type String. That's the only claim required by Stream. After that, we create a JWT object with the user_id claim and sign it using your Stream secret.

func generateJWT(for userId: String) throws -> String {
    struct StreamClaims: Claims {
        let user_id: String
    }
    
    let myClaims = StreamClaims(user_id: userId)

    var myJWT = JWT(claims: myClaims)
    
    let key = "[my_secret]"
    let keyData = key.data(using: .utf8)!
    
    let signer = JWTSigner.hs256(key: keyData)
    
    return try myJWT.sign(using: signer)
}

Note: Before you generate a JWT, it's essential to verify the request with some form of authentication such as a password and return 401 Unauthorized if it's wrong.

Deploy

The main advantage of using Swift Lambda is that it's possible to iterate fast with it. After writing the JWT code, just run the ./Scripts/deploy.sh script again and wait a few seconds.

This image shows the deploy script finishing successfully

Test the Endpoint

To test the endpoint and get a JWT, you can use the following curl command in your terminal. Don't forget to replace the URL with the one you got in the deploy step and change the user id if you want to.

curl --location --request POST 'https://0wshazchfd.execute-api.us-east-1.amazonaws.com/dev/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "user_id": "myuserid"
}'

After running that command, you should get a valid JWT. If it has a % at the end, ignore it.

Image shows curl command running and a JWT output

Conclusion

Congratulations! You've generated a valid JWT for interacting with the Stream Chat service via the REST API or by configuring the iOS Chat SDK. This logic can be adapted to work with other JWT-based services such as Auth0 and Stream Video. To find out what else you can build with Swift on AWS Lambda, check out the Swift Lambda repository on GitHub.

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->