Reputation: 51
I've done a lot of research through Firebase documentation, Swift/Xcode documentation, and here at Stack Overflow. Unfortunately, I have not been able to find a coherent and working solution to this problem--and it seems like this problem has been attempted many times in many different ways.
I'm not a professional coder. Needless to say, I'm figuring this out and teaching my children as we go along with this as a coding project to help them learn.
End goal: Allow swift/xcode application to create a user, but only if that user's userName is not already taken.
Relevant Stack Overflow discussions:
I will attach my current Swift code and I will update it as I make progress on this issue. The current code is for an Xcode view controller for a signup process. This code creates two different collections in the firestore database: "users"--which contains the user's profile information, and "usernames"--which is essentially an index of all usernames.
This index (collection: usernames) has a document named after each username. The idea is to create a firebase rule that will not allow a user to create an account in which username = (name of document in collection: usernames).
It seems like there may be a couple of ways to accomplish the end goal. One is organically in the swift code, but that may have security issues as the code could potentially be bypassed by a nefarious actor. The other way is through rules in the firebase database, but I'm not as fluent in doing this (I'll do more research and report back). Perhaps the best course of action would be to do both.
This is the current code.
Version 1--the initial upload of code with the stated problem.
// SignUpViewController.swift
// Copyright © 2020 by Mix. All rights reserved.
// Version 1.
import UIKit
import Firebase
import FirebaseAuth
import FirebaseFirestore
class SignUpViewController: UIViewController, UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource {
@IBOutlet weak var signUpButton: UIButton!
@IBOutlet weak var errorLabel: UILabel!
@IBOutlet weak var userNameTextField: UITextField!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var verifyPassTextField: UITextField!
@IBOutlet weak var firstNameTextField: UITextField!
@IBOutlet weak var lastNameTextField: UITextField!
@IBOutlet weak var stateTextField: UITextField!
@IBOutlet weak var birthdayTextField: UITextField!
let datePicker = UIDatePicker()
let id = Auth.auth().currentUser!.uid
let email = Auth.auth().currentUser!.email
let state_arr = ["", "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY"]
//picker view
let statePickerView = UIPickerView()
//Hold Current arr
var currentArr : [String] = []
//that hold current text field
var activeTextField : UITextField?
func createDatePicker() {
let toolbar = UIToolbar()
toolbar.sizeToFit()
let done2Button = UIBarButtonItem(title: "Select", style: .plain, target: self, action: #selector(doneTapped2))
let space2Button = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
toolbar.setItems([space2Button, done2Button], animated: false)
birthdayTextField.inputAccessoryView = toolbar
birthdayTextField.inputView = datePicker
datePicker.datePickerMode = .date
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
activeTextField = textField
switch textField {
case stateTextField:
currentArr = state_arr
default:
print("Default")
}
return true
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return currentArr.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return currentArr[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
print("Selected item is", currentArr[row])
activeTextField!.text = currentArr[row]
}
func createToolbar() {
let toolbar = UIToolbar()
toolbar.barStyle = .default
toolbar.sizeToFit()
let doneButton = UIBarButtonItem(title: "Select", style: .plain, target: self, action: #selector(doneTapped))
let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelTapped))
toolbar.setItems([cancelButton,spaceButton, doneButton], animated: false)
stateTextField.inputAccessoryView = toolbar
}
@objc func doneTapped() {
activeTextField?.resignFirstResponder()
}
@objc func cancelTapped() {
activeTextField?.resignFirstResponder()
}
override func viewDidLoad() {
super.viewDidLoad()
self.errorLabel.alpha = 0
self.navigationController?.setNavigationBarHidden(false, animated: false)
setUpElements()
createToolbar()
createDatePicker()
stateTextField.delegate = self
statePickerView.delegate = self
statePickerView.dataSource = self
stateTextField.inputView = statePickerView
}
func setUpElements() {
Utilities.styleFilledButton(signUpButton)
}
func validateFields() -> String? {
if firstNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
lastNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
emailTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
verifyPassTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
userNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
stateTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
birthdayTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == ""
{
return "Please fill in all fields."
}
let cleanedPassword = passwordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
if Utilities.isPasswordValid(cleanedPassword) == false {
// Password isn't secure enough
return "Please make sure your password is at least 8 characters, contains a special character and a number."
}
if passwordTextField.text != verifyPassTextField.text {
// Password isn't secure enough
return "Passwords do not match."
}
return nil
}
@IBAction func termsButtonTapped(_ sender: Any) {
self.view.endEditing(true)
}
@IBAction func policyButtonTapped(_ sender: Any) {
self.view.endEditing(true)
}
@IBAction func signUpTapped(_ sender: Any) {
self.view.endEditing(true)
verifyState()
self.errorLabel.alpha = 0
let error = validateFields()
if error != nil {
showError(error!)
}
else {
// Create cleaned versions of the data
let firstName = firstNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let lastName = lastNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let email = emailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let password = passwordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let username = userNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let state = stateTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let birthday = birthdayTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
// Create the user
Auth.auth().createUser(withEmail: email, password: password) { (result, err) in
// Check for errors
if err != nil {
// There was an error creating the user
self.showError("Error creating user")
}
else {
// User was created successfully, now store the first name and last name
let db = Firestore.firestore()
db.collection("users").document(Auth.auth().currentUser!.uid).setData( ["firstname":firstName, "lastname":lastName, "username":username, "email": email, "birthday":birthday, "state":state, "uid": Auth.auth().currentUser!.uid]) { (error) in
if error != nil {
// Show error message
self.showError("Error saving user data")
}
}
db.collection("usernames").document(username).setData( ["username":username, "email": email, "uid"
: Auth.auth().currentUser!.uid]) { (error) in
if error != nil {
// Show error message
self.showError("Error saving user data")
}
}
// Transition to the home screen
self.transitionToHome()
}
}
self.view.endEditing(true)
}
}
@IBAction func termsSwitch(_ sender: UISwitch) {
self.view.endEditing(true)
if (sender.isOn == true)
{signUpButton.isEnabled = true}
else if (sender.isOn == false)
{signUpButton.isEnabled = false}
}
func showError(_ message:String) {
errorLabel.text = message
errorLabel.alpha = 1
}
func transitionToHome() {
let HomeTabViewController = storyboard?.instantiateViewController(identifier: Constants.Storyboard.homeTabViewController) as? HomeTabViewController
view.window?.rootViewController = HomeTabViewController
view.window?.makeKeyAndVisible()
}
func verifyState() {
if stateTextField.text == "" {
self.showError("Select a State.")
return
}
}
func verifyPassword() {
if passwordTextField.text == verifyPassTextField.text {
}
else {
self.showError("Passwords do not match.")
return
}
}
@objc func doneTapped2() {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
birthdayTextField.text = formatter.string(from: datePicker.date)
self.view.endEditing(true)
}
}
Upvotes: 1
Views: 714
Reputation: 59
There is no out the box solution for this. You're already using Auth.auth().createUser
which will make sure that the email is unique but there's no alternative with usernames.
I believe rules in the database will not work as Authentication and Database are two separate products that don't read each others data. Database rules are there to prevent access to data in the database but not to authentication service which is where you will need to be checking for the usernames.
I think the easiest option if you're just learning it to do the checking on the client. While it may not be 100% best option in terms of security for a first pass it might be the best. This could be accomplished by fetching user IDs from the server every time the user changes the username and only letting the user continue to Auth.auth().createUser
code if the username does not yet exist.
A more involved option that would cover your security concern would be to use a Firebase Cloud Functions. These are small snippets of code (there is no swift option currently) that live on your firebase back-end and are run whenever an event or a call occurs. More info on cloud functions.
If you were to go that route there is a lot of guides online. I would go about it by creating a function that can be called from the app directly whenever the user tried to create a new account. I would pass the information the user is trying to sign up with as well and then have the checking for existing username and the actual account creation (Auth.auth().createUser
) on the back-end function as well and pass back only the information that the your app requires (was it successful, did it fail, what is the username id...). This would solve the security concerns but would be a quite significant increase of effort to make it work.
Upvotes: 4