This repo contains a working app that includes CRUD operations for fruit. It has the beginnings of authentication such as a <Nav>
component with buttons for registering and signing in and a component to conditionally hide/show its child components based on if the user is authenticated.
Our task is to complete the authentication implementation of this app.
-
Clone this repo and
cd
into the new directory -
Install packages, run migrations and seeds, and start the dev server with
npm run dev
Tip
Commands might look like this:
npm install npm run knex migrate:latest npm run knex seed:run npm run dev
-
Visit http://localhost:5173 in your browser
In order to complete the implementation of authentication for this app, we need to make changes on both the client-side and the server-side to enable user registration and sign-in. We're also going to protect certain routes (the ones that alter data) so that only authenticated users can call them.
Requirements summary
- Determine if the current user is logged in or not
- Allow the user to register
- Allow the user to sign in
- Send the access token with each request
- Allow the user to log off
- Hide/show components based on the user's auth status
The following routes should accept only authenticated requests
- POST
/api/v1/fruits
- PUT
/api/v1/fruits
- DELETE
/api/v1/fruits
-
Explore the existing codebase
More about exploring the codebase
No need to rush into this. There might be some patterns you haven't seen before.
For example, this codebase uses
styled-components
. There is also a nice use ofprops.children
in the<Authenticated>
components. -
Get familiar with the user interface
Tips
At this stage it's normal that "Sign out" displays (as if you were logged in), even though you're not logged in.Select some fruits and try to update their values, delete them, and add new ones. See which work and which give errors.
Once you're comfortable enough with the app, proceed with a sense of curiosity as we enable authentication and lock down parts of the UI and some of the web API to only authenticated users.
-
Open your browser, go to Auth0.com and sign up for a new account
-
Use the following settings to configure your user
User configuration settings (including value 1️⃣)
- For the "Role," select Yes, coding, and tick I need advanced settings (you don't need a chat with an expert)
- The default domain will be something like
dev-fsdf1y29
, but you should overwrite it with a domain of your own, in the formatcohortName-yourFirstName
, for examplematai-2021-john
1️⃣. This value will be used later - Select Australia as your Region
- Click Create Account
- Make sure Development is selected as the Environment tag. This should be the default but you can check it by looking at what is displayed at the top left (in the black bar, immediately under your domain) or by going to Settings
-
Go to Applications, and click the Create Application button
-
Use the following settings to configure your application
App configuration settings (including value 2️⃣)
- Give your application a name, for example "Fruits App"
- Select Single Page Web Applications and click the Create button. This application will be used for our front-end app. On creating, you will be taken to the "Quick Start" tab of your new app
- Select the Settings tab
- Auth0 generated a random ClientId 2️⃣, make a note of it, because we will use this value later.
- Set the following values, in the Application URIs section:
Setting Value Allowed Callback Url http://localhost:5173/
Allowed Logout Url http://localhost:5173/
Allowed Web Origins Url http://localhost:5173/
- Scroll down to the bottom of the page and click the Save Changes button
-
On the side bar, expand Applications, click on APIs, then click the Create API button
More about the API
In order to protect our routes in the server-side, we need to verify that tokens passed from the client are valid. Creating an API that is linked to the Auth0 Application, the one that you just created, will check the token's validity.
-
Give your API a name, for example, "fruits"
-
Set the Identifier field to be
https://fruits/api
3️⃣, this value will be used as ouraudience
later
In client/index.tsx
:
-
Observe how
<Auth0Provider>
has been used inclient/index.tsx
More about
<Auth0Provider>
<Auth0Provider>
has been imported from the Auth0 package<Auth0Provider>
wraps the<App>
component<Auth0Provider>
has some attributes with no values... yet
-
Set the values in each attribute of
<Auth0Provider>
to the proper values from previous steps, marked 1️⃣, 2️⃣, and 3️⃣More about these attributes
See the docs for the provider component.
Attribute Value domain
See 1️⃣ above, format is cohortName-yourFirstName.au.auth0.com
clientId
See 2️⃣ above, this is the random string you made a note of earlier audience
See 3️⃣ above, https://fruits/api
-
Refresh your browser and check the Network tab in the DevTools, if you see errors, then double check the steps above
Commit your code and swap driver/navigator if you're pairing.
In client/components/Authenticated.tsx
:
-
Explore
Authenticated.tsx
and the use ofisAuthenticated
More about
Authenticated.tsx
Our existing code contains a couple of clever
<IfAuthenticated>
and<IfNotAuthenticated>
components inclient/components/Authenticated.tsx
. They render their child components based on the authentication status of the user.Fortunately,
@auth0/auth0-react
package exports auseAuth0
hook. This hook exposes useful functions and values. Here we will use theisAuthenticated
boolean value to see if there is an auth token, and that it hasn't yet expired. This hook does the checking behind the scenes.Right now there is a placeholder
useIsAuthenticated
hook which is hard-coded to returntrue
. -
Import the
useAuth0
hook from within@auth0/auth0-react
-
Call
useAuth0
within theuseIsAuthenticated
function, destructure theisAuthenticated
property out of it and return this boolean variableMore about
useIsAuthenticated
Note that because
useIsAuthenticated
calls a hook inside of it and returns its value, it also becomes a hook, which is why we start the function name withuse
.With that in place, you can now see the "Sign in" link in the app.
Now is a good time to commit your changes and swap driver/navigator if you're pairing.
In client/components/Nav.tsx
:
-
Import the
useAuth0
hook from@auth0/auth0-react
and use it inside the<Nav>
component -
Destructure the
logout
, andloginWithRedirect
functions out of theuseAuth0
hook -
Call these functions in the two handlers (instead of the
console.log
placeholders)More about login and logout handlers
- In
handleSignOut
we'll calllogout
- In
handleSignIn
, we'll callloginWithRedirect
The "Sign In" link will redirect you to Auth0's authentication service and prompt you to enter an email and password. If this is your first time signing in, click on Sign up below the Continue button. This form allows you to create a new user (subscription) that is only used for the one Auth0 app. Even if you used the same email and password when creating an account on a different app, Auth0 will treat it as a new account that is specific to your Fruits app.
After you've registered your new user, you will be redirected back to
http://localhost:5173
and "Sign out" will again be visible in the app. - In
Commit your code and swap driver/navigator if you're pairing.
Currently, when we are signed in, we still see "john.doe" rendered as the nickname. Let's replace that with the nickname of the current user.
In client/components/Nav.tsx
:
-
Delete the hard-coded value for
user
and replace it with theuser
object fromuseAuth0
More about
user
inNav.tsx
Notice how
user?.nickname
has a question mark in front of it. This is called the optional chaining operator. When we sign in, the user object may still be null while theuser
object is being fetched from Auth0. This optional chaining operator will prevent the app from crashing if the user object is null (while the user is authenticated). -
When a user is signed in, try
console.log
ing theuser
object. You'll see that it has anickname
property; this is what is being rendered inNav.tsx
. You may try rendering other properties, such asname
,email
, orimage
.
Commit your code and swap driver/navigator if you're pairing.
We only want to allow a user to use our server routes if the user has been authenticated. We can retrieve a JWT token from Auth0 with getAccessTokenSilently
, we want to pass it as a header when calling our server-side routes.
-
In
client/components/Fruits.tsx
destructuregetAccessTokenSilently
, and call it in thehandleAdd
event handler to retrieve a token. Note:getAccessTokenSilently
returns a Promise.Tips
getAccessTokenSilently
returns a Promise, so you'll need to use either:// async/await function handleMyEvent() { const token = await getAccessTokenSilently(); // ... pass to api function } // or // .then/.catch function handleMyEvent() { getAccessTokenSilently() .then((token) => { // ... pass to api function }) }
-
Pass
token
to theaddFruit
function as the second parameter -
Repeat the same steps for
handleUpdate
andhandleDelete
Commit your code and swap driver/navigator if you're pairing.
-
In
server/auth0.ts
, set thedomain
(1️⃣, see tip below for required format) andaudience
(3️⃣) valuesTip
The format of
domain
should behttps://cohortName-yourFirstName.au.auth0.com
andaudience
should behttps://fruits/api
.
There are three routes in server/routes/fruits.ts
that we want to be accessible only for authenticated users.
-
In each of the routes we want to protect, pass
checkJwt
as a second parameterMore about protecting routes
You'll need to import the
checkJwt
function fromserver/auth0.ts
.Passing
checkJwt
to the route might look like...route.post('/', checkJwt, (req, res) => { // do stuff here })
The following routes should accept only authenticated requests
- POST
/api/v1/fruits
- PUT
/api/v1/fruits
- DELETE
/api/v1/fruits
- POST
What's happening with the middleware?
Every time a route receives an HTTP request, the checkJwt
middleware will be activated and issue an HTTP request behind the scenes (machine to machine). The Auth0 service will compare the public signatures. If all goes well, express
will execute the body of your route.
Now our middleware is ready to be used.
🎉 Congratulations, you made it! 🎉
More about stretch challenges
Some of the buttons and/or links are only valid in certain circumstances (if you're logged in, if you're the person who created that fruit, etc.). What improvements can you make to the app so that users only see buttons/links that they're actually allowed to use?
Open cheatsheet
// importing and using the useAuth0 hook
import { useAuth0 } from '@auth0/auth0-react'
function MyComponent() {
const { isAuthenticated, user, getAccessTokenSilently, loginWithRedirect, logout } = useAuth0()
// ...
}
// retrieve access token to give to API functions
// async/await
async function handleMyEvent() {
const token = await getAccessTokenSilently()
const response = await fetchFromApi(token)
}
// or
// .then/.catch
function handleMyEvent() {
getAccessTokenSilently()
.then((token) => {
return fetchFromApi(token)
})
.then((response) => {/* ... */})
}
// secure an API route
router.get('/my-protected-route', checkJwt, (req, res) => {
// ^? this is the middleware
// user is authenticated
const userId = req.auth?.sub
// ...
})