As we discussed in the previous chapter, we are not going to handle actual authentication ourselves.
We'll delegate this to a 3rd party called Auth0. They have a free tier which allows 7000 users and two login providers. It will be enough for our needs. Another very good option is Firebase
There is a very good, and complete tutorial on the official website; we'll be adapting it to work for our case.1
Signing up to Auth0
First thing you'll do is sign up for a free account. This gives you access to:
After signing up, you will have to create an Auth0 Application to represent your app. So, in your dashboard, click on the Applications section on the vertical menu and then click on Create Application.
Choose a name for your application, select "Single Page Web App", and press create.
Heading to the Settings tab, search for the Allowed Callback URLs field and insert http://localhost:3000/callback.
With this value in place, you can click on the Save Changes button and leave this page open.
Setting up Express
First, we'll add helmet, a library that helps secure express servers. (Later on, we'll use a different helmet library, don't mistake them!)
We'll also add:
express-jwt: A middleware that validates a JSON Web Token (JWT) and set the req.user with its attributes.
jwks-rsa: A library to retrieve RSA public keys from a JWKS (JSON Web Key Set) endpoint.
Move the back and run :
npminstall--savehelmetexpress-jwtjwks-rsa
// back/src/app.js...import helmet from'helmet'...app.use(helmet()) // make it the first thing your express app uses
Then open auth.js:
Replace the previous content of auth.js with :
// back/src/auth.jsimport jwt from'express-jwt'import jwksRsa from'jwks-rsa'constAUTH0_DOMAIN=''constAUTH0_CLIENT_ID=''exportconstisLoggedIn=jwt({ secret:jwksRsa.expressJwtSecret({ cache:true, rateLimit:true, jwksRequestsPerMinute:5, jwksUri:`https://${AUTH0_DOMAIN}/.well-known/jwks.json` }),// Validate the audience and the issuer. audience:AUTH0_CLIENT_ID, issuer:`https://${AUTH0_DOMAIN}/`, algorithms: ['RS256']});
This constant is actually an Express middleware that will validate ID tokens. Note that, to make it work, you will have to fill the <AUTH0_CLIENT_ID> const with the value presented in the Client ID field of your Auth0 Application. Also, you will have to fill the <AUTH0_DOMAIN> const with the value presented in the Domain field (e.g. bk-tmp.auth0.com).
note: We will extract those values later so they're not present in the code that you put in a public repo. But for now, it's ok to write them directly in the code. Just don't put those values in a public repo.
note: The other methods in auth.js are not useful anymore. The logging in and logging out will happen directly between React and Auth0. Our server's job remains to simply check if the user is authenticated, nothing more.
Setting up React
We're going to need a few things:
A piece of code to handle all the Auth0 stuff
A way for our app to know if the user is logged in or not
A way for the user to log in (the equivalent of our previous log in form)
First, move to the front and install auth0-js.
npminstall--saveauth0-js
Then let's create the piece of code. Create a new file, auth.js in front/src:
// front/src/auth.jsimport auth0 from"auth0-js";constAUTH0_DOMAIN=''constAUTH0_CLIENT_ID=''let idToken =null;let profile =null;let expiresAt =null;constauth0Client=newauth0.WebAuth({// the following three lines MUST be updated domain:AUTH0_DOMAIN, audience:`https://${AUTH0_DOMAIN}/userinfo`, clientID:AUTH0_CLIENT_ID, redirectUri:"http://localhost:3000/callback", responseType:"id_token", scope:"openid profile"});/** * This is the method that the app will call right after the user is redirected from Auth0. * This method simply reads the hash segment of the URL to fetch the user details and the id token. */exportconsthandleAuthentication=async () => {returnnewPromise((resolve, reject) => {auth0Client.parseHash((err, authResult) => {if (err){ returnreject(err);}if(!authResult ||!authResult.idToken){ returnreject(newError('user was not registered'))} idToken =authResult.idToken; profile =authResult.idTokenPayload;// set the time that the id token will expire at expiresAt = (authResult.expiresIn ||1000) *1000+newDate().getTime();console.log(authResult)resolve(profile); }); });}/** * This method signs a user out by setting the profile, id_token, and expiresAt to null. */exportconstsignOut= () => { idToken =null; profile =null; expiresAt =null;}/** * This method returns the profile of the authenticated user, if any */exportconstgetProfile= () => profile;/** * This method returns the `idToken` generated by Auth0 for the current user. * This is what you will use while issuing requests to your POST endpoints. */exportconstgetIdToken= () => idToken;/** * This method returns whether there is an authenticated user or not. */exportconstisAuthenticated= () =>newDate().getTime() < expiresAt;/** * This method initializes the authentication process. * In other words, this method sends your users to the Auth0 login page. */exportconstsignIn= () =>auth0Client.authorize();
Of course, similarly to the previous time, we'll have to fill AUTH0_DOMAIN and AUTH0_CLIENT_ID.
Now, let's use this instead of our custom authentication system.
note: it's still useful to have those methods; for example, you might want to pop a toast saying "login successful". But we're trying to keep the amount of methods manageable, so for now, remove them.
You can also go ahead and remove the onLoginSubmit, login, and the logout methods, as well as the nick and token keys in state. Auth0 is now handling this, we only have to display the results. Also, you can remove all references to makeUrl({ token: this.state.token}).
However, we will replace this with Auth0's token. Instead of sending it in GET parameters, Auth0 uses a specific header. Everywhere you need to authenticate, you can send:
This is no different than sending the token like we used to, it's just better to keep it in another part of the request so it doesn't show everywhere all the time.
Finally, let's add a new route, /callback, and a handler for it:
...// this route inside renderContent() { ... }: <Routepath="/callback"render={this.handleAuthentication} />...// and this method just after renderContent() { ... }: isLogging =false;login=async () => {if (this.isLogging ===true) {return; }this.isLogging =true;try{awaitauth0Client.handleAuthentication();constname=auth0Client.getProfile().name // get the data from Auth0awaitthis.getPersonalPageData() // get the data from our servertoast(`${name} is logged in`)this.props.history.push('/profile') }catch(err){this.isLogging =falsetoast.error(err.message); } }handleAuthentication= ({history}) => {this.login(history)return <p>wait...</p> }
note: if the app re-renders, it may call login repeatedly. So with the isLogging property, we ensure subsequent calls will be cancelled
And you should be ready to go!
We still have a little problem: the logout button doesn't seem to work. The reason is that, when the button is clicked, the user is logged out, but the state of the App doesn't change, thus, our view doesn't change.
We simply force the state to change by passing an empty object. It's hacky, but does the trick (there are cleaner methods of doing that, but this will suffice for now).
Final Cleanup
Congradulation, we have implemented Auth0 a third party user authentification. If you haven't done it before remove the old methods in back/src/auth.js.
You also have to remove previous route in back/src/index.js.
app.get('/login', authenticateUser)
And
app.get('/logout', logout)
Don't forget to cleanup the import to avoid warnings
import { authenticateUser, logout, isLoggedIn } from './auth'
Become
import { isLoggedIn } from './auth'
Almost there!
One problem, your application forgets the user as soon as the browser is refreshed. Let's fix that in PART 2.
Note if you get an EADDRINUSE error when your server restarts, edit the start script in your back/package.json, and change it to: