Skip to main content

Drupal 8 ve React Native

Drupal is a PHP-based open source content management system. React Native is a framework for building native applications using JavaScript and React.
Seyfettin Kahveci
Seyfettin Kahveci
10 min. read
drupal-8-ve-react-native

Why React Native?

There are countless front-end technologies you can use these days. The most popular ones are Angular and React. Both technologies allow you to build applications, but the most important difference is how to build applications.

The advantage of using React Native is that it allows you to build an application by creating JavaScript while converting JavaScript to native code. On the contrary, Angular or Ionic allow you to create hybrid application, which is basically a website embedded in a web view. Despite this, you can reach the native features of the device.

In this case, we prefer React Native because we want to build iOS and Android apps that run natively.

Interface Independent Drupal

In general, in Interface Independent Drupal, the visitor will see pages created using Javascript frameworks such as Angular.js or Backbone.js. They will not see a traditional Drupal theme.

In this case, Drupal is predominantly used as the data store, which is then read by the framework. The normal Drupal interface is still used by users to input the content that will appear on the website.

However, even the normal Drupal interface can be replaced by other tools, such as a mobile application. In this case, Drupal only provides the logic to run the web application to the back-end.

In this example, you will discover how to set up a native iOS and Android application that retrieves its data from a Drupal website. To access this information, users need to log in to the app, which in this case allows the app to serve content that matches the user's preferences.

So this already brings us to our first problem. Since we are using a native application, it is not possible to authenticate users via cookies or sessions. We will therefore show you how to prepare your React Native application and Drupal site to accept authenticated requests.

Building Architecture

  • The architecture consists of a vanilla Drupal 8 version and a React Native project with Redux.
  • The implemented flow is as follows
  • A user opens the login screen presented in the application.
  • The user fills in the credentials on the form.The application sends the credentials to the endpoint in Drupal.
  • Drupal validates the credentials and logs the user.Drupal responds with a token based on the current user.
  • The application stores the token for future use.
  • The application now uses the token in all other requests the application makes to the Drupal REST API.

Creating an Endpoint in Drupal

First we had to choose our authentication method. In this example, we chose authentication using a JWT or JSON web token, since a module already exists on Drupal.org that is a major contributor (https://www.drupal.org/project/jwt)

This module provides an authentication service that you can use with the REST module, which is now core to Drupal 8. This authentication service will read the token passed through the headers of the request and determine the current user there. All subsequent functionality in Drupal will then use that user to determine whether they have permission to access the requested resources. This authentication service works for all subsequent requests, but does not apply to the original JWT request request.

The original endpoint provided by the JWT module waits for the user to log in before serving as their token. You can use any off-the-shelf basic authentication service available, but we have chosen to set up our own service as an example.

JSON Post Authentication

Instead of passing the username and password in the request header, as the basic authentication service expects, we will send the username and password in the body of our request formatted as JSON.

Our authentication class implements AuthenticationProviderInterface and is added in json_web_token.services.yml as follows:

services:
 authentication.json_web_token:
   class: Drupal\json_web_token\Authentication\Provider\JsonAuthenticationProvider
   arguments: ['@config.factory', '@user.auth', '@flood', '@entity.manager']
   tags:
     - { name: authentication_provider, provider_id: 'json_authentication_provider', priority: 100 }

The interface states that we have to apply two methods, apply and authenticate:

public function applies(Request $request) {
 
 $content = json_decode($request->getContent());
 
 return isset($content->username, $content->password) && !empty($content->username) && 
 !empty($content->password);
}

Here we define when the authenticator should be applied. Therefore, the JSON we require must contain a username and password. In all other cases, this authenticator can be omitted. Each authentication service you define is always called by Drupal. Therefore, it is very important that you define your conditions for implementing the authentication service.

public function authenticate(Request $request) {
 $flood_config = $this->configFactory->get('user.flood');
 $content = json_decode($request->getContent());
 
 $username = $content->username;
 $password = $content->password;
 // Flood protection: this is very similar to the user login form code.
 // @see \Drupal\user\Form\UserLoginForm::validateAuthentication()
 // Do not allow any login from the current user's IP if the limit has been
 // reached. Default is 50 failed attempts allowed in one hour. This is
 // independent of the per-user limit to catch attempts from one IP to log
 // in to many different user accounts.  We have a reasonably high limit
 // since there may be only one apparent IP for all users at an institution.
 if ($this->flood->isAllowed(json_authentication_provider.failed_login_ip', 
$flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
   $accounts = $this->entityManager->getStorage('user')
     ->loadByProperties(array('name' => $username, 'status' => 1));
   $account = reset($accounts);
   if ($account) {
     if ($flood_config->get('uid_only')) {
       // Register flood events based on the uid only, so they apply for any
       // IP address. This is the most secure option.
       $identifier = $account->id();
     }
     else {
       // The default identifier is a combination of uid and IP address. This
       // is less secure but more resistant to denial-of-service attacks that
       // could lock out all users with public user names.
       $identifier = $account->id() . '-' . $request->getClientIP();
     }
     // Don't allow login if the limit for this user has been reached.
     // Default is to allow 5 failed attempts every 6 hours.
     if ($this->flood->isAllowed('json_authentication_provider.failed_login_user',
$flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
       $uid = $this->userAuth->authenticate($username, $password);
       if ($uid) {
         $this->flood->clear('json_authentication_provider.failed_login_user',
$identifier);
         return $this->entityManager->getStorage('user')->load($uid);
       }
       else {
         // Register a per-user failed login event.
         $this->flood->register('json_authentication_provider.failed_login_user', 
$flood_config->get('user_window'), $identifier);
       }
     }
   }
 }
 
 // Always register an IP-based failed login event.
 $this->flood->register('json_authentication_provider.failed_login_ip',
 $flood_config->get('ip_window'));
 return [];
}

Here we have revisited the authentication functionality of the basic authorisation service with the realisation that we are reading the data from a JSON format. This code registers the user with the Drupal application.

JWT Token Retrieval

We used the REST module to get the JWT token and created a new rest resource plugin. We could have used the endpoint that the module already provides, but we prefer to create all our endpoints with one version of it. We defined the plugin with the following description:

/**
* Provides a resource to get a JWT token.
*
* @RestResource(
*   id = "token_rest_resource",
*   label = @Translation("Token rest resource"),
*   uri_paths = {
*     "canonical" = "/api/v1/token",
*     "https://www.drupal.org/link-relations/create" = "/api/v1/token"
*   }
* )
*/

Uri_paths is the most important part of this annotation. By setting the standard and strange looking Drupal.org keys, we are able to set a completely custom path for the endpoint. This allows us to set the version of our API in the URI like this: / api / v1 / token. This way, we can easily propagate new versions of our API and communicate clearly about deprecation of older versions.

Our class extends the ResourceBase class provided by the REST module. We have implemented only one post method in our class so that we only want this endpoint to work on messages.

public function post() {
 
 if($this->currentUser->isAnonymous()){
   $data['message'] = $this->t("Login failed. If you don't have an account register. 
If you forgot your credentials please reset your password.");
 }else{
   $data['message'] = $this->t('Login succeeded');
   $data['token'] = $this->generateToken();
 }
 
 return new ResourceResponse($data);
}
 
/**
* Generates a new JWT.
*/
protected function generateToken() {
 $token = new JsonWebToken();
 $event = new JwtAuthIssuerEvent($token);
 $this->eventDispatcher->dispatch(JwtAuthIssuerEvents::GENERATE, $event);
 $jwt = $event->getToken();
 
 return $this->transcoder->encode($jwt, array());
}

The GenerateToken method is a special method where we use the JWT module to get a token that can be returned to us.

We do not return a JSON object directly. We send a response in the form of an array. This is a useful feature of the REST module, because it allows us to choose the formats of our endpoint using the interface in Drupal. So you can easily return xml, JSON or other supported formats such as hal_json. For this example, we chose hal_json.

Drupal has some built-in security measures for unsafe methods. Safe methods are HEAD, GET, OPTIONS, and TRACE. Since we are implementing an unsafe method, we must take the following considerations into account:

When the application sends a broadcast, it must also send an X-CSRF-Token in the header to prevent cross-site request forgery. This token can be obtained from the / session / token endpoint.

In the case of POST, we need to set the Content-type request header to "application / hal + json" above the query parameter "_format = hal_json".

Bir Şeyleri Bir Araya Getirmek

All that remains is to enable our endpoint through the interface that rest modules provide on / admin / config / services / rest.

Update: To get this generalisation you need to download and activate the Rest UI module (https://www.drupal.org/project/restui).

As you can see, we have configured the token_name with our custom json_authentication_provider service and make it available in hal_json and json formats.

Update: https://github.com/shaksi/json_web_token

INPUT COMPONENT

Our input component contains two input fields and a button.

<Item rounded style={styles.inputGrp}>
   <Icon name="person"/>
   <Input
       placeholder="Username"
       onChangeText={username => this.setState({username})}
       placeholderTextColor="#FFF"
       style={styles.input}
   />
</Item>
 
<Item rounded style={styles.inputGrp}>
   <Icon name="unlock"/>
   <Input
       placeholder="Password"
       secureTextEntry
       placeholderTextColor="#FFF"
       onChangeText={password => this.setState({password})}
       style={styles.input}
   />
</Item>
 
<Button
   rounded primary block large
   style={styles.loginBtn}
   onPress={() => this.login({
       username: this.state.username,
       password: this.state.password
   })}
>
   <Text style={Platform.OS === 'android' ? {
       fontSize: 16,
       textAlign: 'center',
       top: -5
   } : {fontSize: 16, fontWeight: '900'}}>Get Started</Text>
</Button>

When we click the login button, we trigger the login action defined in our bindActions function.

function bindActions(dispatch) {
   return {
       login: (username, password) => dispatch(login(username, password)),
   };
}

The login process is defined in our auth.js file:

import type { Action } from './types';
import axios from 'react-native-axios';
 
export const LOGIN = 'LOGIN';
 
export function login(username, password):Action {
 
   var jwt = '';
  
   var endpoint = "https://example.com/api/v1/token?_format=hal_json";
  
   return {
       type: LOGIN,
       payload: axios({
           method: 'post',
           url: endpoint,
           data:  {
               username: username,
               password: password,
               jwt: jwt,
           },
           headers: {
               'Content-Type':'application/hal+json',
               'X-CSRF-Token':'V5GBdzli7IvPCuRjMqvlEC4CeSeXgufl4Jx3hngZYRw'
           }
       })
   }
}

In this example, we set the X-CSRF-icon to constant to keep it simple. Normally you get this first. We also used the react-native-axios package to respond to our release. This action gives a promise. If you use the promise and thunk middleware in the Redux Store, you can set up your reducer like this.

import type { Action } from './types';
import axios from 'react-native-axios';
 
export const LOGIN = 'LOGIN';
 
export function login(username, password):Action {
 
   var jwt = '';
  
   var endpoint = "https://example.com/api/v1/token?_format=hal_json";
  
   return {
       type: LOGIN,
       payload: axios({
           method: 'post',
           url: endpoint,
           data:  {
               username: username,
               password: password,
               jwt: jwt,
           },
           headers: {
               'Content-Type':'application/hal+json',
               'X-CSRF-Token':'V5GBdzli7IvPCuRjMqvlEC4CeSeXgufl4Jx3hngZYRw'
           }
       })
   }
}

The reducer will be able to act on different types of action of the promise:

LOGIN_PENDING: Allows you to change the state of the component so that you can apply a loader when trying to create an icon.     
LOGIN_REJECTED: When the attempt fails, you can issue a notification telling you why it failed.     
LOGIN_FULFILLED: When the attempt succeeds, you have the token and you have set the state to login.

So after implementing it all, we came up with an iOS and Android app that uses a Drupal 8 site as the main content repository.

Following this example, you should be set up to serve custom content to your users, no matter what user platform they're on.

The purpose of this article was to show how Drupal 8 can be an effective resource for your upcoming iOS or Android app.

Sources:

REST fundamentals: https://www.drupal.org/docs/8/core/modules/rest/1-getting-started-rest-configuration-rest-request-fundamentals

Our Offices

Drupart Locations

Our Officess

London

151 West Green Road, London, England

442038156478

[email protected]

Drupart R&D

GOSB Teknopark Hi-Tech Bina 3.Kat B3 Gebze - KOCAELİ

+90 262 678 8872

[email protected]

Newark

112 Capitol Trail Suite, A437 Newark DE, 19711

+17406666255

[email protected]

Wiesbaden

Hinterbergstraße 27
65207 Wiesbaden
Deutschland

+49 (0) 6151 – 492 70 23

[email protected]