Skip to content

OAuth client

You can use OAuth2 to securely authenticate users with external Authorization Servers.

OAuth2 Client

Ibexa DXP uses an integration with knpuniversity/oauth2-client-bundle to provide OAuth2 authentication.

Configure OAuth2 client

Configure connection to Authorization Server

Details of the configuration depend on the OAuth2 Authorization Server that you want to use. For sample configurations for different providers, see knpuniversity/oauth2-client-bundle configuration. Some client types require additional packages. Missing package is indicated in an error message.

For example, the following configuration creates a google client for Google OAuth2 Authorization Server to log users in. Two environment variables, OAUTH_GOOGLE_CLIENT_ID and OAUTH_GOOGLE_CLIENT_SECRET, correspond to the set-up on Google side.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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: ibexa.oauth2.check
            redirect_params:
                identifier: google

To use the google client type, you need to install the following package:

1
composer require league/oauth2-google

Enable OAuth2 client

The client needs to be a part of the SiteAccess scope.

In the following example, the OAuth2 client google is enabled for the admin SiteAccess:

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

Configure firewall

In config/packages/security.yaml, enable the oauth2_connect firewall and replace the ibexa_front firewall with the ibexa_oauth2_front one.

 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
security:
    #…

    firewalls:
        #…

        # Uncomment ibexa_oauth2_connect, ibexa_oauth2_front rules and comment ibexa_front firewall
        # to enable OAuth2 authentication

        ibexa_oauth2_connect:
            pattern: /oauth2/connect/*
            security: false

        ibexa_oauth2_front:
            pattern: ^/
            user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker
            anonymous: ~
            ibexa_rest_session: ~
            guard:
                authenticators:
                    - Ibexa\Bundle\OAuth2Client\Security\Authenticator\OAuth2Authenticator
                    - Ibexa\PageBuilder\Security\EditorialMode\TokenAuthenticator
                entry_point: Ibexa\Bundle\OAuth2Client\Security\Authenticator\OAuth2Authenticator
            form_login:
                require_previous_session: false
                csrf_token_generator: security.csrf.token_manager
            logout: ~

        #ibexa_front:
        #    pattern: ^/
        #    user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker
        #    anonymous: ~
        #    ibexa_rest_session: ~
        #    form_login:
        #        require_previous_session: false
        #        csrf_token_generator: security.csrf.token_manager
        #    guard:
        #        authenticator: 'Ibexa\PageBuilder\Security\EditorialMode\TokenAuthenticator'
        #    logout: ~

The guard.authenticators setting specifies the Guard authenticators to be used.

By adding the Ibexa\Bundle\OAuth2Client\Security\Authenticator\OAuth2Authenticator guard authenticator you add a possibility to use OAuth2 on those routes.

Resource owner mappers

Resource owner mappers map the data received from the OAuth2 authorization server to user information in the Repository.

Resource owner mappers must implement the Ibexa\Contracts\OAuth2Client\ResourceOwner\ResourceOwnerMapper interface. Four implementations of ResourceOwnerMapper are proposed by default:

  • ResourceOwnerToExistingUserMapper is the base class extended by the following mappers:
    • ResourceOwnerIdToUserMapper - loads a user (resource owner) based on the identifier, but doesn't create a new user.
    • ResourceOwnerEmailToUserMapper - loads a user (resource owner) based on the email, but doesn't create a new user.
  • ResourceOwnerToExistingOrNewUserMapper - checks whether the user exists and loads the data if it does. If not, creates a new user in the Repository.

To use ResourceOwnerToExistingOrNewUserMapper, you need to extend it in your custom mapper.

OAuth User content type

When you implement your own mapper for external login, it is good practice to create a special User content type for users registered in this way. The users who register through an external service do not have a separate password in the system. Instead, they log in by their external service's password.

To avoid issues with password restrictions in the built-in User content type, create a special content type (for example, "OAuth User"), without restrictions on the password.

This new content type must also contain the User (ezuser) Field.

The following example shows how to create a Resource Owner mapper for the google client from previous examples.

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 doesn't exist yet.

The mapper loads a user (line 51) or creates a new one (line 61), based on the information from resourceOwner, that's the OAuth2 authorization server.

The new username is set with a google: prefix (lines 19, 106), 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 Ibexa\Contracts\Core\Repository\LanguageResolver;
use Ibexa\Contracts\Core\Repository\Repository;
use Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType;
use Ibexa\Contracts\OAuth2Client\Repository\OAuth2UserService;
use Ibexa\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\Contracts\OAuth2Client\Repository\OAuth2UserService */
    private \Ibexa\Contracts\OAuth2Client\Repository\OAuth2UserService $userService;

    /** @var \Ibexa\Contracts\Core\Repository\LanguageResolver */
    private \Ibexa\Contracts\Core\Repository\LanguageResolver $languageResolver;

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

    /** @var string|null */
    private ?string $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 to associate it with the google client:

1
2
3
4
5
6
services:
    #…

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

Add login button

After you have activated the OAuth2 client for the admin SiteAccess, you need to add a Log in with Google to the 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>

For more information about the OAuth connection URL Twig functions, see ibexa_oauth2_connect_path and ibexa_oauth2_connect_url.

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

1
2
3
4
5
6
7
8
9
services:
    #…

    app.components.oauth2_login:
        parent: Ibexa\AdminUi\Component\TwigComponent
        arguments:
            $template: '@@ibexadesign/account/login/oauth2_login.html.twig'
        tags:
            - { name: ibexa.admin_ui.component, group: login-form-after }

Log in to the Back Office with Google