Skip to content

Add login through external service

To add an option to log in to the system through an external service, you can use OAuth2 to authorize your users.

The example below shows how to add a Log in with Google option to the Back Office.

Configure OAuth2 client

Configure the OAuth2 client in config/packages/knpu_oauth2_client.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
knpu_oauth2_client:
    clients:
        # Configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
        google:
            type: google
            client_id: '%env(OAUTH_GOOGLE_CLIENT_ID)%'
            client_secret: '%env(OAUTH_GOOGLE_CLIENT_SECRET)%'
            # Redirect route:
            redirect_route: ibexa.oauth2.check
            redirect_params:
                identifier: google

Enable OAuth authentication

Enable OAuth2 authentication through Google for the site SiteAccess:

1
2
3
4
5
6
ezplatform:
    system:
        admin:
            oauth2:
                enabled: true
                clients: ['google']

Configure firewall

Add the Ibexa\Platform\OAuth2Client\Security\Authenticator\OAuth2Authenticator guard authenticator to your firewall configuration in config/packages/security.yaml and ensure that the ibexa.oauth2.connect route is accessible by an anonymous user:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
        oauth2_connect:
            pattern: /oauth2/connect/*
            security: false

        ezpublish_front:
            pattern: ^/
            user_checker: eZ\Publish\Core\MVC\Symfony\Security\UserChecker
            anonymous: ~
            ezpublish_rest_session: ~
            guard:
                authenticators:
                    - 'Ibexa\Platform\Bundle\OAuth2Client\Security\Authenticator\OAuth2Authenticator'
            form_login:
                require_previous_session: false
                csrf_token_generator: security.csrf.token_manager
            logout: ~

Implement a resource owner mapper

Create a resource owner mapper for Google login in src/OAuth/GoogleResourceOwnerMapper.php. The mapper extends ResourceOwnerToExistingOrNewUserMapper, which enables it to create a new user in the Repository if the user does not exist yet.

The mapper loads a user (line 50) or creates a new one (line 60), based on the information from resourceOwner, that is the OAuth provider.

The new user name is set with a google: prefix (lines 18, 105), to avoid conflicts with users registered in a regular way.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
<?php

declare(strict_types=1);

namespace App\OAuth;

use eZ\Publish\API\Repository\LanguageResolver;
use eZ\Publish\API\Repository\Repository;
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
use Ibexa\Platform\OAuth2Client\Repository\OAuth2UserService;
use Ibexa\Platform\OAuth2Client\ResourceOwner\ResourceOwnerToExistingOrNewUserMapper;
use League\OAuth2\Client\Provider\GoogleUser;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

final class GoogleResourceOwnerMapper extends ResourceOwnerToExistingOrNewUserMapper
{
    private const PROVIDER_PREFIX = 'google:';

    /** @var \Ibexa\Platform\OAuth2Client\Repository\OAuth2UserService */
    private $userService;

    /** @var \eZ\Publish\API\Repository\LanguageResolver */
    private $languageResolver;

    /** @var string|null */
    private $contentTypeIdentifier;

    /** @var string|null */
    private $parentGroupRemoteId;

    public function __construct(
        Repository $repository,
        OAuth2UserService $userService,
        LanguageResolver $languageResolver,
        ?string $contentTypeIdentifier = null,
        ?string $parentGroupRemoteId = null
    ) {
        parent::__construct($repository);

        $this->userService = $userService;
        $this->languageResolver = $languageResolver;
        $this->contentTypeIdentifier = $contentTypeIdentifier;
        $this->parentGroupRemoteId = $parentGroupRemoteId;
    }

    /**
     * @param \League\OAuth2\Client\Provider\GoogleUser $resourceOwner
     */
    protected function loadUser(
        ResourceOwnerInterface $resourceOwner,
        UserProviderInterface $userProvider
    ): ?UserInterface {
        return $userProvider->loadUserByUsername($this->getUsername($resourceOwner));
    }

    /**
     * @param \League\OAuth2\Client\Provider\GoogleUser $resourceOwner
     */
    protected function createUser(
        ResourceOwnerInterface $resourceOwner,
        UserProviderInterface $userProvider
    ): ?UserInterface {
        $userCreateStruct = $this->userService->newOAuth2UserCreateStruct(
            $this->getUsername($resourceOwner),
            $resourceOwner->getEmail(),
            $this->getMainLanguageCode(),
            $this->getOAuth2UserContentType($this->repository)
        );

        $userCreateStruct->setField('first_name', $resourceOwner->getFirstName());
        $userCreateStruct->setField('last_name', $resourceOwner->getLastName());

        $parentGroups = [];
        if ($this->parentGroupRemoteId !== null) {
            $parentGroups[] = $this->userService->loadUserGroupByRemoteId($this->parentGroupRemoteId);
        }

        $this->userService->createUser($userCreateStruct, $parentGroups);

        return $userProvider->loadUserByUsername($this->getUsername($resourceOwner));
    }

    private function getOAuth2UserContentType(Repository $repository): ?ContentType
    {
        if ($this->contentTypeIdentifier !== null) {
            $contentTypeService = $repository->getContentTypeService();

            return $contentTypeService->loadContentTypeByIdentifier(
                $this->contentTypeIdentifier
            );
        }

        return null;
    }

    private function getMainLanguageCode(): string
    {
        // Get first prioritized language for current scope
        return $this->languageResolver->getPrioritizedLanguages()[0];
    }

    private function getUsername(GoogleUser $resourceOwner): string
    {
        return self::PROVIDER_PREFIX . $resourceOwner->getId();
    }
}

Configure the service by using the ibexa.oauth2_client.resource_owner_mapper tag:

1
2
3
    App\OAuth\GoogleResourceOwnerMapper:
        tags:
            - { name: ibexa.oauth2_client.resource_owner_mapper, identifier: google }

Add template

To add a Log in with Google button to your Back Office login form, create the following template file in templates/themes/admin/account/login/oauth2_login.html.twig:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<div class="row mt-4">
    <div class="col">
        <p class="text-center">or</p>
        <div class="btn-group d-flex">
            <a href="{{ ibexa_oauth2_connect_path('google') }}" class="btn btn-primary">
                Log in via Google
            </a>
        </div>
    </div>
</div>

Finally, add the template to the login form by using the login-form-after component:

1
2
3
4
5
6
    app.components.oauth2_login:
        parent: EzSystems\EzPlatformAdminUi\Component\TwigComponent
        arguments:
            $template: '@@ezdesign/account/login/oauth2_login.html.twig'
        tags:
            - { name: ezplatform.admin_ui.component, group: login-form-after }

Log in to the Back Office with Google