Skip to content

Commit 3f95ae5

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 <[email protected]>
1 parent 7698adf commit 3f95ae5

File tree

2 files changed

+120
-2
lines changed

2 files changed

+120
-2
lines changed

apps/oauth2/lib/Controller/OauthApiController.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public function __construct(string $appName,
6262
TokenProvider $tokenProvider,
6363
ISecureRandom $secureRandom,
6464
ITimeFactory $time,
65-
Throttler $throttler) {
65+
Throttler $throttler,
66+
IUserManager $userManager) {
6667
parent::__construct($appName, $request);
6768
$this->crypto = $crypto;
6869
$this->accessTokenMapper = $accessTokenMapper;
@@ -162,13 +163,53 @@ public function getToken($grant_type, $code, $refresh_token, $client_id, $client
162163

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

166+
// 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
167+
168+
// Create token header as a JSON string
169+
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
170+
171+
// We need the user to fill in name and email in the id_token
172+
$user = $this->userManager->get($appToken->getUID());
173+
174+
// Create token payload as a JSON string
175+
$payload = json_encode([
176+
// required for OIDC
177+
'iss' => \OC::$server->getURLGenerator()->getBaseUrl(),
178+
'sub' => $appToken->getUID(),
179+
'aud' => $client_id,
180+
'exp' => $appToken->getExpires(),
181+
'iat' => $this->time->getTime(),
182+
'auth_time' => $this->time->getTime(),
183+
184+
// optional, can be requested by claims, we don't support requesting claims as of now, so we just send them always
185+
'email' => $user->getEMailAddress(),
186+
'name' => $user->getDisplayName(),
187+
188+
]);
189+
190+
// Encode Header to Base64Url String
191+
$base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
192+
193+
// Encode Payload to Base64Url String
194+
$base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload));
195+
196+
// Create Signature Hash
197+
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $client->getSecret(), true);
198+
199+
// Encode Signature to Base64Url String
200+
$base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));
201+
202+
// Create JWT
203+
$jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
204+
165205
return new JSONResponse(
166206
[
167207
'access_token' => $newToken,
168208
'token_type' => 'Bearer',
169209
'expires_in' => 3600,
170210
'refresh_token' => $newCode,
171211
'user_id' => $appToken->getUID(),
212+
'id_token' => $jwt,
172213
]
173214
);
174215
}

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)