Search code examples
swiftmiddlewarevaporvapor-fluent

Vapor 4/Swift: Middleware development


I'm completely new to Swift and Vapor;

I'm trying to develop an Authorization Middleware that is called from my UserModel (Fluent Model object) and is past in a Required Access Level (int) and Path (string) and

  1. Ensures the user is authenticated,
  2. Checks if the instance of the UserModel.Accesslevel is => the Passed in Required Access level and if not redirects the user to the 'Path'.

I have tried for a couple of days and always get close but never quite get what I'm after.

I have the following (please excuse the poorly named objects):

import Vapor

public protocol Authorizable: Authenticatable {
    associatedtype ProfileAccess: UserAuthorizable
}

/// status cached using  `UserAuthorizable'
public protocol UserAuthorizable: Authorizable {
    /// Session identifier type.
    associatedtype AccessLevel: LosslessStringConvertible

    /// Identifier identifier.
    var accessLevel: AccessLevel { get }
}

extension Authorizable {
    /// Basic middleware to redirect unauthenticated requests to the supplied path
    ///
    /// - parameters:
    ///    - path: The path to redirect to if the request is not authenticated
    public static func authorizeMiddleware(levelrequired: Int, path: String) -> Middleware {
            return AuthorizeUserMiddleware<Self>(Self.self, levelrequired: levelrequired, path: path)
    }
}

// Helper for creating authorization middleware.
///
//private final class AuthRedirectMiddleware<A>: Middleware
//where A: Authenticatable
final class AuthorizeUserMiddleware<A>: Middleware where A: Authorizable {
    let levelrequired: Int
    let path: String
    let authLevel : Int

init(_ authorizableType: A.Type = A.self, levelrequired: Int, path: String) {
    self.levelrequired = levelrequired
    self.path = path
}

/// See Middleware.respond
   public func respond(to req: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
        if req.auth.has(A.self) {
            print("--------")
            print(path)
            print(levelrequired)
**//            print(A.ProfileAccess.AccessLevel) <- list line fails because the A.ProfileAccess.AccessLevel is a type not value**
            print("--------")
        }
        return next.respond(to: req)
    }
}

I added the following to my usermodel

extension UserModel: Authorizable
{
    typealias ProfileAccess = UserModel
}

extension UserModel: UserAuthorizable {
    typealias AccessLevel = Int
    var accessLevel: AccessLevel { self.userprofile! }
}

And Route as such

    // setup the authentication process
    let session = app.routes.grouped([
      UserModelSessionAuthenticator(),
      UserModelCredentialsAuthenticator(),
      UserModel.authorizeMiddleware(levelrequired: 255, path: "/login"), // this will redirect the user when unauthenticted
      UserModel.authRedirectMiddleware(path: "/login"), // this will redirect the user when unauthenticted
    ])

The Path and required level get passed in correctly but I cannot get the instance of the AccessLevel from the current user. (I'm sure I have my c++ hat on and from what I can 'Guess' the UserModel is not actually a populated instance of a user even though the Authentication has already been completed)

I attempted to merge in the 'SessionAuthenticator' process of using a associatedtype to pass across the account info.

My other thought was to check if the user is authenticated and if so I can safely assume the Session Cookie contains my User Id and therefore I could pull the user from the DB (again) and check the users access level from there.

I could be way off here, after a couple of days I don't know which way is the best approach, any guidance would be greatly appreciated.

Cheers


Solution

  • I use the Vapor 4 approach of conforming my User model to ModelAuthenticatable:

    extension User:ModelAuthenticatable
    {
        static let usernameKey = \User.$email
        static let passwordHashKey = \User.$password
    
        func verify(password: String) throws -> Bool
        {
            try Bcrypt.verify(password, created:self.password)
        }
    }
    

    At the point where I have checked that the user exists and the password is verified as above, then it logs the user in:

    request.auth.login(user)
    

    Once logged in, I use a custom Middleware that checks to see if the user is logged in and a 'superuser', if so then the response is passed to the next Middleware in the chain, otherwise it re-directs to the home/login page if not.

    struct SuperUserMiddleware:Middleware
    {
        func respond(to request:Request, chainingTo next:Responder) -> EventLoopFuture<Response>
        {
            do
            {
                let user = try request.auth.require(User.self)
                if user.superUser { return next.respond(to:request) }
            }
            catch {}
            let redirect = request.redirect(to:"UNPRIVILEGED")
            return request.eventLoop.makeSucceededFuture(redirect)
        }
    }
    

    I register the SuperUserMiddleware and use with certain groups of routes in configure.swift as:

    app.middleware.use(SessionsMiddleware(session:MemorySessions(storage:MemorySessions.Storage())))
    app.middleware.use(User.sessionAuthenticator(.mysql))
    let superUserMW = SuperUserMiddleware()
    let userAuthSessionsMW = User.authenticator()
    let events = app.grouped("/events").grouped(userAuthSessionsMW, superUserMW)
    try EventRoutes(events)