Lexikjwtauthenticationbundle: Multiple identities

Created on 27 Jun 2016  路  10Comments  路  Source: lexik/LexikJWTAuthenticationBundle

Hi,

Is a way to set array in multiple user_identity_field? For example:

lexik_jwt_authentication:
    user_identity_field: [email, facebookId]

Reason: in my app users can register itself or log in by facebook. In register case they fill password and email, and all ok. In log in by facebook case, not always email exists, but we got unique facebookId of user. So, i want to check which field exists in token - email or facebookId.
Currently my Facebook users stuck at this function.

Or maybe exists another way to solve this?

Most helpful comment

@NewOldMax Yes of course, you can override any service, even third-party ones. Here is how you can do:

  • First, create your custom JWTProvider class, it could looks like:
namespace AppBundle\Security;

use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Provider\JWTProvider;

class CustomJWTProvider extends JWTProvider
{
    protected function getUserFromPayload(array $payload)
    {
        if (!isset($payload[$this->userIdentityField])) {
            throw new AuthenticationException('Invalid JWT Token', 401);
        }
        return $this->userProvider->loadUserByIdentity($payload[$this->userIdentityField]);
    }  
}
  • Then, create a CompilerPass that will override the default service, it should looks like:
namespace AppBundle\DependencyInjection;

use AppBundle\Security\CustomJWTProvider;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class JWTCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container
            ->getDefinition('lexik_jwt_authentication.security.authentication.provider')
            ->setClass(CustomJWTProvider::class);
    }
}
  • Last, register the compiler pass in your bundle class:
namespace AppBundle;

use AppBundle\DependencyInjection\JWTCompilerPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AppBundle extends Bundle
{   
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new JWTCompilerPass());
    }
}

That's it! The CustomJWTProvider class will be used instead of the parent one.
This kind of custom stuff will become widely easier to implement in our 2.0 version (that is currently in development).

All 10 comments

Hello @NewOldMax,

ATM we don't support several identity fields. BTW I think it would be good to have this feature, some providers are able to use 2 (at least) different properties (i.e. FOSUserBundle username_email provider).

In your case, you can get around the need of having 2 userIdentityField by looking into the successful facebook OAuth response.
Basically, you'll get a response containing a id key (you already use it as the facebookId property of your User) and a access_token key (not sure you store it).
If the response data don't contain a email key, take the access_token key and perform an HTTP call to the facebook graph api, in order to fetch the email of your user.

The endpoint to call is https://graph.facebook.com/me?fields=email&access_token=[access_token here]
Replace the token part by the one included in the OAuth response, then call it using cURL for instance.

Example of what you need using GuzzleHttp:

private function getEmailFromFbResponse($accessToken)
{
    $client = new \GuzzleHttp\Client(['base_url' => 'https://graph.facebook.com']);
    $request = $client->request('GET', '/me', ['query' => [
        'access_token' => $accessToken,
        'fields' => 'email'
    ]]);

    $response = $client->send($request)->json();

    return $response['email']; // The user email
}

Call $user->setEmail($this->getEmailFromFbResponse($response['access_token'])); just before saving the user and you should be good to authenticate it easily the next time.

Thanks, @chalasr !
You solution will help me in some cases, but not all Facebook users have email (for example, user registered by cell phone. Request to https://graph.facebook.com of this users will return only id field).

Maybe there is a way override getUserFromPayload method (use custom JWT provider, but don't see that ability in docs)?

I made fork and apply little changes for my case. Works perfect, but compatibility is ruined (used non-standard method of user provider).

@NewOldMax Thank's for sharing your try.

It's not very clean but you could simply set the email property of your user with the value of facebook_id, then it should work without touching the user provider.

Again, it's a quick-and-dirty alternative ATM for this edge case.
I'll investigate what's possible (from a user provider side) in order to provide this feature, but seeing your implementation, I'm afraid that's not something possible, I'll keep you informed.

you could simply set the email property of your user with the value of facebook_id

I thinking about it, but I'm not sure whether there will be other third party auth services in my app without email in future

As there is no real solution for that ATM, I close this issue.
I will investigate what is possible to do from a user provider side in order to provide the feature if it is viable, I keep you informed here.

Hi again, @chalasr
I thought a little about it and came to the conclusion that we can do a simple solution: check number of identity fields. For example something like that:

if (count($this->userIdentityField) > 1) {
    /**
     * run custom method with 2 params
     */
    $this->userProvider->loadUserByIdentity($payload[$identity], $identity);
} else {
    /**
     * run default method
     */
    $this->userProvider->loadUserByUsername($payload[$identity]);
}

So in this case backward compatibility will be save. To use new feature developer must implement custom userProvider with loadUserByIdentity method.

@NewOldMax I don't think that is something we can implement here. As we are not responsible of the UserProvider part, we have to be generic and use only methods that are implemented by the Symfony UserProviderInterface.

I think the better alternative for this edge case is to extend the JWTProvider class and override the corresponding service, then modify the inherited getUserFromPayload() method to use your custom UserProvider into.

ATM is there way to extend JWTProvider?

@NewOldMax Yes of course, you can override any service, even third-party ones. Here is how you can do:

  • First, create your custom JWTProvider class, it could looks like:
namespace AppBundle\Security;

use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Provider\JWTProvider;

class CustomJWTProvider extends JWTProvider
{
    protected function getUserFromPayload(array $payload)
    {
        if (!isset($payload[$this->userIdentityField])) {
            throw new AuthenticationException('Invalid JWT Token', 401);
        }
        return $this->userProvider->loadUserByIdentity($payload[$this->userIdentityField]);
    }  
}
  • Then, create a CompilerPass that will override the default service, it should looks like:
namespace AppBundle\DependencyInjection;

use AppBundle\Security\CustomJWTProvider;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class JWTCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container
            ->getDefinition('lexik_jwt_authentication.security.authentication.provider')
            ->setClass(CustomJWTProvider::class);
    }
}
  • Last, register the compiler pass in your bundle class:
namespace AppBundle;

use AppBundle\DependencyInjection\JWTCompilerPass;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AppBundle extends Bundle
{   
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new JWTCompilerPass());
    }
}

That's it! The CustomJWTProvider class will be used instead of the parent one.
This kind of custom stuff will become widely easier to implement in our 2.0 version (that is currently in development).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Walkoss picture Walkoss  路  4Comments

pbalan picture pbalan  路  5Comments

mflasquin picture mflasquin  路  5Comments

AlexDupreWeb picture AlexDupreWeb  路  4Comments

garak picture garak  路  5Comments