Pour les besoins d'une de mes formations, j'ai dû mettre en place un drupal 8 découplé.

CORS (Cross-origin resource sharing)

Le « partage de ressources entre origines multiples » (Cross-Origin Resource Sharing, CORS) est désactivé par défaut dans drupal 8.

Pour régler cela : 2 types d'actions :

sur le serveur apache 

  • sudo a2enmod headers
  • Dans le fichier de conf d'apache (/etc/apache2/sites-available/mysite.conf :
<VirtualHost *:80>
    ServerAdmin admin@example.com
    ServerName local.d8-json.my
    DocumentRoot /home/yvan/dev/d8-json/web
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
<Directory /home/yvan/dev/d8-json/web>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
        Header set Access-Control-Allow-Origin "*"
</Directory>

Puis 

sudo a2dissite d8-json.conf
sudo systemctl reload apache2
sudo a2ensite d8-json.conf
sudo systemctl reload apache2

Côté Drupal

  • dans le fichier web/sites/default/services.yml :

cors.config:
    enabled: true
    # Specify allowed headers, like 'x-allowed-header'.
    allowedHeaders: ['x-csrf-token', 'authorization', 'content-type', 'accept', 'origin', 'x-requested-with']
    # Specify allowed request methods, specify ['*'] to allow all possible ones.
    allowedMethods: ['POST', 'GET', 'OPTIONS', 'DELETE', 'PUT', 'PATCH']
    # Configure requests allowed from specific origins.
    allowedOrigins: ['*']
    # Sets the Access-Control-Expose-Headers header.
    exposedHeaders: false
    # Sets the Access-Control-Max-Age header.
    maxAge: false
    # Sets the Access-Control-Allow-Credentials header.
    supportsCredentials: false

Modules de drupal

  • activer les modules  du coeur dans la catégorie "Web Services"

    • HAL

    • HTTP Basic Authentification
    • RESTful Web Services
    • Serialization
  • le module contrib
    • REST UI (https://www.drupal.org/project/restui)
  • Créer un module custom Ex : test_rest dans lequel j'ai créé
    • le fichier test_rest.info.yml :
name: test_rest
description: 'Test rest
package: Coopernet
type: module
version: 1.0
core: 8.x
dependencies:
  - drupal:node
  - drupal:path
  - drupal:text
  • le fichier web/modules/custom/test_rest/src/Plugin/rest/resource/ListArticles.php
<?php
namespace Drupal\test_rest\Plugin\rest\resource;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
/**
 * Provides a Demo Resource
 *
 * @RestResource(
 *   id = "list_articles",
 *   label = @Translation("Articles Resource"),
 *   uri_paths = {
 *     "canonical" = "/test_rest/list_articles"
 *   }
 * )
 */
class ListCartes extends ResourceBase {
  /**
   * Responds to entity GET requests.
   * @return \Drupal\rest\ResourceResponse
   */
  public function get() {
    $response = ['message' => 'Liste des articles bientôt !!!'];
    return new ResourceResponse($response);
  }
}
  • puis activer le point d'entrée (endpoint) de la ressource, en l'occurence "Articles Resource" dans l'interface de REST UI (/admin/config/services/rest) en donnant tous les droits nécessaires
  • enfin, faire appel à ce point d'entrée vial js :
(function() {
  console.log("Hello");
  // création de la requête
  const req = new XMLHttpRequest();

  function enCours(event) {
    // On teste directement le status de notre instance de XMLHttpRequest
    if (this.status === 200) {
      // Tout baigne, voici le contenu de la réponse
      console.log("Contenu", this.responseText);
    } else {
      // On y est pas encore, voici le statut actuel
      console.log("Statut actuel", this.status, this.statusText);
    }
  }

  req.onload = enCours;

  // ouverture de la requête
  //req.open("GET", "/node/3?_format=hal_json", true);
  req.open("GET", "/memo/list_cartes?_format=json", true);
  // en complément
  req.setRequestHeader("Content-Type", "application/hal+json");
  // envoi de la requête
  req.send(null);
})();
  • Par défaut le super utilisateur est autorisé à obtenir des données via tous les points d'entrée (endpoint) mais si vous souhaitez donner l'accès à d'autres utilisateurs, il faudra vous rendre sur l'interface classique des droits de drupal et gérer chaque accès (rubrique "RESTful Web Services")

S'identifier sur drupal avec une requête fetch js

fetch("http://local.d8.my/user/login?_format=json", {
    credentials: "same-origin",
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": token
    },
    body: JSON.stringify({
      name: "login",
      pass: "pwd;"
    })
  })
    .then(response => {
      console.log("statut de la réponse : ", response.status);
      return response.json()
    })
    .then(data => {
      //console.log("success", data);
      if (data.current_user === undefined) {
        console.log("Erreur de login");
        throw new Error("Erreur de login : " + data);
      } else {
        console.log("user", data.current_user);
        return data;
      }
    })

Attention, l'accès au login n'est possible que si l'utilisateur n'est pas encore identifié

Récupérer des informations sur les utilisateurs via les vues

Globalement, il est assez facile de récupérer tout type d'information via les "views"

Il suffit pour cela de créer une vue dont vous cocherez la case "REST EXPORT SETTINGS > Provide a REST export"

Exemple de vue à importer pour avoir accès aux infos concernant l'utilisateur courant

uuid: deb9920f-4f89-4db0-a2e2-446bf1ef4018
langcode: en
status: true
dependencies:
  module:
    - rest
    - serialization
    - user
id: restuser
label: restuser
module: views
description: ''
tag: ''
base_table: users_field_data
base_field: uid
display:
  default:
    display_plugin: default
    id: default
    display_title: Master
    position: 0
    display_options:
      access:
        type: perm
        options:
          perm: 'access user profiles'
      cache:
        type: tag
        options: {  }
      query:
        type: views_query
        options:
          disable_sql_rewrite: false
          distinct: false
          replica: false
          query_comment: ''
          query_tags: {  }
      exposed_form:
        type: basic
        options:
          submit_button: Apply
          reset_button: false
          reset_button_label: Reset
          exposed_sorts_label: 'Sort by'
          expose_sort_order: true
          sort_asc_label: Asc
          sort_desc_label: Desc
      pager:
        type: mini
        options:
          items_per_page: 10
          offset: 0
          id: 0
          total_pages: null
          expose:
            items_per_page: false
            items_per_page_label: 'Items per page'
            items_per_page_options: '5, 10, 25, 50'
            items_per_page_options_all: false
            items_per_page_options_all_label: '- All -'
            offset: false
            offset_label: Offset
          tags:
            previous: ‹‹
            next: ››
      style:
        type: serializer
      row:
        type: fields
        options:
          inline: {  }
          separator: ''
          hide_empty: false
          default_field_elements: true
      fields:
        name:
          id: name
          table: users_field_data
          field: name
          entity_type: user
          entity_field: name
          label: ''
          alter:
            alter_text: false
            make_link: false
            absolute: false
            trim: false
            word_boundary: false
            ellipsis: false
            strip_tags: false
            html: false
          hide_empty: false
          empty_zero: false
          plugin_id: field
          relationship: none
          group_type: group
          admin_label: ''
          exclude: false
          element_type: ''
          element_class: ''
          element_label_type: ''
          element_label_class: ''
          element_label_colon: true
          element_wrapper_type: ''
          element_wrapper_class: ''
          element_default_classes: true
          empty: ''
          hide_alter_empty: true
          click_sort_column: value
          type: user_name
          settings: {  }
          group_column: value
          group_columns: {  }
          group_rows: true
          delta_limit: 0
          delta_offset: 0
          delta_reversed: false
          delta_first_last: false
          multi_type: separator
          separator: ', '
          field_api_classes: false
      filters:
        status:
          value: '1'
          table: users_field_data
          field: status
          plugin_id: boolean
          entity_type: user
          entity_field: status
          id: status
          expose:
            operator: ''
            operator_limit_selection: false
            operator_list: {  }
          group: 1
        uid_current:
          id: uid_current
          table: users
          field: uid_current
          relationship: none
          group_type: group
          admin_label: ''
          operator: '='
          value: '1'
          group: 1
          exposed: false
          expose:
            operator_id: ''
            label: ''
            description: ''
            use_operator: false
            operator: ''
            operator_limit_selection: false
            operator_list: {  }
            identifier: ''
            required: false
            remember: false
            multiple: false
            remember_roles:
              authenticated: authenticated
          is_grouped: false
          group_info:
            label: ''
            description: ''
            identifier: ''
            optional: true
            widget: select
            multiple: false
            remember: false
            default_group: All
            default_group_multiple: {  }
            group_items: {  }
          entity_type: user
          plugin_id: user_current
      sorts: {  }
      header: {  }
      footer: {  }
      empty: {  }
      relationships: {  }
      arguments: {  }
      display_extenders: {  }
    cache_metadata:
      max-age: -1
      contexts:
        - 'languages:language_content'
        - 'languages:language_interface'
        - request_format
        - url.query_args
        - user
        - user.permissions
      tags: {  }
  rest_export_1:
    display_plugin: rest_export
    id: rest_export_1
    display_title: 'REST export'
    position: 1
    display_options:
      display_extenders: {  }
      path: memo/views/getUser
      pager:
        type: some
        options:
          items_per_page: 10
          offset: 0
      style:
        type: serializer
        options:
          grouping: {  }
          uses_fields: false
          formats: {  }
      row:
        type: data_entity
        options: {  }
      auth:
        - basic_auth
    cache_metadata:
      max-age: -1
      contexts:
        - 'languages:language_content'
        - 'languages:language_interface'
        - request_format
        - user
        - user.permissions
      tags: {  }

Autres points d'entrée user/login, user/login/status, user/logout, user/password

https://www.drupal.org/node/2720655

Problèmes rencontrés 

Status 422

Failed to load resource: the server responded with a status of 422 (Unprocessable Entity)
rest/type/node/article does not correspond to an entity on this site 

Solution : vider les caches : drush cr source : https://www.drupal.org/project/restui/issues/2599364

error 404 -  No route found for "POST ... 

Dans le cas d'une requête post, attention à ajouter une deuxième ligne concernant uri_paths :

uri_paths = {
 *     "canonical" = "/foolzz_rest_api/profile/{uid}",
 *     "https://www.drupal.org/link-relations/create" = "/foolzz_rest_api/profile/{uid}"
 *   }

Explications sur les tokens et les cookies

D'après ce que j'en comprends, quand on utilise js à l'intérieur d'un navigateur pour faire une requête rest (GET, POST, PATCH, DELETE...), le cookie de drupal est automatiquement envoyé. Donc, si un utilisateur malveillant arrive à s'introduire dans application Web écrite en JS et exécutée dans un navigateur, il va pouvoir  effectuer une requête  (POST/PATCH/DELETE) en utilisant le cookie du navigateur Web. Cependant, le navigateur ne saura pas inclure des en-têtes de requête supplémentaires. C'est pourquoi Drupal nécessite que l'en-tête de la requête spécifie un jeton CSRF. Tout comme, pour la même raison, nous avons un jeton CSRF dans les formulaires HTML.