an example of using Auth0 to develop an authentication service

Hello friends!

In this article, I will show you how to create a complete service for authentication and authorization (hereinafter referred to as simply a service) using Auth0

Auth0 Is a platform that provides ready-made solutions for developing services of any complexity. Auth0 supported by the team behind the development JWT (JSON Web Token / web token in the format JSON). It instills a certain amount of confidence in safety. Auth0-сервисов

Free version Auth0 allows you to register up to 7000 users.

In this article I wrote about what is JWTand how to develop your own service from scratch.

Acquaintance with Auth0 you can start from here

Source Auth0 SDKwhich we will use to develop the application can be found here

The source code of the project that we will develop is located here

In this article, I will only talk about the most basic features provided by Auth0

In the examples and screenshots below, you will see real чувствительные данные/sensitive data… This does not mean that you can use them. After the article is published, the service will be removed.

Preparing and setting up a project

Create a directory, go to it and create a client – template React/TypeScript-приложения via Create React App:

mkdir react-auth0
cd react-auth0 # cd !$

yarn create react-app client --template typescript
# or
npx create-react-app ...

Create a directory for the server, go to it and initialize Node.js-приложение:

mkdir server
cd server

yarn init -yp
# or
npm init -y

Create an account Auth0:

We create tenant/арендатора:

We create одностраничное приложение/single page application in the tab Applications/Applications:

Go to the section Settings:

Create a file .env in the directory client and write the field values ​​into it Domain and Client ID:

REACT_APP_AUTH0_DOMAIN = auth0-test-app.eu.auth0.com
REACT_APP_AUTH0_CLIENT_ID = Ykv47YaNC3naGvfljFt8LyhzVPRPZCJY

We register URL customer in the fields Allowed Callback URLs, Allowed Logout URLs and Allowed Web Origins:

We save the changes.

We create API in the tab Applications/API:

Go to the section Settings:

We write the value of the field Identifier and URL server to file .env:

REACT_APP_AUTH0_AUDIENCE='https://auth0-test-app'
REACT_APP_SERVER_URI='http://localhost:4000/api'

Create a file .env in the directory server with the following content:

AUTH0_DOMAIN='auth0-test-app.eu.auth0.com'
AUTH0_AUDIENCE='https://auth0-test-app'
CLIENT_URI='http://localhost:3000'

Customer

Go to the directory client and install additional dependencies:

cd client

# зависимости для продакшна
yarn add @auth0/auth0-react react-router-dom react-loader-spinner
# зависимость для разработки
yarn add -D sass

Directory structure src:

- api
 - messages.ts
- components
 - AuthButton
   - LoginButton
     - LoginButton.tsx
   - LogoutButton
     - LogoutButton.tsx
   - AuthButton.tsx
 - Boundary
   - Error
     - error.scss
     - Error.tsx
   - Spinner
     - Spinner.tsx
   - Boundary.tsx
 - Navbar
   - Navbar.tsx
- pages
 - AboutPage
   - AboutPage.tsx
 - HomePage
   - HomePage.tsx
 - MessagePage
   - message.scss
   - MessagePage.tsx
 - ProfilePage
   - profile.scss
   - ProfilePage.tsx
- providers
 - AppProvider.tsx
 - Auth0ProviderWithNavigate.tsx
- router
 - AppRoutes.tsx
 - AppLinks.tsx
- styles
 - _mixins.scss
 - _variables.scss
- types
 - index.d.ts
- utils
 - createStore.tsx
- App.scss
- App.tsx
- index.tsx
...

Application logic:

  • there is a button for authorization in the navigation panel;
  • the button is rendered conditionally depending on the user’s authorization status;
  • if the user is not logged in, when the button is clicked it is redirected to Auth0 to log in to the system;
  • if the user is authorized, when the button is pressed, the system is logged out;
  • the appendix has 4 pages: HomePage, AboutPage, ProfilePage and MessagePage;
  • the first two pages are in the public domain;
  • the last two require authorization;
  • when an unauthorized user navigates to the page ProfilePage, it gets redirected to Auth0;
  • after logging in, the user is returned to the page ProfilePagewhere he sees information about his profile;
  • On the page MessagePage the user can send two requests to the server: to receive an open message and to receive a secure message;
  • if the user is not logged in, an error is returned when sending a request to receive a secure message.

Further, I will talk only about what concerns directly Auth0

Application integration with Auth0

To integrate an application with Auth0 provider is used Auth0Provider

In order to be able to redirect the user to a custom page after logging in, the default provider must be upgraded as follows (providers/Auth0ProviderWithNavigate):

// импортируем дефолтный провайдер
import { Auth0Provider } from '@auth0/auth0-react'
// хук для выполнения программной навигации
import { useNavigate } from 'react-router-dom'
import { Children } from 'types'

const domain = process.env.REACT_APP_AUTH0_DOMAIN as string
const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID as string
const audience = process.env.REACT_APP_AUTH0_AUDIENCE as string

const Auth0ProviderWithNavigate = ({ children }: Children) => {
 const navigate = useNavigate()

 // функция, вызываемая после авторизации
 const onRedirectCallback = (appState: { returnTo?: string }) => {
   // путь для перенаправления указывается в свойстве `returnTo`
   // по умолчанию пользователь возвращается на текущую страницу
   navigate(appState?.returnTo || window.location.pathname)
 }

 return (
   <Auth0Provider
     domain={domain}
     clientId={clientId}
     // данная настройка нужна для взаимодействия с сервером
     audience={audience}
     redirectUri={window.location.origin}
     onRedirectCallback={onRedirectCallback}
   >
     {children}
   </Auth0Provider>
 )
}

export default Auth0ProviderWithNavigate

The provider’s signature can be found here

We wrap application components in a provider (index.tsx):

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import Auth0ProviderWithNavigate from 'providers/Auth0ProviderWithNavigate'
import { AppProvider } from 'providers/AppProvider'
import App from './App'

ReactDOM.render(
 <React.StrictMode>
   {/* провайдер маршрутизации */}
   <BrowserRouter>
     {/* провайдер авторизации */}
     <Auth0ProviderWithNavigate>
       {/* провайдер состояния */}
       <AppProvider>
         <App />
       </AppProvider>
     </Auth0ProviderWithNavigate>
   </BrowserRouter>
 </React.StrictMode>,
 document.getElementById('root')
)

Login and logout

To enter the system, use the method loginWithRedirect, and for the exit – the method logout… Both methods are returned by a hook useAuth0useAuth0 also returns boolean isAuthenticated (and much more) – authorization status that can be used to conditionally render buttons.

This is how the authentication button is implemented (components/AuthButton/AuthButton.tsx):

// импортируем хук
import { useAuth0 } from '@auth0/auth0-react'
import { LoginButton } from './LoginButton/LoginButton'
import { LogoutButton } from './LogoutButton/LogoutButton'

export const AuthButton = () => {
 // получаем статус авторизации
 const { isAuthenticated } = useAuth0()

 return isAuthenticated ? <LogoutButton /> : <LoginButton />
}

Login button (components/AuthButton/LoginButton/LoginButton.tsx):

// импортируем хук
import { useAuth0 } from '@auth0/auth0-react'

export const LoginButton = () => {
 // получаем метод для входа в систему
 const { loginWithRedirect } = useAuth0()

 return (
   <button className="auth login" onClick={loginWithRedirect}>
     Log In
   </button>
 )
}

Logout button (components/AuthButton/LogoutButton/LogoutButton.tsx):

// импортируем хук
import { useAuth0 } from '@auth0/auth0-react'

export const LogoutButton = () => {
 // получаем метод для выхода из системы
 const { logout } = useAuth0()

 return (
   <button
     className="auth logout"
     // после выхода из системы, пользователь перенаправляется на главную страницу
     onClick={() => logout({ returnTo: window.location.origin })}
   >
     Log Out
   </button>
 )
}

The hook signature can be found here

Authorization status

User authorization state persists for lifetime id_token/токена идентификации… Token lifetime is set on the tab Settings applications in the field ID Token Expiration section ID Token and the default is 36 000 seconds or 10 hours:

The token is written to cookies, which can be found in the section Storage/Cookies tabs Application developer tools in the browser:

This means that the user’s authorization status is preserved across page reloads, closing / opening browser tabs, etc.

When you log out, cookies along with id_token removed.

Creating a secure page

To protect the page from access by unauthorized users, the utility is designed withAuthenticationRequired… Hook useAuth0, among other things, returns an object user with normalized user data.

Page ProfilePage implemented as follows (pages/ProfilePage/ProfilePage.tsx):

import './profile.scss'
import { useAuth0, withAuthenticationRequired } from '@auth0/auth0-react'
import { Spinner } from 'components/index.components'

// оборачиваем код компонента в утилиту
export const ProfilePage = withAuthenticationRequired(
 () => {
   // получаем данные пользователя
   const { user } = useAuth0()

   return (
     <>
       <h1>Profile Page</h1>
       <div className="profile">
         <img src={user?.picture} alt={user?.name} />
         <div>
           <h2>{user?.name}</h2>
           <p>{user?.email}</p>
         </div>
       </div>
     </>
   )
 },
 {
   // обе настройки являются опциональными
   returnTo: '/profile',
   onRedirecting: () => <Spinner />
 }
)

The utility signature can be found here

Client health check

While in the directory client, execute the command yarn start or npm start to start the development server:

Click on the button Log In… We get to the registration / authorization page Auth0:

By default, the option to log in with an account is provided Google (Google OAuth 2.0). Later we will add the ability to authorize using an account GitHub

We enter the system. We return to the main page. We see that the button Log In replaced by a button Log Out

We exit the system. Trying to go to the page Profile… We get to the page again Auth0… We enter the system. Returning to the profile page:

Connection GitHub

Go to the tab Authentication/Social and press the button Create Connection:

We choose GitHub from the suggested list:

Go to profile GitHub… Go to the section Settings/Developer settings/OAuth Apps and click on the button Register a new application:

Fill in the fields Application name, Homepage URL (https://ВАШ-ДОМЕН.auth0.com) and Authorization callback URL (https://ВАШ-ДОМЕН/login/callback):

Click on the button Generate a new client secret… Copying field values Client ID and Client secret and insert them into the appropriate fields Auth0:

In chapter Attributes in addition to Basic Profile choose Email address, and in the section Permissionsread:user

Click on the button Create… We connect the client application and API

Click on the button Try Connection to test the connection.

Click on the button Authorize ВАШЕ_ИМЯ

We get a message that the connection is working:

Now if we press Log In in the application, we will see that we have the opportunity to log in through GitHub:

Concerning Google, then Auth0 provides test keys that must be replaced with real ones before releasing the application to production.

Let’s take a page MessagePage and the server.

Integration of the client with the server

API

Let’s start with API (api/messages.ts):

// адрес сервера
const SERVER_URI = process.env.REACT_APP_SERVER_URI

// сервис для получения открытого сообщения
export async function getPublicMessage() {
 let data = { message: '' }
 try {
   const response = await fetch(`${SERVER_URI}/messages/public`)
   if (!response.ok) throw response
   data = await response.json()
 } catch (e) {
   throw e
 } finally {
   return data.message
 }
}

// сервис для получения защищенного сообщения
// функция принимает `access_token/токен доступа`
export async function getProtectedMessage(token: string) {
 let data = { message: '' }
 try {
   const response = await fetch(`${SERVER_URI}/messages/protected`, {
     headers: {
       // добавляем заголовок авторизации с токеном
       Authorization: `Bearer ${token}`
     }
   })
   if (!response.ok) throw response
   data = await response.json()
 } catch (e) {
   throw e
 } finally {
   return data.message
 }
}

Page MessagePage (pages/MessagePage/MessagePage.tsx).

We import hooks, component, provider, services and styles:

import { useAuth0 } from '@auth0/auth0-react'
import { getProtectedMessage, getPublicMessage } from 'api/messages'
import { Boundary } from 'components/Boundary/Boundary'
import { useAppSetter } from 'providers/AppProvider'
import { useState } from 'react'
import './message.scss'

We get the setters, define the state for the message and its type:

export const MessagePage = () => {
 const { setLoading, setError } = useAppSetter()
 const [message, setMessage] = useState('')
 const [type, setType] = useState('')

 // TODO
}

To generate an access token (access_token) method is intended getAccessTokenSilentlyreturned by hook useAuth0:

const { getAccessTokenSilently } = useAuth0()

We define a function to request an open message:

function onGetPublicMessage() {
   setLoading(true)
   getPublicMessage()
     .then(setMessage)
     .catch(setError)
     .finally(() => {
       setType('public')
       setLoading(false)
     })
 }

We define a function to receive a secure message:

function onGetProtectedMessage() {
   setLoading(true)
   // генерируем токен и передаем его сервису `getProtectedMessage`
   getAccessTokenSilently()
     .then(getProtectedMessage)
     .then(setMessage)
     .catch(setError)
     .finally(() => {
       setType('protected')
       setLoading(false)
     })
 }

Finally, we return the markup:

return (
 <Boundary>
   <h1>Message Page</h1>
   <div className="message">
     <button onClick={onGetPublicMessage}>Get Public Message</button>
     <button onClick={onGetProtectedMessage}>Get Protected Message</button>
     {message && <h2 className={type}>{message}</h2>}
   </div>
 </Boundary>
)

Server

Go to the directory server and install the dependencies:

# зависимости для продакшна
yarn add express helmet cors dotenv express-jwt jwks-rsa
# зависимости для разработки
yarn add -D nodemon

  • express: Node.js-фреймворк for the development of web servers;
  • helmet: utility for installation HTTP-заголовковrelated to security. You can read about these headers. here;
  • cors: utility for installation HTTP-заголовковrelated to CORS;
  • dotenv: utility for working with environment variables;
  • express-jwt: посредник/middleware for validation JWT via module jsonwebtoken;
  • jwks-rsa: utility to extract ключей подписания/signing keys from JWKS (JSON Web Key Set / set of web keys in the format JSON);
  • nodemon: A utility to start the development server.

About what is JWKS and what it is used for, you can read here

Integration example jwks-rsa With express-jwt can be found here

Server structure:

- routes
 - api.routes.js
 - messages.routes.js
- utils
 - checkJwt.js
- .env
- index.js
- ...

Here we are interested in 2 files: messages.routes.js and checkJwt.js

messages.routes.js:

import { Router } from 'express'
import { checkJwt } from '../utils/checkJwt.js'

const router = Router()

router.get('/public', (req, res) => {
 res.status(200).json({ message: 'Public message' })
})

router.get('/protected', checkJwt, (req, res) => {
 res.status(200).json({ message: 'Protected message' })
})

export default router

When asked to api/messages/public message is returned Public message… When asked to api/messages/protected checking in progress JWT… This route (route) is protected. When the check is successful, the message is returned Protected message… Otherwise, the utility returns an error.

Consider this intermediary (utils/checkJwt.js).

We import utilities:

import jwt from 'express-jwt'
import jwksRsa from 'jwks-rsa'
import dotenv from 'dotenv'

Accessing the environment variables stored in the file .env, and retrieve their values:

dotenv.config()

const domain = process.env.AUTH0_DOMAIN
const audience = process.env.AUTH0_AUDIENCE

audience – in simple words, this is the audience of the token, i.e. those for whom the token is intended.

We define the utility:

export const checkJwt = jwt({
 secret: jwksRsa.expressJwtSecret({
   cache: true,
   // ограничение максимального количества запросов
   rateLimit: true,
   // 10 запросов в минуту
   jwksRequestsPerMinute: 10,
   // обратите внимание на сигнатуру пути
   jwksUri: `https://${domain}/.well-known/jwks.json`
 }),
 // аудитория
 audience,
 // тот, кто подписал токен
 issuer: `https://${domain}/`,
 // алгоритм, использованный для подписания токена
 algorithms: ['RS256']
})

Determine the type of server code (module) and commands to start the server in development mode and in production mode in package.json:

"type": "module",
"scripts": {
 "start": "node index.js",
 "dev": "nodemon index.js"
}

We start the development server using the command yarn dev or npm run dev and return to the browser.

We exit the system. Go to the page MessagePage and trying to get an open message:

Working.

Now we try to receive a secure message:

We receive an error message that indicates the need for authorization.

Log in and try again:

Happened!

Our authentication / authorization service seems to be working as expected.

Agree that Auth0 greatly facilitates the implementation of a non-trivial task of developing an authentication / authorization service for a web application.

Perhaps this is all that I wanted to share with you in this article.

I hope you found something interesting for yourself and wasted your time.

Thank you for attention. Happy coding and Happy New Year!


Similar Posts

Leave a Reply