mirror of
https://github.com/espocrm/espocrm.git
synced 2026-03-03 02:57:01 +00:00
oidc pkce support
This commit is contained in:
@@ -242,6 +242,11 @@ class Binding implements BindingProcessor
|
|||||||
'Espo\\Core\\Utils\\Config\\ApplicationConfig',
|
'Espo\\Core\\Utils\\Config\\ApplicationConfig',
|
||||||
'applicationConfig'
|
'applicationConfig'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$binder->bindService(
|
||||||
|
'Espo\\Core\\Session\\Session',
|
||||||
|
'session'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function bindCore(Binder $binder): void
|
private function bindCore(Binder $binder): void
|
||||||
|
|||||||
@@ -222,6 +222,11 @@ class ConfigDataProvider
|
|||||||
return $this->object->get('oidcAuthorizationPrompt') ?? 'consent';
|
return $this->object->get('oidcAuthorizationPrompt') ?? 'consent';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function useAuthorizationPkce(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->object->get('oidcAuthorizationPkce');
|
||||||
|
}
|
||||||
|
|
||||||
public function getAuthorizationMaxAge(): ?int
|
public function getAuthorizationMaxAge(): ?int
|
||||||
{
|
{
|
||||||
return $this->config->get('oidcAuthorizationMaxAge');
|
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\Oidc\UserProvider\UserInfo;
|
||||||
use Espo\Core\Authentication\Result;
|
use Espo\Core\Authentication\Result;
|
||||||
use Espo\Core\Authentication\Result\FailReason;
|
use Espo\Core\Authentication\Result\FailReason;
|
||||||
|
use Espo\Core\Session\Session;
|
||||||
use Espo\Core\Utils\Json;
|
use Espo\Core\Utils\Json;
|
||||||
use Espo\Core\Utils\Log;
|
use Espo\Core\Utils\Log;
|
||||||
use JsonException;
|
use JsonException;
|
||||||
@@ -57,6 +58,8 @@ class Login implements LoginInterface
|
|||||||
private const REQUEST_TIMEOUT = 10;
|
private const REQUEST_TIMEOUT = 10;
|
||||||
private const NONCE_HEADER = 'X-Oidc-Authorization-Nonce';
|
private const NONCE_HEADER = 'X-Oidc-Authorization-Nonce';
|
||||||
|
|
||||||
|
public const string SESSION_KEY_CODE_VERIFIER = 'oidcCodeVerifier';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Espo $espoLogin,
|
private Espo $espoLogin,
|
||||||
private Log $log,
|
private Log $log,
|
||||||
@@ -66,6 +69,7 @@ class Login implements LoginInterface
|
|||||||
private UserProvider $userProvider,
|
private UserProvider $userProvider,
|
||||||
private ApplicationState $applicationState,
|
private ApplicationState $applicationState,
|
||||||
private UserInfoDataProvider $userInfoDataProvider,
|
private UserInfoDataProvider $userInfoDataProvider,
|
||||||
|
private Session $session,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function login(Data $data, Request $request): Result
|
public function login(Data $data, Request $request): Result
|
||||||
@@ -214,6 +218,7 @@ class Login implements LoginInterface
|
|||||||
string $redirectUri,
|
string $redirectUri,
|
||||||
string $clientSecret
|
string $clientSecret
|
||||||
): array {
|
): array {
|
||||||
|
|
||||||
$params = [
|
$params = [
|
||||||
'grant_type' => 'authorization_code',
|
'grant_type' => 'authorization_code',
|
||||||
'client_id' => $clientId,
|
'client_id' => $clientId,
|
||||||
@@ -222,6 +227,12 @@ class Login implements LoginInterface
|
|||||||
'redirect_uri' => $redirectUri,
|
'redirect_uri' => $redirectUri,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($this->configDataProvider->useAuthorizationPkce()) {
|
||||||
|
$codeVerifier = $this->session->get(self::SESSION_KEY_CODE_VERIFIER);
|
||||||
|
|
||||||
|
$params['code_verifier'] = $codeVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
$curl = curl_init();
|
$curl = curl_init();
|
||||||
|
|
||||||
curl_setopt_array($curl, [
|
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,
|
'oidcFallback' => true,
|
||||||
'oidcScopes' => ['profile', 'email', 'phone'],
|
'oidcScopes' => ['profile', 'email', 'phone'],
|
||||||
'oidcAuthorizationPrompt' => 'consent',
|
'oidcAuthorizationPrompt' => 'consent',
|
||||||
|
'oidcAuthorizationPkce' => true,
|
||||||
'listViewSettingsDisabled' => false,
|
'listViewSettingsDisabled' => false,
|
||||||
'cleanupDeletedRecords' => true,
|
'cleanupDeletedRecords' => true,
|
||||||
'phoneNumberNumericSearch' => true,
|
'phoneNumberNumericSearch' => true,
|
||||||
|
|||||||
@@ -166,6 +166,7 @@
|
|||||||
"oidcAllowAdminUser": "OIDC Allow OIDC login for admin users",
|
"oidcAllowAdminUser": "OIDC Allow OIDC login for admin users",
|
||||||
"oidcLogoutUrl": "OIDC Logout URL",
|
"oidcLogoutUrl": "OIDC Logout URL",
|
||||||
"oidcAuthorizationPrompt": "OIDC Authorization Prompt",
|
"oidcAuthorizationPrompt": "OIDC Authorization Prompt",
|
||||||
|
"oidcAuthorizationPkce": "OIDC Use PKCE",
|
||||||
"pdfEngine": "PDF Engine",
|
"pdfEngine": "PDF Engine",
|
||||||
"quickSearchFullTextAppendWildcard": "Append wildcard in quick search",
|
"quickSearchFullTextAppendWildcard": "Append wildcard in quick search",
|
||||||
"authIpAddressCheck": "Restrict access by IP address",
|
"authIpAddressCheck": "Restrict access by IP address",
|
||||||
|
|||||||
@@ -130,6 +130,9 @@
|
|||||||
"oidcLogoutUrl": {
|
"oidcLogoutUrl": {
|
||||||
"level": "admin"
|
"level": "admin"
|
||||||
},
|
},
|
||||||
|
"oidcAuthorizationPkce": {
|
||||||
|
"level": "admin"
|
||||||
|
},
|
||||||
"apiCorsAllowedMethodList": {
|
"apiCorsAllowedMethodList": {
|
||||||
"level": "admin"
|
"level": "admin"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,5 +91,8 @@
|
|||||||
},
|
},
|
||||||
"applicationConfig": {
|
"applicationConfig": {
|
||||||
"className": "Espo\\Core\\Utils\\Config\\ApplicationConfig"
|
"className": "Espo\\Core\\Utils\\Config\\ApplicationConfig"
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"className": "Espo\\Core\\Session\\DefaultSession"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,9 @@
|
|||||||
{
|
{
|
||||||
"name": "oidcAuthorizationPrompt"
|
"name": "oidcAuthorizationPrompt"
|
||||||
},
|
},
|
||||||
false
|
{
|
||||||
|
"name": "oidcAuthorizationPkce"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,6 +84,10 @@
|
|||||||
"select_account"
|
"select_account"
|
||||||
],
|
],
|
||||||
"maxLength": 14
|
"maxLength": 14
|
||||||
|
},
|
||||||
|
"oidcAuthorizationPkce": {
|
||||||
|
"type": "bool",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -912,6 +912,10 @@
|
|||||||
"select_account"
|
"select_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"oidcAuthorizationPkce": {
|
||||||
|
"type": "bool",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
"pdfEngine": {
|
"pdfEngine": {
|
||||||
"type": "enum",
|
"type": "enum",
|
||||||
"view": "views/settings/fields/pdf-engine"
|
"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\ConfigDataProvider;
|
||||||
use Espo\Core\Authentication\Oidc\Login as OidcLogin;
|
use Espo\Core\Authentication\Oidc\Login as OidcLogin;
|
||||||
use Espo\Core\Authentication\Oidc\BackchannelLogout;
|
use Espo\Core\Authentication\Oidc\BackchannelLogout;
|
||||||
|
use Espo\Core\Authentication\Oidc\PkceUtil;
|
||||||
use Espo\Core\Authentication\Util\MethodProvider;
|
use Espo\Core\Authentication\Util\MethodProvider;
|
||||||
use Espo\Core\Exceptions\Error;
|
use Espo\Core\Exceptions\Error;
|
||||||
use Espo\Core\Exceptions\Forbidden;
|
use Espo\Core\Exceptions\Forbidden;
|
||||||
|
use Espo\Core\Session\Session;
|
||||||
use Espo\Core\Utils\Json;
|
use Espo\Core\Utils\Json;
|
||||||
|
|
||||||
class Service
|
class Service
|
||||||
@@ -43,7 +45,8 @@ class Service
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private BackchannelLogout $backchannelLogout,
|
private BackchannelLogout $backchannelLogout,
|
||||||
private MethodProvider $methodProvider,
|
private MethodProvider $methodProvider,
|
||||||
private ConfigDataProvider $configDataProvider
|
private ConfigDataProvider $configDataProvider,
|
||||||
|
private Session $session,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +58,8 @@ class Service
|
|||||||
* claims: ?string,
|
* claims: ?string,
|
||||||
* prompt: 'none'|'login'|'consent'|'select_account',
|
* prompt: 'none'|'login'|'consent'|'select_account',
|
||||||
* maxAge: ?int,
|
* maxAge: ?int,
|
||||||
|
* codeChallenge: ?string,
|
||||||
|
* codeChallengeMethod: ?string,
|
||||||
* }
|
* }
|
||||||
* @throws Forbidden
|
* @throws Forbidden
|
||||||
* @throws Error
|
* @throws Error
|
||||||
@@ -70,6 +75,7 @@ class Service
|
|||||||
$scopes = $this->configDataProvider->getScopes();
|
$scopes = $this->configDataProvider->getScopes();
|
||||||
$groupClaim = $this->configDataProvider->getGroupClaim();
|
$groupClaim = $this->configDataProvider->getGroupClaim();
|
||||||
$redirectUri = $this->configDataProvider->getRedirectUri();
|
$redirectUri = $this->configDataProvider->getRedirectUri();
|
||||||
|
$codeChallenge = $this->configDataProvider->useAuthorizationPkce() ? $this->prepareCodeChallenge() : null;
|
||||||
|
|
||||||
if (!$clientId) {
|
if (!$clientId) {
|
||||||
throw new Error("No client ID.");
|
throw new Error("No client ID.");
|
||||||
@@ -105,6 +111,8 @@ class Service
|
|||||||
'claims' => $claims,
|
'claims' => $claims,
|
||||||
'prompt' => $prompt,
|
'prompt' => $prompt,
|
||||||
'maxAge' => $maxAge,
|
'maxAge' => $maxAge,
|
||||||
|
'codeChallenge' => $codeChallenge,
|
||||||
|
'codeChallengeMethod' => $codeChallenge ? 'S256' : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,4 +131,13 @@ class Service
|
|||||||
throw new Forbidden("OIDC logout: Invalid JWT. " . $e->getMessage());
|
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,
|
* clientId: string,
|
||||||
* redirectUri: string,
|
* redirectUri: string,
|
||||||
* scopes: string[],
|
* scopes: string[],
|
||||||
* claims: ?string,
|
* claims: string|null,
|
||||||
* prompt: 'login'|'consent'|'select_account',
|
* prompt: 'login'|'consent'|'select_account',
|
||||||
* maxAge: ?Number,
|
* maxAge: Number|null,
|
||||||
|
* codeChallenge: string|null,
|
||||||
|
* codeChallengeMethod: string|null
|
||||||
* }} data
|
* }} data
|
||||||
* @param {WindowProxy} proxy
|
* @param {WindowProxy} proxy
|
||||||
* @return {Promise<{code: string, nonce: string}>}
|
* @return {Promise<{code: string, nonce: string}>}
|
||||||
@@ -103,6 +105,11 @@ class OidcLoginHandler extends LoginHandler {
|
|||||||
prompt: data.prompt,
|
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) {
|
if (data.maxAge || data.maxAge === 0) {
|
||||||
params.max_age = data.maxAge;
|
params.max_age = data.maxAge;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user