How to use JWT in the Authorization header of a Vapor API server
The last time I got to yammering on, I mentioned I was building a side project in Vapor 3. When I got to setting up authentication for this project, I decided to use JWTs for reasons that are probably inexplicable to anyone with a clue.
Fortunately my cluelessness didn’t deter me. I quickly found that Vapor has an Authentication framework and a JWT framework. Unfortunately, I also found that they don’t work together out of the box, and there’s not much documentation on either. Cue my patented flailing around and trying to grok someone else’s code. Eventually, figured out something that worked.
Here’s my plan for marrying JWT with Authentication: first, figure out what to put in my JWT. Then I’ll need some code to authenticate that JWT, and some middleware to call it. Finally, I’ll use the results of the middleware to see if the holder of the JWT is authorized to perform the request they made.
Defining the JWT Payload
First, I had to define what my JWT looked like. A JWT is essentially a signed JSON payload, so I had to decide what should go in that payload. The main thing my app needed to know was the user making the request.
Here’s an abridged version of what I ended up doing:
import Foundation
import Vapor
import JWT
import Authentication
struct UserIDClaim: JWTClaim {
var value: UUID
}
struct AuthJWTPayload: JWTPayload {
func verify(using signer: JWTSigner) throws {
// nothing for now
}
let uid: UserIDClaim
let iat: IssuedAtClaim
}
The JWTPayload
is a protocol defined in the JWT framework. Each property in the payload is supposed to be a “claim” that can be verified, and the verify
method is supposed to do the verifying. However, since the verify
method only takes the signer, there’s not much verifying it can actually do. I suppose it could verify that the IssuedAtClaim
isn’t older than a given date.
Speaking of claims, there’s a standard set that the JWT framework defines. The IssuedAtClaim
is an example. However, it’s possible to define your own, which is what I did with UserIDClaim
. As the name suggests, it represents the user ID that the holder of the JWT operates as. All JWTClaim
s must be Codable
, and define a mutable property named value
.
The weird three letter property names appear to be a convention for claims. My assumption is it’s to keep the resulting JSON, and therefore the JWT, small.
Verifying a JWT Payload
Now that I had a payload defined, I needed something to verify it. The Authentication framework defines this kind of type as Authenticatable
. I decided to be a bit fancy and define a protocol for JWT authenticatables before I wrote an implementation to authenticate my specific JWT payload.
import Foundation
import Vapor
import Authentication
import JWT
protocol JWTAuthenticatable: Authenticatable {
associatedtype JWTType: JWTPayload
static func authenticate(jwt: JWTType, on connection: DatabaseConnectable) -> Future<Self?>
}
The Authenticatable
protocol itself has no constraints. I had JWTAuthenticatable
declare a generic type that conforms to JWTPayload
, and a static authenticate
method to verify it. The return value will be an instance of the JWTAuthenticatable
if the payload validates, nil
otherwise.
Now I could define my actual JWTAuthenticatable
type to verify the JWT payload:
import Foundation
import Vapor
import Authentication
import JWT
struct AuthJWTAuthenticatable: JWTAuthenticatable {
typealias JWTType = AuthJWTPayload
let user: User
static func authenticate(jwt: JWTType, on connection: DatabaseConnectable) -> Future<AuthJWTAuthenticatable?> {
return User.query(on: connection)
.filter(\.id == jwt.uid.value)
.first()
.map { maybeUser in
return maybeUser.map { AuthJWTAuthenticatable(user: $0) }
}
}
}
There are a couple of things to note here. First, if authenticate
returns an instance of Authenticatable
, the middleware (described later) is going to stash it on the request. Meaning Authenticatable
is a great place to stash information about the authenticated “caller” because it can be accessed by other middleware or controllers. In my case, the caller represents a user, so I stash the user
on the Authenticatable
so my controllers can use it later.
The second thing of note is that I’m hitting the database to verify the JWT. This negates one of benefits of using a JWT, which is being able to verify a token without hitting the database. However, for my app I always need to have the user struct in memory, so it’s easier to go ahead and load it here. An alternative approach to avoid hitting the database during authentication would be:
struct AuthJWTAuthenticatable: JWTAuthenticatable {
typealias JWTType = AuthJWTPayload
let userId: UUID
static func authenticate(jwt: JWTType, on connection: DatabaseConnectable) -> Future<AuthJWTAuthenticatable?> {
return AuthJWTAuthenticatable(userId: jwt.uid.value)
}
}
With this approach, I’d have to fetch the user in each controller that needed the full User
.
Creating Some Middleware to Authenticate Incoming Requests
Now that I have a payload and the ability to authenticate it, I need to hook into the request handling and authenticate all incoming requests. As with most web frameworks, Vapor provides middleware as a solution for hooking into the request handling. With that in mind, here’s my middleware for authenticating incoming requests:
import Foundation
import Vapor
import JWT
import Authentication
struct JWTAuthenticationMiddleware<A: JWTAuthenticatable>: Middleware {
func respond(to req: Request, chainingTo next: Responder) throws -> Future<Response> {
// If the request is already authenticated, go to next middleware
if try req.isAuthenticated(A.self) {
return try next.respond(to: req)
}
// This parses the JWT string from the Authorization header
guard let tokenString = req.http.headers.bearerAuthorization?.token else {
// If there isn't a header, that's fine. Just go to the next middleware
return try next.respond(to: req)
}
// Use my helper type to decode the string into my JWTPayload struct
let token = try JWTSerialization<A.JWTType>().decode(tokenString, on: req)
// Ask the JWTAuthenticatable if this is a valid JWT
return A.authenticate(jwt: token, on: req).flatMap { a in
if let a = a {
// The JWT was valid, so this method call stashes the JWTAuthenticatable on the request
try req.authenticate(a)
}
// Regardless of if the JWT is valid or not, go to the next middleware
return try next.respond(to: req)
}
}
}
extension JWTAuthenticationMiddleware: ServiceType {
static func makeService(for worker: Container) throws -> JWTAuthenticationMiddleware {
return JWTAuthenticationMiddleware()
}
}
I based my authentication middleware on the authentication middlewares defined in the Authentication framework. The main difference is my middleware uses my JWTAuthenticatable
protocol to do the authentication. The goal of the middleware is to authenticate any Authorization header the request might have, and, if valid, store the resulting Authenticatable
on the request for future use. Notably, the middleware doesn’t try to authorize the caller (i.e. it doesn’t require a valid Authorization header). This is because I don’t yet know at this stage what the request is trying to do, and what level of authorization that would require.
I factored out the JWT deserialization into a helper type to keep the middleware a bit more focused. Here’s what the deserialization code looks like:
import Foundation
import Vapor
import JWT
enum AuthJWTError: Error {
case invalidDataEncoding
}
struct JWTSerialization<T: JWTPayload> {
func encode(_ payload: T, on container: Container) throws -> String {
let jwt = JWT(payload: payload)
let config = try container.make(JWTConfig.self)
let encoded = try jwt.sign(using: config.signer())
guard let jwtString = String(data: encoded, encoding: .utf8) else {
throw AuthJWTError.invalidDataEncoding
}
return jwtString
}
func decode(_ string: String, on container: Container) throws -> T {
let config = try container.make(JWTConfig.self)
let jwt = try JWT<T>(from: string, verifiedUsing: config.signer())
return jwt.payload
}
}
There’s not much to the decoding of JWTs. The JWT
type is defined by Vapor’s JWT framework and can both sign and verify the signature of a JWT. The JWTConfig
is a type that I created so I could abstract out the signing key and signing algorithm. I used Vapor’s dependency injection mechanism to fetch it. Here’s what it looks like:
import Foundation
import Vapor
import JWT
protocol JWTConfig: Service {
func signer() throws -> JWTSigner
}
class HmacJWTConfig: JWTConfig {
private let apiConfig: ApiConfigurationType
public init(apiConfig: ApiConfigurationType) {
self.apiConfig = apiConfig
}
func signer() throws -> JWTSigner {
return try JWTSigner.hs256(key: apiConfig.jwtKey.hexEncodedData())
}
}
The JWTConfig
is a Service
protocol that creates a JWTSigner
(defined in the JWT framework) on demand. I only have one concrete implementation in my app: HmacJWTConfig
. HmacJWTConfig
is pretty simple; it creates a JWTSigner.hs256
signer (which uses HMAC-SHA256) with a key it pulls from the app config. The config implementation isn’t that interesting; on production it pulls a hex encoded key from an environment variable.
I’ve almost got my app where it can authenticate any incoming requests. The last piece is the configuration:
try services.register(AuthenticationProvider())
services.register(JWTAuthenticationMiddleware<AuthJWTAuthenticatable>.self)
services.register(HmacJWTConfig(apiConfig: configuration.api), as: JWTConfig.self)
...
middlewares.use(JWTAuthenticationMiddleware<AuthJWTAuthenticatable>.self)
...
This is simply registering all the services and middleware I defined earlier.
Using the Authenticatable
Finally, I’m ready to use the Authenticatable
to see if a request is properly authorized. For my app, the requests coming in are user-scoped, meaning the user ID being acted on is in the request URL. A user should only be able to act on their own account. Therefore, my authorization method should make sure the request’s user ID parameter matches my JWT payload’s user ID.
Here’s what I do in my controller:
private func authorizedUser(_ request: Request) throws -> User {
let user = try request.requireAuthenticated(AuthJWTAuthenticatable.self).user
let userIdParameter = try request.parameters.next(UUID.self)
guard userIdParameter == user.id else {
throw Abort(.unauthorized, reason: "Unauthorized")
}
return user
}
The first line pulls out the AuthJWTAuthenticatable
that my middleware stashed there. If it’s missing, then requireAuthenticated
will throw an error. Next, I pull the request’s user ID parameter out. Finally, I compare the two to make sure they match. If they don’t, I abort with an unauthorized error.
This isn’t the only way to handle authorization. If I just wanted to make sure certain routes had authentication of some kind (i.e. requireAuthenticated
doesn’t throw an error), then I could make use of GuardAuthenticationMiddleware
defined in the Authentication framework.
And that’s about it for using JWTs to do authentication in Vapor 3. While Vapor has all the tools for JWT and authentication, they’re not put together in a way that works out of the box. For that reason, I’ve done a walkthrough here on how I put them together in my app.