mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-02 22:47:01 +00:00
oidc pkce support
This commit is contained in:
@@ -242,6 +242,11 @@ class Binding implements BindingProcessor
|
||||
'Espo\\Core\\Utils\\Config\\ApplicationConfig',
|
||||
'applicationConfig'
|
||||
);
|
||||
|
||||
$binder->bindService(
|
||||
'Espo\\Core\\Session\\Session',
|
||||
'session'
|
||||
);
|
||||
}
|
||||
|
||||
private function bindCore(Binder $binder): void
|
||||
|
||||
@@ -222,6 +222,11 @@ class ConfigDataProvider
|
||||
return $this->object->get('oidcAuthorizationPrompt') ?? 'consent';
|
||||
}
|
||||
|
||||
public function useAuthorizationPkce(): bool
|
||||
{
|
||||
return (bool) $this->object->get('oidcAuthorizationPkce');
|
||||
}
|
||||
|
||||
public function getAuthorizationMaxAge(): ?int
|
||||
{
|
||||
return $this->config->get('oidcAuthorizationMaxAge');
|
||||
|
||||
@@ -41,6 +41,7 @@ use Espo\Core\Authentication\Jwt\Validator;
|
||||
use Espo\Core\Authentication\Oidc\UserProvider\UserInfo;
|
||||
use Espo\Core\Authentication\Result;
|
||||
use Espo\Core\Authentication\Result\FailReason;
|
||||
use Espo\Core\Session\Session;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Utils\Log;
|
||||
use JsonException;
|
||||
@@ -57,6 +58,8 @@ class Login implements LoginInterface
|
||||
private const REQUEST_TIMEOUT = 10;
|
||||
private const NONCE_HEADER = 'X-Oidc-Authorization-Nonce';
|
||||
|
||||
public const string SESSION_KEY_CODE_VERIFIER = 'oidcCodeVerifier';
|
||||
|
||||
public function __construct(
|
||||
private Espo $espoLogin,
|
||||
private Log $log,
|
||||
@@ -66,6 +69,7 @@ class Login implements LoginInterface
|
||||
private UserProvider $userProvider,
|
||||
private ApplicationState $applicationState,
|
||||
private UserInfoDataProvider $userInfoDataProvider,
|
||||
private Session $session,
|
||||
) {}
|
||||
|
||||
public function login(Data $data, Request $request): Result
|
||||
@@ -214,6 +218,7 @@ class Login implements LoginInterface
|
||||
string $redirectUri,
|
||||
string $clientSecret
|
||||
): array {
|
||||
|
||||
$params = [
|
||||
'grant_type' => 'authorization_code',
|
||||
'client_id' => $clientId,
|
||||
@@ -222,6 +227,12 @@ class Login implements LoginInterface
|
||||
'redirect_uri' => $redirectUri,
|
||||
];
|
||||
|
||||
if ($this->configDataProvider->useAuthorizationPkce()) {
|
||||
$codeVerifier = $this->session->get(self::SESSION_KEY_CODE_VERIFIER);
|
||||
|
||||
$params['code_verifier'] = $codeVerifier;
|
||||
}
|
||||
|
||||
$curl = curl_init();
|
||||
|
||||
curl_setopt_array($curl, [
|
||||
|
||||
54
application/Espo/Core/Authentication/Oidc/PkceUtil.php
Normal file
54
application/Espo/Core/Authentication/Oidc/PkceUtil.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Authentication\Oidc;
|
||||
|
||||
class PkceUtil
|
||||
{
|
||||
private const string CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
||||
private const int CODE_LENGTH = 64;
|
||||
|
||||
public static function generateCodeVerifier(): string
|
||||
{
|
||||
$output = '';
|
||||
|
||||
for ($i = 0; $i < self::CODE_LENGTH; $i++) {
|
||||
$output .= self::CHARACTERS[random_int(0, strlen(self::CHARACTERS) - 1)];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public static function hashAndEncodeCodeVerifier(string $codeVerifier): string
|
||||
{
|
||||
$code = hash('sha256', $codeVerifier, true);
|
||||
|
||||
return rtrim(strtr(base64_encode($code), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
84
application/Espo/Core/Session/DefaultSession.php
Normal file
84
application/Espo/Core/Session/DefaultSession.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Session;
|
||||
|
||||
use const PHP_SESSION_NONE;
|
||||
|
||||
/**
|
||||
* Do not use directly. Require the Session interface instead.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class DefaultSession implements Session
|
||||
{
|
||||
public function __construct(
|
||||
?string $cacheLimiter = null,
|
||||
?int $cacheExpire = null,
|
||||
) {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
if ($cacheLimiter !== null) {
|
||||
session_cache_limiter($cacheLimiter);
|
||||
}
|
||||
|
||||
if ($cacheExpire !== null) {
|
||||
session_cache_expire($cacheExpire);
|
||||
}
|
||||
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $key): mixed
|
||||
{
|
||||
return $_SESSION[$key] ?? null;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value): Session
|
||||
{
|
||||
$_SESSION[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function clear(string $key): void
|
||||
{
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
public function clearAll(): void
|
||||
{
|
||||
session_unset();
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $_SESSION);
|
||||
}
|
||||
}
|
||||
48
application/Espo/Core/Session/Session.php
Normal file
48
application/Espo/Core/Session/Session.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Session;
|
||||
|
||||
/**
|
||||
* A session wrapper.
|
||||
*
|
||||
* @since 9.4.0
|
||||
*/
|
||||
interface Session
|
||||
{
|
||||
public function get(string $key): mixed;
|
||||
|
||||
public function set(string $key, mixed $value): self;
|
||||
|
||||
public function clear(string $key): void;
|
||||
|
||||
public function clearAll(): void;
|
||||
|
||||
public function has(string $key): bool;
|
||||
}
|
||||
@@ -298,6 +298,7 @@ return [
|
||||
'oidcFallback' => true,
|
||||
'oidcScopes' => ['profile', 'email', 'phone'],
|
||||
'oidcAuthorizationPrompt' => 'consent',
|
||||
'oidcAuthorizationPkce' => true,
|
||||
'listViewSettingsDisabled' => false,
|
||||
'cleanupDeletedRecords' => true,
|
||||
'phoneNumberNumericSearch' => true,
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"oidcAllowAdminUser": "OIDC Allow OIDC login for admin users",
|
||||
"oidcLogoutUrl": "OIDC Logout URL",
|
||||
"oidcAuthorizationPrompt": "OIDC Authorization Prompt",
|
||||
"oidcAuthorizationPkce": "OIDC Use PKCE",
|
||||
"pdfEngine": "PDF Engine",
|
||||
"quickSearchFullTextAppendWildcard": "Append wildcard in quick search",
|
||||
"authIpAddressCheck": "Restrict access by IP address",
|
||||
|
||||
@@ -130,6 +130,9 @@
|
||||
"oidcLogoutUrl": {
|
||||
"level": "admin"
|
||||
},
|
||||
"oidcAuthorizationPkce": {
|
||||
"level": "admin"
|
||||
},
|
||||
"apiCorsAllowedMethodList": {
|
||||
"level": "admin"
|
||||
},
|
||||
|
||||
@@ -91,5 +91,8 @@
|
||||
},
|
||||
"applicationConfig": {
|
||||
"className": "Espo\\Core\\Utils\\Config\\ApplicationConfig"
|
||||
},
|
||||
"session": {
|
||||
"className": "Espo\\Core\\Session\\DefaultSession"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,9 @@
|
||||
{
|
||||
"name": "oidcAuthorizationPrompt"
|
||||
},
|
||||
false
|
||||
{
|
||||
"name": "oidcAuthorizationPkce"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
|
||||
@@ -84,6 +84,10 @@
|
||||
"select_account"
|
||||
],
|
||||
"maxLength": 14
|
||||
},
|
||||
"oidcAuthorizationPkce": {
|
||||
"type": "bool",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,6 +912,10 @@
|
||||
"select_account"
|
||||
]
|
||||
},
|
||||
"oidcAuthorizationPkce": {
|
||||
"type": "bool",
|
||||
"default": true
|
||||
},
|
||||
"pdfEngine": {
|
||||
"type": "enum",
|
||||
"view": "views/settings/fields/pdf-engine"
|
||||
|
||||
@@ -33,9 +33,11 @@ use Espo\Core\Authentication\Jwt\Exceptions\Invalid;
|
||||
use Espo\Core\Authentication\Oidc\ConfigDataProvider;
|
||||
use Espo\Core\Authentication\Oidc\Login as OidcLogin;
|
||||
use Espo\Core\Authentication\Oidc\BackchannelLogout;
|
||||
use Espo\Core\Authentication\Oidc\PkceUtil;
|
||||
use Espo\Core\Authentication\Util\MethodProvider;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Session\Session;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
class Service
|
||||
@@ -43,7 +45,8 @@ class Service
|
||||
public function __construct(
|
||||
private BackchannelLogout $backchannelLogout,
|
||||
private MethodProvider $methodProvider,
|
||||
private ConfigDataProvider $configDataProvider
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private Session $session,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -55,6 +58,8 @@ class Service
|
||||
* claims: ?string,
|
||||
* prompt: 'none'|'login'|'consent'|'select_account',
|
||||
* maxAge: ?int,
|
||||
* codeChallenge: ?string,
|
||||
* codeChallengeMethod: ?string,
|
||||
* }
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
@@ -70,6 +75,7 @@ class Service
|
||||
$scopes = $this->configDataProvider->getScopes();
|
||||
$groupClaim = $this->configDataProvider->getGroupClaim();
|
||||
$redirectUri = $this->configDataProvider->getRedirectUri();
|
||||
$codeChallenge = $this->configDataProvider->useAuthorizationPkce() ? $this->prepareCodeChallenge() : null;
|
||||
|
||||
if (!$clientId) {
|
||||
throw new Error("No client ID.");
|
||||
@@ -105,6 +111,8 @@ class Service
|
||||
'claims' => $claims,
|
||||
'prompt' => $prompt,
|
||||
'maxAge' => $maxAge,
|
||||
'codeChallenge' => $codeChallenge,
|
||||
'codeChallengeMethod' => $codeChallenge ? 'S256' : null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -123,4 +131,13 @@ class Service
|
||||
throw new Forbidden("OIDC logout: Invalid JWT. " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareCodeChallenge(): string
|
||||
{
|
||||
$codeVerifier = PkceUtil::generateCodeVerifier();
|
||||
|
||||
$this->session->set(OidcLogin::SESSION_KEY_CODE_VERIFIER, $codeVerifier);
|
||||
|
||||
return PkceUtil::hashAndEncodeCodeVerifier($codeVerifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,11 @@ class OidcLoginHandler extends LoginHandler {
|
||||
* clientId: string,
|
||||
* redirectUri: string,
|
||||
* scopes: string[],
|
||||
* claims: ?string,
|
||||
* claims: string|null,
|
||||
* prompt: 'login'|'consent'|'select_account',
|
||||
* maxAge: ?Number,
|
||||
* maxAge: Number|null,
|
||||
* codeChallenge: string|null,
|
||||
* codeChallengeMethod: string|null
|
||||
* }} data
|
||||
* @param {WindowProxy} proxy
|
||||
* @return {Promise<{code: string, nonce: string}>}
|
||||
@@ -103,6 +105,11 @@ class OidcLoginHandler extends LoginHandler {
|
||||
prompt: data.prompt,
|
||||
};
|
||||
|
||||
if (data.codeChallenge && data.codeChallengeMethod) {
|
||||
params.code_challenge = data.codeChallenge;
|
||||
params.code_challenge_method = data.codeChallengeMethod;
|
||||
}
|
||||
|
||||
if (data.maxAge || data.maxAge === 0) {
|
||||
params.max_age = data.maxAge;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user