- Documentation >
- Users >
- User authentication >
- OAuth client
OAuth client
You can use OAuth2 to securely authenticate users with external Authorization Servers.
Ibexa DXP uses an integration with knpuniversity/oauth2-client-bundle
to provide OAuth2 authentication.
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.
| 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:
| 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:
| ibexa:
system:
admin:
oauth2:
enabled: true
clients: ['google']
|
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's good practice to create a special user content type for users registered in this way.
The users who register through an external service don't 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, 109), 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
109
110
111 | <?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\UserService;
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:';
private OAuth2UserService $oauthUserService;
private LanguageResolver $languageResolver;
private UserService $userService;
/** @var string|null */
private ?string $contentTypeIdentifier;
/** @var string|null */
private ?string $parentGroupRemoteId;
public function __construct(
Repository $repository,
OAuth2UserService $oauthUserService,
LanguageResolver $languageResolver,
UserService $userService,
?string $contentTypeIdentifier = null,
?string $parentGroupRemoteId = null
) {
parent::__construct($repository);
$this->oauthUserService = $oauthUserService;
$this->languageResolver = $languageResolver;
$this->userService = $userService;
$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->oauthUserService->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:
| services:
#…
App\OAuth\GoogleResourceOwnerMapper:
tags:
- { name: ibexa.oauth2_client.resource_owner_mapper, identifier: google }
|
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
:
| <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:
| 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 }
|