Industry

Overhauling the digital landscape of ADA with Drupal 10 & multisite architecture

Industry

Overhauling the digital landscape of ADA with Drupal 10 & multisite architecture

“Research is the art of finding hidden insights in the ordinary. It's about seeing possibilities others miss and using newfound knowledge to make a purposeful difference.”

PIYUESH KUMAR

CTO

decorative underline
decorative image 2
decorative image 3
decorative image 1
UX/UI design
We are experts in crafting user-centered digital experiences using agile research methods that lead to quick deliveries and consistent brand outcomes. Our core team includes UI designers who specialize in creating designs that balance aesthetics and functionality, ensuring a smooth user journey from the first interaction to conversions.
Empathize contextually

UX/UI design

We are experts in crafting user-centered digital experiences using agile research methods that lead to quick deliveries and consistent brand outcomes. Our core team includes UI designers who specialize in creating designs that balance aesthetics and functionality, ensuring a smooth user journey from the first interaction to conversions.
Title
Description
Button Text

“One thing that I really appreciate about this company is the work culture. The management and my colleagues are very supportive and always willing to lend a helping hand. They encourage growth and development and provide various opportunities to learn and enhance our skills.”

JOlly singh Head of everything

Head of everything

decorative underline
View Services

Simplifying content management

Businesses can easily manage and update website content without technical expertise, saving valuable time and resources.

Efficient workflow

CMS platforms provide workflow management tools that allow businesses to define content creation and approval processes, maintaining a consistent and streamlined workflow.

Better collaboration

CMS platforms enable businesses to assign multiple users and manage permissions, allowing various team members to contribute and collaborate, increasing team productivity.

Cost-effective solution

Building a website with a CMS like Drupal is cost-effective because it comes with pre-built features and templates that can be customized to meet business needs.

Security and data protection

CMS platforms feature robust security measures, such as regular updates and patches, to protect sensitive business data and ensure compliance with data protection regulations.

SEO-friendly

Drupal's clean and semantic markup, along with its SEO-friendly features, can make it easier for websites to rank well in search engine results.

Open-source

Drupal is an open-source CMS, meaning its source code is freely available for anyone to use, modify, and distribute. This encourages innovation, collaboration, and cost-effectiveness.

Accessibility

Drupal adheres to web accessibility standards, ensuring that websites built with the CMS are usable and accessible to people with disabilities.

Product Engineering

We specialize in product engineering, transforming ideas into scalable, high-quality digital solutions.

Related Blogs

Categories

CRUD Operations in Decoupled Drupal: A 101 Guide

Reading count

CRUD in computer programming refers to the functions required to perform different operations on specific data within a database. CRUD stands for Create, Read, Update, and Delete. CRUD operations are widely used in applications supported by relational databases.

In this blog, we will look at how to perform CRUD operations utilizing a simple JavaScript front-end system and the Drupal core’s JSON API module.

What is Decoupled Drupal?

Decoupled Drupal refers to a Drupal architecture where Drupal’s back-end exposes content to other front-end systems, essentially serving as a central repository of content that can be served to a wide variety of devices.

What are the advantages of Decoupled Drupal?

A decoupled Drupal architecture enables the development team to take advantage of Drupal’s highly regarded back-end functionality, using Drupal as a repository for content that can be made available to other front-end systems.

Drupal’s JSON API

Drupal Core provides a module named JSON API which provides us with a REST API. This provided API is centered on Drupal’s entity types and bundles. It simply means that the JSON API module converts all the entity types, bundles, etc into a REST API representation. For example, articles, pages, and users are given the types: node--article, node--pages, and user--user, respectively.

To learn more about the JSON API module, click here.

Objective

To understand how CRUD operations are done using the JSON API module used in a Decoupled Drupal Application.

Pre-requisites

Make sure you have a freshly installed Drupal site up and running. That's it!

Authentication using JSON API

  • CRUD operations can only be performed by the authenticated user.
  • To authenticate a user using JSON API, we need to make a POST request to the Drupal Back-end.
  • The body parameter of the POST request must contain the username and password in a JSON format.
  1. Create a JSON body for the request, this JSON body will contain the username and password of the user.

const userData = {
   name: <user-name>,
   pass: <user-password>,
 };
  1. Making the final request.

const response = await fetch("http://example.org/web/user/login?_format=json"
, {
   method: "POST",
   headers: {
     "Content-Type": "application/json",
   },
   body: JSON.stringify(userData), // JSON body goes here
 });
  • This HTTP request will provide us with two tokens.
  1. CSRF token.
  2. Logout token
  • The CSRF token can prevent CSRF attacks by making it impossible for an attacker to construct a fully valid HTTP request suitable for feeding to a victim user.
  • The Logout token can be used as a parameter when logging out of the website.
  • It is recommended to store these tokens in some sort of memory, for example, browser cookies, so that they can be accessed whenever required.
  • For example, to make CRUD operations, the CSRF token is required whereas while logging out, the Logout token is required.

Enabling the JSON API module

  • The JSON API module is a core module available to us OOTB (Out-of-the-box) when we install Drupal.
  • To enable the JSON API module in Drupal CMS, follow the below-mentioned steps :
  • Log in as administrator.
  • Go to the extend tab, present in the admin toolbar (admin/modules)
  • Make sure you are on the List tab of the extend page.
  • In the search bar, search for the JSON API module.
  • Select the JSON API module by clicking the checkbox.
  • Click on Install.
  • The installation process requires the Serialization module to be installed.
  • It will enable the JSON API module.
  • After installing the JSON API module, let's see how to use it.

Using the JSON API module

  • A JSON: API URL looks like this:

GET|POST           /jsonapi/node/article 
PATCH|DELETE       /jsonapi/node/article/{uuid}
  • Every resource type must be uniquely addressable in the API.
  • The Drupal implementation follows the pattern:

/jsonapi/{entity_type_id}/{bundle_id}[/{entity_id}]
  • The URL is always  prefixed by /jsonapi.
  • A simple jsonapi request can be made like

http://localhost/drupal_movie/web/jsonapi/<entity-type>/<bundle>
  • Enter the following URL in the URL tab :

http://localhost/<your-drupal-instance-name>/web/jsonapi
  • Example :

http://localhost/drupal_movie/web/jsonapi
OR
http://example.org/web/jsonapi
  • Make sure the URL ends with the term jsonapi.
  • This URL will generate the following kind of output :

{ "jsonapi": 
  { "version": "1.0",     "meta": { 
       "links": 
          { "self": { "href": "http://jsonapi.org/format/1.0/" } } } }, "data": [],
 "meta": { "links": { "me": { "meta": { "id": "5aa9df1f-5562-4c0e-84f8-055561d6b4cf"
 }, "href": ​
"http://localhost/drupal_movie/web/jsonapi/user/user/5aa9df1f-5562-4c0e-84f8-055561
d6b4cf"  } } }, "links": { "action--action": { "href": 
"http://localhost/drupal_movie/web/jsonapi/action/action" }, 
"base_field_override--base_field_override": { "href": "http://localhost/drupal_movie/web/jsonapi/base_field_override/base_field_override" 
 }, "block--block": { "href": 
"http://localhost/drupal_movie/web/jsonapi/block/block" }, . . . . "user--user": { 
"href": "http://localhost/drupal_movie/web/jsonapi/user/user" }, 
"user_role--user_role": { "href": 
"http://localhost/drupal_movie/web/jsonapi/user_role/user_role" }, "view--view": {  
"href": 
  • As you can see, the JSON API module converts all the Drupal data into JSON representation.
  • This makes the JSON API module one of the most useful core modules in Drupal.
  • Now you can fetch the required data, add new data or alter the existing data by making a HTTP request.
  • Example:
  • We want a list of all articles in JSON, do the following :
  • In URL tab enter :

http://localhost/drupal_movie/web/jsonapi/node/article
OR
http://example.org/web/jsonapi/node/article
  • It will give a list of all the nodes of the article content type or bundle in the Drupal site.
  • The output will look something like this :


{ 
"jsonapi": { 
"version": "1.0", 
"meta": { 
"links": { 
"self": { 
"href": "http://jsonapi.org/format/1.0/" 
} 
} 
} 
}, 
"data": [ 
{ 
"type": "node--article", 
"id": "a90b4731-c573-4503-b893-9825a416ed68", 
"links": { 
"self": { 
"href": "http://localhost/drupal_movie/web/jsonapi/node/article/a90b4731-c573-4503-b893-982 
5a416ed68?resourceVersion=id%3A52" 
} 
}, 
"attributes": { 
"drupal_internal__nid": 47, 
"drupal_internal__vid": 52, 
"langcode": "en", 
"revision_timestamp": "2022-04-22T10:22:58+00:00", 
"revision_log": null, 
"status": true, 
"title": "The Process of UX Design at QED42", 
"created": "2022-04-22T10:22:07+00:00", 
"changed": "2022-04-22T10:22:58+00:00", 
"promote": true, 
"sticky": false, 
"default_langcode": true,  
"revision_translation_affected": true, 
"content_translation_source": "und", 
"content_translation_outdated": false, 
"body": { 
"value": "<p>The idea behind having a process isn't just about trying to be 
organized amidst the chaos. While it does play an important part in giving us 
trusted and well-structured ways of performing tasks, it doesn't always stand true 
on every project. In reality, as every project has certain requirements, trying to 
work it out using a particular process, doesn't hold 
much sense.</p>\r\n\r\n<p>Having said this, we do have a specific process in place 
that we like to follow, along with certain additions and subtractions, as per 
project needs. While sometimes the process stays as linear as can be, sometimes it's a 
combination of pieces, while other times we decide to follow something completely 
new, while trying to stick to the basics.</p>\r\n\r\n<p>Nonetheless, QED42 does 
have six core steps that we mostly stick to while phasing out, as per project 
needs. Our process and how we go about it completely depend on the project at hand, 
business vision and values, and most importantly on people who are going to be 
using that product.</p>\r\n", 
"format": "basic_html", 
"processed": "<p>The idea behind having a process isn't just about trying to be 
organized amidst the chaos. While it does play an important part in giving us 
trusted and well-structured ways of performing tasks, it doesn't always stand true 
on every project. In reality, as every project has certain requirements, trying to 
work it out using a particular process, doesn't hold much sense.</p>\n\n<p>Having 
said this, we do have a specific process in place that we like to follow, along 
with certain additions and subtractions, as per project needs. While sometimes the 
process stays as linear as can be, sometimes it's a combination of pieces, while 
other times we decide to follow something completely new, while trying to stick to 
the basics.</p>\n\n<p>Nonetheless, QED42 does have six core steps that we mostly 
stick to while phasing out, as per project needs. Our process and how we go about 
it completely depend on the project at hand, business vision and values, and most 
importantly on people who are going to be using that product.</p>", 
.
.
.
  • In a similar way, you can access any entity bundle like a basic page (jsonapi/node/page).
  • To access taxonomy terms, the following HTTP request can be made.

http://localhost/drupal_movie/web/jsonapi/taxonomy_term/tags
OR
http://example.org/web/jsonapi/taxonomy_terms/tags

Format : 
http://domain/web/jsonapi/taxonomy_tags/<vocabulary-machine-name>
  • and you will get the entire list of the taxonomies present in tags vocabulary.
  • If you want to target a specific article, we need to append the UUID of the particular article to the HTTP request.
  • Example:

http://localhost/drupal_movie/web/jsonapi/node/article/a90b4731-c573-
4503-b893-9825a416ed68

Format : 
http://domain/web/jsonapi/node/article/<UUID-of-the-specific-article>
  • To learn more about the JSON API module, click here

Creating a Client Secret

Before we move forward, we need a client_id and client_secret, as these terms will be necessary to generate an authorized access token. There is a contributed module in Drupal for generating a client_id and client_secret, the simple OAuth module.

Using the Simple OAuth module :

  • Using this module, we will generate a client_id and a client_secret.

Download the module

  • Using composer:
  • In terminal : composer require drupal/simple_oauth

Enable the module

  • Using drush: drush en simple_oauth
  • Using admin UI :
  • Login as administrator.
  • Go to the extend tab, present in the admin toolbar admin/modules
  • Make sure you are on the List tab of the extend page.
  • In the search bar, search for the Simple OAuth module.
  • Select the Simple OAuth module by clicking the checkbox.
  • Click on Install.
  • The module will be enabled.

Create the client secret

To create a client secret, follow the below-mentioned steps :

  1. Go to the configuration page - admin/config
  2. Under people, select Simple Oauth - admin/config/people/simple_oauth
  3. Select Add client action link
  4. Fill in the details
  5. Note down the New secret field’s data as we will need it in the further process. This data is basically your client-secret data
  6. Click save.
  • The client secret has been created
  • Note the UUID of the client, which is known as ‘client_id’, and the scope of the client for the scope header of the HTTP request
  • So now we have the client_id and client_secret and the client-scope

CRUD operations in Decoupled Drupal

  • There are 4 main operations that most of the users do on a regular basis.
  • Create, Read, Update and Delete.

Create Operation

  • To create some content in a Decoupled Drupal site, we have to make a POST request to the Drupal Back-end.
  • To make a Post request, you need an authorized JSON Web Token (JWT) and a CSRF token.
  • JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties.
  • A CSRF token is a secure random token (e.g., synchronizer token or challenge token) that is used to prevent CSRF attacks.
  • The token needs to be unique per user session and should be of large random value to make it difficult to guess.
  • The CSRF token is provided when the user is authenticated.
  • A CSRF secure application assigns a unique CSRF token for every user session.
  • That means every time the user logs in a new token is generated which prevents the attack on the site.
  • Steps to make a POST request using JSON API in Drupal.

First, generate the access token (JWT) for a user.

  • URL

http://localhost/drupal_movie/web/oauth/token
  • Request Body/Params -
  • grant_type : password
  • client_id : <client-id>
  • client_secret : <client-secret>
  • username : Admin (Drupal User)
  • password : 123 (Drupal password)
  • scope : <client-scope>

Making the request to Drupal.

  • URL

http://localhost/drupal_movie/web/jsonapi/node/article

Headers -

  • Authorization: Bearer <access-token-goes-here>
  • Headers:
  • Content-Type : application/vnd.api+json
  • Accept : application/vnd.api+json
  • “X-CSRF-Token”: <csrf-token-goes-here>
  • Body: select ‘raw’

 {
  "data": {
    "type": "node--article",
    "attributes": {
      "title": "How DXPs help e-commerce brands create memorable 
experiences",
      "body": {
        "value": "Marquee brands like Forever 21 and Yelp offer 
personalized experiences on their homepage. They provide customized 
recommendations for user preferences, such as likes and dislikes, 
food preferences, and lifestyles, to meet customer needs better. The 
new features on the Yelp app have earned them 2.4 million users a 
month, with 2.2 million requests from across 300+ categories. The 
Californian fashion giant Forever 21 has been prioritizing 
personalization. They capture customer attention with personalized 
search results, lucrative offers, and product recommendations. This 
article discusses digital experience platforms and how they play an 
important role in creating a great digital customer experience for 
customer loyalty, retention, and lifetime value. It also covers how 
digital experience platforms can elevate customers' brand experience 
and ultimately business for brands.",
        "format": "basic_html",
        "summary": "Marquee brands like Forever 21 and Yelp offer 
personalized experiences on their homepage. They provide customized 
recommendations for user preferences, such as likes and dislikes, 
food preferences, and lifestyles, to meet customer needs better. The 
new features on the Yelp app have earned them 2.4 million users a 
month, with 2.2 million requests from across 300+ categories."
      }
    }
  }
}
  • The article data should be represented in JSON format.
  • It will generate either a bad request or the article will be created and we will receive this newly created article in JSON response.
  • If a bad request is generated, it means something is wrong with our custom request.

Read Operation

  • To read or search any content, we can make a GET request.
  • Here, the filtering feature of the JSON API is very useful.
  • For example: We want to search for an article using the title, so our HTTP request will be something like this :

http://localhost/drupal_movie/web/jsonapi/node/article?filter[title][
value]=<article-title>
  • Here using filters, we can fetch the entire requested article in a single request, which enhances the time complexity of the request-response cycle.

Update Operation

  • To update any content, we can fire a POST request which contains the updated content and a CSRF token.
  • The request will look something like this

let url = "http://localhost/drupal_movie/web/jsonapi/node/article/" + 
uuid; 

const reqBody = { 
  data: { 
    type: "node--article", 
    id: uuid, 
    attributes: { 
       title: title, 
       body: { 
          value: body, 
          format: "basic_html", 
          summary: summary, 
        }, 
      },
    },
  }; 

let response = await axios({  
    method: "PATCH", 
    url: url, 
    headers: { 
         "Content-Type": "application/vnd.api+json", 
         "X-CSRF-Token": csrf_token, 
    }, data: reqBody, 
});
  • Here UUID is the unique id of the particular content we want to update.

Delete Operation

  • To delete a particular content, we can make a DELETE request to the Drupal BE.
  • We need the UUID of the article we want to delete
  • This delete request must contain a few important headers :
  • ‘Content-Type’
  • ‘X-CSRF-Token’
  • The final DELETE request will look something like this :

let response = await axios({
       method: 'DELETE',
       url: 
`http://localhost/drupal_movie/web/jsonapi/node/article/${uuid}`,
       headers: {
           'Content-Type': 'application/vnd.api+json',
           "X-CSRF-Token": csrf_token,
       }
   });
  • The requested article will be deleted.

CRUD operations are crucial and frequently used in database and database design cases. The above example explains how we can perform CRUD operations on a Drupal Back-end using the JSON API. While there are many other examples to achieve the same, the premise and ideology remain constant.

Categories

Decoupling Drupal Commerce with Next.js

Reading count

Hoping you all are familiar with the term Headless/Decoupled Drupal, I won't be diving into the details. However, if you'd want to know more about what Headless Drupal is, you are just one click away from finding the secrets of the Headless world! Today we will demonstrate how Drupal's JSON API can be leveraged to create a Decoupled application with Next.js as our rendering layer and Drupal Commerce as the content hub.

As a starter here is what we will be covering in this blog post:

  1. Setting up your Drupal commerce site
  2. Setting up the Next.js site and fetching data from the Drupal site
  3. Working with the data (Create pages dynamically)

Note: We will follow this up and cover checkout functionality in another blog post.Here is a link to the starter package. This is still a work in progress but good to get you started 

Set up your Drupal Commerce site

Drupal Commerce provides you with a starter project. This will make your content work easier as it provides you with some preconfigured products. Alternatively, you can set up your site from scratch by following the Commerce documentation here.


composer create-project drupalcommerce/demo-project demo-commerce --stability dev --no-interaction

Once you are done installing the site, you need to do a few more things:

  • Enable JSON API, JSON:API Resources, JSON API Hypermedia, and Commerce API module. You can also add the Simple OAuth module for authentication
  • Add JSON resources cross bundle module and enable it or you can also create a custom resource for the listing of all products. (Note: The cross bundle module is not compatible with JSON API Extras.)
  • Allow read and write permissions for JSON API
  • Edit your services.yml file to allow cross-origin headers


Fetching data from the Drupal site

Here is a link to the starter package. This is still a work in progress but good to get you started. Go through the readme file to check how to set up your Next.js site.

This is how your homepage should look like:

Decoupling Drupal Commerce with Next.js

Let me walk you through the folder structure and the code:

Decoupling Drupal Commerce with Next.js
  1. Pages folder: As the name suggests this is where you are going to be adding your pages. Each page is associated with a route based on its file name. Next.js also supports dynamic page routing.
  2. Components folder: This is where we will abstract our functionality into different components.
  3. api.js file: This is where all the magic happens. This page contains all the code responsible for manipulating the API data.
  4. next.config.js: This file is not included in the browser build and is used by the server. You can read more about it here.
  5. .env file: This is where all your environment variables will be stored.

Creating pages

Here are some of the Next.js functions we will be using to fetch data at the build and run time.

  • getStaticProps (Static Generation): Fetches data at build time
  • getStaticPaths (Static Generation): Specifies dynamic routes to pre-render pages based on data
  • getServerSideProps (Server-side Rendering): Fetches data on each request
  • getInitialProps: Enables server-side rendering in a page and allows you to do initial data population, which means sending the page with the data already populated from the server. getInitialProps will disable Automatic Static Optimization For the initial page load, getInitialProps will run on the server only. getInitialProps will then run on the client when navigating to a different route via the next/link component or by using next/router. However, if getInitialProps is used in a custom _app.js, and the page being navigated to implements getServerSideProps, then getInitialProps will run on the server.

Creating Static Routes

As mentioned above each page in Next.js is associated with a route based on its file name. So the index.js file inside the pages folder will work as our homepage.

Decoupling Drupal Commerce with Next.js

In getStaticProps, we are using our JSON:API Client (you can find the relevant code in the api.js file) to fetch the relevant data and filter it as per our needs. getStaticProps function returns the data as props to our default component which is further manipulated to get the desired result.

Creating dynamic routes

Next.js also supports dynamic page routing. In Next.js you can add brackets to a page ([param]) to create a dynamic route. Inside the product folder in the pages folder, we have created a file [id].js which will be responsible for creating our product detail pages.

Decoupling Drupal Commerce with Next.js

Here, getStaticPaths fetches the products id's for us and passes them as params to getStaticProps and get static props to pass the product data as props to the default product component.

Note that we are using the product id here. Because with JSON:API in Drupal you can't filter by path alias due to technical limitations. JSON:API filters translate to entity queries on field values; because Drupal path aliases aren't fields you can't use the filter query param to look up articles by their alias.

So to overcome this limitation we can use modules like:

  • Decoupled Router: Which provides a new endpoint where a client can query a path for any possible JSON:API resources.
  • Fieldable Path: Which provides a new entity field (and thus queryable via the filter query param) that is designed to mimic the entity's path (with Pathauto support). We will cover this in another blog post.

Another thing to note here is that for better a developer experience in development mode, getStaticProps and getStaticPaths run on every request otherwise they only run at build time.

Conclusion

This is it! You have set up a Decoupled site with a simple listing page and detail pages. As this is still a work in progress. If you face any difficulties please add your queries in the comments section below and I will be happy to answer!

Categories

Implementing User Authentication for Gatsby and Drupal Decoupled site.

Reading count

Authentication is one of the most important functions in any application. This authentication process has to be secure enough such that data being transmitted should not be compromised. Authentication dictates what users are able to see and do when they log in. Everyone knows how important it is to have a secure authentication method and how it works. I’ll skip that part! Let’s dive right into implementing authentication with Drupal and Gatsby.

On Drupal end will be using the simple OAuth module. Head over to my blog Alexa Account Linking and Custom Skill Model to find step by step instructions on how to configure this module. 

Now that we have our OAuth setup ready let's make the required changes on the Gatsby site. I believe that you already have a setup ready to write some code as the blog proceeds.

Before starting to write code we should have in mind what our authentication functionality should be able to perform. Let’s break it down first in small parts:

  1. User should be able to log in
  2. User should be able to logout
  3. Store access token on client-side
  4. Accessing resource using the access token
  5. Use the refresh token to get valid access token when the previous access token expires

Let’s create a service which will help us check the following things throughout the project: 

  • Whether the user is authenticated or not
  • If the user is authenticated but access token is expired, then automatically generate new access token so that user experience shouldn’t break.

Some developers prefer to place their helper functions under the ‘services’ folder while the others keep them under ‘utils’. Hence the location of the file doesn’t actually matter, it’s merely individual preference. I am going to create this file under src/services/auth.js


import { navigate } from 'gatsby';

const token_url = `${process.env.GATSBY_DRUPAL_ROOT}/oauth/token`;
const loginUrl = `${process.env.GATSBY_DRUPAL_ROOT}/user/login?_format=json`;

/* This check is to ensure that this code gets executed in browser because
* If we run this code without this check your gatsby develop will fail as it won't be able
* to access localStorage on build time
*/
export const isBrowser = typeof window !== 'undefined';

// Helper function to get the current status of the user
export const isLoggedIn = async () => {
// Check if code is executing in browser or not
if (typeof window === 'undefined') { 
  return Promise.resolve(false);
}

// Check if we already have access token in localStorage
const token = localStorage.getItem('access-token') !== null ? JSON.parse(localStorage.getItem('access-token')) : null;

// If not, return false as the user is not loggedIn.
if (token === null) {
  return Promise.resolve(false);
}

// Check if access token is still valid
if (token !== null && token.expirationDate > Math.floor(Date.now() / 1000)) {
  return Promise.resolve(token);
}
// If not, use refresh token and generate new token
if (token !== null) {
  const formData = new FormData();
  formData.append('client_id', process.env.GATSBY_CLIENT_ID);
  formData.append('client_secret', process.env.GATSBY_CLIENT_SECRET);
  formData.append('grant_type', 'refresh_token');
  formData.append('scope', process.env.GATSBY_CLIENT_SCOPE);
  formData.append('refresh_token', token.refresh_token);

  const response = await fetch(token_url, {
    method: 'post',
    headers: new Headers({
      Accept: 'application/json',
    }),
    body: formData,
  });

  if (response.ok) {
    const result = await response.json();
    const token =  await saveToken(result);
    return Promise.resolve(token)
  }
  
  // If refresh token is also expired 
    return navigate('/user/login', {state: {message: "your session has been timed out, please login"}});
}
};

/**
*  Login the user.
* 
*  Save the token in local storage.
*/
export const handleLogin = async (username, password) => {
const drupallogIn = await drupalLogIn(username, password);
if (drupallogIn !== undefined && drupallogIn) {
  return fetchSaveOauthToken(username, password);
}
return false;
};

/**
  * Log the current user out.
  *
  * Deletes the token from local storage.
  */
export const handleLogout = async () => {
const drupallogout =  await drupalLogout();
  localStorage.removeItem('access-token');
  navigate('/user/login');
};

/**
  * Get an OAuth token from Drupal.
  *
  * Exchange a username and password for an OAuth token.
  * @param username
  * @param password
  * @returns {Promise}
  *   Returns a promise that resolves with the new token returned from Drupal.
  */
export const fetchOauthToken = async (username, password) => {
const formData = new FormData();
formData.append('client_id', process.env.GATSBY_CLIENT_ID);
formData.append('client_secret', process.env.GATSBY_CLIENT_SECRET);
formData.append('grant_type', 'password');
formData.append('scope', process.env.GATSBY_CLIENT_SCOPE);
formData.append('username', username);
formData.append('password', password);

const response = await fetch(token_url, {
  method: 'post',
  headers: new Headers({
    Accept: 'application/json',
  }),
  body: formData,
});

if (response.ok) {
  const json = await response.json();
  if (json.error) {
    throw new Error(json.error.message);
  }
  return json;
}
};

/**
* Helper function to fetch and store tokens in local storage.
**/
const fetchSaveOauthToken = async (username, password) => {
const response = await fetchOauthToken(username, password);
if (response) {
  return saveToken(response);
}
};

/**
* Helper function to store token into local storage
**/
const saveToken = (json) => {
const token = { ...json };
token.date = Math.floor(Date.now() / 1000);
token.expirationDate = token.date + token.expires_in;
localStorage.setItem('access-token', JSON.stringify(token));
return token;
};

/**
  * Login request to Drupal.
  *
  * Exchange username and password.
  * @param username
  * @param password
  * @returns {Promise}
  *   Returns a promise that resolves to JSON response from Drupal.
  */
const drupalLogIn = async (username, password) => {
const response = await fetch(loginUrl, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: username,
    pass: password,
  }),
});
if (response.ok) {
  const json = await response.json();
  if (json.error) {
    throw new Error(json.error.message);
  }
  return json;
}
};

/**
  * Logout request to Drupal.
  *
  * Logs the user out on Drupal end.
  */
const drupalLogout = async () => {
const oauthToken = await isLoggedIn();
const logoutoken = oauthToken.access_token;
if (logoutoken) {
  const res = await fetch(`${process.env.GATSBY_DRUPAL_ROOT}/user/logout?_format=json`, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${logoutoken}`,
    },
  });
  if (res.ok) {
    return true;
  }
}
};

Adding a UI for users to log in 

Let's create a new SignIn form component that displays a form users can fill out with a username and password to log in.


import React, { useState } from 'react';
import { handleLogin } from '../../Services/auth';
import Layout from '../Layout';
import { navigate } from '@reach/router';

const SignIn = () => {
  const [processing, setProcessing] = useState(false);
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    setProcessing(true);

    if (!username && !password) {
      setProcessing(false);
      setError("Incorrect username or password, please try again.");
    } else {
      handleLogin(username, password).then((res) => {
        if (res !== undefined && res) {
          localStorage.setItem('username', JSON.stringify(username));
          setProcessing(false);
          navigate("/", { state: { message: 'You are now logged in' } });
        } else {
          setProcessing(false);
          setError("User name and password don't exist");
        }
      });
    }
  };

  return (
    <Layout>
      <div className="login-page-wrapper">
        <h3 className="title-28 text-center">Login Form</h3>
        {error && <div className="form-error"><p>{error}</p></div>}
        <form noValidate className="login" id="logIn">
          <fieldset>
            <div className="form-element">
              <label>Username</label>
              <input
                className="form-input"
                name="username"
                type="text"
                placeholder="Username"
                value={username}
                onChange={(event) => setUsername(event.target.value)}
              />
            </div>
            <div className="form-element">
              <label>Password</label>
              <input
                className="form-input"
                name="password"
                type="password"
                id="passwordSignin"
                value={password}
                placeholder="Password"
                onChange={(event) => setPassword(event.target.value)}
              />
            </div>
            {processing ? (
              <div className="text-center">Loading...</div>
            ) : (
              <button
                onClick={handleSubmit}
                className="button-black w-full"
                type="submit"
              >
                Login
              </button>
            )}
          </fieldset>
        </form>
      </div>
    </Layout>
  );
};

export default SignIn;

That’s all! We have our authentication functionality ready on the site. Now let’s take a look at each function and what it does.

SaveToken(): this function is used to store the token in local storage.

fetchOauthtoken(): This function takes a username and password as parameters, and uses them to make a request to the Drupal /oauth/token endpoint attempting to retrieve a new OAuth token.

fetchSaveOauthToken(): it generates new access token by making use on fetchOauthtoken function. Then stores this token in local storage to access various resources.

drupalLogin(): This function gets invoked when a user tries to log in into the site. It makes a request to /user/login REST resource with username and password. Which returns a user object if the passed credentials are correct else it will return an error message.

handleLogin(): It makes a call to the drupalLogin function to verify that the user exists and provided credentials are right. If a user exists, using fetchSaveOauthToken() generates new access token and stores it in local storage so it can be used for subsequent requests.

isLoggedIn(): This function verifies whether the user accessing the site is authenticated or not. Considering the various scenarios mentioned below it will consider that the user is logged in to the site which means that token is available in local storage.


Scenario 1: When the token is present and is not expired, this function will assume that the current user is an authenticated user.



Scenario 2: When the token is present but is an expired one. In this case, the function will make use of refresh token and makes a request to the OAuth server to regenerate a new access token.



Scenario 3. When the token is present but both access and refresh tokens are expired, in this case, the function will redirect the user to the login page. Since there is no other way to get access tokens from the OAuth server if both the tokens are expired.



Scenario 4:  If a token is not present in the local storage, the function will return false as the user is not logged into the site.

drupalLogout():  Verifies if a user is logged in or not, if true then it makes a request to Drupal site using the access token to logout out from the site.

handleLogout(): This function makes use of drupalLogout() function and if it is successful then removes the token stored in local storage.

We have a login page and if we try to log in it works perfectly but do you think there is any problem? If yes, then you are right. If you open the Chrome developer console, go to the Network tab, and inspect your OAuth/token request you will be able to see the client ID and client secret in the request header which is not right this can cause a security issue within your site. 

How do we resolve this?

Let’s take a look at the options we have in our hand.

  1. Netlify function
  2. Overriding OAuth controller

Netlify function

Write simple functions that automatically become APIs. Netlify deploys the functions you write as full API endpoints and will even run them automatically in response to events (like a form submission or a user login). Functions receive request context or event data and return data back to your frontend.

In our case, we get the necessary information i.e. username and password, pass it to the netlify function. This will pre-define our sensitive data in the netlify function which is client ID and client secret and make authentication requests from it to Drupal.

By doing this, only the response will be visible to the user but what is being sent inside the header of the request won’t be available. 

One downside to this is that you have to pay as per your usage to use netlify functions.

Let us know if you would like to hear more about netlify dev setup and netlify function from us. We will get back to you with another post talking about it.

Overriding OAuth controller

When you hit a request to /oauth/token endpoint to get an access token this simple_oauth/src/Controller/Oauth2Token.php controller is being executed. 

This controller is responsible for generating a token for you based on the headers you sent in the request. 

So we thought, what if we override this controller in a custom module and define our client ID and client secret here and when a request comes in and attach these both to the request. The requirement of the Oauth controller to generate a token is being satisfied and our problem of client secret and ID being exposed gets resolved. In addition, we also save some money for our client.

To override the OAuth controller we will have to let Drupal know that when /oauth/token route is requested rather than using a controller provided by a simple OAuth module use the controller from our custom module. 

We will have to alter the route so that we can inform Drupal.

 Add a YAML file my_module.services.yml into the module's folder with the following content


services:
 my_module.route_subscriber:
   class: Drupal\my_module\Routing\RouteSubscriber
   tags:
     - { name: event_subscriber }

Now we have to create a Routsubscriber class under my_module/src/Routing where we will notify Drupal to use our custom controller when oauth2_token.token route is requested.


get('oauth2_token.token')) {
     $route->setDefaults(array(
       '_controller' => 'Drupal\my_module\Controller\OauthTokenGenerator::token',
     ));
   }
 }
}

The overridden controller will look like as below


grantManager = $grant_manager;
    $this->state = $state;
  }

  /**
  * {@inheritdoc}
  */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('plugin.manager.oauth2_grant.processor'),
      $container->get('state')
    );
  }

  /**
  * Processes POST requests to /login/oauth/token.
  */
  public function token(ServerRequestInterface $request) {
    // Extract the grant type from the request body.
    $body = $request->getParsedBody();
    // Add client id and client secret to the request body.
    $body['client_id'] = $this->state->get('client_id');
    $body['client_secret'] = $this->state->get('client_secret');
    $newrequest = $request->withParsedBody($body);
    $grant_type_id = !empty($body['grant_type']) ? $body['grant_type'] : 'implicit';
    $client_drupal_entity = NULL;
    $consumer_storage = $this->entityTypeManager()->getStorage('consumer');
    $client_drupal_entities = $consumer_storage
      ->loadByProperties([
        'uuid' => $this->state->get('client_id'),
      ]);
    if (empty($client_drupal_entities)) {
      return OAuthServerException::invalidClient()
        ->generateHttpResponse(new Response());
    }
    $client_drupal_entity = reset($client_drupal_entities);
      
    // Get the auth server object from that uses the League library.
    try {
      // Respond to the incoming request and fill in the response.
      $auth_server = $this->grantManager->getAuthorizationServer($grant_type_id, $client_drupal_entity);
      $response = $this->handleToken($newrequest, $auth_server);
    }
    catch (OAuthServerException $exception) {
      watchdog_exception('simple_oauth', $exception);
      $response = $exception->generateHttpResponse(new Response());
    }
    return $response;
  }

  /**
  * Handles the token processing.
  *
  * @param \Psr\Http\Message\ServerRequestInterface $psr7_request
  *   The psr request.
  * @param \League\OAuth2\Server\AuthorizationServer $auth_server
  *   The authorization server.
  *
  * @return \Psr\Http\Message\ResponseInterface
  *   The response.
  *
  * @throws \League\OAuth2\Server\Exception\OAuthServerException
  */
  protected function handleToken(ServerRequestInterface $psr7_request, AuthorizationServer $auth_server) {
    // Instantiate a new PSR-7 response object so the library can fill it.
    return $auth_server->respondToAccessTokenRequest($psr7_request, new Response());
  }

}

After this, we can remove the following lines from IsLoggedIn and fetchOauthToken function from auth.js file.


formData.append('client_id', 
process.env.GATSBY_CLIENT_ID);

formData.append('client_secret', 
process.env.GATSBY_CLIENT_SECRET);

Please do let us know if you have any questions or a better approach to achieve this in the comment section. 

Contact Us

Heading

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

Name

Title

decorative underline image
23
Sr. Drupal Developer

Full-time | Remote

Build amazing product for brands

Our pursuit of excellence is perpetual. We strive to deliver quality and improve ourselves. By being consultative and placing ourselves in our partners' shoes, we strengthen our partnerships and exceed expectations.

Dipen Chaudhary

Founder/CEO

Higher Education

Helping Universities Bridge the Digital Gap

Harnessing the power of technology and design to enhance student engagement and reimagine university experiences.

Read more

Businesses can efficiently manage and update content on their websites without technical expertise, saving time and resources.

decorative image 1
decorative image 2
decorative image 3
decorative image 4

Website development

The Drupal website development service is designed to create websites that are flexible, scalable, and customizable to meet the unique needs of different brands. Our team of experts has extensive experience with Drupal's robust CMS to build websites that are visually appealing, functional, and user-friendly. We aim to ensure that your website achieves its maximum potential and delivers an excellent experience for your audience.

Title

Engagement
3
Years
80%
Improvement in self-service for GSB
View All Insights

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

Heading

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.

This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
min read

Higher Ed

Migration & Design System implementation for Stanford GSB