One day you might wake up with the next big idea that will shake the world in the most ungentle way. You decide to build an app, because you’ll have full access to all features of the device that you want your solution to work on. But then it dawns on you: you will actually need to build multiple apps in completely different languages while finding a way for them to serve the same content...
Then you start to realise that you won’t be able to step into the shoes of the greats, because web technology is holding you back. Fortunately, Drupal 8 and React Native are here to save your day - and your dream!
In this blog post you'll read how you can leverage Drupal 8 to serve as the back-end for your React Native app.
Update (03-10): After DrupalCon Vienna, Dries Buytaert posted his thoughts on the further adoption of React and Drupal. You can read his blog on his personal website.
First, a quick definition of what these technologies are:
- Drupal is an open source content management system based on PHP.
- React Native is a framework to build native apps using JavaScript and React.
If you want to read more about Drupal 8 or React Native, you're invited to check the sources at the bottom of this article.
Why React Native?
There are a myriad of front-end technologies available to you these days. The most popular ones are Angular and React. Both technologies allow you to build apps, but there is a big difference in how the apps will be built.
The advantage of employing React Native is that it lets you build an app using JavaScript, while converting the JavaScript into native code. In contrast, Angular or Ionic allow you to create a hybrid app, which basically is a website that gets embedded in a web view. Although the benefit here is that you're able to access the native features of a device.
In this case, we prefer React Native, because we want to build iOS and Android applications that run natively.
Headless Drupal
One of the big buzzwords that's been doing the rounds in the Drupal community lately is 'Headless'. A headless Drupal is actually a Drupal application where the front-end is not served by Drupal, but by a different technology.
You still get the benefits of a top notch and extremely flexible content management system, but you also get the benefits of your chosen front-end technology.
In this example, you'll discover how to set up a native iOS and Android application that gets its data from a Drupal website. To access the information, users will have to log in to the app, which allows the app to serve content tailored to the preferences of the user. Crucial in the current individualized digital world.
So this already brings us to our first hurdle. Because we are using a native application, authenticating users through cookies or sessions is not possible. So we are going to show you how to prepare your React Native application and your Drupal site to accept authenticated requests.
The architecture
The architecture consists of a vanilla Drupal 8 version and a React Native project with Redux.
The implemented flow is as following:
- A user gets the login screen presented on the app.
- The user fills in his credentials in the form
- The app posts the credentials to the endpoint in Drupal
- Drupal validates the credentials and logs the user in
- Drupal responds with a token based on the current user
- The app stores the token for future use
- The app now uses the token for all other requests the app makes to the Drupal REST API.
Creating an endpoint in Drupal
First we had to choose our authentication method. In this example, we opted to authenticate using a JWT or JSON web token, because there already is a great contributed module available for it on Drupal.org (https://www.drupal.org/project/jwt).
This module provides an authentication service that you can use with the REST module that is now in Drupal 8 core. This authentication service will read the token that is passed in the headers of the request and will determine the current user from it. All subsequent functionality in Drupal will then use that user to determine if it has permission to access the requested resources. This authentication service works for all subsequent requests, but not for the original request to get the JWT.
The original endpoint the JWT module provides, already expects the user to be logged in before it can serve the token. You could use the ready available basic authentication service, but we preferred to build our own as an example.
Authentication with JSON post
Instead of passing along the username and password in the headers of the request like 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 the AuthenticationProviderInterface and is announced 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 implement two methods, applies 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. So our requirement is that the JSON that is posted contains a username and password. In all other cases this authenticator can be skipped. Every authenticator service you define will always be called by Drupal. Therefore, it is very important that you define your conditions for applying 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 mostly reimplemented the authentication functionality of the basic authorization service, with the difference that we read the data from a JSON format. This code logs the user into the Drupal application. All the extra code is flood protection.
Getting the JWT token
To get the JWT token we leveraged the REST module, and created a new rest resource plugin. We could have used the endpoint the module already provides, but we prefer to create all our endpoints with a version in it. We defined the plugin with the following annotation:
/**
* 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"
* }
* )
*/
The uri_paths are the most important part of this annotation. By setting both the canonical and the weird looking Drupal.org keys, we are able to set a fully custom path for our endpoint. That allows us to set the version of our API in the URI like this: /api/v1/token. This way we can easily roll out new versions of our API and clearly communicate about deprecating older versions.
Our class extends the ResourceBase class provided by the REST module. We only implemented a post method in our class, as we only want this endpoint to handle posts.
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 custom method where we leverage the JWT module to get us a token that we can return.
We do not return a JSON object directly. We return a response in the form of an array. This is a very handy feature of the REST module, because you can choose the formats of your endpoint using the interface in Drupal. So you could easily return any other supported format like xml, JSON or hal_json. For this example, we chose hal_json.
Drupal has some built-in security measures for non-safe methods. The only safe methods are HEAD, GET, OPTIONS and TRACE. We are implementing a non-safe method, so we have to take into account the following things:
- When the app does a post it also needs to send a X-CSRF-Token in the header to avoid cross site request forgery. This token can be gotten from /session/token endpoint.
- In case of a POST we also need to set the Content-type request header to “application/hal+json” on top of the query parameter “_format=hal_json”.
Putting things together
The only thing left is to enable our endpoint through the interface that the rest modules provides on /admin/config/services/rest.
Update: As Shaksi rightly mentioned, to get this overview you need to download and enable the Rest UI module (https://www.drupal.org/project/restui)
As you can see, we’ve configured our token endpoint with our custom json_authentication_provider service and it is available in hal_json and json formats.
Update: Shaksi was so kind to recreate the code and host it on github (https://github.com/shaksi/json_web_token). We haven't been able to test it yet, but if there are any issues report them on github and Shaksi will be able to get in touch with us if he needs some help.
Calling the endpoint in our React Native application
The login component
Our login component contain 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 that is defined in our bindActions function.
function bindActions(dispatch) {
return {
login: (username, password) => dispatch(login(username, password)),
};
}
The login action is defined in our auth.js:
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-token fixed to keep it simple. Normally you would get this first. We’ve also used the react-native-axios package to handle our post. This action will return a promise. If you use the promise and thunk middleware in your Redux Store you can set up your reducer in the following way.
import type { Action } from '../actions/types';
import { LOGIN_PENDING, LOGOUT} from '../actions/auth';
import { REHYDRATE } from 'redux-persist/constants';
export type State = {
fetching: boolean,
isLoggedIn: boolean,
username:string,
password:string,
jwt: string,
error: boolean,
}
const initialState = {
fetching: false,
username: '',
password: '',
error: null,
}
export default function (state:State = initialState, action:Action): State {
switch (action.type) {
case "LOGIN_PENDING":
return {...state, fetching: true}
case "LOGIN_REJECTED":
return {...state, fetching: false, error: action.payload}
case "LOGIN_FULFILLED":
return {...state, fetching: false, isLoggedIn: true, jwt:action.payload.data.token}
case "REHYDRATE":
var incoming = action.payload.myReducer
if (incoming) return {...state, ...incoming, specialKey: processSpecial(incoming.specialKey)}
return state
default:
return state;
}
}
The reducer will be able to act on the different action types of the promise:
- LOGIN_PENDING: Allows you to change the state of your component so you could implement a loader while it is trying to get the token.
- LOGIN_REJECTED: When the attempt fails you could give a notification why it failed.
- LOGIN_FULFILLED: When the attempt succeeds you have the token and set the state to logged in.
So once we had implemented all of this, we had an iOS and Android app that actually used a Drupal 8 site as it main content store.
Following this example, you should be all set up to deliver tailored content to your users on whichever platform they may be.
The purpose of this article was to demonstrate how effective Drupal 8 can be as a source for your upcoming iOS or Android application.
Useful resources:
- Drupal 8: https://www.drupal.org/8
- React Native: https://facebook.github.io/react-native/
- REST fundamentals: https://www.drupal.org/docs/8/core/modules/rest/1-getting-started-rest-configuration-rest-request-fundamentals
More articles by our Dropsolid Technical Leads, strategist and marketeers? Check them out here.