Skip to content

Commit

Permalink
fixes
Browse files Browse the repository at this point in the history
- tests passing
- tokens use URandom and SHA256
- passwords use bcrypt with verification
  • Loading branch information
rafiki270 committed Sep 11, 2018
1 parent d135777 commit d2f633f
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 27 deletions.
3 changes: 3 additions & 0 deletions Package.swift
Expand Up @@ -12,6 +12,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/core.git", from: "3.4.1"),
.package(url: "https://github.com/vapor/crypto.git", from: "3.2.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0-rc.4"),
.package(url: "https://github.com/vapor/jwt.git", from: "3.0.0-rc.2"),
Expand Down Expand Up @@ -39,6 +40,8 @@ let package = Package(
.target(name: "ApiCore", dependencies: [
"Vapor",
"Fluent",
"Crypto",
"Random",
"FluentPostgreSQL",
"ErrorsCore",
"DbCore",
Expand Down
14 changes: 8 additions & 6 deletions Sources/ApiCore/Controllers/AuthController.swift
Expand Up @@ -95,7 +95,7 @@ public class AuthController: Controller {

let templateModel = User.Auth.RecoveryTemplate(
verification: jwtToken,
link: recoveryData.targetUri ?? "" + "?token=" + jwtToken,
link: (recoveryData.targetUri ?? "invalid_target_uri") + "?token=" + jwtToken,
user: user
)
return try PasswordRecoveryEmailTemplate.parsed(model: templateModel, on: req).flatMap(to: Response.self) { template in
Expand Down Expand Up @@ -192,13 +192,15 @@ extension AuthController {
guard let user = user, let password = user.password, login.password.verify(against: password) else {
throw AuthError.authenticationFailed
}
let token = try Token(user: user)
let tokenBackup = token
let token = try Token(user: user, type: .authentication)
let tokenBackup = token.token
token.token = try token.token.sha()
return token.save(on: req).flatMap(to: Response.self) { token in
tokenBackup.id = token.id

let publicToken = Token.PublicFull(token: tokenBackup, user: user)
guard let _ = token.id else {
throw AuthError.serverError
}
let publicToken = Token.PublicFull(token: token, user: user)
publicToken.token = tokenBackup
return try publicToken.asResponse(.ok, to: req).map(to: Response.self) { response in
let jwtService = try req.make(JWTService.self)
try response.http.headers.replaceOrAdd(name: "Authorization", value: "Bearer \(jwtService.signUserToToken(user: user))")
Expand Down
17 changes: 17 additions & 0 deletions Sources/ApiCore/Extensions/Data+Tools.swift
@@ -0,0 +1,17 @@
//
// Data+Tools.swift
// ApiCore
//
// Created by Ondrej Rafaj on 11/09/2018.
//

import Foundation


extension Data {

public func asUTF8String() -> String? {
return String(data: self, encoding: .utf8)
}

}
4 changes: 2 additions & 2 deletions Sources/ApiCore/Libs/AuthenticationCache.swift
Expand Up @@ -38,7 +38,7 @@ struct JWTConfirmEmailPayload: JWTPayload {
var exp: ExpirationClaim

/// User Id
var userId: UUID
var userId: UUID?

/// User email
var email: String
Expand Down Expand Up @@ -112,7 +112,7 @@ final class JWTService: Service {
/// Sign any email confirmation JWT token
func signEmailConfirmation(user: User, type: TokenType, redirectUri: String?) throws -> String {
let exp = ExpirationClaim(value: Date(timeIntervalSinceNow: (36 * hour)))
var jwt = JWT(payload: JWTConfirmEmailPayload(exp: exp, userId: user.id!, email: user.email, type: type, redirectUri: redirectUri ?? ""))
var jwt = JWT(payload: JWTConfirmEmailPayload(exp: exp, userId: user.id, email: user.email, type: type, redirectUri: redirectUri ?? ""))

jwt.header.typ = nil // set to nil to avoid dictionary re-ordering causing probs
let data = try signer.sign(jwt)
Expand Down
35 changes: 21 additions & 14 deletions Sources/ApiCore/Model/Token.swift
Expand Up @@ -11,6 +11,7 @@ import Fluent
import FluentPostgreSQL
import DbCore
import ErrorsCore
import Random


/// Tokens array type typealias
Expand All @@ -21,7 +22,7 @@ public typealias Tokens = [Token]
public final class Token: DbCoreModel {

/// Token type
public enum TokenType: String, Codable, CaseIterable, ReflectionDecodable {
public enum TokenType: String, PostgreSQLRawEnum {

/// Authentication
case authentication = "auth"
Expand All @@ -41,14 +42,17 @@ public final class Token: DbCoreModel {
/// User Id is missing
case missingUserId

/// HTTP status
public var status: HTTPStatus {
return .preconditionFailed
}

/// Error identifier
public var identifier: String {
return "token.missing_user_id"
}

/// Reason for failure
public var reason: String {
return "User ID is missing"
}
Expand All @@ -70,15 +74,17 @@ public final class Token: DbCoreModel {

/// Token expiry date
public var expires: Date
public var type: TokenType

/// Token type
// public var type: TokenType

/// Initializer
public init(token: Token, user: User) {
self.id = token.id
self.user = User.Display(user)
self.token = token.token
self.expires = token.expires
self.type = token.type
// self.type = token.type
}
}

Expand All @@ -94,19 +100,21 @@ public final class Token: DbCoreModel {
public var expires: Date

/// Token type
public var type: TokenType
// public var type: TokenType

/// Initializer
public init(token: Token) {
self.id = token.id
self.token = token.token
self.expires = token.expires
self.type = token.type
// self.type = token.type
}
}

/// Displayable public object
/// for security reasons, the original object should never be displayed
public final class Public: DbCoreModel {

/// Object id
public var id: DbCoreIdentifier?

Expand Down Expand Up @@ -135,30 +143,29 @@ public final class Token: DbCoreModel {

/// Token expiry date
public var expires: Date
public var type: TokenType

/// Initializer
convenience init(user: User) throws {
try self.init(user: user, type: .authentication)
}
/// Token type
// public var type: TokenType

/// Initializer
init(user: User, type: TokenType) throws {
guard let userId = user.id else {
throw Error.missingUserId
}
self.userId = userId
self.token = ":)"
let randData = try URandom().generateData(count: 60)
let rand = randData.base64EncodedString()
self.token = String(rand.prefix(60))
self.expires = Date().addMonth(n: 1)
self.type = type
// self.type = type
}

enum CodingKeys: String, CodingKey {
case id
case userId = "user_id"
case token
case expires
case type
// case type
}

}
Expand All @@ -174,7 +181,7 @@ extension Token: Migration {
schema.field(for: \.userId, type: .uuid, .notNull)
schema.field(for: \.token, type: .varchar(64), .notNull)
schema.field(for: \.expires, type: .timestamp, .notNull)
schema.field(for: \.type, type: .varchar(4), .notNull)
// schema.field(for: \.type, type: .varchar(4), .notNull)
}
}

Expand Down
Expand Up @@ -50,6 +50,11 @@ extension TestableProperty where TestableType: Application {

try! ApiCoreBase.configure(&config, &env, &services)

// Check the database ... if it doesn't contain test then make sure we are not pointing to a production DB
if !ApiCoreBase.configuration.database.database.contains("test") {
ApiCoreBase.configuration.database.database = ApiCoreBase.configuration.database.database + "-test"
}

// Set mailer mock
MailerMock(services: &services)

Expand Down
8 changes: 4 additions & 4 deletions Tests/ApiCoreTests/Controllers/UsersControllerTests.swift
Expand Up @@ -89,27 +89,27 @@ class UsersControllerTests: XCTestCase, UsersTestCase, LinuxTests {
XCTAssertEqual(user.firstname, post.firstname, "Firstname doesn't match")
XCTAssertEqual(user.lastname, post.lastname, "Lastname doesn't match")
XCTAssertEqual(user.email, post.email, "Email doesn't match")
XCTAssertEqual(user.password, try! post.password.passwordHash(r.request), "Password doesn't match")
XCTAssertTrue(post.password.verify(against: user.password!), "Password doesn't match")
XCTAssertEqual(user.disabled, false, "Disabled should be false")
XCTAssertEqual(user.su, false, "SU should be false")

// Test email has been sent (on a mock email client ... obviously)
let mailer = try! r.request.make(MailerService.self) as! MailerMock
XCTAssertEqual(mailer.receivedMessage!.from, "ondrej.rafaj@gmail.com", "Email has a wrong sender")
XCTAssertEqual(mailer.receivedMessage!.from, "admin@apicore", "Email has a wrong sender")
XCTAssertEqual(mailer.receivedMessage!.to, "lemmy@liveui.io", "Email has a wrong recipient")
XCTAssertEqual(mailer.receivedMessage!.subject, "Registration", "Email has a wrong subject")

let token = String(mailer.receivedMessage!.text.split(separator: "|")[1])

XCTAssertEqual(mailer.receivedMessage!.text, """
Hi Lemmy Kilmister
Please confirm your email lemmy@liveui.io by clicking on this link http://www.liveui.io/fake_url
Please confirm your email lemmy@liveui.io by clicking on this link http://localhost:8080/users/verify?token=\(token)
Verification code is: |\(token)|
Boost team
""", "Email has a wrong text")
XCTAssertEqual(mailer.receivedMessage!.html, """
<h1>Hi Lemmy Kilmister</h1>
<p>Please confirm your email lemmy@liveui.io by clicking on this <a href=\"http://www.liveui.io/fake_url\">link</a></p>
<p>Please confirm your email lemmy@liveui.io by clicking on this <a href=\"http://localhost:8080/users/verify?token=\(token)\">link</a></p>
<p>Verification code is: <strong>\(token)</strong></p>
<p>Boost team</p>
""", "Email has a wrong html")
Expand Down
2 changes: 1 addition & 1 deletion Tests/ApiCoreTests/Libs/StringCryptoTests.swift
Expand Up @@ -37,7 +37,7 @@ final class StringCryptoTests : XCTestCase {
func testPasswordHash() throws {
let req = app.testable.fakeRequest()
let hashed = try! "password".passwordHash(req)
XCTAssert(hashed == "password")
XCTAssertTrue("password".verify(against: hashed), "Hashed password is invalid")
}

}

0 comments on commit d2f633f

Please sign in to comment.