Search code examples
basic-authenticationcontent-typevapor

How can I protect my routes when signing up a new user in Vapor 4?


I would like to know if I am using the best practices here as I send the user credentials in a Content-Type: application/json way, without protecting the password. I would like to know if I can protect my route with a middleware and make the user signing up in the safest way possible on my Vapor 4 application.

Can I use Authorization: Basic to sign up a user on my Vapor app ? Is it the normal flow that I am doing here?

This is the way I sign up a user. There is my table with the UserModel, and this is the extension to handle the sign up flow.

extension UserModel {
  //  MARK: UserSignUp
  /// Values needed for a user to sign up.
  ///
  struct SignUp {

    let email: String
    let password: String
  }
}

extension UserModel.SignUp: Codable {}

extension UserModel.SignUp: Validatable {
  /// Setting up sign up rules for email and password to validate
  /// new user.
  ///
  static func validations(_ validations: inout Validations) {
    validations.add("email", as: String.self, is: .email)
    validations.add("password", as: String.self, is: .count(1...))
  }
}

The route where I would like to protect it to safely pass user credentials.

struct UsersAuthController: RouteCollection {
  func boot(routes: RoutesBuilder) throws {

    let unprotectedRoute = routes.grouped("users")
    unprotectedRoute.post("signup", use: signUp)
  }
}

And here is the way I sign up the user in the Vapor app. Is this the safest way?

extension UsersAuthController {
  func signUp(_ req: Request) throws -> EventLoopFuture<UserModel.NewSession> {

    try UserModel.SignUp.validate(content: req)
    let signUpUser = try req.content.decode(UserModel.SignUp.self)
    let user = try create(from: signUpUser)
    var token: UserTokenModel!

    return checkIfUserExists(signUpUser.email, req: req)
      .flatMapThrowing { exists -> UserModel in
        guard !exists else {
          throw Abort(.conflict)
        }
        return user }
      .flatMap { $0.save(on: req.db) }
      .flatMapThrowing { _ -> UserTokenModel in
        guard let newToken = try? user.createUserToken(source: .signUp) else {
          throw Abort(.internalServerError)
        }
        token = newToken
        return token }
      .flatMap { $0.save(on: req.db) }
      .flatMapThrowing {
        UserModel.NewSession(token: token.value,
                             user: try user.asPublic())  // Just returning a token and use id.
      }
  }

  func create(from user: UserModel.SignUp) throws -> UserModel {
    UserModel(email: user.email,
              password: try Bcrypt.hash(user.password))
  }
}

Solution

  • What are you trying to protect from? There's no real difference between sending the username and password in the JSON body or as a HTTP Basic credentials header. Otherwise things look OK, though I'd nest some of the futures to avoid having var token: UserTokenModel!