New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multitype capability to TokenAuthenticationMiddleware #34

Closed
damirstuhec opened this Issue Apr 7, 2018 · 3 comments

Comments

4 participants
@damirstuhec
Copy link

damirstuhec commented Apr 7, 2018

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

This comment has been minimized.

Copy link
Member

0xTim commented Apr 7, 2018

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

@tanner0101 tanner0101 referenced this issue May 9, 2018

Merged

model cleanup #39

3 of 3 tasks complete
@Sorix

This comment has been minimized.

Copy link
Contributor

Sorix commented May 16, 2018

Don't we have that now with GuardMiddleware?

@tanner0101

This comment has been minimized.

Copy link
Member

tanner0101 commented May 16, 2018

Yes, this was fixed in RC 4, thanks!

@tanner0101 tanner0101 closed this May 16, 2018

@tanner0101 tanner0101 added this to the 2.0.0-rc.4 milestone May 16, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment