vapor / auth

👤 Authentication and Authorization framework for Fluent.
53 stars 34 forks source link

Add multitype capability to TokenAuthenticationMiddleware #34

Closed damirstuhec closed 6 years ago

damirstuhec commented 6 years ago

Currently, if we want to secure routes with a TokenAuthenticationMiddleware we can only specify a single TokenAuthenticatable type, which we require to be authenticated in order to execute the route.

In multi-role systems (ie. blog), we usually have multiple different user types (which do not necessarily share a common superclass). Therefore, we often want to secure routes with a middleware that would allow us to specify multiple different types where one of them has to be authenticated for the route to be executed.

After some discussion with @tanner0101, a possible solution could be a new "non-throwing" TokenAuthenticationMiddleware or BearerAuthenticationMiddleware, which would authenticate a specified user type if possible, otherwise just continue, instead of throwing. Having this "non-throwing" middleware, it would allow us to compose aka chain middlewares in order to achieve the "multitype authentication" capability. At the end of the chain, we would of course still need a throwing middleware that would check if one of the types has been authenticated otherwise finally throw with unauthorized.


I've been playing around with this a bit and here is the rough version of how I managed to achieve this.

NonThrowingBearerAuthenticationMiddleware

public final class NonThrowingBearerAuthenticationMiddleware<A>: Middleware where A: BearerAuthenticatable {

    public func respond(to req: Request, chainingTo next: Responder) throws -> Future<Response> {

        if try req.isAuthenticated(A.self) {
            return try next.respond(to: req)
        }

        guard let token = req.http.headers.bearerAuthorization else {
            // TODO: Throw AuthenticationError when initializer is exposed publicly
            // @tanner0101 is AuthenticationError intentionally hidden?
            return try next.respond(to: req)
        }

        // auth token on connection
        return A.authenticate(using: token, on: req).flatMap(to: Response.self) { a in
            guard let a = a else {
                return try next.respond(to: req)
            }
            // set authed on request
            try req.authenticate(a)
            return try next.respond(to: req)
        }
    }
}

extension BearerAuthenticatable {

    public static func nonThrowingBearerAuthenticationMiddleware(
        database: DatabaseIdentifier<Database>? = nil
        ) -> NonThrowingBearerAuthenticationMiddleware<Self> {
        return .init()
    }
}

ThrowIfNoneAuthenticatedMiddleware

public final class ThrowIfNoneAuthenticatedMiddleware: Middleware {

    public enum UserType {
        case admin
        case user
        case moderator
    }

    private let allowedUserTypes: [UserType]

    /// Create a new `ThrowIfNoneAuthenticatedMiddleware`
    public init(allowedUserTypes: [UserType]) {
        self.allowedUserTypes = allowedUserTypes
    }

    /// See Middleware.respond
    public func respond(to req: Request, chainingTo next: Responder) throws -> Future<Response> {

        for userType in allowedUserTypes {
            switch userType {
            case .admin:
                if try req.isAuthenticated(Admin.TokenType.self) {
                    return try next.respond(to: req)
                }
            case .user:
                if try req.isAuthenticated(User.TokenType.self) {
                    return try next.respond(to: req)
                }
            case .moderator:
                if try req.isAuthenticated(Moderator.TokenType.self) {
                    return try next.respond(to: req)
                }
            }
        }
        throw Abort(.unauthorized, reason: "Invalid credentials")
    }
}

Route

let routes = router.grouped("api", "posts")
            .grouped(Admin.TokenType.nonThrowingBearerAuthenticationMiddleware())
            .grouped(User.TokenType.nonThrowingBearerAuthenticationMiddleware())
            .grouped(ThrowIfNoneAuthenticatedMiddleware(allowedUserTypes: [.admin, .user]))

Above routes will be protected with bearer authentication, allowing only Admin and User types to access them and throwing unauthorized for everyone else.

0xTim commented 6 years ago

This should probably live in an Authorization module (i.e. in the module in this repo that is yet to be created)

Sorix commented 6 years ago

Don't we have that now with GuardMiddleware?

tanner0101 commented 6 years ago

Yes, this was fixed in RC 4, thanks!