mirror of https://github.com/MISP/MISP
new: [oidc] Check user validity
parent
384d517a11
commit
e1774abe80
|
@ -46,6 +46,14 @@ class UserShell extends AppShell
|
|||
],
|
||||
],
|
||||
]);
|
||||
$parser->addSubcommand('check_validity', [
|
||||
'help' => __('Check users validity from external identity provider and block not valid user.'),
|
||||
'parser' => [
|
||||
'options' => [
|
||||
'block_invalid' => ['help' => __('Block user that are considered invalid.'), 'boolean' => true],
|
||||
],
|
||||
]
|
||||
]);
|
||||
$parser->addSubcommand('change_pw', [
|
||||
'help' => __('Change user password.'),
|
||||
'parser' => [
|
||||
|
@ -237,6 +245,22 @@ class UserShell extends AppShell
|
|||
$this->out("User $userId unblocked.");
|
||||
}
|
||||
|
||||
public function check_validity()
|
||||
{
|
||||
$users = $this->User->find('all', [
|
||||
'recursive' => -1,
|
||||
'contain' => ['UserSetting'],
|
||||
'conditions' => ['disabled' => false], // fetch just not disabled users
|
||||
]);
|
||||
$blockInvalid = $this->params['block_invalid'];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user['User']['UserSetting'] = $user['UserSetting'];
|
||||
$result = $this->User->checkIfUserIsValid($user['User'], $blockInvalid, true);
|
||||
$this->out("{$user['User']['email']}: " . ($result ? 'valid' : 'INVALID'));
|
||||
}
|
||||
}
|
||||
|
||||
public function change_pw()
|
||||
{
|
||||
list($userId, $newPassword) = $this->args;
|
||||
|
|
|
@ -541,7 +541,7 @@ class AppController extends Controller
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($user['disabled']) {
|
||||
if ($user['disabled'] || (isset($user['logged_by_authkey']) && $user['logged_by_authkey']) && !$this->User->checkIfUserIsValid($user)) {
|
||||
if ($this->_shouldLog('disabled:' . $user['id'])) {
|
||||
$this->Log = ClassRegistry::init('Log');
|
||||
$this->Log->createLogEntry($user, 'auth_fail', 'User', $user['id'], 'Login attempt by disabled user.');
|
||||
|
|
|
@ -733,6 +733,9 @@ class User extends AppModel
|
|||
$user['User']['Role'] = $user['Role'];
|
||||
$user['User']['Organisation'] = $user['Organisation'];
|
||||
$user['User']['Server'] = $user['Server'];
|
||||
if (isset($user['UserSetting'])) {
|
||||
$user['User']['UserSetting'] = $user['UserSetting'];
|
||||
}
|
||||
return $user['User'];
|
||||
}
|
||||
|
||||
|
@ -820,7 +823,7 @@ class User extends AppModel
|
|||
*/
|
||||
public function sendEmail(array $user, $body, $bodyNoEnc = false, $subject, $replyToUser = false)
|
||||
{
|
||||
if ($user['User']['disabled']) {
|
||||
if ($user['User']['disabled'] || !$this->checkIfUserIsValid($user['User'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1406,6 +1409,32 @@ class User extends AppModel
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user still valid at identity provider.
|
||||
* @param array $user
|
||||
* @param bool $blockInvalid Block invalid user
|
||||
* @param bool $ignoreValidityTime Block invalid user
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function checkIfUserIsValid(array $user, $blockInvalid = false, $ignoreValidityTime = false)
|
||||
{
|
||||
$auth = Configure::read('Security.auth');
|
||||
if (!$auth) {
|
||||
return true;
|
||||
}
|
||||
if (!is_array($auth)) {
|
||||
throw new Exception("`Security.auth` config value must be array.");
|
||||
}
|
||||
if (!in_array('OidcAuth.Oidc', $auth, true)) {
|
||||
return true; // this method currently makes sense just for OIDC auth provider
|
||||
}
|
||||
App::uses('OidcAuthenticate', 'OidcAuth.Controller/Component/Auth');
|
||||
App::uses('ComponentCollection', 'Controller');
|
||||
$oidc = new OidcAuthenticate(new ComponentCollection(), []);
|
||||
return $oidc->isUserValid($user, $blockInvalid, $ignoreValidityTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize GPG. Returns `null` if initialization failed.
|
||||
*
|
||||
|
|
|
@ -12,13 +12,18 @@ App::uses('BaseAuthenticate', 'Controller/Component/Auth');
|
|||
* - OidcAuth.organisation_property (default: `organization`)
|
||||
* - OidcAuth.roles_property (default: `roles`)
|
||||
* - OidcAuth.default_org
|
||||
* - OidcAuth.unblock
|
||||
* - OidcAuth.unblock (boolean, default: false)
|
||||
* - OidcAuth.offline_access (boolean, default: false)
|
||||
* - OidcAuth.check_user_validity (integer, default `0`)
|
||||
*/
|
||||
class OidcAuthenticate extends BaseAuthenticate
|
||||
{
|
||||
/** @var User|null */
|
||||
private $userModel;
|
||||
|
||||
/** @var \JakubOnderka\OpenIDConnectClient|\Jumbojett\OpenIDConnectClient */
|
||||
private $oidc;
|
||||
|
||||
/**
|
||||
* @param CakeRequest $request
|
||||
* @param CakeResponse $response
|
||||
|
@ -70,12 +75,18 @@ class OidcAuthenticate extends BaseAuthenticate
|
|||
|
||||
$organisationId = $this->checkOrganization($organisationName, $user, $mispUsername);
|
||||
if (!$organisationId) {
|
||||
if ($user) {
|
||||
$this->block($user);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$roleId = $this->getUserRole($roles, $mispUsername);
|
||||
if ($roleId === null) {
|
||||
$this->log($mispUsername, 'No role was assigned.');
|
||||
if ($user) {
|
||||
$this->block($user);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -111,7 +122,10 @@ class OidcAuthenticate extends BaseAuthenticate
|
|||
$this->log($mispUsername, "Unblocking user.");
|
||||
$user['disabled'] = false;
|
||||
}
|
||||
$this->storeMetadata($user['id'], $verifiedClaims);
|
||||
|
||||
$refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null;
|
||||
$this->storeMetadata($user['id'], $verifiedClaims, $refreshToken);
|
||||
|
||||
$this->log($mispUsername, 'Logged in.');
|
||||
return $user;
|
||||
}
|
||||
|
@ -132,19 +146,91 @@ class OidcAuthenticate extends BaseAuthenticate
|
|||
throw new RuntimeException("Could not save user `$mispUsername` to database.");
|
||||
}
|
||||
|
||||
$this->storeMetadata($this->userModel()->id, $verifiedClaims);
|
||||
$refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null;
|
||||
$this->storeMetadata($this->userModel()->id, $verifiedClaims, $refreshToken);
|
||||
|
||||
$this->log($mispUsername, "Saved in database with ID {$this->userModel()->id}");
|
||||
$this->log($mispUsername, 'Logged in.');
|
||||
return $this->_findUser($mispUsername);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $user
|
||||
* @param bool $blockInvalid Block invalid user
|
||||
* @param bool $ignoreValidityTime Ignore `check_user_validity` setting and always check if user is valid
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function isUserValid(array $user, $blockInvalid = false, $ignoreValidityTime = false)
|
||||
{
|
||||
if (!$this->getConfig('offline_access', false)) {
|
||||
return true; // offline access is not enabled, so it is not possible to verify user
|
||||
}
|
||||
|
||||
if (!$ignoreValidityTime) {
|
||||
$checkUserValidityEvery = $this->getConfig('check_user_validity', 0);
|
||||
if ($checkUserValidityEvery === 0) {
|
||||
return true; // validity checking is disabled
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($user['sub'])) {
|
||||
return true; // user is not OIDC managed user
|
||||
}
|
||||
|
||||
$userInfo = $this->findUserInfo($user);
|
||||
if (!isset($userInfo['refresh_token'])) {
|
||||
if ($blockInvalid) {
|
||||
$this->block($user);
|
||||
}
|
||||
$this->log($user['email'], "User don't have refresh token, considering user is not valid");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$ignoreValidityTime && $userInfo['validity_check_timestamp'] > time() - $checkUserValidityEvery) {
|
||||
return true; // user was checked in last `check_user_validity`, do not check again
|
||||
}
|
||||
|
||||
$oidc = $this->prepareClient();
|
||||
|
||||
try {
|
||||
$oidc->refreshToken($userInfo['refresh_token']);
|
||||
} catch (JakubOnderka\ErrorResponse $e) {
|
||||
if ($e->getError() === 'invalid_grant') {
|
||||
if ($blockInvalid) {
|
||||
$this->block($user);
|
||||
}
|
||||
$this->log($user['email'], "Refreshing token is not possible because of `{$e->getMessage()}`, considering user is not valid");
|
||||
return false;
|
||||
} else {
|
||||
$this->log($user['email'], "Refreshing token is not possible because of `{$e->getMessage()}`, considering user is still valid");
|
||||
return true;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->log($user['email'], "Refreshing token is not possible because of `{$e->getMessage()}`, considering user is still valid");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update refresh token if new token provided
|
||||
if ($oidc->getRefreshToken()) {
|
||||
$userInfo['validity_check_timestamp'] = time();
|
||||
$userInfo['refresh_token'] = $oidc->getRefreshToken();
|
||||
$this->userModel()->UserSetting->setSettingInternal($user['id'], 'oidc', $userInfo);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \JakubOnderka\OpenIDConnectClient|\Jumbojett\OpenIDConnectClient
|
||||
* @throws Exception
|
||||
*/
|
||||
private function prepareClient()
|
||||
{
|
||||
if ($this->oidc) {
|
||||
return $this->oidc;
|
||||
}
|
||||
|
||||
$providerUrl = $this->getConfig('provider_url');
|
||||
if (!filter_var($providerUrl, FILTER_VALIDATE_URL)) {
|
||||
throw new RuntimeException("Config option `OidcAuth.provider_url` must be valid URL.");
|
||||
|
@ -180,7 +266,12 @@ class OidcAuthenticate extends BaseAuthenticate
|
|||
$oidc->setCodeChallengeMethod($ccm);
|
||||
}
|
||||
|
||||
if ($this->getConfig('offline_access', false)) {
|
||||
$oidc->addScope('offline_access');
|
||||
}
|
||||
|
||||
$oidc->setRedirectURL(Configure::read('MISP.baseurl') . '/users/login');
|
||||
$this->oidc = $oidc;
|
||||
return $oidc;
|
||||
}
|
||||
|
||||
|
@ -276,13 +367,30 @@ class OidcAuthenticate extends BaseAuthenticate
|
|||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $user
|
||||
* @return array
|
||||
*/
|
||||
private function findUserInfo(array $user)
|
||||
{
|
||||
if (isset($user['UserSetting'])) {
|
||||
foreach ($user['UserSetting'] as $userSetting) {
|
||||
if ($userSetting['setting'] === 'oidc') {
|
||||
return $userSetting['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->userModel()->UserSetting->getValueForUser($user['id'], 'oidc');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $userId
|
||||
* @param stdClass $verifiedClaims
|
||||
* @param string|null $refreshToken
|
||||
* @return array|bool|mixed|null
|
||||
* @throws Exception
|
||||
*/
|
||||
private function storeMetadata($userId, \stdClass $verifiedClaims)
|
||||
private function storeMetadata($userId, \stdClass $verifiedClaims, $refreshToken = null)
|
||||
{
|
||||
// OIDC session ID
|
||||
if (isset($verifiedClaims->sid)) {
|
||||
|
@ -295,10 +403,25 @@ class OidcAuthenticate extends BaseAuthenticate
|
|||
$value[$field] = $verifiedClaims->{$field};
|
||||
}
|
||||
}
|
||||
if ($refreshToken) {
|
||||
$value['validity_check_timestamp'] = time();
|
||||
$value['refresh_token'] = $refreshToken;
|
||||
}
|
||||
|
||||
return $this->userModel()->UserSetting->setSettingInternal($userId, 'oidc', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $user
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private function block(array $user)
|
||||
{
|
||||
$this->userModel()->updateField($user, 'disabled', true);
|
||||
$this->log($user['email'], "User blocked by OIDC");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $username
|
||||
* @param string $message
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# MISP OpenID Connect Authentication
|
||||
|
||||
This plugin provides ability to use OpenID as Single sign-on for login users to MISP.
|
||||
When plugin is enabled, users are direcly redirected to SSO provider and it is not possible
|
||||
When plugin is enabled, users are directly redirected to SSO provider and it is not possible
|
||||
to login with passwords stored in MISP.
|
||||
|
||||
## Usage
|
||||
|
@ -45,5 +45,9 @@ $config = array(
|
|||
|
||||
## Caveats
|
||||
|
||||
* When user is blocked in SSO (IdM), he/she will be not blocked in MISP. He could not log in, but users authentication keys will still work and also he/she will still receive all emails.
|
||||
When user is blocked in SSO (IdM), he/she will be not blocked in MISP. He could not log in, but users authentication keys will still work and also he/she will still receive all emails.
|
||||
|
||||
To solve this problem:
|
||||
1) set `OidcAuth.offline_access` to `true` - with that, IdP will be requested to provide offline access token
|
||||
2) set `OidcAuth.check_user_validity` to number of seconds, after which user will be revalidated if he is still active in IdP. Zero means that this functionality is disabled. Recommended value is `300`.
|
||||
3) because offline tokens will expire when not used, you can run `cake user check_user_validity` to check all user in one call
|
||||
|
|
Loading…
Reference in New Issue