Symfony headless drupal login bug

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
abdofariss's picture

Hi,

I'm trying to build a symfony headless drupal application, i made a custom module to handle login, logout and registration process,

this is my custom module:

<?php
/<strong>
* @
file
* Contains \Drupal\remoteuser\Controller\RemoteUserController
*/

namespace
Drupal\remoteuser\Controller;

use
Drupal\Core\Controller\ControllerBase;
use
Drupal\Core\Form\FormState;
use
Drupal\Core\Entity;
use
Drupal\user\Controller;
use
Symfony\Component\HttpFoundation\JsonResponse;

class
RemoteUserController extends ControllerBase
{
  public function
login() {
   
$form_state = (new FormState())->setValues($_POST);
    \
Drupal::formBuilder()->submitForm('\Drupal\user\Form\UserLoginForm', $form_state);

   
// Check for errors from the from
   
if ($errors = $form_state->getErrors()) {
     
// Return errors to notify the client.
     
return new JsonResponse( array( 'error' => $errors ) );
    }
    else {
     
// Return new user session to client.
     
$uid = \Drupal::service('user.auth')->authenticate($_POST['name'], $_POST['pass']);
     
$session_manager = \Drupal::service('session_manager');
     
$session_id = $session_manager->getId();
      return new
JsonResponse( array( 'uid' => $uid, 'session_id' => $session_id ) );
    }
  }

  public function
register() {
   
// Validate the e-mail address first.
   
if (!\Drupal::service('email.validator')->isValid($_POST['mail'])) {
      return new
JsonResponse( array( 'error' => 'Invalid e-mail address' ) );
    }

   
// Create password if it was not provided.
   
$password = $_POST['pass'] ? $_POST['pass'] : user_password();

    /</
strong> @var \Drupal\user\Entity\User $user */
   
$user = entity_create('user', array(
     
'name' => $_POST['name'],
     
'mail' => $_POST['mail'],
     
'pass' => $password,
    ));

   
// Validate the object.
   
$errors = $user->validate();

    if (
$errors->count() > 0) {
     
// Return errors to notify the client.
     
return new JsonResponse( array( 'error' => $errors->__toString() ) );
    } else {
     
// Save new user
     
$user->save();
     
// Return new user credentials
     
return new JsonResponse( array( 'user' => $user->toArray() ) );
    }
  }

  public function
logout() {
   
user_logout();

    return new
JsonResponse(null);
  }

  public function
session() {
   
$uid = \Drupal::currentUser()->id();

   
$session_manager = \Drupal::service('session_manager');
   
$session_id = $session_manager->getId();

   
// $permissions = $perms = array_keys(\Drupal::service('user.permissions')->getPermissions());
  
   
return new JsonResponse( array( 'uid' => $uid, 'session_id' => $session_id ) );
  }
}
?>

and this is the routing yml file:

remoteuser.user_login:
  path: '/api/user/login'
  defaults:
    _title: 'Remote User Login API'
    _controller: '\Drupal\remoteuser\Controller\RemoteUserController::login'
  requirements:
    _permission: 'access content'

remoteuser.user_register:
  path: '/api/user/register'
  defaults:
    _title: 'Remote User Register API'
    _controller: '\Drupal\remoteuser\Controller\RemoteUserController::register'
  requirements:
    _permission: 'access content'

remoteuser.user_session:
  path: '/api/user/session'
  defaults:
    _title: 'Remote User Session API'
    _controller: '\Drupal\remoteuser\Controller\RemoteUserController::session'
  requirements:
    _permission: 'access content'

remoteuser.user_logout:
  path: '/api/user/logout'
  defaults:
    _title: 'Remote User Logout API'
    _controller: '\Drupal\remoteuser\Controller\RemoteUserController::logout'
  requirements:
    _permission: 'access content'

the problem is with the login process, when i send a POST request to /api/user/login, it returns successfully a json response with uid and session_id
{"uid":"2","session_id":"s4tfYEYGGDZ7MJqcR1f8sSde4sTwcfZNua5WS5s8nOc"}

but when i check the user login statusby sending a GET request to /api/user/session, it returns:
{"uid":0,"session_id":"eBkwCGKv8qdetpPT42uCiSKqf8WS6UkLTuyNsk4hxLk"}

this problem is happening only when i execute the requests inside the application in symfony with curl, when i execute the same process in Postman, /api/user/session returns successfully the same result from /api/user/login
{"uid":"2","session_id":"s4tfYEYGGDZ7MJqcR1f8sSde4sTwcfZNua5WS5s8nOc"}

base url of symfony: http://app.local/web/
and for drupal, the url is : http://app.local/headless

can you help figure out what's the problem with my code please ?

Comments

Don't use UserLoginForm for that

SiliconMind's picture

As a proof of concept I did something very similar. I don't use login form at all. The key part is calling ugly static user_login_finalize($account) function which will make sure that current session is set up correctly.

<?php
use Drupal\Core\Controller\ControllerBase;
use
Drupal\user\UserAuthInterface;
use
Drupal\user\UserStorageInterface;
use
Symfony\Component\DependencyInjection\ContainerInterface;
use
Symfony\Component\HttpFoundation\JsonResponse;
use
Symfony\Component\HttpFoundation\Session\SessionInterface;

class
UserController extends ControllerBase {

  protected
$userAuth;
  protected
$userStorage;
  protected
$session;

 
/**
   * @inheritDoc
   */
 
public function __construct(UserAuthInterface $userAuth, UserStorageInterface $userStorage, SessionInterface $session) {
   
$this->userAuth = $userAuth;
   
$this->userStorage = $userStorage;
   
$this->session = $session;
  }

 
/**
   * @inheritDoc
   */
 
public static function create(ContainerInterface $container) {
    return new static(
     
$container->get('user.auth'),
     
$container->get('entity.manager')->getStorage('user'),
     
$container->get('session')
    );
  }

 
/**
   * Authenticate user.
   *
   * @param string $username
   * @param string $password
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   */
 
public function login($username, $password){
   
// TODO: Implement flood protection the same way as Drupal\user\Form\UserLoginForm does
   
$uid = $this->userAuth->authenticate($username, $password);

    if (
$uid) {
     
$account = $this->userStorage->load($uid);

     
// Damn legacy Drupal code!
      // This line is needed to set up current session for newly logged in user.
     
user_login_finalize($account);

     
$data = [
       
'uid' => $uid,
       
'username' => $username,
       
'sessionName' => $this->session->getName(),
       
'sessionId' => $this->session->getId(),
       
'result' => true,
      ];
    } else {
     
$data = ['result' => false];
    }

   
$response = new JsonResponse($data);
    return
$response;
  }

 
/**
   * Destroy user session. Works only for authenticated users.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   */
 
public function logout() {
   
// Damn legacy Drupal code!
   
user_logout();

   
$response = new JsonResponse(['result' => true]);
    return
$response;
  }
}
?>

Pass a session id properly

SiliconMind's picture

I just realised I forgot to answer this:

but when i check the user login statusby sending a GET request to /api/user/session, it returns:
{"uid":0,"session_id":"eBkwCGKv8qdetpPT42uCiSKqf8WS6UkLTuyNsk4hxLk"}

This might be happening because you're not providing a session cookie correctly. First of all you need to know what is the session name, then you assign a session id for it. Session name is mangled string and you can get it from session service by calling getName() function.

And more over, you have to provide session id inside a cookie, so making manual curl requests make sure you set cookie header properly.

Thanks for your the code

pjcarly's picture

Thanks for your the code example, I need to implement something similar in an EmberJS application, if everything works I'll make a blog post about this.

Headless Drupal

Group organizers

Group notifications

This group offers an RSS feed. Or subscribe to these personalized, sitewide feeds:

Hot content this week