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
69 changes: 61 additions & 8 deletions app/helpers/Recodex/RecodexApiHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Exceptions\ConfigException;
use App\Exceptions\RecodexApiException;
use App\Exceptions\InvalidAccessTokenException;
use App\Model\Entity\SisScheduleEvent;
use App\Model\Entity\User;
use Nette;
Expand Down Expand Up @@ -129,10 +130,16 @@ private function prepareOptions(array $query = [], $body = null, array $headers
* Decode and verify JSON body.
* @return array|string|int|bool|null decoded JSON response
* @throws RecodexApiException
* @throws InvalidAccessTokenException
*/
private function processJsonBody($response)
{
$code = $response->getStatusCode();
if ($code === 401) { // unauthorized, token is probably invalid
Debugger::log("HTTP request to ReCodEx API failed (response $code).", Debugger::DEBUG);
throw new InvalidAccessTokenException("Unauthorized request to ReCodEx API. Token is probably invalid.");
}

if ($code !== 200) {
Debugger::log("HTTP request to ReCodEx API failed (response $code).", Debugger::DEBUG);
throw new RecodexApiException("HTTP request failed (response $code).");
Expand All @@ -154,17 +161,42 @@ private function processJsonBody($response)
return $body['payload'] ?? null;
}

/**
* Perform an HTTP request and return decoded JSON response.
* @param string $method HTTP method
* @param string $url suffix for the base URL
* @param array $options for GuzzleHttp request
* @return array|string|int|bool|null decoded JSON response
* @throws RecodexApiException
* @throws InvalidAccessTokenException
*/
private function request(string $method, string $url, array $options)
{
try {
$response = $this->client->request($method, $url, $options);
} catch (GuzzleHttp\Exception\ClientException $e) {
if ($e->hasResponse()) {
return $this->processJsonBody($e->getResponse());
}
throw new RecodexApiException("HTTP request to ReCodEx API failed: " . $e->getMessage(), $e);
} catch (GuzzleHttp\Exception\GuzzleException $e) {
throw new RecodexApiException("HTTP request to ReCodEx API failed: " . $e->getMessage(), $e);
}
return $this->processJsonBody($response);
}

/**
* Perform a GET request and return decoded JSON response.
* @param string $url suffix for the base URL
* @param array $params to be encoded in URL query
* @param array $headers initial HTTP headers
* @return array|string|int|bool|null decoded JSON response
* @throws RecodexApiException
* @throws InvalidAccessTokenException
*/
private function get(string $url, array $params = [], array $headers = [])
{
$response = $this->client->get($url, $this->prepareOptions($params, null, $headers));
return $this->processJsonBody($response);
return $this->request('GET', $url, $this->prepareOptions($params, null, $headers));
}

/**
Expand All @@ -174,11 +206,12 @@ private function get(string $url, array $params = [], array $headers = [])
* @param string|array|null $body (array is encoded as JSON)
* @param array $headers initial HTTP headers
* @return array|string|int|bool|null decoded JSON response
* @throws RecodexApiException
* @throws InvalidAccessTokenException
*/
private function post(string $url, array $params = [], $body = null, array $headers = [])
{
$response = $this->client->post($url, $this->prepareOptions($params, $body, $headers));
return $this->processJsonBody($response);
return $this->request('POST', $url, $this->prepareOptions($params, $body, $headers));
}

/**
Expand All @@ -187,11 +220,12 @@ private function post(string $url, array $params = [], $body = null, array $head
* @param array $params to be encoded in URL query
* @param array $headers initial HTTP headers
* @return array|string|int|bool|null decoded JSON response
* @throws RecodexApiException
* @throws InvalidAccessTokenException
*/
private function delete(string $url, array $params = [], array $headers = [])
{
$response = $this->client->delete($url, $this->prepareOptions($params, null, $headers));
return $this->processJsonBody($response);
return $this->request('DELETE', $url, $this->prepareOptions($params, null, $headers));
}

/**
Expand Down Expand Up @@ -225,13 +259,14 @@ public function getTempTokenInstance(string $token): string
/**
* Complete the authentication process. Use tmp token to fetch full-token and user info.
* The tmp token is expected to be set as the auth token already.
* @return array|null ['accessToken' => string, 'user' => RecodexUser] on success
* @return array ['accessToken' => string, 'user' => RecodexUser] on success
*/
public function getTokenAndUser(): ?array
public function getTokenAndUser(): array
{
Debugger::log('ReCodEx::getTokenAndUser()', Debugger::DEBUG);
$body = $this->post('extensions/' . $this->extensionId);
if (!is_array($body) || empty($body['accessToken']) || empty($body['user'])) {
Debugger::log($body, Debugger::DEBUG);
throw new RecodexApiException("Unexpected ReCodEx API response from extension token endpoint.");
}

Expand All @@ -240,6 +275,24 @@ public function getTokenAndUser(): ?array
return $body;
}

/**
* Refresh the token using ReCodEx API. The current token is expected to be set as the auth token already.
* @return array ['accessToken' => string, 'user' => RecodexUser] on success (same as getTokenAndUser())
*/
public function refreshToken(): array
{
Debugger::log('ReCodEx::refreshToken()', Debugger::DEBUG);
$body = $this->post('login/refresh');
if (!is_array($body) || empty($body['accessToken']) || empty($body['user'])) {
Debugger::log($body, Debugger::DEBUG);
throw new RecodexApiException("Unexpected ReCodEx API response from token refresh endpoint.");
}

// wrap the user into a structure
$body['user'] = new RecodexUser($body['user'], $this);
return $body;
}

/**
* Retrieve user data.
* @param string $id
Expand Down
90 changes: 62 additions & 28 deletions app/presenters/LoginPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

namespace App\Presenters;

use App\Exceptions\BadRequestException;
use App\Exceptions\ForbiddenRequestException;
use App\Exceptions\InvalidAccessTokenException;
use App\Exceptions\NotFoundException;
use App\Exceptions\RecodexApiException;
use App\Exceptions\WrongCredentialsException;
use App\Model\Entity\User;
use App\Model\Repository\Users;
use App\Helpers\RecodexApiHelper;
use App\Helpers\RecodexUser;
Expand Down Expand Up @@ -42,6 +46,33 @@ class LoginPresenter extends BasePresenter
*/
public $roles;

/**
* Split the ReCodEx API token (save it to DB and suffix to the newly generated token),
* generate a new token for our frontend and send the response.
* @param User $user The user to log in
* @param string $token The token from ReCodEx API to split and save
*/
private function finalizeLogin(User $user, string $token): void
{
// part of the token is stored in the database, suffix goes into our token (payload)
$tokenSuffix = $user->setRecodexToken($token);
$this->users->persist($user);

// generate our token for our frontend
$token = $this->accessManager->issueToken(
$user,
null, // no effective role override
[TokenScope::MASTER, TokenScope::REFRESH],
null, // default expiration
['suffix' => $tokenSuffix]
);

$this->sendSuccessResponse([
"accessToken" => $token,
"user" => $user,
]);
}

/**
* Log in using temp token from ReCodEx.
* @POST
Expand All @@ -55,7 +86,11 @@ public function actionDefault()
{
$req = $this->getRequest();
$tempToken = $req->getPost("token");
$instanceId = $this->recodexApi->getTempTokenInstance($tempToken);
try {
$instanceId = $this->recodexApi->getTempTokenInstance($tempToken);
} catch (RecodexApiException $e) {
throw new BadRequestException("Invalid token from ReCodEx API", $e);
}

// Call ReCodEx API and get full token + user info using the temporary token
$this->recodexApi->setAuthToken($tempToken);
Expand All @@ -72,23 +107,7 @@ public function actionDefault()
}
$user->updatedNow();

// part of the token is stored in the database, suffix goes into our token (payload)
$tokenSuffix = $user->setRecodexToken($recodexResponse['accessToken']);
$this->users->persist($user);

// generate our token for our frontend
$token = $this->accessManager->issueToken(
$user,
null, // no effective role override
[TokenScope::MASTER, TokenScope::REFRESH],
null, // default expiration
['suffix' => $tokenSuffix]
);

$this->sendSuccessResponse([
"accessToken" => $token,
"user" => $user,
]);
$this->finalizeLogin($user, $recodexResponse['accessToken']);
}

/**
Expand All @@ -104,23 +123,38 @@ public function checkRefresh()
}

/**
* Refresh the access token of current user
* Refresh the access token of current user (as well as the ReCodEx API token).
* @GET
* @LoggedIn
* @throws AuthenticationException
* @throws ForbiddenRequestException
* @throws NotFoundException
* @throws InvalidAccessTokenException
*/
public function actionRefresh()
{
$token = $this->getAccessToken();

// We need to inject the token manually here (this class is not derived from BasePresenterWithApi)
$user = $this->getCurrentUser();
$this->users->flush();
$prefix = $user->getRecodexToken();
$suffix = $this->getAccessToken()->getPayloadOrDefault('suffix', null);

$this->sendSuccessResponse(
[
"accessToken" => $this->accessManager->issueRefreshedToken($token),
"user" => $user,
]
);
if (!$prefix || !$suffix) {
throw new ForbiddenRequestException("Cannot refresh token - user does not have a ReCodEx token.");
}

// Call ReCodEx API to refresh the token
$this->recodexApi->setAuthToken($prefix . $suffix);
$recodexResponse = $this->recodexApi->refreshToken();
/** @var RecodexUser */
$recodexUser = $recodexResponse['user'];

// Update the user entity if the token uses the same identity as the user
// (token may use identity override, in which case we do not want to update the user)
if ($recodexUser->getId() === $user->getId()) {
$recodexUser->updateUser($user);
$user->updatedNow();
}

$this->finalizeLogin($user, $recodexResponse['accessToken']);
}
}
6 changes: 3 additions & 3 deletions app/security/AccessManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ public function getUser(AccessToken $token): User
*/
public function issueToken(
User $user,
string $effectiveRole = null,
?string $effectiveRole = null,
array $scopes = [],
int $exp = null,
?int $exp = null,
array $payload = []
) {
if ($exp === null) {
Expand Down Expand Up @@ -155,7 +155,7 @@ public static function getGivenAccessToken(IRequest $request)
{
$accessToken = $request->getQuery("access_token");
if ($accessToken !== null && Strings::length($accessToken) > 0) {
return $accessToken; // the token specified in the URL is prefered
return $accessToken; // the token specified in the URL is preferred
}

// if the token is not in the URL, try to find the "Authorization" header with the bearer token
Expand Down
Loading