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.
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.
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.
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.