Skip to content

Commit

Permalink
Merge pull request #324 from auth0/feature-username-validation
Browse files Browse the repository at this point in the history
Username validation improvements
  • Loading branch information
hzalaz authored Sep 20, 2016
2 parents 86734d1 + ee59d91 commit 6b65664
Show file tree
Hide file tree
Showing 21 changed files with 249 additions and 117 deletions.
2 changes: 1 addition & 1 deletion App/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class ViewController: UIViewController {
]
}
.withConnections { connections in
connections.database(name: "Username-Password-Authentication", requiresUsername: true)
connections.database(name: "Username-Password-Authentication", requiresUsername: true, usernameValidator: UsernameValidator(withLength: 1...20))
}
},
actionButton(withTitle: "LOGIN ONLY WITH DB") {
Expand Down
12 changes: 8 additions & 4 deletions Lock.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
5F2496B81D665AC500A1C6E2 /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496B71D665AC500A1C6E2 /* UserAttribute.swift */; };
5F2496BA1D665AE900A1C6E2 /* DatabaseAuthenticatableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496B91D665AE900A1C6E2 /* DatabaseAuthenticatableError.swift */; };
5F2496BC1D665AF700A1C6E2 /* DatabaseUserCreatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496BB1D665AF700A1C6E2 /* DatabaseUserCreatorError.swift */; };
5F2496BE1D67ADB300A1C6E2 /* ValidatorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496BD1D67ADB300A1C6E2 /* ValidatorSpec.swift */; };
5F2496BE1D67ADB300A1C6E2 /* EmailValidatorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2496BD1D67ADB300A1C6E2 /* EmailValidatorSpec.swift */; };
5F390E871D638A6D00FC549C /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F390E861D638A6D00FC549C /* Logger.swift */; };
5F390E8A1D63A5B800FC549C /* CleanroomLogger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F390E881D63A5B800FC549C /* CleanroomLogger.framework */; };
5F390E8B1D63A5B800FC549C /* CleanroomASL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F390E891D63A5B800FC549C /* CleanroomASL.framework */; };
Expand All @@ -51,6 +51,7 @@
5F5F98D81D22ED120016FC22 /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F5F98D71D22ED120016FC22 /* Presentable.swift */; };
5F5F98DA1D22EF490016FC22 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F5F98D91D22EF490016FC22 /* Router.swift */; };
5F5F98DC1D22F0B40016FC22 /* RouterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F5F98DB1D22F0B40016FC22 /* RouterSpec.swift */; };
5F6C01551D91656100198ACD /* UsernameValidatorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F6C01541D91656100198ACD /* UsernameValidatorSpec.swift */; };
5F70F1DF1D7904A3004698DA /* ConnectionBuildable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F70F1DE1D7904A3004698DA /* ConnectionBuildable.swift */; };
5F70F1E11D790500004698DA /* Connections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F70F1E01D790500004698DA /* Connections.swift */; };
5F70F1E31D79057A004698DA /* OfflineConnections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F70F1E21D79057A004698DA /* OfflineConnections.swift */; };
Expand Down Expand Up @@ -205,7 +206,7 @@
5F2496B71D665AC500A1C6E2 /* UserAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UserAttribute.swift; path = Lock/UserAttribute.swift; sourceTree = SOURCE_ROOT; };
5F2496B91D665AE900A1C6E2 /* DatabaseAuthenticatableError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DatabaseAuthenticatableError.swift; path = Lock/DatabaseAuthenticatableError.swift; sourceTree = SOURCE_ROOT; };
5F2496BB1D665AF700A1C6E2 /* DatabaseUserCreatorError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DatabaseUserCreatorError.swift; path = Lock/DatabaseUserCreatorError.swift; sourceTree = SOURCE_ROOT; };
5F2496BD1D67ADB300A1C6E2 /* ValidatorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidatorSpec.swift; sourceTree = "<group>"; };
5F2496BD1D67ADB300A1C6E2 /* EmailValidatorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailValidatorSpec.swift; sourceTree = "<group>"; };
5F390E861D638A6D00FC549C /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Logger.swift; path = Lock/Logger.swift; sourceTree = SOURCE_ROOT; };
5F390E881D63A5B800FC549C /* CleanroomLogger.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CleanroomLogger.framework; path = Carthage/Build/iOS/CleanroomLogger.framework; sourceTree = "<group>"; };
5F390E891D63A5B800FC549C /* CleanroomASL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CleanroomASL.framework; path = Carthage/Build/iOS/CleanroomASL.framework; sourceTree = "<group>"; };
Expand All @@ -227,6 +228,7 @@
5F5F98D71D22ED120016FC22 /* Presentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Presentable.swift; path = Lock/Presentable.swift; sourceTree = SOURCE_ROOT; };
5F5F98D91D22EF490016FC22 /* Router.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Router.swift; path = Lock/Router.swift; sourceTree = SOURCE_ROOT; };
5F5F98DB1D22F0B40016FC22 /* RouterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouterSpec.swift; sourceTree = "<group>"; };
5F6C01541D91656100198ACD /* UsernameValidatorSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsernameValidatorSpec.swift; sourceTree = "<group>"; };
5F70F1DE1D7904A3004698DA /* ConnectionBuildable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ConnectionBuildable.swift; path = Lock/ConnectionBuildable.swift; sourceTree = SOURCE_ROOT; };
5F70F1E01D790500004698DA /* Connections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Connections.swift; path = Lock/Connections.swift; sourceTree = SOURCE_ROOT; };
5F70F1E21D79057A004698DA /* OfflineConnections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OfflineConnections.swift; path = Lock/OfflineConnections.swift; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -592,7 +594,8 @@
5F92C6901D510AFE00CCE6C0 /* LazyImageSpec.swift */,
5F390E8C1D63B99300FC549C /* LoggerSpec.swift */,
5F2496AE1D66210500A1C6E2 /* LockViewControllerSpec.swift */,
5F2496BD1D67ADB300A1C6E2 /* ValidatorSpec.swift */,
5F2496BD1D67ADB300A1C6E2 /* EmailValidatorSpec.swift */,
5F6C01541D91656100198ACD /* UsernameValidatorSpec.swift */,
);
path = LockTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -949,7 +952,8 @@
buildActionMask = 2147483647;
files = (
5FE50DBF1D7A254000D82290 /* ConnectionLoadingPresenterSpec.swift in Sources */,
5F2496BE1D67ADB300A1C6E2 /* ValidatorSpec.swift in Sources */,
5F6C01551D91656100198ACD /* UsernameValidatorSpec.swift in Sources */,
5F2496BE1D67ADB300A1C6E2 /* EmailValidatorSpec.swift in Sources */,
5FBE5CCA1D3EA1380038536D /* MultifactorPresenterSpec.swift in Sources */,
5F92C68D1D50E47100CCE6C0 /* AuthStyleSpec.swift in Sources */,
5F92C68B1D4FE90F00CCE6C0 /* Auth0OAuth2InteractorSpec.swift in Sources */,
Expand Down
27 changes: 23 additions & 4 deletions Lock/CDNLoaderInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ struct CDNLoaderInteractor: RemoteConnectionLoader, Loggable {
let json = try NSJSONSerialization.JSONObjectWithData(jsonp.dataUsingEncoding(NSUTF8StringEncoding)!, options: []) as? JSONObject
self.logger.debug("Client configuration is \(json)")
let info = ClientInfo(json: json)
if let auth0 = info.auth0, let connection = auth0.connections.first {
let requiresUsername = connection.booleanValue(forKey: "requires_username")
connections.database(name: connection.name, requiresUsername: requiresUsername)
if let auth0 = info.auth0 {
auth0.connections.forEach { connection in
let requiresUsername = connection.booleanValue(forKey: "requires_username")
connections.database(name: connection.name, requiresUsername: requiresUsername, usernameValidator: connection.usernameValidation)
}
}
info.oauth2.forEach { strategy in
strategy.connections.forEach { connections.social(name: $0.name, style: AuthStyle.style(forStrategy: strategy.name, connectionName: $0.name)) }
Expand Down Expand Up @@ -149,6 +151,23 @@ private struct ConnectionInfo {
var name: String { return json["name"] as! String }

func booleanValue(forKey key: String, defaultValue: Bool = false) -> Bool { return json[key] as? Bool ?? defaultValue }

var usernameValidation: UsernameValidator {
let validation = json["validation"] as? JSONObject
let username = validation?["username"] as? JSONObject
switch (username?["min"], username?["max"]) {
case let (min as Int, max as Int):
return UsernameValidator(withLength: min...max)
case let (minString as String, maxString as String):
guard
let min = Int(minString),
let max = Int(maxString)
else { return UsernameValidator() }
return UsernameValidator(withLength: min...max)
default:
return UsernameValidator()
}
}
}

private func cdnURL(from url: NSURL) -> NSURL {
Expand All @@ -157,4 +176,4 @@ private func cdnURL(from url: NSURL) -> NSURL {
guard components.count == 4 else { return NSURL(string: "https://cdn.auth0.com")! }
let region = components[1]
return NSURL(string: "https://cdn.\(region).auth0.com")!
}
}
22 changes: 19 additions & 3 deletions Lock/ConnectionBuildable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ public protocol ConnectionBuildable: Connections {
/**
Configure a database connection
- parameter name: name of the database connection
- parameter requiresUsername: if the database connection requires username
- parameter name: name of the database connection
- parameter requiresUsername: if the database connection requires username
- parameter usernameValidator: custom validator for username. Connection must allow username
- important: Only **ONE** database connection can be used so subsequent calls will override the previous value
*/
mutating func database(name name: String, requiresUsername: Bool)
mutating func database(name name: String, requiresUsername: Bool, usernameValidator: UsernameValidator)

/**
Adds a new social connection
Expand All @@ -55,3 +56,18 @@ public protocol ConnectionBuildable: Connections {
*/
mutating func oauth2(name name: String, style: AuthStyle)
}

public extension ConnectionBuildable {
/**
Configure a database connection
- parameter name: name of the database connection
- parameter requiresUsername: if the database connection requires username
- parameter usernameValidator: custom validator for username. Connection must allow username and defaults to 1..15 characters
- important: Only **ONE** database connection can be used so subsequent calls will override the previous value
*/
public mutating func database(name name: String, requiresUsername: Bool, usernameValidator: UsernameValidator = UsernameValidator()) {
self.database(name: name, requiresUsername: requiresUsername, usernameValidator: usernameValidator)
}

}
7 changes: 7 additions & 0 deletions Lock/Connections.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ public protocol Connections {
public struct DatabaseConnection {
public let name: String
public let requiresUsername: Bool
public let usernameValidator: UsernameValidator

public init(name: String, requiresUsername: Bool, usernameValidator: UsernameValidator = UsernameValidator()) {
self.name = name
self.requiresUsername = requiresUsername
self.usernameValidator = usernameValidator
}
}

public protocol OAuth2Connection {
Expand Down
3 changes: 1 addition & 2 deletions Lock/DatabaseAuthenticatableError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ enum DatabaseAuthenticatableError: ErrorType, LocalizableError {
case PasswordChangeRequired
case PasswordLeaked
case TooManyAttempts
case NoDatabaseConnection
case MultifactorRequired
case MultifactorInvalid

Expand Down Expand Up @@ -61,4 +60,4 @@ enum DatabaseAuthenticatableError: ErrorType, LocalizableError {
return true
}
}
}
}
4 changes: 2 additions & 2 deletions Lock/DatabaseForgotPasswordPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class DatabaseForgotPasswordPresenter: Presentable, Loggable {
try self.interactor.updateEmail(input.text)
input.showValid()
} catch let error as InputValidationError {
input.showError(error.localizedMessage)
input.showError(error.localizedMessage(withConnection: self.database))
} catch {
input.showError()
}
Expand Down Expand Up @@ -80,4 +80,4 @@ class DatabaseForgotPasswordPresenter: Presentable, Loggable {
}
return view
}
}
}
16 changes: 7 additions & 9 deletions Lock/DatabaseInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ struct DatabaseInteractor: DatabaseAuthenticatable, DatabaseUserCreator, Loggabl
var validPassword: Bool { return self.user.validPassword }

let authentication: Authentication
let connections: Connections
let connection: DatabaseConnection
let emailValidator: InputValidator = EmailValidator()
let usernameValidator: InputValidator = UsernameValidator()
let passwordValidator: InputValidator = NonEmptyValidator()
let onAuthentication: Credentials -> ()
let options: Options
let customFields: [String: CustomTextField]

init(connections: Connections, authentication: Authentication, user: DatabaseUser, options: Options, callback: Credentials -> ()) {
init(connection: DatabaseConnection, authentication: Authentication, user: DatabaseUser, options: Options, callback: Credentials -> ()) {
self.authentication = authentication
self.connections = connections
self.connection = connection
self.onAuthentication = callback
self.user = user
self.options = options
Expand Down Expand Up @@ -95,21 +95,19 @@ struct DatabaseInteractor: DatabaseAuthenticatable, DatabaseUserCreator, Loggabl
}

guard let password = self.password where self.validPassword else { return callback(.NonValidInput) }
guard let databaseName = self.connections.database?.name else { return callback(.NoDatabaseConnection) }

self.authentication
.login(
usernameOrEmail: identifier,
password: password,
connection: databaseName,
connection: self.connection.name,
scope: self.options.scope,
parameters: self.options.parameters
)
.start { self.handleLoginResult($0, callback: callback) }
}

func create(callback: (DatabaseUserCreatorError?, DatabaseAuthenticatableError?) -> ()) {
guard let connection = self.connections.database else { return callback(.NoDatabaseConnection, nil) }
let databaseName = connection.name

guard
Expand Down Expand Up @@ -158,21 +156,21 @@ struct DatabaseInteractor: DatabaseAuthenticatable, DatabaseUserCreator, Loggabl
}
}

private mutating func updateEmail(value: String?) -> InputValidationError? {
private mutating func updateEmail(value: String?) -> ErrorType? {
self.user.email = value?.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
let error = self.emailValidator.validate(value)
self.user.validEmail = error == nil
return error
}

private mutating func updateUsername(value: String?) -> InputValidationError? {
private mutating func updateUsername(value: String?) -> ErrorType? {
self.user.username = value?.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
let error = self.usernameValidator.validate(value)
self.user.validUsername = error == nil
return error
}

private mutating func updatePassword(value: String?) -> InputValidationError? {
private mutating func updatePassword(value: String?) -> ErrorType? {
self.user.password = value
let error = self.passwordValidator.validate(value)
self.user.validPassword = error == nil
Expand Down
4 changes: 2 additions & 2 deletions Lock/DatabasePresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class DatabasePresenter: Presentable, Loggable {
try self.authenticator.update(attr, value: input.text)
input.showValid()
} catch let error as InputValidationError {
input.showError(error.localizedMessage)
input.showError(error.localizedMessage(withConnection: self.database))
} catch {
input.showError()
}
Expand All @@ -205,4 +205,4 @@ private func safariBuilder(forURL url: NSURL, navigator: Navigable) -> (UIAlertA
let safari = SFSafariViewController(URL: url)
navigator.present(safari)
}
}
}
3 changes: 1 addition & 2 deletions Lock/DatabaseUserCreatorError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ enum DatabaseUserCreatorError: ErrorType, LocalizableError {
case PasswordHasUserInfo
case PasswordInvalid
case PasswordAlreadyUsed
case NoDatabaseConnection

var localizableMessage: String {
switch self {
Expand All @@ -57,4 +56,4 @@ enum DatabaseUserCreatorError: ErrorType, LocalizableError {
return true
}
}
}
}
7 changes: 4 additions & 3 deletions Lock/InputValidationError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ enum InputValidationError: ErrorType {
case NotAUsername
case NotAOneTimePassword

var localizedMessage: String {
func localizedMessage(withConnection connection: DatabaseConnection) -> String {
switch self {
case .NotAUsername:
return "Can only contain between 1 to 15 alphanumeric characters and \'_\'.".i18n(key: "com.auth0.lock.input.username.error", comment: "invalid username")
let format = "Can only contain between %d to %d alphanumeric characters and \'_\'.".i18n(key: "com.auth0.lock.input.username.error", comment: "invalid username")
return String(format: format, connection.usernameValidator.min, connection.usernameValidator.max)
case .NotAnEmailAddress:
return "Must be a valid email address".i18n(key: "com.auth0.lock.input.email.error", comment: "invalid email")
case .MustNotBeEmpty:
Expand All @@ -40,4 +41,4 @@ enum InputValidationError: ErrorType {
return "Must be a valid numeric code".i18n(key: "com.auth0.lock.input.otp.error", comment: "invalid otp")
}
}
}
}
4 changes: 2 additions & 2 deletions Lock/MultifactorPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class MultifactorPresenter: Presentable, Loggable {
try self.interactor.setMultifactorCode(input.text)
input.showValid()
} catch let error as InputValidationError {
input.showError(error.localizedMessage)
input.showError(error.localizedMessage(withConnection: self.database))
} catch {
input.showError()
}
Expand Down Expand Up @@ -75,4 +75,4 @@ class MultifactorPresenter: Presentable, Loggable {
view.primaryButton?.onPress = action
return view
}
}
}
Loading

0 comments on commit 6b65664

Please sign in to comment.