Skip to content

Commit 2397458

Browse files
strorchSilverFiretafid
authored
feature Added mfa api endpoints to work with oauth2 access_token (#16)
* fast commit * removed action code * minor * minor Co-authored-by: Dmytro Naumenko <[email protected]> * minor * fixed auth bug, suggesting oauth2 package, updated mfa interfaces, minor * Update src/base/ApiMfaIdentityInterface.php Co-authored-by: Andrey Klochok <[email protected]> * Update src/GrantType/UserCredentials.php Co-authored-by: Andrey Klochok <[email protected]> * Update src/controllers/TotpController.php Co-authored-by: Andrey Klochok <[email protected]> * Update src/behaviors/OauthLoginBehavior.php Co-authored-by: Andrey Klochok <[email protected]> * minor Co-authored-by: Dmytro Naumenko <[email protected]> Co-authored-by: Andrey Klochok <[email protected]>
1 parent 89c200c commit 2397458

9 files changed

+282
-17
lines changed

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,35 @@ which will return or set MFA properties. For example:
100100
IPs and TOTP functions are independent and you can provide just one of properties to have only
101101
corresponding functionality.
102102

103+
## Usage with OAuth2
104+
105+
Also there is a configuration to provide MFA for OAuth2.
106+
107+
- Require suggested `"bshaffer/oauth2-server-php": '~1.7'` package
108+
109+
- Use `hiqdev\yii2\mfa\GrantType\UserCredentials` for configuring `/oauth/token` command via totp code.
110+
For example:
111+
112+
113+
'modules' => [
114+
'oauth2' => [
115+
'grantTypes' => [
116+
'user_credentials' => [
117+
'class' => \hiqdev\yii2\mfa\GrantType\UserCredentials::class,
118+
],
119+
],
120+
],
121+
]
122+
123+
- Extend you `Identity` class from `ApiMfaIdentityInterface`.
124+
125+
- Use actions:
126+
127+
128+
POST /mfa/totp/api-temporary-secret - Proviedes temporary secret to generate QR-code
129+
POST /mfa/totp/api-enable - Enables totp
130+
POST /mfa/totp/api-disable - Disables totp
131+
103132
## License
104133

105134
This project is released under the terms of the BSD-3-Clause [license](LICENSE).

composer.json

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
"hiqdev/hidev-php": "dev-master",
5353
"hiqdev/hidev-hiqdev": "dev-master"
5454
},
55+
"suggest": {
56+
"bshaffer/oauth2-server-php": "Require '~1.7' version to use TOTP with OAuth2 protocol"
57+
},
5558
"autoload": {
5659
"psr-4": {
5760
"hiqdev\\yii2\\mfa\\": "src"

src/GrantType/UserCredentials.php

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace hiqdev\yii2\mfa\GrantType;
5+
6+
use OAuth2\RequestInterface;
7+
use OAuth2\ResponseInterface;
8+
use OAuth2\Storage\UserCredentialsInterface;
9+
use RobThree\Auth\TwoFactorAuth;
10+
11+
class UserCredentials extends \OAuth2\GrantType\UserCredentials
12+
{
13+
private TwoFactorAuth $totpService;
14+
15+
public function __construct(UserCredentialsInterface $storage)
16+
{
17+
parent::__construct($storage);
18+
19+
$this->totpService = \Yii::createObject(TwoFactorAuth::class);
20+
}
21+
22+
public function validateRequest(RequestInterface $request, ResponseInterface $response): bool
23+
{
24+
$result = parent::validateRequest($request, $response);
25+
if (!$result) {
26+
return $result;
27+
}
28+
29+
$prop = new \ReflectionProperty(parent::class, 'userInfo');
30+
$prop->setAccessible(true);
31+
$userInfo = $prop->getValue($this);
32+
33+
if (!$this->checkTotp($userInfo, $request)) {
34+
$prop->setValue($this, null);
35+
$response->setError(400, 'invalid_totp', 'Failed to validate TOTP');
36+
37+
return false;
38+
}
39+
40+
return true;
41+
}
42+
43+
private function checkTotp(array $userInfo, RequestInterface $request): bool
44+
{
45+
if (empty($userInfo['totp_secret'])) {
46+
return true;
47+
}
48+
49+
$code = $request->request('code');
50+
if (empty($code)) {
51+
return false;
52+
}
53+
54+
return $this->totpService->verifyCode($userInfo['totp_secret'], $code);
55+
}
56+
}

src/base/ApiMfaIdentityInterface.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace hiqdev\yii2\mfa\base;
5+
6+
use yii\web\IdentityInterface;
7+
8+
/**
9+
* Interface which describes Identity functionality to work with temporary TOTP secret via API.
10+
*
11+
* @see \hiqdev\yii2\mfa\controllers\TotpController::actionApiEnable()
12+
*
13+
* In general case temporary TOTP secret is stored in Session
14+
*
15+
* @see \hiqdev\yii2\mfa\base\Totp::getSecret()
16+
* @see \hiqdev\yii2\mfa\controllers\TotpController::actionEnable()
17+
*/
18+
interface ApiMfaIdentityInterface extends TotpSecretStorageInterface, MfaSaveInterface
19+
{
20+
public function getTemporarySecret(): ?string;
21+
22+
public function setTemporarySecret(?string $secret): void;
23+
}

src/base/MfaIdentityInterface.php

+4-16
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@
1010
* Interface MfaIdentityInterface
1111
* @package hiqdev\yii2\mfa\base
1212
*/
13-
interface MfaIdentityInterface extends IdentityInterface
13+
interface MfaIdentityInterface extends IdentityInterface, TotpSecretStorageInterface, MfaSaveInterface
1414
{
1515
/**
1616
* @inheritDoc
1717
*
18-
* @return MfaIdentityInterface
18+
* @return MfaIdentityInterface|null
1919
*/
2020
public static function findIdentity($id);
2121

2222
/**
2323
* @inheritDoc
2424
*
25-
* @return MfaIdentityInterface
25+
* @return MfaIdentityInterface|null
2626
*/
2727
public static function findIdentityByAccessToken($token, $type = null);
2828

@@ -31,25 +31,13 @@ public static function findIdentityByAccessToken($token, $type = null);
3131
*/
3232
public function getUsername(): string;
3333

34-
/**
35-
* @return string
36-
*/
37-
public function getTotpSecret(): string;
38-
3934
/**
4035
* @return string[]
4136
*/
4237
public function getAllowedIps(): array;
4338

44-
/**
45-
* @param string $secret
46-
* @return $this
47-
*/
48-
public function setTotpSecret(string $secret): self;
49-
5039
/**
5140
* @param string $allowedIp
52-
* @return $this
5341
*/
54-
public function addAllowedIp(string $allowedIp): self;
42+
public function addAllowedIp(string $allowedIp): void;
5543
}

src/base/MfaSaveInterface.php

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace hiqdev\yii2\mfa\base;
5+
6+
interface MfaSaveInterface
7+
{
8+
public function save(): bool;
9+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace hiqdev\yii2\mfa\base;
5+
6+
interface TotpSecretStorageInterface
7+
{
8+
/**
9+
* @return string
10+
*/
11+
public function getTotpSecret(): string;
12+
13+
/**
14+
* @param string $secret
15+
*/
16+
public function setTotpSecret(string $secret): void;
17+
}

src/behaviors/OauthLoginBehavior.php

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace hiqdev\yii2\mfa\behaviors;
5+
6+
use hiqdev\yii2\mfa\Module;
7+
use yii\base\ActionFilter;
8+
use yii\web\Request;
9+
use yii\web\Response;
10+
use yii\web\User;
11+
12+
class OauthLoginBehavior extends ActionFilter
13+
{
14+
private bool $isLogined = false;
15+
16+
private User $user;
17+
private Request $request;
18+
private Response $response;
19+
private Module $module;
20+
21+
public function __construct(
22+
User $user,
23+
Request $request,
24+
Response $response,
25+
$config = []
26+
) {
27+
parent::__construct($config);
28+
29+
$this->user = $user;
30+
$this->request = $request;
31+
$this->response = $response;
32+
$this->module = \Yii::$app->getModule('mfa');;
33+
}
34+
35+
public function beforeAction($action)
36+
{
37+
if (!empty($this->user->identity)) {
38+
$this->isLogined = true;
39+
return true;
40+
}
41+
42+
$token = $this->request->post('access_token', '');
43+
$identity = $this->user->identityClass::findIdentityByAccessToken($token);
44+
if ($identity === null) {
45+
$this->response->setStatusCode(400);
46+
$this->response->data = ['_error' => 'invalid_token'];
47+
return false;
48+
}
49+
50+
$this->module->getTotp()->setIsVerified(true);
51+
$this->user->login($identity);
52+
53+
return true;
54+
}
55+
56+
public function afterAction($action, $result)
57+
{
58+
if (!$this->isLogined) {
59+
$this->user->logout();
60+
}
61+
62+
return parent::afterAction($action, $result);
63+
}
64+
}

src/controllers/TotpController.php

+77-1
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,31 @@
1010

1111
namespace hiqdev\yii2\mfa\controllers;
1212

13+
use hiqdev\yii2\mfa\base\ApiMfaIdentityInterface;
1314
use hiqdev\yii2\mfa\base\MfaIdentityInterface;
15+
use hiqdev\yii2\mfa\behaviors\OauthLoginBehavior;
1416
use hiqdev\yii2\mfa\forms\InputForm;
1517
use Yii;
1618
use yii\filters\AccessControl;
19+
use yii\filters\ContentNegotiator;
20+
use yii\filters\VerbFilter;
21+
use yii\web\Response;
1722

1823
/**
1924
* TOTP controller.
2025
* Time-based One Time Password.
2126
*/
2227
class TotpController extends \yii\web\Controller
2328
{
29+
public $enableCsrfValidation = false;
30+
2431
public function behaviors()
2532
{
2633
return array_merge(parent::behaviors(), [
34+
'filterApi' => [
35+
'class' => OauthLoginBehavior::class,
36+
'only' => ['api-temporary-secret', 'api-disable', 'api-enable'],
37+
],
2738
'access' => [
2839
'class' => AccessControl::class,
2940
'denyCallback' => [$this, 'denyCallback'],
@@ -36,12 +47,27 @@ public function behaviors()
3647
],
3748
// @ - authenticated
3849
[
39-
'actions' => ['enable', 'disable', 'toggle'],
50+
'actions' => ['enable', 'disable', 'toggle', 'api-temporary-secret', 'api-disable', 'api-enable'],
4051
'roles' => ['@'],
4152
'allow' => true,
4253
],
4354
],
4455
],
56+
'verbFilter' => [
57+
'class' => VerbFilter::class,
58+
'actions' => [
59+
'api-temporary-secret' => ['POST'],
60+
'api-enable' => ['POST'],
61+
'api-disable' => ['POST'],
62+
],
63+
],
64+
'contentNegotiator' => [
65+
'class' => ContentNegotiator::class,
66+
'only' => ['api-temporary-secret', 'api-disable', 'api-enable'],
67+
'formats' => [
68+
'application/json' => Response::FORMAT_JSON,
69+
],
70+
],
4571
]);
4672
}
4773

@@ -159,4 +185,54 @@ public function goBack($defaultUrl = null)
159185

160186
return parent::goBack($defaultUrl);
161187
}
188+
189+
public function actionApiEnable()
190+
{
191+
/** @var ApiMfaIdentityInterface $identity */
192+
$identity = \Yii::$app->user->identity;
193+
$secret = $identity->getTotpSecret();
194+
if (!empty($secret)) {
195+
return ['_error' => 'mfa already enabled' . $secret];
196+
}
197+
198+
if (!$this->module->getTotp()->verifyCode($identity->getTemporarySecret(), $this->request->post('code', ''))) {
199+
return ['_error' => 'invalid totp code'];
200+
}
201+
202+
$identity->setTotpSecret($identity->getTemporarySecret());
203+
$identity->setTemporarySecret(null);
204+
$identity->save();
205+
206+
return ['id' => $identity->getId()];
207+
}
208+
209+
public function actionApiDisable()
210+
{
211+
/** @var ApiMfaIdentityInterface $identity */
212+
$identity = \Yii::$app->user->identity;
213+
$secret = $identity->getTotpSecret();
214+
if (empty($secret)) {
215+
return ['_error' => 'mfa disabled, enable first'];
216+
}
217+
218+
if (!$this->module->getTotp()->verifyCode($secret, $this->request->post('code', ''))) {
219+
return ['_error' => 'invalid totp code'];
220+
}
221+
222+
$identity->setTotpSecret('');
223+
$identity->save();
224+
225+
return ['id' => $identity->getId()];
226+
}
227+
228+
public function actionApiTemporarySecret()
229+
{
230+
/** @var ApiMfaIdentityInterface $identity */
231+
$identity = \Yii::$app->user->identity;
232+
$secret = $this->module->getTotp()->getSecret();
233+
$identity->setTemporarySecret($secret);
234+
$identity->save();
235+
236+
return ['secret' => $secret];
237+
}
162238
}

0 commit comments

Comments
 (0)