mirror of
https://github.com/elyby/accounts.git
synced 2024-12-23 13:50:06 +05:30
Validate user_code
expiry during the Device Code grant.
Add mock responses related to the Device Code grant.
This commit is contained in:
parent
2cc27d34ad
commit
119a0f8078
@ -9,6 +9,7 @@ use api\modules\accounts\actions\ChangeEmailAction;
|
|||||||
use api\modules\accounts\actions\EmailVerificationAction;
|
use api\modules\accounts\actions\EmailVerificationAction;
|
||||||
use api\modules\accounts\actions\NewEmailVerificationAction;
|
use api\modules\accounts\actions\NewEmailVerificationAction;
|
||||||
use api\modules\accounts\controllers\DefaultController;
|
use api\modules\accounts\controllers\DefaultController;
|
||||||
|
use api\modules\oauth\controllers\AuthorizationController as OauthAuthorizationController;
|
||||||
use Closure;
|
use Closure;
|
||||||
use yii\base\ActionEvent;
|
use yii\base\ActionEvent;
|
||||||
use yii\base\BootstrapInterface;
|
use yii\base\BootstrapInterface;
|
||||||
@ -37,11 +38,15 @@ final class MockDataResponse implements BootstrapInterface {
|
|||||||
$event->isValid = false;
|
$event->isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<mixed>|null
|
||||||
|
*/
|
||||||
private function getResponse(ActionEvent $event): ?array {
|
private function getResponse(ActionEvent $event): ?array {
|
||||||
$action = $event->action;
|
$action = $event->action;
|
||||||
/** @var \yii\web\Controller $controller */
|
/** @var \yii\web\Controller $controller */
|
||||||
$controller = $action->controller;
|
$controller = $action->controller;
|
||||||
$request = $controller->request;
|
$request = $controller->request;
|
||||||
|
$response = $controller->response;
|
||||||
if ($controller instanceof SignupController && $action->id === 'index') {
|
if ($controller instanceof SignupController && $action->id === 'index') {
|
||||||
$email = $request->post('email');
|
$email = $request->post('email');
|
||||||
if ($email === 'let-me-register@ely.by') {
|
if ($email === 'let-me-register@ely.by') {
|
||||||
@ -91,6 +96,63 @@ final class MockDataResponse implements BootstrapInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($controller instanceof OauthAuthorizationController) {
|
||||||
|
if ($action->id === 'validate') {
|
||||||
|
$userCode = $request->get('user_code');
|
||||||
|
if ($userCode === 'E2E-APPROVED' || $userCode === 'E2E-UNAPPROVED') {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'client' => [
|
||||||
|
'id' => 'test',
|
||||||
|
'name' => 'Ely.by Test',
|
||||||
|
'description' => "Some client's description",
|
||||||
|
],
|
||||||
|
'session' => [
|
||||||
|
'scopes' => 'account_info minecraft_server_session',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userCode === 'E2E-EXPIRED') {
|
||||||
|
$response->setStatusCode(400);
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'expired_token',
|
||||||
|
'parameter' => 'user_code',
|
||||||
|
'statusCode' => 400,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userCode === 'E2E-COMPLETED') {
|
||||||
|
$response->setStatusCode(400);
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'used_user_code',
|
||||||
|
'parameter' => 'user_code',
|
||||||
|
'statusCode' => 400,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action->id === 'complete') {
|
||||||
|
$userCode = $request->get('user_code');
|
||||||
|
$accept = $request->post('accept');
|
||||||
|
if ($userCode === 'E2E-APPROVED' || ($userCode === 'E2E-UNAPPROVED' && $accept !== null)) {
|
||||||
|
return ['success' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userCode === 'E2E-UNAPPROVED' && $accept === null) {
|
||||||
|
$response->setStatusCode(401);
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'accept_required',
|
||||||
|
'parameter' => null,
|
||||||
|
'statusCode' => 401,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($controller instanceof DefaultController && $action->id === 'get') {
|
if ($controller instanceof DefaultController && $action->id === 'get') {
|
||||||
$httpAuth = $request->getHeaders()->get('authorization');
|
$httpAuth = $request->getHeaders()->get('authorization');
|
||||||
if ($httpAuth === 'Bearer dummy_token') {
|
if ($httpAuth === 'Bearer dummy_token') {
|
||||||
|
@ -314,8 +314,8 @@ final readonly class OauthProcess {
|
|||||||
$parameter = $matches[1];
|
$parameter = $matches[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($parameter === null && str_starts_with($e->getErrorType(), 'invalid_')) {
|
if ($parameter === null && $hint === 'user_code') {
|
||||||
$parameter = substr($e->getErrorType(), 8); // 8 is the length of the "invalid_"
|
$parameter = $hint;
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = [
|
$response = [
|
||||||
|
@ -156,15 +156,22 @@ final class CompleteFlowCest {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tryToCompleteExpiredDeviceCodeFlow(FunctionalTester $I): void {
|
||||||
|
$I->amAuthenticated();
|
||||||
|
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||||
|
'user_code' => 'EXPIRED',
|
||||||
|
]), ['accept' => true]);
|
||||||
|
$I->canSeeResponseCodeIs(400);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'expired_token',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function tryToCompleteAlreadyCompletedDeviceCodeFlow(FunctionalTester $I): void {
|
public function tryToCompleteAlreadyCompletedDeviceCodeFlow(FunctionalTester $I): void {
|
||||||
$I->amAuthenticated();
|
$I->amAuthenticated();
|
||||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
||||||
'user_code' => 'AAAABBBB',
|
'user_code' => 'COMPLETED',
|
||||||
]), ['accept' => true]);
|
|
||||||
$I->canSeeResponseCodeIs(200);
|
|
||||||
|
|
||||||
$I->sendPOST('/api/oauth2/v1/complete?' . http_build_query([
|
|
||||||
'user_code' => 'AAAABBBB',
|
|
||||||
]), ['accept' => true]);
|
]), ['accept' => true]);
|
||||||
$I->canSeeResponseCodeIs(400);
|
$I->canSeeResponseCodeIs(400);
|
||||||
$I->canSeeResponseContainsJson([
|
$I->canSeeResponseContainsJson([
|
||||||
|
@ -36,6 +36,19 @@ final class DeviceCodeCest {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function pollExpiredDeviceCode(FunctionalTester $I): void {
|
||||||
|
$I->sendPOST('/api/oauth2/v1/token', [
|
||||||
|
'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
'client_id' => 'ely',
|
||||||
|
'device_code' => 'ZFPbWycSxdRpHiYP2wnv3S0KHBgYky8fRDqfhhCqzke7nKuYFfwckZywqU8iUKv3ek4VtiMiMCkiC0YT',
|
||||||
|
]);
|
||||||
|
$I->canSeeResponseCodeIs(400);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'error' => 'expired_token',
|
||||||
|
'message' => 'The `device_code` has expired and the device authorization session has concluded.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Example<array{boolean}> $case
|
* @param Example<array{boolean}> $case
|
||||||
*/
|
*/
|
||||||
|
@ -108,6 +108,32 @@ final class ValidateCest {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function expiredCodeForDeviceCode(FunctionalTester $I): void {
|
||||||
|
$I->sendGET('/api/oauth2/v1/validate', [
|
||||||
|
'user_code' => 'EXPIRED',
|
||||||
|
]);
|
||||||
|
$I->canSeeResponseCodeIs(400);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'expired_token',
|
||||||
|
'parameter' => 'user_code',
|
||||||
|
'statusCode' => 400,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function completedCodeForDeviceCode(FunctionalTester $I): void {
|
||||||
|
$I->sendGET('/api/oauth2/v1/validate', [
|
||||||
|
'user_code' => 'COMPLETED',
|
||||||
|
]);
|
||||||
|
$I->canSeeResponseCodeIs(400);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'used_user_code',
|
||||||
|
'parameter' => 'user_code',
|
||||||
|
'statusCode' => 400,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function invalidScopesAuthFlow(FunctionalTester $I): void {
|
public function invalidScopesAuthFlow(FunctionalTester $I): void {
|
||||||
$I->wantTo('check behavior on some invalid scopes');
|
$I->wantTo('check behavior on some invalid scopes');
|
||||||
$I->sendGET('/api/oauth2/v1/validate', [
|
$I->sendGET('/api/oauth2/v1/validate', [
|
||||||
|
@ -50,11 +50,15 @@ final class DeviceCodeGrant extends BaseDeviceCodeGrant {
|
|||||||
|
|
||||||
$deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByUserCode($userCode);
|
$deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByUserCode($userCode);
|
||||||
if ($deviceCode === null) {
|
if ($deviceCode === null) {
|
||||||
throw new OAuthServerException('Unknown user code', 4, 'invalid_user_code', 401);
|
throw new OAuthServerException('Unknown user code', 4, 'invalid_user_code', 401, 'user_code');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deviceCode->getExpiryDateTime()->getTimestamp() < time()) {
|
||||||
|
throw OAuthServerException::expiredToken('user_code');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($deviceCode->getUserIdentifier() !== null) {
|
if ($deviceCode->getUserIdentifier() !== null) {
|
||||||
throw new OAuthServerException('The user code has already been used', 6, 'used_user_code', 400);
|
throw new OAuthServerException('The user code has already been used', 6, 'used_user_code', 400, 'user_code');
|
||||||
}
|
}
|
||||||
|
|
||||||
$authorizationRequest = new AuthorizationRequest();
|
$authorizationRequest = new AuthorizationRequest();
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[
|
'pending-code' => [
|
||||||
'device_code' => 'nKuYFfwckZywqU8iUKv3ek4VtiMiMCkiC0YTZFPbWycSxdRpHiYP2wnv3S0KHBgYky8fRDqfhhCqzke7',
|
'device_code' => 'nKuYFfwckZywqU8iUKv3ek4VtiMiMCkiC0YTZFPbWycSxdRpHiYP2wnv3S0KHBgYky8fRDqfhhCqzke7',
|
||||||
'user_code' => 'AAAABBBB',
|
'user_code' => 'AAAABBBB',
|
||||||
'client_id' => 'ely',
|
'client_id' => 'ely',
|
||||||
@ -12,4 +12,24 @@ return [
|
|||||||
'last_polled_at' => null,
|
'last_polled_at' => null,
|
||||||
'expires_at' => time() + 1800,
|
'expires_at' => time() + 1800,
|
||||||
],
|
],
|
||||||
|
'expired-code' => [
|
||||||
|
'device_code' => 'ZFPbWycSxdRpHiYP2wnv3S0KHBgYky8fRDqfhhCqzke7nKuYFfwckZywqU8iUKv3ek4VtiMiMCkiC0YT',
|
||||||
|
'user_code' => 'EXPIRED',
|
||||||
|
'client_id' => 'ely',
|
||||||
|
'scopes' => ['minecraft_server_session', 'account_info'],
|
||||||
|
'account_id' => null,
|
||||||
|
'is_approved' => null,
|
||||||
|
'last_polled_at' => time() - 1200,
|
||||||
|
'expires_at' => time() - 1800,
|
||||||
|
],
|
||||||
|
'completed-code' => [
|
||||||
|
'device_code' => 'xdRpHiYP2wnv3S0KHBgYky8fRDqfhhCqzke7nKuYFfwckZywqU8iUKv3ek4VtiMiMCkiC0YTZFPbWycS',
|
||||||
|
'user_code' => 'COMPLETED',
|
||||||
|
'client_id' => 'ely',
|
||||||
|
'scopes' => ['minecraft_server_session', 'account_info'],
|
||||||
|
'account_id' => 1,
|
||||||
|
'is_approved' => true,
|
||||||
|
'last_polled_at' => time(),
|
||||||
|
'expires_at' => time() + 1800,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -130,11 +130,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: api/controllers/SignupController.php
|
path: api/controllers/SignupController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method api\\\\eventListeners\\\\MockDataResponse\\:\\:getResponse\\(\\) return type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: api/eventListeners/MockDataResponse.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset string on array\\|\\(callable\\(\\)\\: mixed\\)\\.$#"
|
message: "#^Cannot access offset string on array\\|\\(callable\\(\\)\\: mixed\\)\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
Loading…
Reference in New Issue
Block a user