Skip to content

Commit 9c515f2

Browse files
committed
Processing extra IDs in external authentication tokens.
1 parent eacbeb8 commit 9c515f2

6 files changed

Lines changed: 84 additions & 20 deletions

File tree

app/config/config.local.neon.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ parameters:
5151
externalAuthenticators:
5252
- name: "cas-auth-ext"
5353
jwtSecret: "secretStringSharedWithExternAuth"
54+
jwtAlgorithm: HS256 # optional, HS256 is default
5455
expiration: 60 # seconds passed since iat
55-
usedAlgorithm: HS256 # optional, HS256 is default
56+
extraIds: [] # additional service types whose IDs may be provided as extra IDs in the auth token
5657

5758
emails:
5859
footerUrl: "%webapp.address%"

app/helpers/ExternalLogin/ExternalServiceAuthenticator.php

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use App\Model\Repository\Users;
1616
use App\Model\Repository\Instances;
1717
use App\Helpers\EmailVerificationHelper;
18+
use App\Helpers\FailureHelper;
1819
use Nette\Utils\Arrays;
1920
use Nette\Http\IResponse;
2021
use Firebase\JWT\JWT;
@@ -42,6 +43,9 @@ class ExternalServiceAuthenticator
4243
/** @var EmailVerificationHelper */
4344
public $emailVerificationHelper;
4445

46+
/** @var FailureHelper */
47+
public $failureHelper;
48+
4549
/**
4650
* @var array [ name => { jwtSecret, expiration } ]
4751
*/
@@ -61,22 +65,25 @@ public function __construct(
6165
Users $users,
6266
Logins $logins,
6367
Instances $instances,
64-
EmailVerificationHelper $emailVerificationHelper
68+
EmailVerificationHelper $emailVerificationHelper,
69+
FailureHelper $failureHelper,
6570
) {
6671
$this->externalLogins = $externalLogins;
6772
$this->users = $users;
6873
$this->logins = $logins;
6974
$this->instances = $instances;
7075
$this->emailVerificationHelper = $emailVerificationHelper;
76+
$this->failureHelper = $failureHelper;
7177

7278
foreach ($authenticators as $auth) {
7379
if (!empty($auth['name'] && !empty($auth['jwtSecret']))) {
7480
$this->authenticators[$auth['name']] = (object)[
7581
'jwtSecret' => $auth['jwtSecret'],
7682
'expiration' => Arrays::get($auth, 'expiration', 60),
7783
'defaultRole' => Arrays::get($auth, 'defaultRole', null),
78-
'usedAlgorithm' => Arrays::get($auth, 'usedAlgorithm', 'HS256'),
7984
// if set, users may register even when extrnal authenticator does not provide role
85+
'usedAlgorithm' => Arrays::get($auth, 'jwtAlgorithm', 'HS256'),
86+
'extraIds' => Arrays::get($auth, 'extraIds', []),
8087
];
8188
}
8289
}
@@ -144,6 +151,7 @@ public function authenticate(string $authName, string $token, string $instanceId
144151
}
145152
}
146153

154+
// failures throw exceptions...
147155
if ($user === null) {
148156
throw new WrongCredentialsException(
149157
"User authenticated through '$authName' has no corresponding account in ReCodEx.",
@@ -158,6 +166,7 @@ public function authenticate(string $authName, string $token, string $instanceId
158166
);
159167
}
160168

169+
$this->handleExtraIds($user, $decodedToken, $this->authenticators[$authName]->extraIds);
161170

162171
return $user;
163172
}
@@ -237,4 +246,53 @@ private function getInstance($decodedToken, string $instanceId = null): ?Instanc
237246

238247
return null;
239248
}
249+
250+
/**
251+
* Process possible additional (extra) identifiers present in the token.
252+
* @param User $user being authenticated
253+
* @param object $decodedToken
254+
* @param string[] $allowedServices whose extra IDs may be added from the token
255+
*/
256+
private function handleExtraIds(User $user, $decodedToken, array $allowedServices)
257+
{
258+
if (empty($decodedToken->extId)) {
259+
return;
260+
}
261+
262+
foreach ($decodedToken->extId as $service => $eid) {
263+
if (!in_array($service, $allowedServices)) {
264+
continue; // skip services that are not allowed
265+
}
266+
267+
$extUser = $this->externalLogins->getUser($service, $eid);
268+
if ($extUser) {
269+
if ($extUser->getId() !== $user->getId()) {
270+
// Identity crysis! ID belongs to another user...
271+
$this->failureHelper->report(
272+
FailureHelper::TYPE_API_ERROR,
273+
sprintf(
274+
"User '%s' was provided with extra ID '%s' (%s), "
275+
. "but that is already associated with user '%s'.",
276+
$user->getId(),
277+
$eid,
278+
$service,
279+
$extUser->getId()
280+
)
281+
);
282+
}
283+
284+
continue; // either already exist or we cannot proceed anyway
285+
}
286+
287+
$login = $this->externalLogins->findByUser($user, $service);
288+
if ($login->getExternalId() !== $eid) {
289+
// extra ID has changed (strange, but possible)
290+
$login->setExternalId($eid);
291+
$this->externalLogins->persist($login);
292+
continue;
293+
}
294+
295+
$this->externalLogins->connect($service, $user, $eid);
296+
}
297+
}
240298
}

app/model/entity/ExternalLogin.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ public function getExternalId(): string
6060
return $this->externalId;
6161
}
6262

63+
public function setExternalId(string $externalId): void
64+
{
65+
$this->externalId = $externalId;
66+
}
67+
6368
public function getUser(): User
6469
{
6570
return $this->user;

app/model/repository/ExternalLogins.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
*/
1212
class ExternalLogins extends BaseRepository
1313
{
14-
1514
public function __construct(EntityManagerInterface $em)
1615
{
1716
parent::__construct($em, ExternalLogin::class);

tests/ExternalLogin/ExternalServiceAuthenticator.phpt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
<?php
22

3-
use App\Exceptions\BadRequestException;
43
use App\Exceptions\WrongCredentialsException;
54
use App\Exceptions\InvalidExternalTokenException;
65
use App\Helpers\ExternalLogin\ExternalServiceAuthenticator;
7-
use App\Helpers\ExternalLogin\UserData;
86
use App\Helpers\EmailVerificationHelper;
9-
use App\Model\Entity\ExternalLogin;
10-
use App\Model\Entity\Instance;
11-
use App\Model\Entity\User;
127
use App\Model\Repository\Instances;
138
use App\Model\Repository\ExternalLogins;
149
use App\Model\Repository\Logins;
@@ -49,16 +44,17 @@ class ExternalServiceAuthenticatorTestCase extends Tester\TestCase
4944
$this->users = $container->getByType(Users::class);
5045
$this->logins = $container->getByType(Logins::class);
5146
$this->authenticator = new ExternalServiceAuthenticator(
52-
[ [
47+
[[
5348
'name' => self::AUTH_NAME,
5449
'jwtSecret' => self::AUTH_SECRET,
5550
'expiration' => 60,
56-
] ],
51+
]],
5752
$this->externalLogins,
5853
$this->users,
5954
$this->logins,
6055
$container->getByType(Instances::class),
61-
$container->getByType(EmailVerificationHelper::class)
56+
$container->getByType(EmailVerificationHelper::class),
57+
$container->getByType(App\Helpers\FailureHelper::class)
6258
);
6359
}
6460

tests/Presenters/LoginPresenter.phpt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class TestLoginPresenter extends Tester\TestCase
5252
/** @var \App\Helpers\EmailVerificationHelper */
5353
private $emailVerificationHelper;
5454

55+
/** @var \App\Helpers\FailureHelper */
56+
private $failureHelper;
57+
5558
public function __construct($container)
5659
{
5760
$this->container = $container;
@@ -62,6 +65,7 @@ class TestLoginPresenter extends Tester\TestCase
6265
$this->externalLogins = $container->getByType(\App\Model\Repository\ExternalLogins::class);
6366
$this->instances = $container->getByType(\App\Model\Repository\Instances::class);
6467
$this->emailVerificationHelper = $container->getByType(\App\Helpers\EmailVerificationHelper::class);
68+
$this->failureHelper = $container->getByType(App\Helpers\FailureHelper::class);
6569
}
6670

6771
protected function setUp()
@@ -86,8 +90,8 @@ class TestLoginPresenter extends Tester\TestCase
8690
"POST",
8791
["action" => "default"],
8892
[
89-
"username" => $this->userLogin,
90-
"password" => $this->userPassword
93+
"username" => $this->userLogin,
94+
"password" => $this->userPassword
9195
]
9296
);
9397

@@ -114,8 +118,8 @@ class TestLoginPresenter extends Tester\TestCase
114118
"POST",
115119
["action" => "default"],
116120
[
117-
"username" => $this->userLogin,
118-
"password" => $this->userPassword . "42"
121+
"username" => $this->userLogin,
122+
"password" => $this->userPassword . "42"
119123
]
120124
);
121125

@@ -135,15 +139,16 @@ class TestLoginPresenter extends Tester\TestCase
135139
Assert::count(0, $events);
136140

137141
$authenticator = new ExternalServiceAuthenticator(
138-
[ [
142+
[[
139143
'name' => 'test-cas',
140144
'jwtSecret' => 'tajnyRetezec',
141-
] ],
145+
]],
142146
$this->externalLogins,
143147
$this->users,
144148
$this->logins,
145149
$this->instances,
146-
$this->emailVerificationHelper
150+
$this->emailVerificationHelper,
151+
$this->failureHelper
147152
);
148153

149154
$user = $this->presenter->users->getByEmail($this->userLogin);
@@ -159,7 +164,7 @@ class TestLoginPresenter extends Tester\TestCase
159164

160165
$this->presenter->externalServiceAuthenticator = $authenticator;
161166

162-
$request = new Request("V1:Login", "POST", ["action" => "external", "authenticatorName" => "test-cas"], [ 'token' => $token ]);
167+
$request = new Request("V1:Login", "POST", ["action" => "external", "authenticatorName" => "test-cas"], ['token' => $token]);
163168

164169
$response = $this->presenter->run($request);
165170
Assert::type(JsonResponse::class, $response);

0 commit comments

Comments
 (0)