Introduction
Security on the internet comes under scrutiny the more our personal lives and business data moves online. Users want integration between applications without having to continuously enter user login data. Users also want security without noticing that the security is there.
In this article, we are going to talk about two aspects of security—authentication and authorization—and how they are applied to the web and APIs. Then, I will walk you through a tutorial that demonstrates a framework for using these concepts in a React application.
Authentication and Authorization
Every article that talks about security needs to make the distinction between authentication and authorization.
Authentication deals with identifying an entity (i.e user, server, or device). When identifying a user, a web application may request a username and password. If a cloud service needs to identify a server, it could ask for the IP address. Finally, devices are sometimes identified with their unique MAC addresses.
In contract, authorization allows an entity to access another entity (or resource).
One example of how authorization differs from authentication can be explained with a house key. The lock on a door does not care who is using the key to enter the house. It simply knows that people in possession of the house key are authorized to enter.
Now, let’s say that this door had the ability to identify people by their fingerprints. A person may approach the door, submit their fingerprint, and be identified by their name. At this point, they would have authenticated as an entity, but without a key, they would not be authorized to enter the home.
In the next section, let’s introduce different methods for authorizing API access.
Methods for Securing APIs
API Keys
API keys are a form of authorization. An API service issues a key to an entity allowing the key to be used for their service. The API service doesn’t check whether the key is used by the owner (or requestor) of the key.
This is why keeping an API key private is important. If it falls into the wrong hands, it could be used without your knowledge.
CORS
API and cloud service providers may also offer the ability for users to enact CORS policies for their resources. CORS stands for Cross-Origin-Resource-Sharing.
It can be used to limit the HTTP request types that are allowed for a resource or restrict access to a resource based on the domain that is it originating from.
For example, I only allow access to my API routes at http://example.com
from http://shop.example.com
and http://example.com
. All other requests are denied access.
If you operate an API designed to service a limited number of domains—with strict API specifications—enacting a CORS policy would provide further protection because it restricts access to a few authorized entities.
IP Whitelisting
IP whitelisting is often used in system administration for network security. If there are only a few IP addresses that should be accessing a server, then all other IP addresses should be blocked. This is what a firewall is capable of doing.
Sendgrid is an email delivery service that allows users API access to their accounts. Sendgrid gives its users the option to specify the IP addresses that should be accessing their account resources through the API.
The above methods are used by API providers and users to protect resources from unwanted use. In the next part of this article, we’ll talk about OAuth and OpenID Connect. Both of these are standards that deal with authorization and authentication.
Ways of Implementing API Authorization and Authentication
OAuth
OAuth is an open standard for access delegation, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords.
OAuth (short of open authorization) is a way to authorize services to access other resources. In a more concrete example, you may “sign-up” for Netlify by authorizing Netlify to access your Github account. This is commonly shown as Sign Up with Github or Sign Up with Facebook on a login page.
Some services that implement OAuth, in this way, don’t require usernames or passwords. That’s because they aren’t concerned with authentication. They have been authorized to access your information (in the tutorial application later in this article, we will implement an OAuth application workflow with ReactJS and Github).
OAuth is sometimes implemented behind an authenticated service. A user may create an account providing a name, email, password, etc. Then, they confirm that account through their email address. After signing in, they are given the option to integrate with another service. Using OAuth, these two services can work together to make the users’ experience better by sharing authorized data.
Let’s consider a scenario involving two services: Service-A and Service-B. Service-A offers the user to integrate with Service-B. If accepted, Service-A sends user information (email address) to Service-B asking if Service-B has any users with this email. If Service-B has a user with the same email as Service-A—and they have implemented OAuth—Service-B can assume that this authorization request is valid and provide a token to allow access.
This is a convenient workflow for users but still can expose holes for attackers to exploit.
OpenID Connect
OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.
At times, OAuth may not be enough. OpenID Connect seeks to extend OAuth 2.0 by adding a layer of authentication. How does this change the example above about the exchange between Service-A and Service-B?
Service-A may request access to the users’ data at Service-B. However, before allowing access, Service-B may require the user to authenticate as the owner of the data that Service-A is requesting.
That is a very simple and high-level overview of how these two specifications differ. In reality, there can third-party OAuth and OpenID Connect servers that handle the exchange between users and services. To learn more about the OpenID Connect specification, you can visit the OpenID.net website.
ReactJS
ReactJS is a front-end Javascript framework for building user interfaces. Furthermore, the framework is often used to build SPAs (single page applications). Single-page applications are pulled into the browser. This means that none of the data in the SPA is safe. Therefore, React applications that are using the SPA paradigm are not able to store API keys on the front-end for authorized access.
This hampers the ability for the application to authorize itself for third-party API calls. However, this does not disable its ability to authenticate.
In the rest of this article, we are going to use ReactJS to build an application that can authenticate users. Then, the application will offer the ability for the user to authorize our application to pull data about the user from Github. We have a lot to do, so let’s get started!
Build a React App Using Authentication and API Authorization
View the code repository on Github
In this application, we are going to demonstrate the differences between authentication and authorization in a React app. We can do this by simulating the authentication principles of a React app and implementing an OAuth application through Github that authorizes our application to fetch Github user data through the Github API.
To build the application, we are going to use the Next.js framework. Next.js features include (according to their documentation):
- An intuitive page-based routing system (with support for dynamic routes)
- Pre-rendering, both static generation (SSG) and server-side rendering (SSR) are supported on a per-page basis
- Automatic code splitting for faster page loads
- Client-side routing with optimized prefetching
- Built-in CSS and Sass support, and support for any CSS-in-JS library
- Development environment with Fast Refresh support
- API routes to build API endpoints with Serverless Functions
- Fully extendable
The important thing to note about Next.js is that it, “generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO.” Furthermore, Next.js lets us create API routes inside of our application project folder. This is great for backend-code, or code that processes sensitive data.
Next.js includes many features out-of-the-box and is recommended by ReactJS.org as one of the technologies to use when creating a React app. We have a lot to cover in this application so let’s begin!
Prerequisites
- NodeJS installed locally (10.13 or later)
- Knowledge of how to use a command-line application
- If on Windows, download Git for Windows, and use the Git BASH application that comes with it.
- Basic NodeJS and React knowledge
- Internet access
- Code editor (Sublime, VSCode, etc.)
- Github account
1. Set Up Next.js Project
Open up a terminal application (BASH, Git BASH, Terminal, Powershell, etc.) and navigate to the directory that you wish to create the app in. This can be done in the terminal using the command cd
.
In the terminal, run the command npx create-next-app
. Then, provide a name for the project (i.e react-authentication
) when prompted.
Open up your code editor in the newly created project folder. I am using Visual Studio Code.
pages
The Javascript files created in the pages
directory are built as individual pages. Create a file in this directory named account.js
and insert the following code.
import Head from 'next/head' import styles from '../styles/Home.module.css' export default function Account({ query }) { React.useEffect(() => { // Call the Github API route to fetch user data }, []) return ( <div className={styles.container}> <Head> <title>Account</title> <link rel="icon" href="/favicon.ico" /> </Head> {/* Add logout button */} <main className={styles.main}> <h1>Authenticated Account Page</h1> <section className={styles.data}> <h2>Basic User Information</h2> <small>Since we know it's you.. here's your information!</small> {/* Display user information */} </section> <section className={styles.data}> <h2>Github OAuth</h2> <small>Authorize this application to acces your Github information.</small> {/* Add Github component */} </section> </main> <footer className={styles.footer}> <a href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" target="_blank" rel="noopener noreferrer" > Powered by{' '} <img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} /> </a> </footer> </div> ) }
In the file above, we created a page using the same styles and structure from index.js
. Let’s start the development server and see what we have so far.
In the root of your project directory (react-authentication
) execute npm run dev
. This starts a development server on http://localhost:3000
. Visit the webpage in your browser. You should see the default starter page for Next.js projects.
In the browser, manually change the URL to http://localhost:3000/account
.
The page we created in pages
should now appear.
pages/api
Theapi
folder inside of the pages
directory holds the names of the API routes that our client application can call at /api/[route name]
.
The starter project provides an example API route for us at http://localhost:3000/api/hello
. If you navigate to that URL, you see:
{"name":"John Doe"}
If we inspect the pages/api/hello
file we see this is exactly what this function should return if it receives an HTTP request.
Delete pages/api/hello
and create four new files in the pages/api
folder:
- auth.js
- logout.js
- user.js
- github.js
Insert the code below into each of the new files.
const allowedMethods = [] export default (req, res) => { res.setHeader('Allow', allowedMethods) if (!allowedMethods.includes(req.method)) { return res.status(405).end() } return res.status(200).end() }
styles
The styles
directory can hold global or modular CSS files. We can create CSS classes in the Home.module.css
file and import that file as an object into a component file. Then, we can access the defined class names on the imported styles
object.
The globals.css
file is used to apply styles to all elements like <li>
, <body>
, etc.
Open up the styles/Home.module.css
and insert the following styles. Do not delete the styles that are already in the file.
.... .input { display: block; margin: 5px; border-radius: 5px; border: 1px solid #CCC; font-size: 1.2em; line-height: 1.7em; -webkit-border-radius: 5px; -moz-border-radius: 5px; -ms-border-radius: 5px; -o-border-radius: 5px; } .button { font-size: 1.2em; color: white; border: none; padding: .4rem .7rem; background: #0070f3; border-radius: 2px; margin-right: auto; -webkit-border-radius: 2px; -moz-border-radius: 2px; -ms-border-radius: 2px; -o-border-radius: 2px; } .formGroup { margin: 10px; } .data { border: 1px solid #CCC; margin: 1rem auto; padding: 1rem; border-radius: 5px; -webkit-border-radius: 5px; -moz-border-radius: 5px; -ms-border-radius: 5px; -o-border-radius: 5px; } .github { margin: 1rem auto; } .github > a { color: blue; } ....
You now have an idea about the structure of a Next.js project. In the next section, we are going to add a login component, user data, and implement user authentication.
2. Add User Authentication
First, add a form that will accept a user’s email and password to the file index.js
. Replace all the code inside the <main>
HTML tag with the code:
... <h1 className={styles.title}> React API Authorization </h1> <div className={styles.grid}> <div className={styles.card}> <h3>Login →</h3> <form> <div className={styles.formGroup}> <label htmlFor="email">Email</label> <input onChange={(e) => setUsername(e.target.value)} className={styles.input} autoComplete='on' type="email" id="email" name="email" /> </div> <div className={styles.formGroup}> <label htmlFor="password">Password</label> <input onChange={(e) => setPassword(e.target.value)} className={styles.input} type="password" id="password" name="password" /> </div> <button onClick={(e) => login(e)} className={styles.button}>Login</button> </form> <p> Authenticate as a user via the mock server </p> </div> </div> ...
The code above creates a form that submits two React state variables (set up using React Hooks). The submit button executes a function login
. Next, we need to define this function and our state variables.
Above the return
statement in index.js
, add;
... const router = useRouter(); let [username, setUsername] = React.useState('') let [password, setPassword] = React.useState('') const login = async (e) => { e.preventDefault() try { await api.post('/api/auth', { username, password }) router.push('/account') } catch (e) { setPassword('') console.log(e) } } ...
Then, import the needed modules at the top of the file.
import React from 'react' import api from '../api' import { useRouter } from 'next/router' ...
Saving the file will throw an error because we haven’t created the api
module yet. However, before adding that file, let’s take a look at the component code.
We imported the useRouter
hook from Next. This allows us to manage the navigation of the user while taking advantage of Next’s routing features.
Then, we declared our React state hooks for username and password.
Finally, we created an asynchronous function that calls the /api/auth
API route (sending the user credentials). If the call to that route is successful, the user is redirected (via the router) to the /account
page. If it fails, the password is cleared and the error logs to the console.
Create api
File
The api
module is a separate file containing an Axios instance. In the root of the project, create the file api.js
. Insert the following code into that file.
import axios from 'axios' const api = axios.create({ baseURL: 'http://localhost:3000', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); export default api;
Save the file.
We are almost ready to move on to the auth.js
file, but we need to import Axios into our project with npm
.
Head back to your command line and in the project folder, run the command npm install axios
. You may need to restart the development server. Once all the new file changes have been saved your home page should look like the image below.
We are going to create a file to mock a database with user information. In the project root (same directory as the api.js
file) create the file data.js
and add the following code.
export default [ { id: 1, name: 'Jarrett', email: 'jarrett@app.org', password: 'react-authentication123', } ]
You can, of course, modify the data for the user object or manually add more user objects.
When a user hits the Login button the function login
executes calling the /api/auth
route. This route needs to:
- Make sure that the request method is allowed
- Check if the user exists
- Check if the password is correct
- If the credentials are found, set an authorization cookie and return the HTTP status code 200
Cookie Authentication
Open up /api/auth
and add 'POST'
to the allowedMethods
array.
Import data.js
at the top of the file with the line import data from '../../data'
Then, extract the credentials from the request and search for a user.
Add the following code underneath the if
statement that checks for allowed HTTP methods.
... const { body } = req const { username, password } = body // Validate credentials middleware const user = data.find(user => user.email === username) ...
Next, we add two more if
statements, below the code above, that check if the user exists or if the password is correct. If either fails we send back a 404 Not found status code.
... if (!user) { return res.status(404).end() } if (!user.password !== 'password') { return res.status(404).end() } ...
WARNING: This is not a secure way to check passwords for users or to validate credentials. Passwords should be encrypted and validation middleware should be used instead. This setup is only used as an example of the structure for authentication.
Finally, if the credentials match, we set a cookie on the res
object.
An easy way to serialize cookies is with the cookie
module. Import this module into the project by running npm install cookie
in the project root.
Import cookie
at the top of the file and add this code below the if
block containing the password check.
... res.setHeader('Set-Cookie', cookie.serialize('authorization', user.name, { httpOnly: true, // Javascript can't access value secure: process.env.NODE_ENV === 'development' ? false : true, // Only use HTTPS sameSite: 'strict', // Only send cookie to this site maxAge: 600, // In seconds path: '/' // Should be set to avoid problems later })); ...
This cookie is only sent to our domain, cannot be accessed (hijacked) by malicious Javascript, can expire, and has the option to only be used with HTTPS. Typically, the value for this cookie is set to a session ID that is managed in the database or by a session library/database. Alternatively, it can be set to a JWT.
You should not set sensitive information as the cookie value that is not encrypted but, for this tutorial, we are going to make an exception and set the user’s name as the value. This allows us to find the user more easily in later functions.
You can now use the credentials saved in data.js
to sign into the application. If the sign-in is successful, you are redirected to the account
page.
Create the Logout Function
On the account page, let’s implement a logout function that immediately expires the authorization cookie by setting a new cookie and redirecting the user back to the home page.
In the file /api/logout
, import the cookie module at the top.
import cookie from 'cookie'
Then, add 'GET'
as one of the allowed methods.
Next, insert this set cookie code before the return statement.
... res.setHeader('Set-Cookie', cookie.serialize('authorization', '', { httpOnly: true, secure: process.env.NODE_ENV === 'development' ? false : true, sameSite: 'strict', maxAge: new Date(), path: '/' })); ...
In pages/account
, add the function logout
,
... const logout = () => { api.get('/api/logout').then(() => { router.push('/') }) } ...
insert a button that calls the function below the Head
component,
... <button className={styles.button} style={{ background: 'red', margin: 'none' }} onClick={() => logout()}>← Logout</button> ...
add the necessary imports at the top of the page,
import api from '../api' import { useRouter } from 'next/router' ...
and initialize the router
object above the logout
function.
... const router = useRouter(); ...
Now, you can logout, and—when you do—you are redirected to the homepage. In the next section, we’ll make the account page private and display user data.
3. Fetch User Data
Next.js has an approach for fetching data that, “…works well for user dashboard pages, for example. Because a dashboard is a private, user-specific page, SEO is not relevant and the page doesn’t need to be pre-rendered. The data is frequently updated, which requires request-time data fetching.”
This can be done using a library created by Next.js named SWR that stands for stale-while-revalidate. We provide the SWR hook with a URL and ‘fetcher’, which can be any HTTP library that returns a promise like fetch
and Axios
.
First, install the package on the command line with npm install swr
.
Next, import the useSWR
hook at the top of the file.
import useSWR from 'swr'
Then, in account.js
, create the fetcher function below the imports.
const fetcher = async (url) => api.get(url)
router
initialization, call the useSWR
function. This hook returns a data
and an error
object.const { data, error } = useSWR('/api/user', fetcher)
useSWR
function call.... if (error) { router.push('/') } if (!data) { return <div>Loading...</div> } ...
If there is an error, redirect the browser back to the home page. If there is no data, display a loading div
. This will stop us from viewing the account page because the user
API route does not exist.
Create user
Route
Import data
at the top of the page. This is the mock user data file that we created earlier.
Then, in the allowed methods, add 'GET'
.
Below the if
statement that checks the allowed methods insert:
... const name = req.cookies.authorization; if (!name) { return res.status(404).end() } const { email } = data.find(user => user.name === name) ...
This code tries to locate the cookie for authentication then uses the value to find data for the user. To reiterate, the value of the cookie would be a session ID that could be used to access user information (instead of user information in the cookie value).
Finally, let’s return user data. Modify the return
statement to be:
... return res.status(200).json({ name, email }) ...
We can now display that data on the account page if it is available.
In account.js
, insert the following code where there is the comment “Display user information”.
... <p><b>Name:</b> {data && data.data && data.data.name}</p> <p><b>Email:</b> {data && data.data && data.data.email}</p> ...
Restart the development server and sign in. Your account page now shows user data if authenticated.
4. Authorize App to Access Github (OAuth)
Create a Github OAuth App
You will need to have a Github account to create the OAuth application.
Github has a guide for creating an OAuth app that is quite good. I recommend clicking on the link and following the instructions until you reach the Register a new OAuth application page.
Enter the values as follows:
- Application name: React Authorization App
- Homepage URL: http://localhost:3000
- Application description: optional
- Authorization callback URL: http://locahost:3000/account
The most important value is the Authorization callback URL. This is the URL that Github will send a code to that can be used in a POST request to retrieve an access token. It’s also the URL that Github will redirect the user to after they authorize Github.
Implement OAuth Flow
At the top of account.js
, underneath the fetcher
function, add the following <Github>
component.
... const Github = ({ data }) => { return ( <div className={styles.github}> {!data ? <a href={`https://github.com/login/oauth/authorize?client_id=yourClientId`}>Authorize Github</a> : <div> <pre> <code> {JSON.stringify(data, null, 4)} </code> </pre> </div> } </div> ) } ...
The link in the <a>
tag directs the user to an authorization page. At the end of the URL, there is a query parameter client_id
. You can hardcode this value from the OAuth app that you just created on Github.
Add this new component to the Account
functional component underneath the comment “Add Github Component”.
... <Github /> ...
Save the file and there should now be a link in the account page that says Authorize Github. When you click on the link you are taken to a Github authorization page.
This is the beginning of the OAuth workflow. We now have to implement files and functions that:
- Use the code parameter that is sent to the callback URL to retrieve an access token for the user
- Requests data from the Github API with the retrieved access token
If you click the green Authorize button, you are redirected to the callback URL page with the code query parameter attached to the URL.
http://localhost:3000/account?code=githubCode
With Next.js, we can easily access this query parameter and use it to call the API route /api/github
that will send a POST request to get an access token.
Send Code to /api/github
Route
In account.js
, we can access the query parameters in the URL through the router
object. Insert this line below the router
variable.
let { code } = router.query
router.query
object (containing the query parameters) in search for the value code
./api/github
route. We are going to do this in the useEffect
React hook.React.useEffect()
function, insert:... if (code) { api.get(`/api/github?code=${code}`) .then((res) => { setGithubData(res.data) }) .catch((e) => { console.log(e) }) .finally(() => { code = '' }) } ...
In the API call, we are passing along the code
variable as a query parameter.
The second argument for useEffect
is a list of dependents for the hook. In the list, insert the values code
and api
.
code
is now being used in React.useEffect()
, it needs to be initialized before the function call. Move the variable declarations for router
and code
above the call to useEffect
.code
variable declaration, add,let [githubData, setGithubData] = React.useState('')
Github
component to be displayed in a <code>
HTML element.data
to the Github
component and pass in the githubData
variable.<Github data={githubData}/>
/api/github
route to fetch and return user data./api/github
'GET'
as the allowed method for the function.import axios from 'axios' const allowedMethods = ['GET'] ...
Next, we are expecting a query parameter to be passed to this function. Add the following code to retrieve the parameter and save it as a variable.
... const { query } = req const { code } = query if (!code) { res.status(404).end() } ...
Create the variable url
that holds the Github URL we use to fetch access tokens.
const url = 'https://github.com/login/oauth/access_token'
async
before the function parameters in the first line of the function declaration.... export default async (req, res) => { ...
Finally, replace the last part of the function with the code,
... try { // Fetch access token from Github using code const { data } = await axios.post(url, { client_id: 'yourClientId', client_secret: process.env.GITHUB_CLIENT_SECRET, code, }, { headers: { 'Accept': 'application/json' } }) if (!data.access_token) { throw new Error('No access token found.') } // Fetch user taken using access token const githubData = await axios.get('https://api.github.com/user', { headers: { Authorization: `token ${data.access_token}` } }) return res.json(githubData.data) } catch (e) { console.log(e) return res.status(500).end() }
This part of the code sends a POST request to Github with your client ID, client secret, and code parameter. It then uses the returned access code to fetch user data. Finally, it sends that data back to the front-end.
The last thing we need to do is add our client secret to the environment.
.env.local
.GITHUB_CLIENT_SECRET=yourClientSecret
Authorize Github and Display User Data
After authorizing the app to fetch Github data, you are redirected back to the account page. The code that is in the URL is picked up in the component and triggers an API call to /api/github
in the React useEffect()
hook that runs after the component mounts.
The code is passed to the API route and used to fetch an access token from Github. Upon receiving the access token, the API route uses the token to retrieve user data. If the retrieval is successful, it passes that data back to the front end where it is displayed.
In truth, this application fetches access tokens more than it needs to. Applications that implement the OAuth flow through Github should save a single access token with a user’s data to use in the future for requests. This application fetches, and uses, a new access token whenever we click the Authorize Github link.
Conclusion
In this article, we discussed common methods for securing APIs that deal with authorization. Furthermore, we introduced popular specifications that are used in the industry to address authorization and authentication.
Additionally, we worked through a tutorial that implemented OAuth through Github and used cookies for web authentication with Next.js.
If you are looking for a challenge, you could change the application to offer a Sign Up with Github button on the home page. The application could then save data retrieved from Github about the user to the data.js
file. This would include the access token that Github provides for the user.
There is plenty to explore in the world of authentication, authorization, and security. I hope you enjoyed this article and thanks for reading!
Jonatas says
After several tries, with 410 erros on the user request after the login, I cloned your repo and on the sign-in process, on the user request I receive a 410 error
Jarrett Retz says
Hey Jonatas,
The message for a 410 status code is ‘Gone’. I would assume that means that the Github user or the access token that you are trying to use has been deleted for no longer exists.
Were you able to authorize the app for your user?
Jarrett