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
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
149 changes: 128 additions & 21 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 All @@ -63,7 +74,7 @@ public function __construct(
* @param ?string $client_id Client ID
* @param ?string $client_secret Client secret
* @throws Exception
* @return JSONResponse<Http::STATUS_OK, array{access_token: string, token_type: string, expires_in: int, refresh_token: string, user_id: string}, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
* @return JSONResponse<Http::STATUS_OK, array{access_token: string, token_type: string, expires_in: int, refresh_token: string, user_id: string, "x.nc-gss.secondary_url"?: ?string}, array{}>|JSONResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
*
* 200: Token returned
* 400: Getting token is not possible
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,14 +241,109 @@ public function getToken(

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

return new JSONResponse(
[
'access_token' => $newToken,
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => $newCode,
'user_id' => $appToken->getUID(),
]
);
$data = [
'access_token' => $newToken,
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => $newCode,
'user_id' => $appToken->getUID(),
];

if ($this->globalScaleConfig->isGlobalScaleEnabled() && $this->globalScaleConfig->isPrimary()) {
// Also make sure the access token is available on the secondary instance
$data['x.nc-gss.secondary_url'] = $this->pushTokenToSecondary($appToken, $newToken, $expires);
}

return new JSONResponse($data);
}

/**
* 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): ?string {
$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 null;
}

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 null;
}

try {
return $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]);
}
return null;
}

/**
* 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([]);
}
}
6 changes: 5 additions & 1 deletion apps/oauth2/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
}
},
"303": {
"description": "Redirect to login URL",
"description": "Redirect to the login URL",
"headers": {
"Location": {
"schema": {
Expand Down Expand Up @@ -199,6 +199,10 @@
},
"user_id": {
"type": "string"
},
"x.nc-gss.secondary_url": {
"type": "string",
"nullable": true
}
}
}
Expand Down
Loading
Loading