Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<name>OpenID Connect user backend</name>
<summary>Use an OpenID Connect backend to login to your Nextcloud</summary>
<description>Allows flexible configuration of an OIDC server as Nextcloud login user backend.</description>
<version>7.3.2</version>
<version>7.4.0</version>
<licence>agpl</licence>
<author>Roeland Jago Douma</author>
<author>Julius Härtl</author>
Expand Down
5 changes: 5 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use OCA\UserOIDC\Listener\ExternalTokenRequestedListener;
use OCA\UserOIDC\Listener\InternalTokenRequestedListener;
use OCA\UserOIDC\Listener\TimezoneHandlingListener;
use OCA\UserOIDC\Listener\TokenInvalidatedListener;
use OCA\UserOIDC\Service\ID4MeService;
use OCA\UserOIDC\Service\SettingsService;
use OCA\UserOIDC\Service\TokenService;
Expand Down Expand Up @@ -59,6 +60,10 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(ExchangedTokenRequestedEvent::class, ExchangedTokenRequestedListener::class);
$context->registerEventListener(ExternalTokenRequestedEvent::class, ExternalTokenRequestedListener::class);
$context->registerEventListener(InternalTokenRequestedEvent::class, InternalTokenRequestedListener::class);

if (class_exists(\OCP\Authentication\Events\TokenInvalidatedEvent::class)) {
$context->registerEventListener(\OCP\Authentication\Events\TokenInvalidatedEvent::class, TokenInvalidatedListener::class);
}
}

public function boot(IBootContext $context): void {
Expand Down
17 changes: 12 additions & 5 deletions lib/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -610,12 +610,15 @@ public function code(string $state = '', string $code = '', string $scope = '',
// for backchannel logout
try {
$authToken = $this->authTokenProvider->getToken($this->session->getId());
$this->sessionMapper->createSession(
$this->sessionMapper->createOrUpdateSession(
$idTokenPayload->sid ?? 'fallback-sid',
$idTokenPayload->sub ?? 'fallback-sub',
$idTokenPayload->iss ?? 'fallback-iss',
$authToken->getId(),
$this->session->getId()
$this->session->getId(),
$idTokenRaw,
$user->getUID(),
$providerId,
);
} catch (InvalidTokenException $e) {
$this->logger->debug('Auth token not found after login');
Expand Down Expand Up @@ -687,7 +690,7 @@ public function singleLogoutService() {
return $this->buildErrorTemplateResponse($message, Http::STATUS_NOT_FOUND, ['provider_id' => $providerId]);
}

// Check if a custom end_session_endpoint is deposited otherwise use the default one provided by the openid-configuration
// Check if a custom end_session_endpoint is set in the provider otherwise use the default one provided by the openid-configuration
$discoveryData = $this->discoveryService->obtainDiscovery($provider);
$defaultEndSessionEndpoint = $discoveryData['end_session_endpoint'] ?? null;
$customEndSessionEndpoint = $provider->getEndSessionEndpoint();
Expand Down Expand Up @@ -842,8 +845,12 @@ public function backChannelLogout(string $providerIdentifier, string $logout_tok
}

foreach ($oidcSessionsToKill as $oidcSession) {
// i don't know why but the cast is necessary
$authTokenId = (int)$oidcSession->getAuthtokenId();
// we know the IdP session is closed
// we need this to prevent requesting the end_session_endpoint when we catch the TokenInvalidatedEvent
$oidcSession->setIdpSessionClosed(1);
$this->sessionMapper->update($oidcSession);

$authTokenId = $oidcSession->getAuthtokenId();
try {
$authToken = $this->authTokenProvider->getTokenById($authTokenId);
// we could also get the auth token by nc session ID
Expand Down
26 changes: 13 additions & 13 deletions lib/Db/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
use OCP\AppFramework\Db\Entity;

/**
* @method string getIdentifier()
* @method void setIdentifier(string $identifier)
* @method string getClientId()
* @method void setClientId(string $clientId)
* @method string getClientSecret()
* @method void setClientSecret(string $clientSecret)
* @method string getDiscoveryEndpoint()
* @method void setDiscoveryEndpoint(string $discoveryEndpoint)
* @method string getEndSessionEndpoint()
* @method void setEndSessionEndpoint(string $endSessionEndpoint)
* @method void setScope(string $scope)
* @method \string getIdentifier()
* @method \void setIdentifier(string $identifier)
* @method \string getClientId()
* @method \void setClientId(string $clientId)
* @method \string getClientSecret()
* @method \void setClientSecret(string $clientSecret)
* @method \string|\null getDiscoveryEndpoint()
* @method \void setDiscoveryEndpoint(?string $discoveryEndpoint)
* @method \string|\null getEndSessionEndpoint()
* @method \void setEndSessionEndpoint(?string $endSessionEndpoint)
* @method \void setScope(string $scope)
*/
class Provider extends Entity implements \JsonSerializable {

Expand All @@ -34,10 +34,10 @@ class Provider extends Entity implements \JsonSerializable {
/** @var string */
protected $clientSecret;

/** @var string */
/** @var ?string */
protected $discoveryEndpoint;

/** @var string */
/** @var ?string */
protected $endSessionEndpoint;

/** @var string */
Expand Down
66 changes: 42 additions & 24 deletions lib/Db/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,49 @@
use OCP\AppFramework\Db\Entity;

/**
* @method string getSid()
* @method void setSid(string $sid)
* @method string getSub()
* @method void setSub(string $sub)
* @method string getIss()
* @method void setIss(string $iss)
* @method int getAuthtokenId()
* @method void setAuthtokenId(int $authtokenId)
* @method string getNcSessionId()
* @method void setNcSessionId(string $ncSessionId)
* @method int getCreatedAt()
* @method void setCreatedAt(int $createdAt)
* @method \string getSid()
* @method \void setSid(string $sid)
* @method \string getSub()
* @method \void setSub(string $sub)
* @method \string getIss()
* @method \void setIss(string $iss)
* @method \int getAuthtokenId()
* @method \void setAuthtokenId(int $authtokenId)
* @method \string getNcSessionId()
* @method \void setNcSessionId(string $ncSessionId)
* @method \int getCreatedAt()
* @method \void setCreatedAt(int $createdAt)
* @method \string|\null getIdToken()
* @method \void setIdToken(?string $idToken)
* @method \string|\null getUserId()
* @method \void setUserId(?string $userId)
* @method \int getProviderId()
* @method \void setProviderId(int $providerId)
* @method \int getIdpSessionClosed()
* @method \void setIdpSessionClosed(int $idpSessionClosed)
*/
class Session extends Entity implements \JsonSerializable {

/** @var string */
protected $sid;

/** @var string */
protected $sub;

/** @var string */
protected $iss;

/** @var int */
protected $authtokenId;

/** @var string */
protected $ncSessionId;

/** @var int */
protected $createdAt;
/** @var ?string */
protected $idToken;
/** @var ?string */
protected $userId;
/** @var int */
protected $providerId;
/** @var int */
protected $idpSessionClosed;

public function __construct() {
$this->addType('sid', 'string');
Expand All @@ -51,18 +62,25 @@ public function __construct() {
$this->addType('authtoken_id', 'integer');
$this->addType('nc_session_id', 'string');
$this->addType('created_at', 'integer');
$this->addType('id_token', 'string');
$this->addType('user_id', 'string');
$this->addType('provider_id', 'integer');
$this->addType('idp_session_closed', 'integer');
}

#[\ReturnTypeWillChange]
public function jsonSerialize() {
return [
'id' => $this->id,
'sid' => $this->sid,
'sub' => $this->sub,
'iss' => $this->iss,
'authtoken_id' => $this->authtokenId,
'nc_session_id' => $this->ncSessionId,
'created_at' => $this->createdAt,
'id' => $this->getId(),
'sid' => $this->getSid(),
'sub' => $this->getSub(),
'iss' => $this->getIss(),
'authtoken_id' => $this->getAuthtokenId(),
'nc_session_id' => $this->getNcSessionId(),
'created_at' => $this->getCreatedAt(),
'user_id' => $this->getUserId(),
'provider_id' => $this->getProviderId(),
'idp_session_closed' => $this->getIdpSessionClosed() !== 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

];
}
}
82 changes: 69 additions & 13 deletions lib/Db/SessionMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@
use OCP\DB\QueryBuilder\IQueryBuilder;

use OCP\IDBConnection;
use OCP\Security\ICrypto;

/**
* @extends QBMapper<Session>
*/
class SessionMapper extends QBMapper {
public function __construct(IDBConnection $db) {
public function __construct(
IDBConnection $db,
private ICrypto $crypto,
) {
parent::__construct($db, 'user_oidc_sessions', Session::class);
}

/**
* @param int $id
* @return Session
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException
* @throws Exception
* @throws MultipleObjectsReturnedException
*/
public function getSession(int $id): Session {
$qb = $this->db->getQueryBuilder();
Expand Down Expand Up @@ -99,10 +104,33 @@ public function findSessionBySid(string $sid, ?string $sub = null, ?string $iss
return $this->findEntity($qb);
}

/**
* @param int $authTokenId
* @param string $userId
* @return Session
* @throws DoesNotExistException
* @throws Exception
* @throws MultipleObjectsReturnedException
*/
public function getSessionByAuthTokenAndUid(int $authTokenId, string $userId): Session {
$qb = $this->db->getQueryBuilder();

$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('authtoken_id', $qb->createNamedParameter($authTokenId, IQueryBuilder::PARAM_INT))
)
->andWhere(
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
);

return $this->findEntity($qb);
}

/**
* @param string $ncSessionId
* @return int
* @throws \OCP\DB\Exception
* @throws Exception
*/
public function deleteFromNcSessionId(string $ncSessionId): int {
$qb = $this->db->getQueryBuilder();
Expand All @@ -116,7 +144,7 @@ public function deleteFromNcSessionId(string $ncSessionId): int {

/**
* @param int $minCreationTimestamp
* @throws \OCP\DB\Exception
* @throws Exception
*/
public function cleanupSessions(int $minCreationTimestamp): void {
$qb = $this->db->getQueryBuilder();
Expand All @@ -129,34 +157,62 @@ public function cleanupSessions(int $minCreationTimestamp): void {
}

/**
* Create a session
* Create or update a Nextcloud Oidc session
*
* We have a unique constraint on the "sid" column because there cannot be multiple Nextcloud Oidc sessions for the same IdP session (sid)
* So if we log in with an IdP session that was already used in a previous Nextcloud Oidc session, we can safely assume
* the related real Nextcloud session does not exist anymore. So we update the row for this "sid".
*
* In short: If there are multiple Nextcloud logins using the same IdP session, we only store the last one
*
* @param string $sid
* @param string $sub
* @param string $iss
* @param int $authtokenId
* @param string $ncSessionid
* @return mixed|Session|\OCP\AppFramework\Db\Entity
* @param string $ncSessionId
* @param string $idToken
* @param string $userId
* @param int $providerId
* @param bool $idpSessionClosed
* @return Session|null
* @throws Exception
*/
public function createSession(string $sid, string $sub, string $iss, int $authtokenId, string $ncSessionid) {
public function createOrUpdateSession(
string $sid, string $sub, string $iss, int $authtokenId, string $ncSessionId,
string $idToken, string $userId, int $providerId, bool $idpSessionClosed = false,
): ?Session {
$createdAt = (new DateTime())->getTimestamp();

try {
// do not create if one with same sid already exists (which should not happen)
return $this->findSessionBySid($sid);
$existingSession = $this->findSessionBySid($sid);
$existingSession->setSub($sub);
$existingSession->setIss($iss);
$existingSession->setAuthtokenId($authtokenId);
$existingSession->setNcSessionId($ncSessionId);
$existingSession->setCreatedAt($createdAt);
$existingSession->setIdToken($this->crypto->encrypt($idToken));
$existingSession->setUserId($userId);
$existingSession->setProviderId($providerId);
$existingSession->setIdpSessionClosed($idpSessionClosed ? 1 : 0);
return $this->update($existingSession);
} catch (MultipleObjectsReturnedException $e) {
// this can't happen
return null;
} catch (DoesNotExistException $e) {
}

$createdAt = (new DateTime())->getTimestamp();

$session = new Session();
$session->setSid($sid);
$session->setSub($sub);
$session->setIss($iss);
$session->setAuthtokenId($authtokenId);
$session->setNcSessionId($ncSessionid);
$session->setNcSessionId($ncSessionId);
$session->setCreatedAt($createdAt);
$session->setIdToken($this->crypto->encrypt($idToken));
$session->setUserId($userId);
$session->setProviderId($providerId);
$session->setIdpSessionClosed($idpSessionClosed ? 1 : 0);
return $this->insert($session);
}
}
6 changes: 4 additions & 2 deletions lib/Helper/HttpClientHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ public function get($url, array $headers = [], array $options = []) {

$client = $this->clientService->newClient();

if (isset($oidcConfig['httpclient.allowselfsigned'])
&& !in_array($oidcConfig['httpclient.allowselfsigned'], [false, 'false', 0, '0'], true)) {
$debugModeEnabled = $this->config->getSystemValueBool('debug', false);
if ($debugModeEnabled
|| (isset($oidcConfig['httpclient.allowselfsigned'])
&& !in_array($oidcConfig['httpclient.allowselfsigned'], [false, 'false', 0, '0'], true))) {
$options['verify'] = false;
}

Expand Down
Loading
Loading