An application where you get to create and edit data isn’t that helpful if you don’t have a way to show that the data is yours. To get around this, we need to allow users to sign up and authenticate and own the songs that they create. In this tutorial, we’ll utilize the authentication functionality in Firebase create users and allow them to sign into the application.
Goals
- Add user sign up and authentication using Firebase.
Firebase is already acting as the database for our application so that we didn’t need to create a custom backend, and thankfully we can also utilize it for authentication. We’re going to give our potential users the ability to sign up with email and password or to sign in with Facebook.
We won’t be associating data with the user’s in this tutorial because it would be too long.
Creating Firebase Authentication Methods
Before we can make any changes to our application, we need to set up “sign-in methods” for Facebook and email/password within Firebase. You can get to where you need to go for this by clicking “Authentication” in the sidebar of the Firebase console and then clicking “Set up Sign-in Methods.”
From here you’ll want to enable the “Email/Password” provider by clicking the row and then selecting to enable it from the dialog. When you enable the “Facebook” provider, you’ll then need to get an application id and secret.
To add Facebook, you’ll need to first create a Facebook app by heading to the Facebook Developers site. Once you’ve created the app, you’ll want to add the “Facebook login” product to the app.
By default, you’ll be taken to the quick start for this product, but we don’t want that. Select “Settings” under “Facebook Login” in the sidebar. Next, you’ll need to enable “Embedded Browser OAuth Login” and paste the callback URL that Firebase provided in the Facebook configuration dialog into the “Valid OAuth redirect URIs” field (hit “enter” to make sure that it looks like a blue bubble) and save the settings.
Finally, with the app configured we’re ready to copy the values that we went to Facebook to get. Click on “Dashboard” from the sidebar and then you can copy the client id and secret to paste into Firebase.
Adding Sign Up/Sign In to the Application
With the backend configured it is time for us to add authentication to the React application. The first thing that we need to do is to create a way for a new user to sign up. This will require a few things:
- Calls to action to “Register” and “Log In” in the navigation if the user isn’t already signed in.
- A new
Login
component to render an email password field or Facebook login. - Rendering the header that we currently have if the user is signed in.
We’re going to create a single component for the registration and sign in processes since we’re supporting Facebook auth and that handles both at the same time.
Modifying the Header
The first thing we need is to have some state reflected in the Header
, and we’re going to accomplish this by passing in a prop from App
. Let’s add that state to App
now.
src/App.js
// imports omitted
class App extends Component {
constructor() {
super();
this.addSong = this.addSong.bind(this);
this.updateSong = this.updateSong.bind(this);
this.state = {
songs: { },
authenticated: false,
};
}
// unchanged life cycle and custom functions omitted
render() {
return (
<div style={{maxWidth: "1160px", margin: "0 auto"}}>
<BrowserRouter>
<div>
<Header authenticated={this.state.authenticated} />
<div className="main-content" style={{padding: "1em"}}>
<div className="workspace">
<Route exact path="/songs" render={(props) => {
return (
<SongList songs={this.state.songs} />
)
}} />
<Route path="/songs/:songId" render={(props) => {
const song = this.state.songs[props.match.params.songId];
return (
song
? <ChordEditor song={song} updateSong={this.updateSong} />
: <h1>Song not found</h1>
)
}} />
</div>
</div>
</div>
</BrowserRouter>
<Footer />
</div>
);
}
}
With the authenticated
value passed to the Header
we now have something that we can use to conditionally render content in the navigation. Now let’s make that change now:
src/components/Header.js
// imports omitted
class Header extends Component {
render() {
return (
<nav className="pt-navbar">
<div className="pt-navbar-group pt-align-left">
<div className="pt-navbar-heading">Chord Creator</div>
{this.props.authenticated
? <input className="pt-input" placeholder="Search Songs..." type="text" />
: null
}
</div>
{this.props.authenticated
? (
<div className="pt-navbar-group pt-align-right">
<Link className="pt-button pt-minimal pt-icon-music" to="/songs">Songs</Link>
<span className="pt-navbar-divider"></span>
<button className="pt-button pt-minimal pt-icon-user"></button>
<button className="pt-button pt-minimal pt-icon-cog"></button>
</div>
)
: (
<div className="pt-navbar-group pt-align-right">
<Link className="pt-button pt-intent-primary" to="/login">Register/Log In</Link>
</div>
)
}
</nav>
);
}
}
Note: We’re putting the
.pt-navbar-group
in both versions of the right side because JSX can’t be returned from anything as more than one component side-by-side if it’s not wrapped in a container.
Creating the Login Component
Now when we look at the application, we can see a button in the navigation to log in. It’s time to create the component to handle that and set up the routing for the /login
route. Let’s first create the Login
component:
src/components/Login.js
import React, { Component } from 'react'
const loginStyles = {
width: "90%",
maxWidth: "315px",
margin: "20px auto",
border: "1px solid #ddd",
borderRadius: "5px",
padding: "10px"
};
class Login extends Component {
constructor() {
super()
this.authWithFacebook = this.authWithFacebook.bind(this)
this.authWithEmailPassword = this.authWithEmailPassword.bind(this)
}
authWithFacebook() {
console.log("We're authing with Facebook")
}
authWithEmailPassword(event) {
event.preventDefault()
console.log("We're authing with password")
console.table([{
email: this.emailInput.value,
password: this.passwordInput.value,
}])
}
render() {
return (
<div style={loginStyles}>
<button style={{width: "100%"}} className="pt-button pt-intent-primary" onClick={() => this.authWithFacebook()}>Log In with Facebook</button>
<hr style={{marginTop: "10px", marginBottom: "10px"}} />
<form onSubmit={(event) => this.authWithEmailPassword(event)}>
<div style={{marginBottom: "10px"}} className="pt-callout pt-icon-info-sign">
<h5>Note</h5>
If you don't have an account already, this form will create your account.
</div>
<label className="pt-label">
Email
<input style={{width: "100%"}} className="pt-input" name="email" type="email" ref={(input) => {this.emailInput = input}} placeholder="Email"></input>
</label>
<label className="pt-label">
Password
<input style={{width: "100%"}} className="pt-input" name="password" type="password" ref={(input) => {this.passwordInput = input}} placeholder="Password"></input>
</label>
<input style={{width: "100%"}} type="submit" className="pt-button pt-intent-primary" value="Log In"></input>
</form>
</div>
)
}
}
export default Login
This version of the component doesn’t handle the Firebase authenticate portion yet; we’re first making sure that we’re wired up the basic React events properly. We need to render the this route before we can test the code.
src/App.js
// Only showing `render` function
render() {
return (
<div style={{maxWidth: "1160px", margin: "0 auto"}}>
<BrowserRouter>
<div>
<Header authenticated={this.state.authenticated} />
<div className="main-content" style={{padding: "1em"}}>
<div className="workspace">
<Route exact path="/login" component={Login}/>
<!-- Additional Routes omitted -->
</div>
</div>
</div>
</BrowserRouter>
<Footer />
</div>
);
}
Now if we click the navigation button to “Register/Log In” we will see our new form. When we fill it out and submit, we should see the field values in the console.
Utilizing Firebase Authentication
Re-base provides us with a way to interact with the data that we’ve stored in Firebase, but the most recent version of the library dropped support for authentication (because you can use the firebase.Application
itself for that). Our src/base.js
is currently only exporting the base
constant, so we’ll need to add app
to the list so that we can utilize it from our Login
component. We’ll also need to have a FacebookAuthProvider
, so we’ll create one of those in src/base.js
and return that too.
src/base.js
// lines above omitted
const facebookProvider = new firebase.auth.FacebookAuthProvider()
export { app, base, facebookProvider }
Let’s pull in app
and facebookProvider
and set up our Facebook login first by implementing
our authWithFacebook
function. We’re also going to add in a way to redirect using the Redirect
component from react-router. To show errors, we’ll use a Toaster
from Blueprintjs.
src/components/Login.js
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import { Toaster, Intent } from "@blueprintjs/core";
import { app, facebookProvider } from '../base'
// loginStyles omitted (unchanged)
class Login extends Component {
constructor() {
super()
this.authWithFacebook = this.authWithFacebook.bind(this)
this.authWithEmailPassword = this.authWithEmailPassword.bind(this)
this.state = {
redirect: false
}
}
authWithFacebook() {
app.auth().signInWithPopup(facebookProvider)
.then((result, error) => {
if (error) {
this.toaster.show({ intent: Intent.DANGER, message: "Unable to sign in with Facebook" })
} else {
this.setState({ redirect: true })
}
})
}
// authWithEmailPassword omitted (unchanged)
render() {
const { from } = this.props.location.state || { from: { pathname: '/' } }
const { redirect } = this.state
if (redirect) {
return (
<Redirect to={from} />
)
}
return (
<div style={loginStyles}>
<Toaster ref={(element) => { this.toaster = element }} />
<button style={{width: "100%"}} className="pt-button pt-intent-primary" onClick={() => this.authWithFacebook()}>Log In with Facebook</button>
<hr style={{marginTop: "10px", marginBottom: "10px"}} />
<form onSubmit={(event) => this.authWithEmailPassword(event)}
ref={(form) => { this.loginForm = form }}>
<div style={{marginBottom: "10px"}} className="pt-callout pt-icon-info-sign">
<h5>Note</h5>
If you've never logged in, this will create your account.
</div>
<label className="pt-label">
Email
<input style={{width: "100%"}} className="pt-input" name="email" type="email" ref={(input) => {this.emailInput = input}} placeholder="Email"></input>
</label>
<label className="pt-label">
Password
<input style={{width: "100%"}} className="pt-input" name="password" type="password" ref={(input) => {this.passwordInput = input}} placeholder="Password"></input>
</label>
<input style={{width: "100%"}} type="submit" className="pt-button pt-intent-primary" value="Log In"></input>
</form>
</div>
)
}
}
export default Login
If we click the button to “Log In with Facebook” we will see a popup the first time that requires us to authorize the application, but every time after that the popup should open and then quickly close and redirect us. The redirect works, but our header didn’t change as we expected. It makes sense because we didn’t set the authenticated
state. For our application to work the way we want it to we’re going to need to tie some authentication login into the life-cycle of our application in componentWillMount
. Let’s add this to App
now.
src/App.js
// everything but componentWillMount and componentWillUnmount omitted (unchanged)
componentWillMount() {
this.removeAuthListener = app.auth().onAuthStateChanged((user) => {
if (user) {
this.setState({
authenticated: true
})
} else {
this.setState({
authenticated: false
})
}
})
this.songsRef = base.syncState('songs', {
context: this,
state: 'songs'
});
}
componentWillUnmount() {
this.removeAuthListener();
base.removeBinding(this.songsRef);
}
The page should have auto refreshed when you changed this, and you’ll now see the header buttons we expected.
Refreshing the page again you’ll also see that the header flashes the button to log in briefly. This flash isn’t ideal, and we can get around it by adding a piece of state to indicate that the app is loading and then always set it to false
once our onAuthStateChanged
callback has run. If we’re loading we’ll render a Spinner
from blueprintjs, otherwise, we’ll render the application as we have been.
src/App.js
import React, { Component } from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import { Spinner } from '@blueprintjs/core';
import Header from './components/Header';
import Footer from './components/Footer';
import ChordEditor from './components/ChordEditor';
import Login from './components/Login';
import SongList from './components/SongList';
import { app, base } from './base';
class App extends Component {
constructor() {
super();
this.addSong = this.addSong.bind(this);
this.updateSong = this.updateSong.bind(this);
this.state = {
songs: { },
authenticated: false,
loading: true
};
}
// addSong & updateSong omitted (unchanged)
componentWillMount() {
this.removeAuthListener = app.auth().onAuthStateChanged((user) => {
if (user) {
this.setState({
authenticated: true,
loading: false
})
} else {
this.setState({
authenticated: false,
loading: false
})
}
})
this.songsRef = base.syncState('songs', {
context: this,
state: 'songs'
});
}
// componentWillUnmount omitted (unchanged)
render() {
if (this.state.loading === true) {
return (
<div style={{ textAlign: "center", position: "absolute", top: "25%", left: "50%" }}>
<h3>Loading</h3>
<Spinner />
</div>
)
}
return (
<div style={{maxWidth: "1160px", margin: "0 auto"}}>
<BrowserRouter>
<div>
<Header authenticated={this.state.authenticated} />
<div className="main-content" style={{padding: "1em"}}>
<div className="workspace">
<Route exact path="/login" component={Login}/>
<Route exact path="/songs" render={(props) => {
return (
<SongList songs={this.state.songs} />
)
}} />
<Route path="/songs/:songId" render={(props) => {
const song = this.state.songs[props.match.params.songId];
return (
song
? <ChordEditor song={song} updateSong={this.updateSong} />
: <h1>Song not found</h1>
)
}} />
</div>
</div>
</div>
</BrowserRouter>
<Footer />
</div>
);
}
}
export default App;
Logging Out
Now the initial app experience isn’t looking too bad, but we, unfortunately, don’t have a way to log out so we
can’t test our email/password login if we wrote that next. Let’s create a simple Logout
component and
route before moving on.
src/components/Logout.js
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import { Spinner } from '@blueprintjs/core';
import { app } from '../base'
class Logout extends Component {
constructor() {
super()
this.state = {
redirect: false
}
}
componentWillMount() {
app.auth().signOut().then((user, error) => {
this.setState({ redirect: true })
});
}
render() {
if (this.state.redirect === true) {
return <Redirect to="/" />
}
return (
<div style={{ textAlign: "center", position: "absolute", top: "25%", left: "50%" }}>
<h3>Logging Out</h3>
<Spinner />
</div>
)
}
}
export default Logout
We get to utilize componentWillMount
again to trigger the sign-out process, and we’re even using the Spinner
code again. This repetition shows that we might want to extract out a LoadingIndicator
component that takes in a message
prop, but I’ll leave that to you. Now logging out (by visiting “/logout”) and logging in should both work, but we should probably add an icon to the Header
if signed in.
src/components/Header.js
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
class Header extends Component {
render() {
return (
<nav className="pt-navbar">
<div className="pt-navbar-group pt-align-left">
<div className="pt-navbar-heading">Chord Creator</div>
{this.props.authenticated
? <input className="pt-input" placeholder="Search Songs..." type="text" />
: null
}
</div>
{this.props.authenticated
? (
<div className="pt-navbar-group pt-align-right">
<Link className="pt-button pt-minimal pt-icon-music" to="/songs">Songs</Link>
<span className="pt-navbar-divider"></span>
<button className="pt-button pt-minimal pt-icon-user"></button>
<button className="pt-button pt-minimal pt-icon-cog"></button>
<Link className="pt-button pt-minimal pt-icon-log-out" aria-label="Log Out" to="/logout"></Link>
</div>
)
: (
<div className="pt-navbar-group pt-align-right">
<Link className="pt-button pt-intent-primary" to="/login">Register/Log In</Link>
</div>
)
}
</nav>
);
}
}
export default Header;
Setting Up Email/Password Authentication
Now that the application can handle Facebook sign in and we’re able to log out we need to implement email/password authentication. This function is more complicated than the Facebook authentication method because:
- A user with the given email may exist, having signed in using Facebook.
- The user might not yet exist, so we need to register them.
- The user might exist and we get an error when signing in.
To handle these cases we’re going to be making first a call to get the providers attached to the email address. Using the provider list, we’ll be able to determine where to go next or what message to show the user. Here’s what the final method looks like:
src/components/Login.js
// only authWithEmailPassword was changed
authWithEmailPassword(event) {
event.preventDefault()
const email = this.emailInput.value
const password = this.passwordInput.value
app.auth().fetchProvidersForEmail(email)
.then((providers) => {
console.log("we're hitting the first then")
if (providers.length === 0) {
// create user
return app.auth().createUserWithEmailAndPassword(email, password)
} else if (providers.indexOf("password") === -1) {
this.toaster.show({ intent: Intent.WARNING, message: "Try alternative login." })
} else {
// sign in with email/password
return app.auth().signInWithEmailAndPassword(email, password)
}
})
.then((user) => {
if (user && user.email) {
this.loginForm.reset()
this.setState({ redirect: true })
}
})
.catch((error) => {
this.toaster.show({ intent: Intent.DANGER, message: error.message })
})
}
If the user needs to be created or can sign in with a password, then we return the Promise from signInWithEmailAndPassword
or createUserWithEmailAndPassword
from our first handler so that the second then
call can resolve the results and any errors will still be displayed.
Recap
We covered a lot in this tutorial. You now know how to create a Facebook app, use Firebase authentication, redirect using react-router, and render a “toast” and a “spinner” with Blueprintjs. In the next tutorial, we’ll continue working with our logged in user by changing some of our routes to require authentication and by associating songs with the users themselves.