Skip to content

Commit 92ecfc0

Browse files
author
Markus Heberling
committed
Implement basic OIDC core server handling
Allow Nextcloud to be used as a OpenID Connect server. CLients can authenticate against it. Signed-off-by: Markus Heberling <markus.heberling@hengsbeck.de>
1 parent b7767a5 commit 92ecfc0

File tree

2 files changed

+125
-2
lines changed

2 files changed

+125
-2
lines changed

apps/oauth2/lib/Controller/OauthApiController.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use OCP\AppFramework\Http\JSONResponse;
3535
use OCP\AppFramework\Utility\ITimeFactory;
3636
use OCP\IRequest;
37+
use OCP\IUserManager;
3738
use OCP\Security\ICrypto;
3839
use OCP\Security\ISecureRandom;
3940

@@ -52,6 +53,8 @@ class OauthApiController extends Controller {
5253
private $time;
5354
/** @var Throttler */
5455
private $throttler;
56+
/** @var IUserManager */
57+
private $userManager;
5558

5659
/**
5760
* @param string $appName
@@ -63,6 +66,7 @@ class OauthApiController extends Controller {
6366
* @param ISecureRandom $secureRandom
6467
* @param ITimeFactory $time
6568
* @param Throttler $throttler
69+
* @param IUserManager $userManager
6670
*/
6771
public function __construct($appName,
6872
IRequest $request,
@@ -72,7 +76,8 @@ public function __construct($appName,
7276
TokenProvider $tokenProvider,
7377
ISecureRandom $secureRandom,
7478
ITimeFactory $time,
75-
Throttler $throttler) {
79+
Throttler $throttler,
80+
IUserManager $userManager) {
7681
parent::__construct($appName, $request);
7782
$this->crypto = $crypto;
7883
$this->accessTokenMapper = $accessTokenMapper;
@@ -81,6 +86,7 @@ public function __construct($appName,
8186
$this->secureRandom = $secureRandom;
8287
$this->time = $time;
8388
$this->throttler = $throttler;
89+
$this->userManager = $userManager;
8490
}
8591

8692
/**
@@ -172,13 +178,53 @@ public function getToken($grant_type, $code, $refresh_token, $client_id, $client
172178

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

181+
// The id token needs to be correctly build as JWT. Taken from https://dev.to/robdwaller/how-to-create-a-json-web-token-using-php-3gml
182+
183+
// Create token header as a JSON string
184+
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
185+
186+
// We need the user to fill in name and email in the id_token
187+
$user = $this->userManager->get($appToken->getUID());
188+
189+
// Create token payload as a JSON string
190+
$payload = json_encode([
191+
// required for OIDC
192+
'iss' => \OC::$server->getURLGenerator()->getBaseUrl(),
193+
'sub' => $appToken->getUID(),
194+
'aud' => $client_id,
195+
'exp' => $appToken->getExpires(),
196+
'iat' => $this->time->getTime(),
197+
'auth_time' => $this->time->getTime(),
198+
199+
// optional, can be requested by claims, we don't support requesting claims as of now, so we just send them always
200+
'email' => $user->getEMailAddress(),
201+
'name' => $user->getDisplayName(),
202+
203+
]);
204+
205+
// Encode Header to Base64Url String
206+
$base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
207+
208+
// Encode Payload to Base64Url String
209+
$base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload));
210+
211+
// Create Signature Hash
212+
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $client->getSecret(), true);
213+
214+
// Encode Signature to Base64Url String
215+
$base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));
216+
217+
// Create JWT
218+
$jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
219+
175220
return new JSONResponse(
176221
[
177222
'access_token' => $newToken,
178223
'token_type' => 'Bearer',
179224
'expires_in' => 3600,
180225
'refresh_token' => $newCode,
181226
'user_id' => $appToken->getUID(),
227+
'id_token' => $jwt,
182228
]
183229
);
184230
}

apps/oauth2/tests/Controller/OauthApiControllerTest.php

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
use OCP\AppFramework\Http\JSONResponse;
3838
use OCP\AppFramework\Utility\ITimeFactory;
3939
use OCP\IRequest;
40+
use OCP\IUser;
41+
use OCP\IUserManager;
4042
use OCP\Security\ICrypto;
4143
use OCP\Security\ISecureRandom;
4244
use Test\TestCase;
@@ -58,6 +60,8 @@ class OauthApiControllerTest extends TestCase {
5860
private $time;
5961
/** @var Throttler|\PHPUnit_Framework_MockObject_MockObject */
6062
private $throttler;
63+
/** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
64+
private $userManager;
6165
/** @var OauthApiController */
6266
private $oauthApiController;
6367

@@ -72,6 +76,7 @@ public function setUp() {
7276
$this->secureRandom = $this->createMock(ISecureRandom::class);
7377
$this->time = $this->createMock(ITimeFactory::class);
7478
$this->throttler = $this->createMock(Throttler::class);
79+
$this->userManager = $this->createMock(IUserManager::class);
7580

7681
$this->oauthApiController = new OauthApiController(
7782
'oauth2',
@@ -82,7 +87,8 @@ public function setUp() {
8287
$this->tokenProvider,
8388
$this->secureRandom,
8489
$this->time,
85-
$this->throttler
90+
$this->throttler,
91+
$this->userManager
8692
);
8793
}
8894

@@ -287,6 +293,16 @@ public function testGetTokenValidAppToken() {
287293
'expires_in' => 3600,
288294
'refresh_token' => 'random128',
289295
'user_id' => 'userId',
296+
'id_token' => $this->encodeJWT(json_encode([
297+
'iss' => 'http://localhost',
298+
'sub' => 'userId',
299+
'aud' => 'clientId',
300+
'exp' => 4600,
301+
'iat' => 1000,
302+
'auth_time' => 1000,
303+
'email' => null,
304+
'name' => null
305+
]), 'clientSecret')
290306
]);
291307

292308
$this->request->method('getRemoteAddress')
@@ -300,6 +316,13 @@ public function testGetTokenValidAppToken() {
300316
['user' => 'userId']
301317
);
302318

319+
$user = $this->createMock(IUser::class);;
320+
321+
$this->userManager->expects($this->once())
322+
->method('get')
323+
->with('userId')
324+
->willReturn($user);
325+
303326
$this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
304327
}
305328

@@ -379,6 +402,16 @@ public function testGetTokenValidAppTokenBasicAuth() {
379402
'expires_in' => 3600,
380403
'refresh_token' => 'random128',
381404
'user_id' => 'userId',
405+
'id_token' => $this->encodeJWT(json_encode([
406+
'iss' => 'http://localhost',
407+
'sub' => 'userId',
408+
'aud' => 'clientId',
409+
'exp' => 4600,
410+
'iat' => 1000,
411+
'auth_time' => 1000,
412+
'email' => null,
413+
'name' => null
414+
]), 'clientSecret'),
382415
]);
383416

384417
$this->request->server['PHP_AUTH_USER'] = 'clientId';
@@ -395,6 +428,13 @@ public function testGetTokenValidAppTokenBasicAuth() {
395428
['user' => 'userId']
396429
);
397430

431+
$user = $this->createMock(IUser::class);;
432+
433+
$this->userManager->expects($this->once())
434+
->method('get')
435+
->with('userId')
436+
->willReturn($user);
437+
398438
$this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null));
399439
}
400440

@@ -474,6 +514,16 @@ public function testGetTokenExpiredAppToken() {
474514
'expires_in' => 3600,
475515
'refresh_token' => 'random128',
476516
'user_id' => 'userId',
517+
'id_token' => $this->encodeJWT(json_encode([
518+
'iss' => 'http://localhost',
519+
'sub' => 'userId',
520+
'aud' => 'clientId',
521+
'exp' => 4600,
522+
'iat' => 1000,
523+
'auth_time' => 1000,
524+
'email' => null,
525+
'name' => null
526+
]), 'clientSecret'),
477527
]);
478528

479529
$this->request->method('getRemoteAddress')
@@ -487,6 +537,33 @@ public function testGetTokenExpiredAppToken() {
487537
['user' => 'userId']
488538
);
489539

540+
$user = $this->createMock(IUser::class);;
541+
542+
$this->userManager->expects($this->once())
543+
->method('get')
544+
->with('userId')
545+
->willReturn($user);
546+
490547
$this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret'));
491548
}
549+
550+
private function encodeJWT($payload, $secret) {
551+
// Create token header as a JSON string
552+
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
553+
554+
// Encode Header to Base64Url String
555+
$base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
556+
557+
// Encode Payload to Base64Url String
558+
$base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload));
559+
560+
// Create Signature Hash
561+
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
562+
563+
// Encode Signature to Base64Url String
564+
$base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));
565+
566+
// Create JWT
567+
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
568+
}
492569
}

0 commit comments

Comments
 (0)