chg: [oidc] Move OIDC to different class

pull/8144/head
Jakub Onderka 2022-02-19 12:27:51 +01:00
parent f5e32123c5
commit 8409a1871e
4 changed files with 503 additions and 456 deletions

View File

@ -248,6 +248,20 @@ class UserShell extends AppShell
public function check_validity()
{
$auth = Configure::read('Security.auth');
if (!$auth) {
$this->error('External authentication is not enabled');
}
if (!is_array($auth)) {
throw new Exception("`Security.auth` config value must be array.");
}
if (!in_array('OidcAuth.Oidc', $auth, true)) {
$this->error('This method is currently supported just by OIDC auth provider');
}
App::uses('Oidc', 'OidcAuth.Lib');
$oidc = new Oidc($this->User);
$users = $this->User->find('all', [
'recursive' => -1,
'contain' => ['UserSetting'],
@ -258,8 +272,15 @@ class UserShell extends AppShell
foreach ($users as $user) {
$user['User']['UserSetting'] = $user['UserSetting'];
$result = $this->User->checkIfUserIsValid($user['User'], $blockInvalid, true, $update);
$this->out("{$user['User']['email']}: " . ($result ? '<success>valid</success>' : '<error>invalid</error>'));
$user = $user['User'];
if ($blockInvalid) {
$result = $oidc->blockInvalidUser($user, true, $update);
} else {
$result = $oidc->isUserValid($user, true, $update);
}
$this->out("{$user['email']}: " . ($result ? '<success>valid</success>' : '<error>invalid</error>'));
}
}

View File

@ -1412,13 +1412,10 @@ class User extends AppModel
/**
* Check if user still valid at identity provider.
* @param array $user
* @param bool $blockInvalid Block invalid user
* @param bool $ignoreValidityTime
* @param bool $update Update user role or organisation from identity provider
* @return bool
* @throws Exception
*/
public function checkIfUserIsValid(array $user, $blockInvalid = false, $ignoreValidityTime = false, $update = false)
public function checkIfUserIsValid(array $user)
{
$auth = Configure::read('Security.auth');
if (!$auth) {
@ -1430,14 +1427,9 @@ class User extends AppModel
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(), []);
if ($blockInvalid) {
return $oidc->blockInvalidUser($user, $ignoreValidityTime, $update);
} else {
return $oidc->isUserValid($user, $ignoreValidityTime, $update);
}
App::uses('Oidc', 'OidcAuth.Lib');
$oidc = new Oidc($this);
return $oidc->isUserValid($user);
}
/**

View File

@ -1,5 +1,6 @@
<?php
App::uses('BaseAuthenticate', 'Controller/Component/Auth');
App::uses('Oidc', 'OidcAuth.Lib');
/**
* Config options:
@ -18,12 +19,6 @@ App::uses('BaseAuthenticate', 'Controller/Component/Auth');
*/
class OidcAuthenticate extends BaseAuthenticate
{
/** @var User|null */
private $userModel;
/** @var \JakubOnderka\OpenIDConnectClient */
private $oidc;
/**
* @param CakeRequest $request
* @param CakeResponse $response
@ -32,441 +27,8 @@ class OidcAuthenticate extends BaseAuthenticate
*/
public function authenticate(CakeRequest $request, CakeResponse $response)
{
$oidc = $this->prepareClient();
if (!$oidc->authenticate()) {
throw new Exception("OIDC authentication was not successful.");
}
$claims = $oidc->getVerifiedClaims();
$mispUsername = $claims->email ?? $oidc->requestUserInfo('email');
$this->log($mispUsername, "Trying login.");
$sub = $claims->sub; // sub is required
// Try to find user by `sub` field, that is unique
$this->settings['fields'] = ['username' => 'sub'];
$user = $this->_findUser($sub);
if (!$user) { // User by sub not found, try to find by email
$this->settings['fields'] = ['username' => 'email'];
$user = $this->_findUser($mispUsername);
if ($user && $user['sub'] !== null && $user['sub'] !== $sub) {
$this->log($mispUsername, "User sub doesn't match ({$user['sub']} != $sub), could not login.");
return false;
}
}
$organisationProperty = $this->getConfig('organisation_property', 'organization');
$organisationName = $claims->{$organisationProperty} ?? $this->getConfig('default_org');
$organisationId = $this->checkOrganization($organisationName, $user, $mispUsername);
if (!$organisationId) {
if ($user) {
$this->block($user);
}
return false;
}
$roleProperty = $this->getConfig('roles_property', 'roles');
$roles = $claims->{$roleProperty} ?? $oidc->requestUserInfo($roleProperty);
if ($roles === null) {
$this->log($user['email'], "Role property `$roleProperty` is missing in claims.");
return false;
}
$roleId = $this->getUserRole($roles, $mispUsername);
if ($roleId === null) {
$this->log($mispUsername, 'No role was assigned.');
if ($user) {
$this->block($user);
}
return false;
}
if ($user) {
$this->log($mispUsername, "Found in database with ID {$user['id']}.");
if ($user['sub'] === null) {
$this->userModel()->updateField($user, 'sub', $sub);
$this->log($mispUsername, "User sub changed from NULL to $sub.");
$user['sub'] = $sub;
}
if ($user['email'] !== $mispUsername) {
$this->userModel()->updateField($user, 'email', $mispUsername);
$this->log($mispUsername, "User e-mail changed from {$user['email']} to $mispUsername.");
$user['email'] = $mispUsername;
}
if ($user['org_id'] != $organisationId) {
$this->userModel()->updateField($user, 'org_id', $organisationId);
$this->log($mispUsername, "User organisation changed from {$user['org_id']} to $organisationId.");
$user['org_id'] = $organisationId;
}
if ($user['role_id'] != $roleId) {
$this->userModel()->updateField($user, 'role_id', $roleId);
$this->log($mispUsername, "User role changed from {$user['role_id']} to $roleId.");
$user['role_id'] = $roleId;
}
if ($user['disabled'] && $this->getConfig('unblock', false)) {
$this->userModel()->updateField($user, 'disabled', false);
$this->log($mispUsername, "Unblocking user.");
$user['disabled'] = false;
}
$refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null;
$this->storeMetadata($user['id'], $claims, $refreshToken);
$this->log($mispUsername, 'Logged in.');
return $user;
}
$this->log($mispUsername, 'Not found in database.');
$userData = [
'email' => $mispUsername,
'org_id' => $organisationId,
'newsread' => time(),
'role_id' => $roleId,
'change_pw' => 0,
'date_created' => time(),
'sub' => $sub,
];
if (!$this->userModel()->save($userData)) {
throw new RuntimeException("Could not save user `$mispUsername` to database.");
}
$refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null;
$this->storeMetadata($this->userModel()->id, $claims, $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 $ignoreValidityTime Ignore `check_user_validity` setting and always check if user is valid
* @param bool $update Update user role or organisation from OIDC
* @return bool True if user is still valid, false if not
* @throws Exception
*/
public function isUserValid(array $user, $ignoreValidityTime = false, $update = 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'])) {
$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') {
$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;
}
$claims = $oidc->getVerifiedClaims();
if ($user['sub'] !== $claims->sub) {
throw new Exception("User {$user['email']} sub doesn't match ({$user['sub']} != $claims->sub)");
}
// Check user role
$roleProperty = $this->getConfig('roles_property', 'roles');
$roles = $claims->{$roleProperty} ?? $oidc->requestUserInfo($roleProperty);
if ($roles === null) {
$this->log($user['email'], "Role property `$roleProperty` is missing in claims.");
return false;
}
$roleId = $this->getUserRole($roles, $user['email']);
if ($roleId === null) {
$this->log($user['email'], 'No role was assigned.');
return false;
}
if ($update && $user['role_id'] != $roleId) {
$this->userModel()->updateField($user, 'role_id', $roleId);
$this->log($user['email'], "User role changed from {$user['role_id']} to $roleId.");
}
// Check user org
$organisationProperty = $this->getConfig('organisation_property', 'organization');
$organisationName = $claims->{$organisationProperty} ?? $this->getConfig('default_org');
$organisationId = $this->checkOrganization($organisationName, $user, $user['email']);
if (!$organisationId) {
return false;
}
if ($update && $user['org_id'] != $organisationId) {
$this->userModel()->updateField($user, 'org_id', $organisationId);
$this->log($user['email'], "User organisation changed from {$user['org_id']} to $organisationId.");
}
// Update refresh token if new token provided
if ($oidc->getRefreshToken()) {
$this->storeMetadata($user['id'], $claims, $oidc->getRefreshToken());
}
return true;
}
/**
* @param array $user
* @param bool $ignoreValidityTime
* @param bool $update Update user role or organisation
* @return bool True if user was blocked, false if not
* @throws Exception
*/
public function blockInvalidUser(array $user, $ignoreValidityTime = false, $update = false)
{
$isValid = $this->isUserValid($user, $ignoreValidityTime, $update);
if (!$isValid) {
$this->block($user);
}
return $isValid;
}
/**
* @return \JakubOnderka\OpenIDConnectClient
* @throws Exception
*/
private function prepareClient()
{
if ($this->oidc) {
return $this->oidc;
}
$providerUrl = $this->getConfig('provider_url');
$clientId = $this->getConfig('client_id');
$clientSecret = $this->getConfig('client_secret');
if (class_exists("\JakubOnderka\OpenIDConnectClient")) {
$oidc = new \JakubOnderka\OpenIDConnectClient($providerUrl, $clientId, $clientSecret);
} else if (class_exists("\Jumbojett\OpenIDConnectClient")) {
throw new Exception("Jumbojett OIDC implementation is not supported anymore, please use JakubOnderka's client");
} else {
throw new Exception("OpenID Connect client is not installed.");
}
$authenticationMethod = $this->getConfig('authentication_method', false);
if ($authenticationMethod !== false && $authenticationMethod !== null) {
$oidc->setAuthenticationMethod($authenticationMethod);
}
$ccm = $this->getConfig('code_challenge_method', false);
if ($ccm) {
$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;
}
/**
* @param string $org
* @param array|null $user
* @param string $mispUsername
* @return int
* @throws Exception
*/
private function checkOrganization($org, $user, $mispUsername)
{
if (empty($org)) {
$this->log($mispUsername, "Organisation name not provided.");
return false;
}
$orgIsUuid = Validation::uuid($org);
$orgAux = $this->userModel()->Organisation->find('first', [
'fields' => ['Organisation.id'],
'conditions' => $orgIsUuid ? ['uuid' => strtolower($org)] : ['name' => $org],
]);
if (empty($orgAux)) {
if ($orgIsUuid) {
$this->log($mispUsername, "Could not found organisation with UUID `$org`.");
return false;
}
$orgUserId = 1; // By default created by the admin
if ($user) {
$orgUserId = $user['id'];
}
$orgId = $this->userModel()->Organisation->createOrgFromName($org, $orgUserId, true);
$this->log($mispUsername, "User organisation `$org` created with ID $orgId.");
} else {
$orgId = $orgAux['Organisation']['id'];
$this->log($mispUsername, "User organisation `$org` found with ID $orgId.");
}
return $orgId;
}
/**
* @param array $roles Role list provided by OIDC
* @param string $mispUsername
* @return int|null Role ID or null if no role matches
*/
private function getUserRole(array $roles, $mispUsername)
{
$this->log($mispUsername, 'Provided roles: ' . implode(', ', $roles));
$roleMapper = $this->getConfig('role_mapper');
if (!is_array($roleMapper)) {
throw new RuntimeException("Config option `OidcAuth.role_mapper` must be array.");
}
$roleNameToId = $this->userModel()->Role->find('list', [
'fields' => ['Role.name', 'Role.id'],
]);
$roleNameToId = array_change_key_case($roleNameToId); // normalize role names to lowercase
foreach ($roleMapper as $oidcRole => $mispRole) {
if (in_array($oidcRole, $roles, true)) {
if (!is_numeric($mispRole)) {
$mispRole = mb_strtolower($mispRole);
if (isset($roleNameToId[$mispRole])) {
$mispRole = $roleNameToId[$mispRole];
} else {
$this->log($mispUsername, "MISP Role with name `$mispRole` not found, skipping.");
continue;
}
}
return $mispRole; // first match wins
}
}
return null;
}
/**
* @param string $config
* @param mixed|null $default
* @return mixed
*/
private function getConfig($config, $default = null)
{
$value = Configure::read("OidcAuth.$config");
if (empty($value)) {
if ($default === null) {
throw new RuntimeException("Config option `OidcAuth.$config` is not set.");
}
return $default;
}
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, $refreshToken = null)
{
// OIDC session ID
if (isset($verifiedClaims->sid)) {
CakeSession::write('oidc_sid', $verifiedClaims->sid);
}
$value = [];
foreach (['preferred_username', 'given_name', 'family_name'] as $field) {
if (property_exists($verifiedClaims, $field)) {
$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
*/
private function log($username, $message)
{
CakeLog::info("OIDC: User `$username` $message");
}
/**
* @return User
*/
private function userModel()
{
if (isset($this->userModel)) {
return $this->userModel;
}
$this->userModel = ClassRegistry::init($this->settings['userModel']);
return $this->userModel;
$userModel = ClassRegistry::init($this->settings['userModel']);
$oidc = new Oidc($userModel);
return $oidc->authenticate($this->settings);
}
}

View File

@ -0,0 +1,472 @@
<?php
class Oidc
{
private $oidcClient;
/** @var User */
private $User;
public function __construct(User $user)
{
$this->User = $user;
}
/**
* @return array|false
* @throws Exception
*/
public function authenticate(array $settings)
{
$oidc = $this->prepareClient();
if (!$oidc->authenticate()) {
throw new Exception("OIDC authentication was not successful.");
}
$claims = $oidc->getVerifiedClaims();
$mispUsername = $claims->email ?? $oidc->requestUserInfo('email');
$this->log($mispUsername, "Trying login.");
$sub = $claims->sub; // sub is required
// Try to find user by `sub` field, that is unique
$user = $this->_findUser($settings, ['sub' => $sub]);
if (!$user) { // User by sub not found, try to find by email
$user = $this->_findUser($settings, ['email' => $mispUsername]);
if ($user && $user['sub'] !== null && $user['sub'] !== $sub) {
$this->log($mispUsername, "User sub doesn't match ({$user['sub']} != $sub), could not login.");
return false;
}
}
$organisationProperty = $this->getConfig('organisation_property', 'organization');
$organisationName = $claims->{$organisationProperty} ?? $this->getConfig('default_org');
$organisationId = $this->checkOrganization($organisationName, $user, $mispUsername);
if (!$organisationId) {
if ($user) {
$this->block($user);
}
return false;
}
$roleProperty = $this->getConfig('roles_property', 'roles');
$roles = $claims->{$roleProperty} ?? $oidc->requestUserInfo($roleProperty);
if ($roles === null) {
$this->log($user['email'], "Role property `$roleProperty` is missing in claims.");
return false;
}
$roleId = $this->getUserRole($roles, $mispUsername);
if ($roleId === null) {
$this->log($mispUsername, 'No role was assigned.');
if ($user) {
$this->block($user);
}
return false;
}
if ($user) {
$this->log($mispUsername, "Found in database with ID {$user['id']}.");
if ($user['sub'] === null) {
$this->User->updateField($user, 'sub', $sub);
$this->log($mispUsername, "User sub changed from NULL to $sub.");
$user['sub'] = $sub;
}
if ($user['email'] !== $mispUsername) {
$this->User->updateField($user, 'email', $mispUsername);
$this->log($mispUsername, "User e-mail changed from {$user['email']} to $mispUsername.");
$user['email'] = $mispUsername;
}
if ($user['org_id'] != $organisationId) {
$this->User->updateField($user, 'org_id', $organisationId);
$this->log($mispUsername, "User organisation changed from {$user['org_id']} to $organisationId.");
$user['org_id'] = $organisationId;
}
if ($user['role_id'] != $roleId) {
$this->User->updateField($user, 'role_id', $roleId);
$this->log($mispUsername, "User role changed from {$user['role_id']} to $roleId.");
$user['role_id'] = $roleId;
}
if ($user['disabled'] && $this->getConfig('unblock', false)) {
$this->User->updateField($user, 'disabled', false);
$this->log($mispUsername, "Unblocking user.");
$user['disabled'] = false;
}
$refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null;
$this->storeMetadata($user['id'], $claims, $refreshToken);
$this->log($mispUsername, 'Logged in.');
return $user;
}
$this->log($mispUsername, 'Not found in database.');
$userData = [
'email' => $mispUsername,
'org_id' => $organisationId,
'newsread' => time(),
'role_id' => $roleId,
'change_pw' => 0,
'date_created' => time(),
'sub' => $sub,
];
if (!$this->User->save($userData)) {
throw new RuntimeException("Could not save user `$mispUsername` to database.");
}
$refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null;
$this->storeMetadata($this->User->id, $claims, $refreshToken);
$this->log($mispUsername, "Saved in database with ID {$this->User->id}");
$this->log($mispUsername, 'Logged in.');
$user = $this->_findUser($settings, ['id' => $this->User->id]);
if ($user['User']['sub'] !== $sub) { // just to be sure that we have the correct user
throw new Exception("User {$user['email']} sub doesn't match ({$user['sub']} != $sub)");
}
return $user;
}
/**
* @param array $user
* @param bool $ignoreValidityTime Ignore `check_user_validity` setting and always check if user is valid
* @param bool $update Update user role or organisation from OIDC
* @return bool True if user is still valid, false if not
* @throws Exception
*/
public function isUserValid(array $user, $ignoreValidityTime = false, $update = 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['id'])) {
throw new InvalidArgumentException("Invalid user model provided.");
}
if (empty($user['sub'])) {
return true; // user is not OIDC managed user
}
$userInfo = $this->findUserInfo($user);
if (!isset($userInfo['refresh_token'])) {
$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') {
$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;
}
$claims = $oidc->getVerifiedClaims();
if ($user['sub'] !== $claims->sub) {
throw new Exception("User {$user['email']} sub doesn't match ({$user['sub']} != $claims->sub)");
}
// Check user role
$roleProperty = $this->getConfig('roles_property', 'roles');
$roles = $claims->{$roleProperty} ?? $oidc->requestUserInfo($roleProperty);
if ($roles === null) {
$this->log($user['email'], "Role property `$roleProperty` is missing in claims.");
return false;
}
$roleId = $this->getUserRole($roles, $user['email']);
if ($roleId === null) {
$this->log($user['email'], 'No role was assigned.');
return false;
}
if ($update && $user['role_id'] != $roleId) {
$this->User->updateField($user, 'role_id', $roleId);
$this->log($user['email'], "User role changed from {$user['role_id']} to $roleId.");
}
// Check user org
$organisationProperty = $this->getConfig('organisation_property', 'organization');
$organisationName = $claims->{$organisationProperty} ?? $this->getConfig('default_org');
$organisationId = $this->checkOrganization($organisationName, $user, $user['email']);
if (!$organisationId) {
return false;
}
if ($update && $user['org_id'] != $organisationId) {
$this->User->updateField($user, 'org_id', $organisationId);
$this->log($user['email'], "User organisation changed from {$user['org_id']} to $organisationId.");
}
// Update refresh token if new token provided
if ($oidc->getRefreshToken()) {
$this->storeMetadata($user['id'], $claims, $oidc->getRefreshToken());
}
return true;
}
/**
* @param array $user
* @param bool $ignoreValidityTime
* @param bool $update Update user role or organisation
* @return bool True if user was blocked, false if not
* @throws Exception
*/
public function blockInvalidUser(array $user, $ignoreValidityTime = false, $update = false)
{
$isValid = $this->isUserValid($user, $ignoreValidityTime, $update);
if (!$isValid) {
$this->block($user);
}
return $isValid;
}
/**
* @return \JakubOnderka\OpenIDConnectClient
* @throws Exception
*/
private function prepareClient()
{
if ($this->oidcClient) {
return $this->oidcClient;
}
$providerUrl = $this->getConfig('provider_url');
$clientId = $this->getConfig('client_id');
$clientSecret = $this->getConfig('client_secret');
if (class_exists("\JakubOnderka\OpenIDConnectClient")) {
$oidc = new \JakubOnderka\OpenIDConnectClient($providerUrl, $clientId, $clientSecret);
} else if (class_exists("\Jumbojett\OpenIDConnectClient")) {
throw new Exception("Jumbojett OIDC implementation is not supported anymore, please use JakubOnderka's client");
} else {
throw new Exception("OpenID Connect client is not installed.");
}
$authenticationMethod = $this->getConfig('authentication_method', false);
if ($authenticationMethod !== false && $authenticationMethod !== null) {
$oidc->setAuthenticationMethod($authenticationMethod);
}
$ccm = $this->getConfig('code_challenge_method', false);
if ($ccm) {
$oidc->setCodeChallengeMethod($ccm);
}
if ($this->getConfig('offline_access', false)) {
$oidc->addScope('offline_access');
}
$oidc->setRedirectURL(Configure::read('MISP.baseurl') . '/users/login');
$this->oidcClient = $oidc;
return $oidc;
}
/**
* @param string $org
* @param array|null $user
* @param string $mispUsername
* @return int
* @throws Exception
*/
private function checkOrganization($org, $user, $mispUsername)
{
if (empty($org)) {
$this->log($mispUsername, "Organisation name not provided.");
return false;
}
$orgIsUuid = Validation::uuid($org);
$orgAux = $this->User->Organisation->find('first', [
'fields' => ['Organisation.id'],
'conditions' => $orgIsUuid ? ['uuid' => strtolower($org)] : ['name' => $org],
]);
if (empty($orgAux)) {
if ($orgIsUuid) {
$this->log($mispUsername, "Could not found organisation with UUID `$org`.");
return false;
}
$orgUserId = 1; // By default created by the admin
if ($user) {
$orgUserId = $user['id'];
}
$orgId = $this->User->Organisation->createOrgFromName($org, $orgUserId, true);
$this->log($mispUsername, "User organisation `$org` created with ID $orgId.");
} else {
$orgId = $orgAux['Organisation']['id'];
$this->log($mispUsername, "User organisation `$org` found with ID $orgId.");
}
return $orgId;
}
/**
* @param array $roles Role list provided by OIDC
* @param string $mispUsername
* @return int|null Role ID or null if no role matches
*/
private function getUserRole(array $roles, $mispUsername)
{
$this->log($mispUsername, 'Provided roles: ' . implode(', ', $roles));
$roleMapper = $this->getConfig('role_mapper');
if (!is_array($roleMapper)) {
throw new RuntimeException("Config option `OidcAuth.role_mapper` must be array.");
}
$roleNameToId = $this->User->Role->find('list', [
'fields' => ['Role.name', 'Role.id'],
]);
$roleNameToId = array_change_key_case($roleNameToId); // normalize role names to lowercase
foreach ($roleMapper as $oidcRole => $mispRole) {
if (in_array($oidcRole, $roles, true)) {
if (!is_numeric($mispRole)) {
$mispRole = mb_strtolower($mispRole);
if (isset($roleNameToId[$mispRole])) {
$mispRole = $roleNameToId[$mispRole];
} else {
$this->log($mispUsername, "MISP Role with name `$mispRole` not found, skipping.");
continue;
}
}
return $mispRole; // first match wins
}
}
return null;
}
/**
* @param array $settings
* @param array $conditions
* @return array|null
*/
private function _findUser(array $settings, array $conditions)
{
$result = $this->User->find('first', [
'conditions' => $conditions,
'recursive' => $settings['recursive'],
'fields' => $settings['userFields'],
'contain' => $settings['contain'],
]);
if ($result) {
$user = $result['User'];
unset($result['User']);
return array_merge($user, $result);
}
return null;
}
/**
* @param string $config
* @param mixed|null $default
* @return mixed
*/
private function getConfig($config, $default = null)
{
$value = Configure::read("OidcAuth.$config");
if (empty($value)) {
if ($default === null) {
throw new RuntimeException("Config option `OidcAuth.$config` is not set.");
}
return $default;
}
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->User->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, $refreshToken = null)
{
// OIDC session ID
if (isset($verifiedClaims->sid)) {
CakeSession::write('oidc_sid', $verifiedClaims->sid);
}
$value = [];
foreach (['preferred_username', 'given_name', 'family_name'] as $field) {
if (property_exists($verifiedClaims, $field)) {
$value[$field] = $verifiedClaims->{$field};
}
}
if ($refreshToken) {
$value['validity_check_timestamp'] = time();
$value['refresh_token'] = $refreshToken;
}
return $this->User->UserSetting->setSettingInternal($userId, 'oidc', $value);
}
/**
* @param array $user
* @return void
* @throws Exception
*/
private function block(array $user)
{
$this->User->updateField($user, 'disabled', true);
$this->log($user['email'], "User blocked by OIDC");
}
/**
* @param string $username
* @param string $message
*/
private function log($username, $message)
{
CakeLog::info("OIDC: User `$username` $message");
}
}