-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into fix-redirect-url
- Loading branch information
Showing
12 changed files
with
824 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<!-- | ||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors | ||
- SPDX-License-Identifier: AGPL-3.0-or-later | ||
--> | ||
### Token exchange | ||
|
||
If your IdP supports token exchange, user_oidc can exchange the login token against another token. | ||
|
||
Keycloak supports token exchange if its "Preview" mode is enabled. See https://www.keycloak.org/securing-apps/token-exchange . | ||
|
||
:warning: Your IdP need to be configured accordingly. For example, Keycloak requires that token exchange is explicitely | ||
authorized for the target Oidc client. | ||
|
||
The type of token exchange that user_oidc can perform is "Internal token to internal token" | ||
(https://www.keycloak.org/securing-apps/token-exchange#_internal-token-to-internal-token-exchange). | ||
This means you can exchange a token delivered for an audience "A" for a token delivered for an audience "B". | ||
In other words, you can get a token of a different Oidc client than the one you configured in user_oidc. | ||
|
||
In short, you don't need the client ID and client secret of the target audience's client. | ||
Providing a token for the audience "A" (the login token) is enough to obtain a token for the audience "B". | ||
|
||
user_oidc is storing the login token in the user's Nextcloud session and takes care of refreshing it when needed. | ||
When another app wants to exchange the current login token for another one, | ||
it can dispatch the `OCA\UserOIDC\Event\ExchangedTokenRequestedEvent` event. | ||
The exchanged token is immediately stored in the event object itself. | ||
|
||
```php | ||
if (class_exists('OCA\UserOIDC\Event\ExchangedTokenRequestedEvent')) { | ||
$event = new OCA\UserOIDC\Event\ExchangedTokenRequestedEvent('my_target_audience'); | ||
try { | ||
$this->eventDispatcher->dispatchTyped($event); | ||
} catch (OCA\UserOIDC\Exception\TokenExchangeFailedException $e) { | ||
$this->logger->debug('Failed to exchange token: ' . $e->getMessage()); | ||
$error = $e->getError(); | ||
$errorDescription = $e->getErrorDescription(); | ||
if ($error && $errorDescription) { | ||
$this->logger->debug('Token exchange error response from the IdP: ' . $error . ' (' . $errorDescription . ')'); | ||
} | ||
} | ||
$token = $event->getToken(); | ||
if ($token === null) { | ||
$this->logger->debug('ExchangedTokenRequestedEvent event has not been caught by user_oidc'); | ||
} else { | ||
$this->logger->debug('Obtained a token that expires in ' . $token->getExpiresInFromNow()); | ||
// use the token | ||
$accessToken = $token->getAccessToken(); | ||
} | ||
} else { | ||
$this->logger->debug('The user_oidc app is not installed/available'); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
/** | ||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
namespace OCA\UserOIDC\Event; | ||
|
||
use OCA\UserOIDC\Model\Token; | ||
use OCP\EventDispatcher\Event; | ||
|
||
/** | ||
* This event is emitted with by other apps which need an exchanged token for another audience (another client ID) | ||
*/ | ||
class ExchangedTokenRequestedEvent extends Event { | ||
|
||
private ?Token $token = null; | ||
|
||
public function __construct( | ||
private string $targetAudience, | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
public function getTargetAudience(): string { | ||
return $this->targetAudience; | ||
} | ||
|
||
public function setTargetAudience(string $targetAudience): void { | ||
$this->targetAudience = $targetAudience; | ||
} | ||
|
||
public function getToken(): ?Token { | ||
return $this->token; | ||
} | ||
|
||
public function setToken(?Token $token): void { | ||
$this->token = $token; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
/** | ||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
namespace OCA\UserOIDC\Exception; | ||
|
||
use Exception; | ||
|
||
class TokenExchangeFailedException extends Exception { | ||
|
||
public function __construct( | ||
$message = '', | ||
$code = 0, | ||
$previous = null, | ||
private ?string $error = null, | ||
private ?string $errorDescription = null, | ||
) { | ||
parent::__construct($message, $code, $previous); | ||
} | ||
|
||
public function getError(): ?string { | ||
return $this->error; | ||
} | ||
|
||
public function getErrorDescription(): ?string { | ||
return $this->errorDescription; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
/** | ||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
namespace OCA\UserOIDC\Listener; | ||
|
||
use OCA\UserOIDC\Event\ExchangedTokenRequestedEvent; | ||
use OCA\UserOIDC\Service\TokenService; | ||
use OCP\EventDispatcher\Event; | ||
use OCP\EventDispatcher\IEventListener; | ||
use OCP\IUserSession; | ||
use Psr\Log\LoggerInterface; | ||
|
||
/** | ||
* @implements IEventListener<ExchangedTokenRequestedEvent|Event> | ||
*/ | ||
class ExchangedTokenRequestedListener implements IEventListener { | ||
|
||
public function __construct( | ||
private IUserSession $userSession, | ||
private TokenService $tokenService, | ||
private LoggerInterface $logger, | ||
) { | ||
} | ||
|
||
public function handle(Event $event): void { | ||
if (!$event instanceof ExchangedTokenRequestedEvent) { | ||
return; | ||
} | ||
|
||
if (!$this->userSession->isLoggedIn()) { | ||
return; | ||
} | ||
|
||
$targetAudience = $event->getTargetAudience(); | ||
$this->logger->debug('[TokenExchange Listener] received request for audience: ' . $targetAudience); | ||
$token = $this->tokenService->getExchangedToken($targetAudience); | ||
$event->setToken($token); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
/** | ||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
namespace OCA\UserOIDC\Model; | ||
|
||
use JsonSerializable; | ||
|
||
class Token implements JsonSerializable { | ||
|
||
private ?string $idToken; | ||
private string $accessToken; | ||
private int $expiresIn; | ||
private int $refreshExpiresIn; | ||
private string $refreshToken; | ||
private int $createdAt; | ||
private ?int $providerId; | ||
|
||
public function __construct(array $tokenData) { | ||
$this->idToken = $tokenData['id_token'] ?? null; | ||
$this->accessToken = $tokenData['access_token']; | ||
$this->expiresIn = $tokenData['expires_in']; | ||
$this->refreshExpiresIn = $tokenData['refresh_expires_in']; | ||
$this->refreshToken = $tokenData['refresh_token']; | ||
$this->createdAt = $tokenData['created_at'] ?? time(); | ||
$this->providerId = $tokenData['provider_id'] ?? null; | ||
} | ||
|
||
public function getAccessToken(): string { | ||
return $this->accessToken; | ||
} | ||
|
||
public function getIdToken(): ?string { | ||
return $this->idToken; | ||
} | ||
|
||
public function getExpiresIn(): int { | ||
return $this->expiresIn; | ||
} | ||
|
||
public function getExpiresInFromNow(): int { | ||
$expiresAt = $this->createdAt + $this->expiresIn; | ||
return $expiresAt - time(); | ||
} | ||
|
||
public function getRefreshExpiresIn(): int { | ||
return $this->refreshExpiresIn; | ||
} | ||
|
||
public function getRefreshExpiresInFromNow(): int { | ||
$refreshExpiresAt = $this->createdAt + $this->refreshExpiresIn; | ||
return $refreshExpiresAt - time(); | ||
} | ||
|
||
public function getRefreshToken(): string { | ||
return $this->refreshToken; | ||
} | ||
|
||
public function getProviderId(): ?int { | ||
return $this->providerId; | ||
} | ||
|
||
public function isExpired(): bool { | ||
return time() > ($this->createdAt + $this->expiresIn); | ||
} | ||
|
||
public function isExpiring(): bool { | ||
return time() > ($this->createdAt + (int)($this->expiresIn / 2)); | ||
} | ||
|
||
public function refreshIsExpired(): bool { | ||
return time() > ($this->createdAt + $this->refreshExpiresIn); | ||
} | ||
|
||
public function refreshIsExpiring(): bool { | ||
return time() > ($this->createdAt + (int)($this->refreshExpiresIn / 2)); | ||
} | ||
|
||
public function getCreatedAt() { | ||
return $this->createdAt; | ||
} | ||
|
||
public function jsonSerialize(): array { | ||
return [ | ||
'id_token' => $this->idToken, | ||
'access_token' => $this->accessToken, | ||
'expires_in' => $this->expiresIn, | ||
'refresh_expires_in' => $this->refreshExpiresIn, | ||
'refresh_token' => $this->refreshToken, | ||
'created_at' => $this->createdAt, | ||
'provider_id' => $this->providerId, | ||
]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.