Skip to content
Draft
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
6 changes: 6 additions & 0 deletions apps/oauth2/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,11 @@
'url' => '/api/v1/token',
'verb' => 'POST'
],
[
/** @see \OCA\OAuth2\Controller\OauthApiController::pushToken() */
'name' => 'OauthApi#pushToken',
'url' => '/api/v1/pushtoken',
'verb' => 'POST'
],
],
];
31 changes: 11 additions & 20 deletions apps/oauth2/lib/Controller/LoginRedirectorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,16 @@

#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
class LoginRedirectorController extends Controller {
/**
* @param string $appName
* @param IRequest $request
* @param IURLGenerator $urlGenerator
* @param ClientMapper $clientMapper
* @param ISession $session
* @param IL10N $l
*/
public function __construct(
string $appName,
IRequest $request,
private IURLGenerator $urlGenerator,
private ClientMapper $clientMapper,
private ISession $session,
private IL10N $l,
private ISecureRandom $random,
private IAppConfig $appConfig,
private IConfig $config,
private readonly IURLGenerator $urlGenerator,
private readonly ClientMapper $clientMapper,
private readonly ISession $session,
private readonly IL10N $l,
private readonly ISecureRandom $random,
private readonly IAppConfig $appConfig,
private readonly IConfig $config,
) {
parent::__construct($appName, $request);
}
Expand All @@ -62,15 +54,14 @@ public function __construct(
* @return TemplateResponse<Http::STATUS_OK, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
*
* 200: Client not found
* 303: Redirect to login URL
* 303: Redirect to the login URL
*/
#[PublicPage]
#[NoCSRFRequired]
#[UseSession]
public function authorize($client_id,
$state,
$response_type,
string $redirect_uri = ''): TemplateResponse|RedirectResponse {
public function authorize(
string $client_id, string $state, string $response_type, string $redirect_uri = '',
): TemplateResponse|RedirectResponse {
try {
$client = $this->clientMapper->getByIdentifier($client_id);
} catch (ClientNotFoundException $e) {
Expand Down
128 changes: 117 additions & 11 deletions apps/oauth2/lib/Controller/OauthApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Exceptions\ExpiredTokenException;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Authentication\Token\IToken;
use OCP\DB\Exception;
use OCP\GlobalScale\IConfig as GlobalScaleConfig;
use OCP\GlobalScale\IGlobalScaleService;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
Expand All @@ -40,16 +47,20 @@ class OauthApiController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private ICrypto $crypto,
private AccessTokenMapper $accessTokenMapper,
private ClientMapper $clientMapper,
private TokenProvider $tokenProvider,
private ISecureRandom $secureRandom,
private ITimeFactory $time,
private LoggerInterface $logger,
private IThrottler $throttler,
private ITimeFactory $timeFactory,
private IDBConnection $db,
private readonly ICrypto $crypto,
private readonly AccessTokenMapper $accessTokenMapper,
private readonly ClientMapper $clientMapper,
private readonly TokenProvider $tokenProvider,
private readonly ISecureRandom $secureRandom,
private readonly ITimeFactory $time,
private readonly LoggerInterface $logger,
private readonly IThrottler $throttler,
private readonly ITimeFactory $timeFactory,
private readonly IDBConnection $db,
private readonly GlobalScaleConfig $globalScaleConfig,
private readonly IUserManager $userManager,
private readonly IURLGenerator $urlGenerator,
private readonly ContainerInterface $container,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -212,7 +223,8 @@ public function getToken(
);

// Expiration is in 1 hour again
$appToken->setExpires($this->time->getTime() + 3600);
$expires = $this->time->getTime() + 3600;
$appToken->setExpires($expires);
$this->tokenProvider->updateToken($appToken);

$this->db->commit();
Expand All @@ -229,6 +241,11 @@ public function getToken(

$this->throttler->resetDelay($this->request->getRemoteAddress(), 'login', ['user' => $appToken->getUID()]);

if ($this->globalScaleConfig->isGlobalScaleEnabled() && $this->globalScaleConfig->isPrimary()) {
// Also make sure the access token is available on the secondary instance
$this->pushTokenToSecondary($appToken, $newToken, $expires);
}

return new JSONResponse(
[
'access_token' => $newToken,
Expand All @@ -239,4 +256,93 @@ public function getToken(
]
);
}

/**
* Push the freshly issued app token to the secondary instance holding the
* user's account, so the OAuth client can use it there directly.
*/
private function pushTokenToSecondary(IToken $appToken, string $newToken, ?int $expires): void {
$user = $this->userManager->get($appToken->getUID());
if ($user === null) {
$this->logger->warning('could not push oauth token to secondary: unknown user', ['uid' => $appToken->getUID()]);
return;
}

try {
/** @var IGlobalScaleService $globalScaleService */
$globalScaleService = $this->container->get(IGlobalScaleService::class);
} catch (ContainerExceptionInterface $e) {
$this->logger->warning('could not push oauth token to secondary: globalsiteselector is not available', ['exception' => $e]);
return;
}

try {
$globalScaleService->sendToSecondary($user, $this->urlGenerator->linkToRoute('oauth2.OauthApi.pushToken'), [
'uid' => $appToken->getUID(),
'loginName' => $appToken->getLoginName(),
'name' => $appToken->getName(),
'type' => $appToken->getType(),
'remember' => $appToken->getRemember(),
'scope' => $appToken->getScopeAsArray(),
'expires' => $expires,
'token' => $newToken,
]);
} catch (\Exception $e) {
$this->logger->warning('could not push oauth token to secondary', ['exception' => $e]);
}
}

/**
* Receive an app token pushed from the primary instance, so it can be used
* directly against this (secondary) instance.
*/
#[PublicPage]
#[NoCSRFRequired]
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
#[BruteForceProtection(action: 'oauth2PushToken')]
public function pushToken(string $jwt): JSONResponse {
if (!$this->globalScaleConfig->isGlobalScaleEnabled() || !$this->globalScaleConfig->isSecondary() || $jwt === '') {
$response = new JSONResponse([], Http::STATUS_BAD_REQUEST);
$response->throttle();
return $response;
}

try {
/** @var IGlobalScaleService $globalScaleService */
$globalScaleService = $this->container->get(IGlobalScaleService::class);
} catch (ContainerExceptionInterface $e) {
$this->logger->warning('could not receive oauth token from primary: globalsiteselector is not available', ['exception' => $e]);
$response = new JSONResponse([], Http::STATUS_BAD_REQUEST);
$response->throttle();
return $response;
}

try {
$decoded = $globalScaleService->decodePayload($jwt);

$uid = (string)$decoded['uid'];
if (!$this->userManager->userExists($uid)) {
throw new \InvalidArgumentException('unknown user: ' . $uid);
}

$this->tokenProvider->generateToken(
(string)$decoded['token'],
$uid,
(string)$decoded['loginName'],
null,
(string)$decoded['name'],
(int)$decoded['type'],
(int)$decoded['remember'],
(array)$decoded['scope'],
$decoded['expires'] !== null ? (int)$decoded['expires'] : null,
);
} catch (\Exception $e) {
$this->logger->warning('could not create pushed oauth token', ['exception' => $e]);
$response = new JSONResponse([], Http::STATUS_BAD_REQUEST);
$response->throttle();
return $response;
}

return new JSONResponse([]);
}
}
Loading
Loading