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.
Currently, if we want to secure routes with a
TokenAuthenticationMiddleware
we can only specify a singleTokenAuthenticatable
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
orBearerAuthenticationMiddleware
, 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 withunauthorized
.I've been playing around with this a bit and here is the rough version of how I managed to achieve this.
NonThrowingBearerAuthenticationMiddleware
ThrowIfNoneAuthenticatedMiddleware
Route
Above routes will be protected with bearer authentication, allowing only
Admin
andUser
types to access them and throwingunauthorized
for everyone else.