Skip to content
110 changes: 95 additions & 15 deletions lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
use OCA\UserOIDC\Service\DiscoveryService;
use OCA\UserOIDC\Service\LdapService;
use OCA\UserOIDC\Service\ProviderService;
use OCA\UserOIDC\Service\EventProvisioningService;
use OCA\UserOIDC\Service\ProvisioningDeniedException;
use OCA\UserOIDC\Service\ProvisioningService;
use OCA\UserOIDC\Vendor\Firebase\JWT\JWT;
use OCP\AppFramework\Db\DoesNotExistException;
Expand Down Expand Up @@ -54,7 +56,68 @@ class LoginController extends BaseOidcController {
public const PROVIDERID = 'oidc.providerid';
private const REDIRECT_AFTER_LOGIN = 'oidc.redirect';
private const ID_TOKEN = 'oidc.id_token';
private const CODE_VERIFIER = 'oidc.code_verifier';

/** @var ISecureRandom */
private $random;

/** @var ISession */
private $session;

/** @var IClientService */
private $clientService;

/** @var IURLGenerator */
private $urlGenerator;

/** @var IUserSession */
private $userSession;

/** @var IUserManager */
private $userManager;

/** @var ITimeFactory */
private $timeFactory;

/** @var ProviderMapper */
private $providerMapper;

/** @var IEventDispatcher */
private $eventDispatcher;

/** @var ILogger */
private $logger;

/** @var ProviderService */
private $providerService;

/** @var DiscoveryService */
private $discoveryService;

/** @var IConfig */
private $config;

/** @var LdapService */
private $ldapService;

/** @var IProvider */
private $authTokenProvider;

/** @var SessionMapper */
private $sessionMapper;

/** @var EventProvisioningService */
private $eventProvisioningService;

/** @var ProvisioningService */
private $provisioningService;

/** @var IL10N */
private $l10n;
/**
* @var ICrypto
*/
private $crypto;


public function __construct(
IRequest $request,
Expand Down Expand Up @@ -455,26 +518,39 @@ public function code(string $state = '', string $code = '', string $scope = '',
}

$autoProvisionAllowed = (!isset($oidcSystemConfig['auto_provision']) || $oidcSystemConfig['auto_provision']);
$softAutoProvisionAllowed = (!isset($oidcSystemConfig['soft_auto_provision']) || $oidcSystemConfig['soft_auto_provision']);

$shouldDoUserLookup = !$autoProvisionAllowed || ($softAutoProvisionAllowed && !$this->provisioningService->hasOidcUserProvisitioned($userId));
if ($shouldDoUserLookup && $this->ldapService->isLDAPEnabled()) {
// in case user is provisioned by user_ldap, userManager->search() triggers an ldap search which syncs the results
// so new users will be directly available even if they were not synced before this login attempt
$this->userManager->search($userId, 1, 0);
$this->ldapService->syncUser($userId);
}

// in case user is provisioned by user_ldap, userManager->search() triggers an ldap search which syncs the results
// so new users will be directly available even if they were not synced before this login attempt
$this->userManager->search($userId);
$this->ldapService->syncUser($userId);
$userFromOtherBackend = $this->userManager->get($userId);
if ($userFromOtherBackend !== null && $this->ldapService->isLdapDeletedUser($userFromOtherBackend)) {
$userFromOtherBackend = null;
}

if ($autoProvisionAllowed) {
$softAutoProvisionAllowed = (!isset($oidcSystemConfig['soft_auto_provision']) || $oidcSystemConfig['soft_auto_provision']);
if (!$softAutoProvisionAllowed && $userFromOtherBackend !== null) {
// if soft auto-provisioning is disabled,
// we refuse login for a user that already exists in another backend
$message = $this->l10n->t('User conflict');
return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => 'non-soft auto provision, user conflict'], false);
// TODO: (proposal) refactor all provisioning strategies into event handlers
$user = null;
try {
$user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload);
} catch (ProvisioningDeniedException $denied) {
// TODO MagentaCLOUD should upstream the exception handling
$redirectUrl = $denied->getRedirectUrl();
if ($redirectUrl === null) {
$message = $this->l10n->t('Failed to provision user');
return $this->build403TemplateResponse($message, Http::STATUS_BAD_REQUEST, ['reason' => $denied->getMessage()]);
} else {
// error response is a redirect, e.g. to a booking site
// so that you can immediately get the registration page
return new RedirectResponse($redirectUrl);
}
}
// use potential user from other backend, create it in our backend if it does not exist
$user = $this->provisioningService->provisionUser($userId, $providerId, $idTokenPayload, $userFromOtherBackend);
// no default exception handling to pass on unittest assertion failures
} else {
// when auto provision is disabled, we assume the user has been created by another user backend (or manually)
$user = $userFromOtherBackend;
Expand Down Expand Up @@ -566,7 +642,8 @@ public function singleLogoutService() {
$endSessionEndpoint .= '&client_id=' . $provider->getClientId();
$shouldSendIdToken = $this->providerService->getSetting(
$provider->getId(),
ProviderService::SETTING_SEND_ID_TOKEN_HINT, '0'
ProviderService::SETTING_SEND_ID_TOKEN_HINT,
'0'
) === '1';
$idToken = $this->session->get(self::ID_TOKEN);
if ($shouldSendIdToken && $idToken) {
Expand Down Expand Up @@ -714,7 +791,10 @@ public function backChannelLogout(string $providerIdentifier, string $logout_tok
* @return JSONResponse
*/
private function getBackchannelLogoutErrorResponse(
string $error, string $description, array $throttleMetadata = [],
string $error,
string $description,
array $throttleMetadata = [],
?bool $throttle = null
): JSONResponse {
$this->logger->debug('Backchannel logout error. ' . $error . ' ; ' . $description);
return new JSONResponse(
Expand Down
50 changes: 50 additions & 0 deletions lib/Db/UserMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@
use OCP\AppFramework\Db\QBMapper;
use OCP\Cache\CappedMemoryCache;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;

/**
* @extends QBMapper<User>
*/
class UserMapper extends QBMapper {

private CappedMemoryCache $userCache;
private LoggerInterface $logger;

public function __construct(
IDBConnection $db,
LoggerInterface $logger,
private LocalIdService $idService,
) {
parent::__construct($db, 'user_oidc', User::class);
$this->userCache = new CappedMemoryCache();
$this->logger = $logger;
}

/**
Expand Down Expand Up @@ -57,6 +61,29 @@ public function getUser(string $uid): User {
public function find(string $search, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder();

$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$stack = [];

foreach ($backtrace as $index => $trace) {
$class = $trace['class'] ?? '';
$type = $trace['type'] ?? '';
$function = $trace['function'] ?? '';
$file = $trace['file'] ?? 'unknown file';
$line = $trace['line'] ?? 'unknown line';

$stack[] = sprintf(
"#%d %s%s%s() called at [%s:%s]",
$index,
$class,
$type,
$function,
$file,
$line
);
}

$this->logger->debug("Find user by string: " . $search . " -- Call Stack:\n" . implode("\n", $stack));

$qb->select('user_id', 'display_name')
->from($this->getTableName(), 'u')
->leftJoin('u', 'preferences', 'p', $qb->expr()->andX(
Expand All @@ -77,6 +104,29 @@ public function find(string $search, $limit = null, $offset = null): array {
public function findDisplayNames(string $search, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder();

$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$stack = [];

foreach ($backtrace as $index => $trace) {
$class = $trace['class'] ?? '';
$type = $trace['type'] ?? '';
$function = $trace['function'] ?? '';
$file = $trace['file'] ?? 'unknown file';
$line = $trace['line'] ?? 'unknown line';

$stack[] = sprintf(
"#%d %s%s%s() called at [%s:%s]",
$index,
$class,
$type,
$function,
$file,
$line
);
}

$this->logger->debug("Find user display names by string: " . $search . " -- Call Stack:\n" . implode("\n", $stack));

$qb->select('user_id', 'display_name')
->from($this->getTableName(), 'u')
->leftJoin('u', 'preferences', 'p', $qb->expr()->andX(
Expand Down
87 changes: 87 additions & 0 deletions lib/Event/UserAccountChangeEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php
/*
* @copyright Copyright (c) 2023 T-Systems International
*
* @author B. Rederlechner <bernd.rederlechner@t-systems.com>
*
* @license GNU AGPL version 3 or any later version
*
*/

declare(strict_types=1);

namespace OCA\UserOIDC\Event;

use OCP\EventDispatcher\Event;

/**
* Event to provide custom mapping logic based on the OIDC token data
* In order to avoid further processing the event propagation should be stopped
* in the listener after processing as the value might get overwritten afterwards
* by other listeners through $event->stopPropagation();
*/
class UserAccountChangeEvent extends Event {
private $uid;
private $displayname;
private $mainEmail;
private $quota;
private $claims;
private $result;


public function __construct(string $uid, ?string $displayname, ?string $mainEmail, ?string $quota, object $claims, bool $accessAllowed = false) {
parent::__construct();
$this->uid = $uid;
$this->displayname = $displayname;
$this->mainEmail = $mainEmail;
$this->quota = $quota;
$this->claims = $claims;
$this->result = new UserAccountChangeResult($accessAllowed, 'default');
}

/**
* @return get event username (uid)
*/
public function getUid(): string {
return $this->uid;
}

/**
* @return get event displayname
*/
public function getDisplayName(): ?string {
return $this->displayname;
}

/**
* @return get event main email
*/
public function getMainEmail(): ?string {
return $this->mainEmail;
}

/**
* @return get event quota
*/
public function getQuota(): ?string {
return $this->quota;
}

/**
* @return array the array of claim values associated with the event
*/
public function getClaims(): object {
return $this->claims;
}

/**
* @return value for the logged in user attribute
*/
public function getResult(): UserAccountChangeResult {
return $this->result;
}

public function setResult(bool $accessAllowed, string $reason = '', ?string $redirectUrl = null) : void {
$this->result = new UserAccountChangeResult($accessAllowed, $reason, $redirectUrl);
}
}
Loading