eZ Platform Discussions

SSO - Login handler


#1

Hello everyone. I’m starting a big part of my project: The implementation of the SSO.

Before I start doing anything crazy, I come to take some advice from the community :slight_smile:

Here is an extract from the SSO specs… (Which doesn’t look standard…)

URL='http://auth.myclient.fr'
CLIENT_ID='client_id'
CLIENT_SECRET='client_secret'
LOGIN='foo'
PASSWORD='bar'

Authentification

curl -X POST --user "${CLIENT_ID}:${CLIENT_SECRET}" -d "grant_type=password&username=${LOGIN}&password=${PASSWORD}" ${URL}/oauth/token
{
"access_token":"27c1d964-fcad-470f-b32b-219c662e6099",
"token_type":"bearer",
"refresh_token":"d7fe669c-cf46-46ee-b790-a9ef39ea7e63",
"expires_in":3599,
"scope":"read write"
}
REFRESH_TOKEN="d7fe669c-cf46-46ee-b790-a9ef39ea7e63"
ACCESS_TOKEN="27c1d964-fcad-470f-b32b-219c662e6099"

Vérification de token

curl -X GET --user "${CLIENT_ID}:${CLIENT_SECRET}" -d "grant_type=password&username=${LOGIN}&password=${PASSWORD}" ${URL}/oauth/check_token?token=${ACCESS_TOKEN}
{
"access_token":"27c1d964-fcad-470f-b32b-219c662e6099",
"token_type":"bearer",
"refresh_token":"d7fe669c-cf46-46ee-b790-a9ef39ea7e63",
"expires_in":3599,
"scope":"read write"
}

Récupération des permissions

curl -X GET ' -H "Authorization: Bearer ${ACCESS_TOKEN}" ${URL}/account/permissions?
{
  "email ":"...",
  "permissions":[
    //
  ]
}

I feel like what I have to do is more like a login-handler than SSO.
eZ 4 login-handler : http://share.ez.no/blogs/thiago-campos-viana/tip-custom-login-handler

How do you do that with eZPlatform?

Thank you for your help :slight_smile:


HWIOAuthBundle / BeSimpleSsoAuthBundle - Composer errors
#2

From the postman conf file of my authentication service I developed this small service:

class MySsoService
{
    private $baseUrl;
    private $authorization;

    public function __construct($baseUrl, $authorization)
    {
        $this->baseUrl = $baseUrl;
        $this->authorization = $authorization;
    }

    public function OAuthPassword($login, $password)
    {
        $body = [
            'grant_type' => 'password',
            'username' => $login,
            'password' => $password,
        ];
        $headers = [
            'Content-Type' => 'application/x-www-form-urlencoded',
            'Accept' => 'application/json',
            'Authorization' => 'Basic '.$this->authorization,
        ];
            $res = $this->__rest_call('POST', '/sso/oauth/token', $body, $headers);

            /*{
                "access_token": "aaa",
                "token_type": "bearer",
                "refresh_token": "rrr",
                "expires_in": 4999,
                "scope": "read write trust",
                "service_ticket": "sss",
                "token": "ttt",
                "jti": "jjj"
            }*/

            return $this->__json_decode($res);
    }

    public function OAuthRefreshToken($refresh_token)
    {
        $body = [
            'grant_type' => 'refresh_token',
            'refresh_token' => $refresh_token,
        ];
        $headers = [
            'Content-Type' => 'application/x-www-form-urlencoded',
            'Accept' => 'application/json',
            'Authorization' => 'Basic '.$this->authorization,
        ];
            $res = $this->__rest_call('POST', '/sso/oauth/token', $body, $headers);
            return $this->__json_decode($res);
    }

    public function OAuthCheckToken($access_token)
    {
        $parameters = [
            'token' => $access_token,
        ];
        $headers = [
            'Accept' => 'application/json',
            'Authorization' => 'Basic '.$this->authorization,
        ];

            $res = $this->__rest_call('GET', '/sso/oauth/check_token', $parameters, $headers);
            return $this->__json_decode($res);
    }

    public function OAuthPermissions($access_token)
    {
        $parameters = [
        ];
        $headers = [
            'Accept' => 'application/json',
            'Authorization' => 'Bearer ' . $access_token,
        ];
            $res = $this->__rest_call('GET', '/sso/account/permissions', $parameters, $headers);

            /*{
                "email": "foo@bar.fr",
                "permissions": [
			...
                ]
            }*/

            return $this->__json_decode($res);
    }


    // ===============

    private function __rest_call($method, $service, $parameters = [], $additional_headers = [])
    {
        $url = $this->baseUrl.$service;
        $curl = curl_init();

        switch ($method)
        {
            case "POST":
                curl_setopt($curl, CURLOPT_POST, 1);
                if ($parameters) {
                    curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($parameters));
                }
                break;
            case "PUT":
                curl_setopt($curl, CURLOPT_PUT, 1);
                break;
            default:
                if ($parameters) {
                    $url = sprintf("%s?%s", $url, http_build_query($parameters));
                }
        }

        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

        $headers = [];
        foreach ($additional_headers as $k => $v) {
            $headers[] = "$k: $v";
        }
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);

        $result = curl_exec($curl);
        curl_close($curl);
        return json_decode($result, true);
    }
}

My service is working.

All that remains is to plug it into the eZ authentication system.

$SSO = $this->container->get('my_sso.service');

$login = 'foo@bar.fr';
$mdp = 'zag';

$tokens = $SSO->OAuthPassword($login, $mdp);
$refresh_token = $tokens['refresh_token'];
$tokens = $SSO->OAuthRefreshToken($refresh_token);
$access_token = $tokens['access_token'];
$SSO->OAuthCheckToken($access_token);
$permissions = $SSO->OAuthPermissions($access_token);

#3

https://doc.ezplatform.com/en/2.3/cookbook/authenticating_a_user_with_multiple_user_providers

WIP


#4

So I defined 3 services.

The first is the one that allows me to communicate with the remote API. The class is detailed in a previous post.
The second is the user provider which allows to get the SF user
The last one is the interactive event listener which allows you to select the eZ user

services:
    my_sso.service:
        class: MySSOBundle\Services\MySsoService
        arguments:
                - '%my.sso.base_url%'
                - '%my.sso.authorization%'

    my_sso.user_provider:
        class: MySSOBundle\User\MySsoUserProvider
        arguments:
                - '@my_sso.service'

    my_sso.interactive_event_listener:
        class: MySSOBundle\EventListener\InteractiveLoginListener
        arguments:
                - '@ezpublish.api.service.user'
                - '@my_sso.service'
        tags:
            - { name: kernel.event_subscriber }

Here my user provider

use MySSOBundle\Services\MySsoService;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;

class MySsoUserProvider implements UserProviderInterface
{
    private $sso;

    public function __construct(MySsoService $sso)
    {
        $this->sso = $sso;
    }

    public function supportsClass($class)
    {
        return MySsoUser::class === $class;
    }

    public function loadUserByUsername($username)
    {
        return $this->fetchUser($username);
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof MySsoUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }
        return $this->fetchUser($user->getUsername());
    }

    private function fetchUser($username)
    {
        // Now I don't know what to do... 
        // My API does not have a function based only on the username.... 
	//$this->sso->???? 
        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $username)
        );
    }
}

My user class is the same as the one described in the SF doc.

class MySsoUser implements UserInterface, EquatableInterface
{
    private $username;
    private $password;
    private $salt;
    private $roles;

    public function __construct($username, $password, $salt, array $roles)
    {
        $this->username = $username;
        $this->password = $password;
        $this->salt = $salt;
        $this->roles = $roles;
    }

    public function getRoles()
    {
        return $this->roles;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getSalt()
    {
        return $this->salt;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function eraseCredentials()
    {
    }

    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof MySsoUser) {
            return false;
        }
        if ($this->password !== $user->getPassword()) {
            return false;
        }
        if ($this->salt !== $user->getSalt()) {
            return false;
        }
        if ($this->username !== $user->getUsername()) {
            return false;
        }
        return true;
    }
}

And finally, my interactive event listener

class InteractiveLoginListener implements EventSubscriberInterface//, UserProviderInterface
{
    private $userService;
    private $sso;

    public function __construct(UserService $userService, $sso)
    {
        $this->userService = $userService;
        $this->sso = $sso;
    }

    public static function getSubscribedEvents()
    {
        return [MVCEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin'];
    }

    public function onInteractiveLogin(InteractiveLoginEvent $event)
    {
        $event->setApiUser($this->userService->loadUserByLogin( 'abonnees' ));
    }
}

WIP


#5

Do I need to make a Custom Authentication System with Guard ?


#6
security:
    providers:
        ezpublish:
            id: ezpublish.security.user_provider
    public function onInteractiveLogin(InteractiveLoginEvent $event)
    {
        /** @var User $user */
        $user = $event->getAuthenticationToken()->getUser();
        $login = $user->getUsername();
        $password = $user->getPassword();

        dump($event);die(__METHOD__); // don't die()

        $tokens = $this->sso->OAuthPassword($login, $password);
        $refresh_token = $tokens['refresh_token'];
        $tokens = $this->sso->OAuthRefreshToken($refresh_token);
        $access_token = $tokens['access_token'];
        $r = $this->sso->OAuthCheckToken($access_token);
        $permissions = $this->sso->OAuthPermissions($access_token);

        foreach ($permissions['permissions'] as $perm) {
            if (true) { // TODO
                $event->setApiUser($this->userService->loadUserByLogin( 'abonnees' ));
                return;
            }
        }

        $event->setApiUser($this->userService->loadUserByLogin( 'non_abonnees' ));
        return;
    }

#7

I confirm that I do need a user provider.

By hijacking the in_memory provider I can trigger the onInteractiveLogin.

Unfortunately I still don’t see what to do in the UserProvider

security:
    providers:
        chain_provider:
            chain:
                providers:
                    - in_memory # If first in_memory user work but no ezpublish
                    - ezpublish
                    - my_sso
       my_sso:
            id: my_sso.user_provider
        ezpublish:
            id: ezpublish.security.user_provider
        in_memory:
            memory:
                users:
                    # You will then be able to login with username "user" and password "userpass"
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    bob@client.com:  { password: 34RtYr6p, roles: [ 'ROLE_USER' ] }
                    lea@client.com:  { password: 1c:9Np7=, roles: [ 'ROLE_USER' ] }
                    roy@client.com:  { password: Zl23u2~S, roles: [ 'ROLE_USER' ] }
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        MySSOBundle\User\MySsoUser: plaintext

#8

I’m getting close to the result!

So I have 2 providers: my_sso and ezpublish

The eZ provider work well.
If the user is not found in eZ the my_sso takes over.

The problem is the loadUserByUsername() function that asks me to find a user with his login only.
But I don’t have that information.

security:
    providers:
        chain_provider:
            chain:
                providers:
                    - my_sso
                    - ezpublish
        my_sso:
            id: my_sso.user_provider
        ezpublish:
            id: ezpublish.security.user_provider
    encoders:
        MySSOBundle\User\MySsoUser: plaintext
class MySsoUserProvider implements UserProviderInterface
{
    private $sso;

    public function __construct(MySsoService $sso)  {
        $this->sso = $sso;
    }

    public function supportsClass($class)    {
        return MySsoUser::class === $class;
    }

    public function loadUserByUsername($username)    {
        return $this->fetchUser($username, '' /* ??? Je ne peux pas récupérer d'info sur le user si je n'ai pas son mot de passe...  ??? */);
    }

    public function refreshUser(UserInterface $user)    {
        if (!$user instanceof MySsoUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }
        return $this->fetchUser($user->getUsername(), $user->getPassword());
    }

    private function fetchUser($username, $password = '')    {
        if ($password) {
            return new MySsoUser($username, $password);
        }
         // If call by loadUserByUsername().  then I don't have the password
        if ($username === 'bob') {
            return new MySsoUser('bob', 'bobpass');
        }
        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $username)
        );
    }
}

But maybe the problem is encoder.
What other values are possible?


#9

I continue my research with the FR community of SF…
If that interests you…

https://www.developpez.net/forums/d1916958/php/bibliotheques-frameworks/symfony/ez-mise-place-d-user-provider/#post10621010


#10

I did it! I did it!

https://www.developpez.net/forums/d1916958/php/bibliotheques-frameworks/symfony/ez-mise-place-d-user-provider/#post10623776