Front End Authentication with Gatsby & JWT
Now we are at the point where we want to password protect our Gatsby Todo App. Remember way way back in Part 1 of this tutorial where we implemented JWT authentication for our Django backend. Implementing JWT authentication for Gatsby is a little different.
Client-only Routes
To create pages with gated content that are restricted to only authenticated users, we will use Gatsby’s client-only routes to tell Gatsby which pages we want to password protect. To keep it simple we will create a password protected user profile page which displays the logged in user’s name, email, etc.
We start by breaking out the main menu into its own component.
NavBar.js
Create a new navbar.js file in the components folder:
touch frontend/src/components/navbar.js
Add the following to the navbar.js file:
// frontend/src/components/navbar.js
import { Link } from "gatsby"
import React from "react"
import styled from "styled-components"
const Nav = styled.nav`
a {
padding: 0 1.0rem 0 0;
}
`
export const Navbar = () => {
return(
<div>
<Nav>
<Link to="/">Home</Link>
{/* More to come here */}
</Nav>
</div>
)
}
export default Navbar
… and refactor the header.js file to include our Navbar component:
// frontend/src/components/header.js
import { Link } from "gatsby"
import PropTypes from "prop-types"
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import styled from "styled-components"
import Navbar from "./navbar"
const HeaderWrapper = styled.div`
padding: 2.0rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: rebeccapurple;
a {
color: white;
text-decoration: none;
}
`
const SiteTitle = styled.div`
font-family: Arial, Helvetica, sans-serif;
`
const HeaderH1 = styled.h1`
font-size: 2.6rem;
`
export const PureHeader = ({ data }) => (
<HeaderWrapper>
<SiteTitle>
<HeaderH1>
<Link
to="/"
>
{data.site.siteMetadata.title}
</Link>
</HeaderH1>
</SiteTitle>
<Navbar />
</HeaderWrapper>
)
export const Header = props => {
const data = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
}
}
}
`)
return <PureHeader {...props} data={data}></PureHeader>
}
Header.propTypes = {
siteTitle: PropTypes.string,
}
Header.defaultProps = {
siteTitle: ``,
}
export default Header
In the refactoring of the Header component we imported the Navbar component and replaced the MainMenu styled-component wrapper with the Navbar component. As well, the Main Menu styled component definition was removed since we are no longer using it.
Authentication Helpers
It would be nice to have a couple of helper functions for:
- Telling us if a user is logged in
- Get the logged in user’s info
Lets create a new folder for services with a new file auth.js:
mkdir frontend/src/services
touch frontend/src/services/auth.js
In the auth.js file we’ll create getUser() and isLoggedIn() functions:
// frontend/src/services/auth.js
export const isBrowser = () => typeof window !== "undefined";
export const setUser = (username, token) => {
window.localStorage.setItem("user", JSON.stringify({
"username": username,
"token": token
}))
}
export const getUser = () => {
let user = JSON.parse(window.localStorage.getItem("user"))
return user
}
export const getToken = () => {
if (isBrowser() && window.localStorage.getItem("token")) {
return window.localStorage.getItem("token")
} else {
return {}
}
}
export const isLoggedIn = () => {
try {
var userObj = getUser()
var userToken = userObj.token.tokenAuth.token
userToken = "JWT " + userToken
// token that Django set
var setToken = getToken()
// compare tokens as security caution
if (userToken == setToken) {
return true
}
} catch (error) {
return false
}
}
export const logout = () => {
window.localStorage.clear()
}
Gatsby Client-Only Routes
We will be using @reach/router to create our password pages. The @reach/router library comes with Gatsby so we don’t need to install it.
First we create gatsby-node.js file in the root of the frontend and we will define routes that start with /app/ as our password protected pages.
touch frontend/gatsby-node.js
// frontend/gatsby-node.js
// Implement the Gatsby API “onCreatePage”. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/app/)) {
page.matchPath = "/app/*"
// Update the page.
createPage(page)
}
}
Now we need to create an app.js to generate the restricted access pages.
touch frontend/src/pages/app.js
// frontend/src/pages/app.js
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import Profile from "../components/profile"
import Login from "../components/login"
const App = () => (
<Layout>
<Router>
<Profile path="/app/profile" />
<Login path="/app/login" />
</Router>
</Layout>
)
export default App
Next, lets create the components for these two routes:
touch frontend/src/components/profile.js
// frontend/src/components/profile.js
import React from "react"
const Profile = () => (
<>
<h1>Your profile</h1>
<ul>
<li>Name: Your name will appear here</li>
<li>E-mail: And here goes the mail</li>
</ul>
</>
)
export default Profile
touch frontend/src/components/login.js
Connecting to Django as the Authentication Service
This is where we diverge from the Gatsby authentication tutorial in order for us to connect to our Django backend which will handle user authentication leveraging JWT.
// frontend/src/components/login.js
import React from "react"
import { navigate } from "gatsby"
import { useMutation } from "@apollo/client"
import gql from "graphql-tag"
import styled from "styled-components"
// Request token from Django JWT
const LOGIN_MUTATION = gql`
mutation tokenAuth($username: String!, $password: String!) {
tokenAuth(username: $username, password: $password) {
token
}
}
`
/*
* Begin Styled Components
*/
const FormHeader = styled.div`
font-size: 2.0rem;
`
const LoginErrorWrapper = styled.div`
color: red;
padding: 0.2rem;
font-size: 1.4rem;
font-weight: bold;
`
const FormWrapper = styled.div`
div {
margin-bottom: 1.0rem;
}
label {
text-align: right;
width: 300px;
}
input {
float: right;
margin-left: 1.0rem;
}
`
const ButtonWrapper = styled.div`
width: 100%;
text-align: center;
button {
margin-top: 0.5rem;
width: 100px;
background-color: red;
color: white;
padding: 0.2rem 1.0rem;
font-weight: bold;
cursor: pointer;
}
`
const Login = (props) => {
let username = ''
let password = ''
const [tokenAuth, { data, error }] = useMutation(LOGIN_MUTATION, {
onCompleted({ tokenAuth }) {
let jwtToken = "JWT" + tokenAuth.token
localStorage.setItem("token", jwtToken)
navigate(`/app/profile`)
}
})
let loginError = ''
if (error) {
loginError = 'Incorrect credentials'
}
return (
<FormWrapper>
<LoginErrorWrapper>{loginError}</LoginErrorWrapper>
<FormHeader>Log in</FormHeader>
<form
method = "post"
onSubmit = {e => {
e.preventDefault()
tokenAuth({ variables: {
username: username.value,
password: password.value,
}})
}}
>
<div>
<label>
Username:
<input
ref = { node => {
username = node
}}
type = "text" name="username"
/>
</label>
</div>
<div>
<label>
Password:
<input
ref = { node => {
password = node
}}
type = "password"
name = "password"
/>
</label>
</div>
<ButtonWrapper>
<button type = "submit">Log in</button>
</ButtonWrapper>
</form>
</FormWrapper>
)
}
export default Login
Controlling Private Routes
Next, we wrap our restricted routes inside a PrivateRoute component:
touch frontend/src/components/privateRoute.js
// frontend/src/components/privateRoute.js
import React from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../services/auth"
const PrivateRoute = ({ component: Component, location, ...rest }) => {
if (!isLoggedIn() && location.pathname !== `/app/login`) {
navigate("/app/login")
return null
}
return <Component {...rest} />
}
export default PrivateRoute
Refactor the app.js page to use the PrivateRoute component:
// frontend/src/pages/app.js
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import PrivateRoute from "../components/privateRoute"
import Profile from "../components/profile"
import Login from "../components/login"
const App = () => (
<Layout>
<Router>
<PrivateRoute path="/app/profile" component={Profile} />
<Login path="/app/login" />
</Router>
</Layout>
)
export default App
Since we’ve added the gatsby-node.js file we will need to rebuild our Docker frontend container:
docker-compose build frontend
# boot up dev env
docker-compose -f docker-compose.yml -f docker-compose-dev.yml up
# if you have a problem booting up the Django server run:
docker-compose up -d database
docker-compose up -d server
docker-compose -f docker-compose.yml -f docker-compose-dev.yml up
# or
make dev
Refining Authentication UI
You should now be able to hit http://localhost:8000 and be able to login which should redirect you to the profile page.
Now that we can login we need a way of logging out which is essentially clearing the token from localStorage. So to add a functional logout link to the navbar when the user is logged in, update the Navbar component with the following additions:
// frontend/src/components/navbar.js
import { Link } from "gatsby"
import React from "react"
import styled from "styled-components"
import { isLoggedIn, logout } from "../services/auth"
const Nav = styled.nav`
a {
padding: 0 1.0rem 0 0;
}
`
const handleLogout = () => {
logout()
}
export const Navbar = () => {
return(
<div>
<Nav>
<Link to="/">Home</Link>
{!isLoggedIn() ? <Link to="/app/login">Login</Link> : ''}
{isLoggedIn() ? <Link href="#" onClick={handleLogout}>Logout</Link> : ''}
</Nav>
</div>
)
}
export default Navbar
If you are not logged in a Log In link should appear in the navbar which will take you to your log in page.
This completes our infrastructure allowing users to authenticate with our Django back end from our Gatsby front end.
What’s next? For fun, we will leverage the power of styled components to build a fly-in mobile menu in our next chapter.