Vincular distintos proveedores de autenticación en Firebase
Vincular distintos proveedores de autenticación en Firebase

🔥 FIREBASE AUTHENTICATION, VINCULA Cuentas de Facebook, Google, etc

Vincular cuentas con Firebase Authentication nos permite que un user pueda acceder a nuestra app desde distintos proveedores como Email y Password, Facebook, Twitter, Google, etc. Nuestro Login es más flexible permitiendo a un user que escoja qué proveedor quiere utilizar.

SwiftBeta

Tabla de contenido

Tutorial iOS Firebase desde cero

Hoy lo que vamos a ver en SwiftBeta es a como vincular diferentes proveedores de autenticación. ¿Esto qué significa? imagina que te registras en Firebase con el proveedor de Email y Password, si te deslogueas y quieres usar otro proveedor como Facebook o Twitter y estás usando el mismo email, la autenticación con Firebase fallará. Aparecerá un mensaje como el siguiente:

An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address.

Para poder vincular varias cuentas y que un user comparta la misma sesión con diferentes proveedores (entendiendo proveedor como Facebook, Twitter, Google, Github, etc), primero tendremos que autenticarnos con el proveedor con el que nos hayamos registrado, y segundo dentro de la app tendremos que vincular la cuenta con el nuevo proveedor. Esto es muy útil para que un user pueda acceder de varias formas a nuestra app y se comparta la sesión.
¿Qué pasos seguiremos?

  1. Estar logueados con nuestro primer proveedor, por ejemplo, Email y Password
  2. Autenticarnos con otro proveedor
  3. Vincular los credenciales

Para poder autenticarnos con otro proveedor mientras estamos logueados con Email y Password, crearemos un TabView y en una de las opciones del TabView añadiré un Form con la opción de poder vincular una cuenta de Facebook. También aparecerá poder vincular con Email y Password. Pero nos vamos a centrar en el caso de que nos hemos registrado con Email y Password y queremos vincular nuestra cuenta de Facebook (cuando tengamos esta parte haremos el caso contrario, nos registraremos con Facebook y luego vincularemos nuestro Email y Password).

Para hacer esta parte, registrate dentro de la app con un email que también estés usando en Facebook. Así podrás comprobar que la vinculación de cuentas funciona.

Es decir, deberías tener un user al menos registrado usando el proveedor de Email y Password.

Comprobamos que tenemos nuestro user registrado en Firebase (con Email y Password)
Comprobamos que tenemos nuestro user registrado en Firebase (con Email y Password)

Creamos la vista en SwiftUI

Lo primero de todo que vamos hacer es crear una nueva vista, la vamos a llamar ProfileView. Y va a ser un simple Form con dos Buttons en SwiftUI:

import SwiftUI

struct ProfileView: View {
    @ObservedObject var authenticationViewModel: AuthenticationViewModel
    @State var textFieldEmail: String = ""
    @State var textFieldPassword: String = ""
    
    var body: some View {
        Form {
            Section {
                Button(action: {
                    print("Vincular Email y Password")
                }, label: {
                    Label("Vincula Email", systemImage: "envelope.fill")
                })
                Button(action: {
                    print("Vincular Facebook")
                }, label: {
                    Label("Vincula Facebook", image: "facebook")
                })
            } header: {
                Text("Vincula otras cuentas a la sesión actual")
            }
        }
    }
}
Código ProfileView en SwiftUI

Ahora vamos a usar nuestra nueva vista ProfileView en HomeView, para hacerlo vamos a crear un TabView como mencionábamos hace un momento:

import SwiftUI

struct HomeView: View {
    @ObservedObject var authenticationViewModel: AuthenticationViewModel
    
    var body: some View {
        NavigationView {
            TabView {
                VStack {
                    Text("Bienvenido \(authenticationViewModel.user?.email ?? "No user")")
                        .padding(.top, 32)
                    Spacer()
                }
                .tabItem {
                    Label("Home", systemImage: "house.fill")
                }
                ProfileView(authenticationViewModel: authenticationViewModel)
                    .tabItem {
                        Label("Profile", systemImage: "person.fill")
                    }
            }
            .navigationBarTitleDisplayMode(.inline)
            .navigationTitle("Home")
            .toolbar {
                Button("Logout") {
                    authenticationViewModel.logout()
                }
            }
        }
    }
}
Código HomeView en SwiftUI

Una vez tenemos una idea visual de lo que queremos hacer, vamos a seguir el mismo proceso de los otros posts. Vamos a empezar por las capas inferiores de nuestra arquitectura y luego iremos subiendo hasta la vista.

Pues lo primero que vamos hacer es saber qué Proveedor estamos usando actualmente, así desde nuestra vista marcaremos esa opción como deshabilitada (ya que no queremos vincular el mismo Proveedor con la que tenemos activa la sesión, no tendría sentido).

Creamos un modelo nuevo llamado LinkedAccounts

Vamos al listado de ficheros y nos situamos en el mismo nivel que tenemos AuthenticationFirebaseDatasource, allí vamos a crear una carpeta que vamos a llamar Model y vamos a meter un nuevo modelo llamado LinkedAccounts:

import Foundation

enum LinkedAccounts: String {
    case emailAndPassword = "password"
    case facebook = "facebook.com"
    case unknown
}
Creamos modelo LinkedAccounts

También vamos a mover el modelo User que tenemos en el AuthenticationFirebaseDatasource, vamos a crear en un fichero a parte dentro de la nueva carpeta Model.

import Foundation

struct User {
    let email: String
}
Modelo User

CurrentProvider

Ahora vamos a nuestro AuthenticationFirebaseDatasource y vamos a crear un nuevo método, lo llamaremos currentProvider() y nos dará un array con todos los proveedores que tengamos vinculados con nuestra cuenta de email. En nuestro caso, ahora debería retornar solo un proveedor que sería el de Email y Password (si tuvieramos más proveedores asociados a ese email también los retornaría):

    func currentProvider() -> [LinkedAccounts] {
        guard let currentUser = Auth.auth().currentUser else {
            return []
        }
        let linkedAccounts = currentUser.providerData.map { userInfo in
            LinkedAccounts(rawValue: userInfo.providerID)
        }.compactMap { $0 }
        return linkedAccounts
    }
Método que nos retorna todos los providers vinculados al currentUser

Vamos a crear un método en el Repository:

    func getCurrentProvider() -> [LinkedAccounts] {
        authenticationFirebaseDatasource.currentProvider()
    }
Creamos método en el Repository

Y finalmente llegamos a nuestro AuthenticationViewModel. Aquí crearemos una propiedad y 3 métodos:

  • El primer método llamará al método del repositorio
  • El segundo método es un helper que nos indicará si tenemos como Proveedor Email y Password
  • El tercer método es un helper que nos indicará si tenemos como Proveedor a Facebook
    @Published var linkedAccounts: [LinkedAccounts] = []

    func getCurrentProvider() {
        linkedAccounts = authenticationRepository.getCurrentProvider()
        print("User Provider \(linkedAccounts)")
    }
    
    func isEmailAndPasswordLinked() -> Bool {
        linkedAccounts.contains(where: { $0.rawValue == "password" })
    }
    
    func isFacebookLinked() -> Bool {
        linkedAccounts.contains(where: { $0.rawValue == "facebook.com" })
    }
Código en el ViewModel

Ahora podemos llamar estos nuevos métodos del AuthenticationViewModel en nuestra vista ProfileView.

import SwiftUI

struct ProfileView: View {
    @ObservedObject var authenticationViewModel: AuthenticationViewModel
    
    var body: some View {
        Form {
            Section {
                Button(action: {
                    print("Vincular Email y Password")
                }, label: {
                    Label("Vincula Email", systemImage: "envelope.fill")
                })
                    .disabled(authenticationViewModel.isEmailAndPasswordLinked())
                Button(action: {
                    print("Vincular Facebook")
                }, label: {
                    Label("Vincula Facebook", image: "facebook")
                })
                    .disabled(authenticationViewModel.isFacebookLinked())
            } header: {
                Text("Vincula otras cuentas a la sesión actual")
            }
        }
        .task {
            authenticationViewModel.getCurrentProvider()
        }
    }
}
Código de ProfileView en SwiftUI con modificador .disabled y .task

Vamos a añadir:

  • El modificador .task y dentro de él vamos a pedir los Proveedores
  • El modificador .disabled en cada Button, y cada Button llamará al método helper que hemos creado hace un momento. Así, si tenemos un proveedor deshabilitaremos la opción de vincularlo.

Si compilamos nuestra app y vamos a la sección de Profile, deberíamos ver algo parecido a esto. En mi caso estoy registrado y logueado usando el Proveedor de Email y Password.

Pantalla ProfileView en SwiftUI para vincular proveedores de Firebase
Pantalla ProfileView en SwiftUI para vincular proveedores de Firebase

Vincular Facebook con una cuenta existente a Firebase con el mismo Email

Lo que vamos hacer a continuación es que cuando el user pulse el button de Vincula Facebook, se ejecute toda la lógica necesaria para crear este vínculo con Firebase.

Pues vamos a crear esta nueva lógica creando un método en nuestro AuthenticationFirebaseDatasource

    func linkFacebook(completionBlock: @escaping (Bool) -> Void) {
        facebookAuthentication.loginFacebook { result in
            switch result {
            case .success(let accessToken):
                let credential = FacebookAuthProvider.credential(withAccessToken: accessToken)
                Auth.auth().currentUser?.link(with: credential, completion: { authDataResult, error in
                    if let error = error {
                        print("Error creating a new user \(error.localizedDescription)")
                        completionBlock(false)
                        return
                    }
                    let email = authDataResult?.user.email ?? "No email"
                    print("New user linked with email \(email)")
                    completionBlock(true)
                })
            case .failure(let error):
                print("Error signIn with Facebook \(error.localizedDescription)")
                completionBlock(false)
            }
        }
    }
Método para vincular proveedor de Facebook en Firebase

Ahora vamos a crear un método nuevo en nuestro AuthenticationRepository que llame a este nuevo que acabamos de crear:

    func linkFacebook(completionBlock: @escaping (Bool) -> Void) {
        authenticationFirebaseDatasource.linkFacebook(completionBlock: completionBlock)
    }
Creamos el método en nuestro Repository

y vamos hacer lo mismo en el AuthenticationViewModel:

    func linkFacebook() {
        authenticationRepository.linkFacebook { [weak self] isSuccess in
            print("Linked Facebook \(isSuccess.description)")
        }
    }
Y creamos el método en el ViewModel

Este closure solo está printando un mensaje por consola, lo que vamos hacer es crear algunas propiedades en nuestro AuthenticationViewModel y así mostrar un alert en nuestra vista ProfileView. Este alert se mostrará para indicar al user si se ha podido vincular la cuenta con el nuevo proveedor o no. Así que el método de linkFacebook() quedaría de la siguiente manera:

    func linkFacebook() {
        authenticationRepository.linkFacebook { [weak self] isSuccess in
            print("Linked Facebook \(isSuccess.description)")
            self?.isAccountLinked = isSuccess
            self?.showAlert.toggle()
            self?.getCurrentProvider()
        }
    }
Creamos propiedades que lanzarán alguna acción en la vista (@Published)

Creamos 2 propiedades nuevas:

@Published var showAlert: Bool = false
@Published var isAccountLinked: Bool = false
Creamos la propiedades @Published

y llamamos al método getCurrentProvider() para que obtenga otra vez el listado de Proveedores actualizado, y así marcar como disabled el último proveedor añadido, que en este caso será Facebook.

Ahora nos vamos a la vista ProfileView para:

  • Añadir la lógica de que cuando se pulse el Button de vincular Facebook llame al método correcto del AuthenticationViewModel.
  • Añadir un alert que se mostrará cuando el User quiera vincular un Proveedor nuevo. Este alert mostrará un mensaje tanto si hay éxito como si hay un error.

ProfileView quedaría de la siguiente manera:

import SwiftUI

struct ProfileView: View {
    @ObservedObject var authenticationViewModel: AuthenticationViewModel
    
    var body: some View {
        Form {
            Section {
                Button(action: {
                    print("Vincular Email y Password")
                }, label: {
                    Label("Vincula Email", systemImage: "envelope.fill")
                })
                    .disabled(authenticationViewModel.isEmailAndPasswordLinked())
                Button(action: {
                    authenticationViewModel.linkFacebook()
                }, label: {
                    Label("Vincula Facebook", image: "facebook")
                })
                    .disabled(authenticationViewModel.isFacebookLinked())
            } header: {
                Text("Vincula otras cuentas a la sesión actual")
            }
        }
        .task {
            authenticationViewModel.getCurrentProvider()
        }
        .alert(authenticationViewModel.isAccountLinked ? "¡Cuenta Vinculada!" : "Error", isPresented: $authenticationViewModel.showAlert, actions: {
            Button("Aceptar") {
                print("Dismiss Alert")
            }
        }, message: {
            Text(authenticationViewModel.isAccountLinked ? "✅ Acabas de vincular tu cuenta" : "❌ Error al Vincular la cuenta")
        })
    }
}
Añadimos .alert con Title y Message en SwiftUI

Vamos a compilar nuestra app y vamos a probar de vincular nuestra cuenta de Facebook, cuando pulsamos el Button de vincular una cuenta con Facebook aparece el mismo proceso que si hicieramos la autenticación:

Vincular proveedor de Facebook
Vincular proveedor de Facebook

Al aparecer la vista anterior le damos a continuar y añadimos nuestros credenciales. Si todo ha ido bien verems el siguiente mensaje:

Alert mostrando que la cuenta de Facebook ha sido vinculada
Alert mostrando que la cuenta de Facebook ha sido vinculada

Ha aparecido el Alert y se ha deshabilitado el Button de Facebook ya que hemos vinculado este nuevo Proveedor.

Si ahora vamos a Firebase podemos ver que aparece el icono de Facebook al lado de nuestro email.

User en Firebase con la cuenta vinculada de Facebook y Email y Password
User en Firebase con la cuenta vinculada de Facebook y Email y Password

Vincular Email y Password con una cuenta existente a Firebase con el mismo Email (En este caso será Facebook)q

Para hacerlo, borra el user que tienes vinculado con los dos proveedores y vuelvete a registrar usando solo Facebook. Y te debería de quedar un user como en la siguiente imagen:

Comprobamos que tenemos nuestro user registrado en Firebase (esta vez con Facebook)
Comprobamos que tenemos nuestro user registrado en Firebase (esta vez con Facebook)

En este momento tu único Proveedor es Facebook y si haces logout e intentas registrarte con tu Email y Password te dará un error.

The email address is already in use by another account.
Error al intentar registrar una cuenta con el mismo Email de un user préviamente creado con Facebook
Error al intentar registrar una cuenta con el mismo Email de un user préviamente creado con Facebook

Antes debes vincular los dos Proveedores. Para hacerlo vamos a empezar por crear un método en la clase FacebookAutentication, el método se va a llamar getAccessToken y nos va a servir para crear un credencial de Facebook en el AuthenticationFirebaseDatasource:

    func getAccessToken() -> String? {
        AccessToken.current?.tokenString
    }
Creamos método para obtener el AccessToken de Facebook

Una vez creado este método en la clase FacebookAuthentication, nos vamos al AuthenticationFirebaseDatasource, aquí vamos a crear un método para que nos cree un AuthCredential, necesitaremos un credencial de Facebook para reautenticarnos con Facebook antes de crear el vinculo entre otros proveedores, en nuestro caso con Email y Password. Crearemos el siguiente método:

    func getCurrentCredential() -> AuthCredential? {
        guard let providerId = currentProvider().last else {
            return nil
        }
        switch providerId {
        case .facebook:
            guard let accessToken = facebookAuthentication.getAccessToken() else {
                return nil
            }
            
            let credential = FacebookAuthProvider.credential(withAccessToken: accessToken)
            return credential
        case .emailAndPassword, .unknown:
            return nil
        }
    }
Método para obtener un credential de algún proveerdor del currentUser

Y a continuación crearemos el siguiente método para crear esta reautenticación con Facebook, crear un credencial de Email y Password válido y vincularlo con el current user, vamos a verlo paso a paso:

    func linkEmailAndPassword(email: String, password: String, completionBlock: @escaping (Bool) -> Void) {
        
        guard let credential = getCurrentCredential() else {
            print("Error Creating Credential")
            completionBlock(false)
            return
        }
        
        Auth.auth().currentUser?.reauthenticate(with: credential, completion: { authDataResult, error in
            if let error = error {
                print("Error reauthenticating a user \(error.localizedDescription)")
                completionBlock(false)
                return
            }
            
            let emailAndPasswordCredential = EmailAuthProvider.credential(withEmail: email, password: password)
            
            Auth.auth().currentUser?.link(with: emailAndPasswordCredential, completion: { authDataResult, error in
                if let error = error {
                    print("Error creating a new user \(error.localizedDescription)")
                    completionBlock(false)
                    return
                }
                let email = authDataResult?.user.email ?? "No email"
                print("New user linked with email \(email)")
                completionBlock(true)
            })
        })
    }
Método para vincular con el proveedor de Email y Password
  • Si te fijas el método recibe dos parámetros, que son el email y password que se recibirán desde las capas superiores (View-> ViewModel-> Repository-> Datasource).
  • Obtenemos un credencial de algunos de los proveedor que el user esté usando. En nuestro caso será Facebook (es el único proveedor con el que hemos registrado al user)
  • Reautenticamos la sesión del user, este paso es por seguridad.
  • Una vez autenticado, vinculamos el credencial de email y password con Firebase.

Una vez hemos creado este método vamos a crear el método en el AuthenticationRepository:

    func linkEmailAndPassword(email: String, password: String, completionBlock: @escaping (Bool) -> Void) {
        authenticationFirebaseDatasource.linkEmailAndPassword(email: email,
                                                              password: password,
                                                              completionBlock: completionBlock)
    }
Creamos el método de vincular Email y Password en el Repository

y finalmente creamos el método en nuestro AuthenticationViewModel:

    func linkEmailAndPassword(email: String, password: String) {
        authenticationRepository.linkEmailAndPassword(email: email, password: password) { [weak self] isSuccess in
            print("Linked Email and password \(isSuccess.description)")
            self?.isAccountLinked = isSuccess
            self?.showAlert.toggle()
            self?.getCurrentProvider()
        }
    }
Creamos el método en el ViewModel

Este método que acabamos de crear en el ViewModel, necesitamos llamarlo desde la vista de ProfileView. Pero en esa vista no tenemos ningún Form para poderle pasar un email o password. Es por eso que añadiremos esta opción a nuestro Formulario actual. Vamos a crear una propiedad @State para poder expanding un formulario al pulsar en Vincular con Email

    @State var expandVerificationWithEmailForm: Bool = false
Propiedad para controlar cuando abrimos o cerramos el Form

Y la vista final quedaría de la siguiente manera:

import SwiftUI

struct ProfileView: View {
    @ObservedObject var authenticationViewModel: AuthenticationViewModel
    @State var expandVerificationWithEmailForm: Bool = false
    @State var textFieldEmail: String = ""
    @State var textFieldPassword: String = ""
    
    var body: some View {
        Form {
            Section {
                Button(action: {
                    expandVerificationWithEmailForm.toggle()
                }, label: {
                    Label("Vincula Email", systemImage: "envelope.fill")
                })
                .disabled(authenticationViewModel.isEmailAndPasswordLinked())
                if expandVerificationWithEmailForm {
                    Group {
                        Text("Vincula tu correo elecrónico con la sesión que tienes actualmente iniciada")
                            .tint(.secondary)
                            .multilineTextAlignment(.center)
                            .padding(.top, 2)
                            .padding(.bottom, 32)
                        TextField("Añade tu correo electrónico", text: $textFieldEmail)
                        TextField("Añade tu contraseña", text: $textFieldPassword)
                        Button("Login") {
                            authenticationViewModel.linkEmailAndPassword(email: textFieldEmail, password: textFieldPassword)
                        }
                        .padding(.top, 18)
                        .buttonStyle(.bordered)
                        .tint(.blue)
                        if let messageError = authenticationViewModel.messageError {
                            Text(messageError)
                                .bold()
                                .font(.body)
                                .foregroundColor(.red)
                                .padding(.top, 20)
                        }
                    }
                }
                Button(action: {
                    authenticationViewModel.linkFacebook()
                }, label: {
                    Label("Vincula Facebook", image: "facebook")
                })
                    .disabled(authenticationViewModel.isFacebookLinked())
            } header: {
                Text("Vincula otras cuentas a la sesión actual")
            }
        }
        .task {
            authenticationViewModel.getCurrentProvider()
        }
        .alert(authenticationViewModel.isAccountLinked ? "¡Cuenta Vinculada!" : "Error", isPresented: $authenticationViewModel.showAlert, actions: {
            Button("Aceptar") {
                if authenticationViewModel.isAccountLinked {
                    expandVerificationWithEmailForm = false
                }
            }
        }, message: {
            Text(authenticationViewModel.isAccountLinked ? "✅ Acabas de vincular tu cuenta" : "❌ Error al Vincular la cuenta")
        })
    }
}
Versión final de ProfileView en SwiftUI

Si compilamos nuestra app deberíamos ver la opción de Vincula Facebook deshabilitada y solo estar disponible la opción de Vincula Email.

Pantalla ProfileView para vincular con otros proveedores de Firebase
Pantalla ProfileView para vincular con otros proveedores de Firebase

Si pulsamos en Vincula Email debería aparecer un formulario:

Formulario en SwiftUI para vincular Email y Password al currentUser
Formulario en SwiftUI para vincular Email y Password al currentUser

Aquí podemos añadir nuestras credenciales, añadiremos nuestro Email y Password. Al rellenar la información le damos al Button y si todo va bien debería salir el Alert y al dar Aceptar debería aparecer la opción de Vincula Email deshabilitada.

Proveedor de Email y Password vinculado
Proveedor de Email y Password vinculado

Si vamos a nuestro panel de Firebase, podemos ver que aparecen los dos proveedores.

User con dos proveedores vinculados correctamente
User con dos proveedores vinculados correctamente

Conclusión

Hoy hemos aprendido a como vincular distintos proveedores de autenticación de Firebase. De esta manera permitimos que un user pueda entrar con el sistema de autenticación que quiera (y que nuestra app soporte). Hemos visto a como vincular una cuenta con Facebook y más tarde hemos aprendido a vincular una cuenta con Email y Password.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple