From f9d384080403c1e66007c4516417de93d61de904 Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Wed, 21 Nov 2018 11:49:21 +0100 Subject: [PATCH 1/2] 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 --- .../lib/Controller/OauthApiController.php | 47 ++++++++++- .../Controller/OauthApiControllerTest.php | 79 ++++++++++++++++++- 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/apps/oauth2/lib/Controller/OauthApiController.php b/apps/oauth2/lib/Controller/OauthApiController.php index b05d3781e5..e3feeadb51 100644 --- a/apps/oauth2/lib/Controller/OauthApiController.php +++ b/apps/oauth2/lib/Controller/OauthApiController.php @@ -35,6 +35,7 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; +use OCP\IUserManager; use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; @@ -53,6 +54,8 @@ class OauthApiController extends Controller { private $time; /** @var Throttler */ private $throttler; + /** @var IUserManager */ + private $userManager; public function __construct(string $appName, IRequest $request, @@ -62,7 +65,8 @@ class OauthApiController extends Controller { TokenProvider $tokenProvider, ISecureRandom $secureRandom, ITimeFactory $time, - Throttler $throttler) { + Throttler $throttler, + IUserManager $userManager) { parent::__construct($appName, $request); $this->crypto = $crypto; $this->accessTokenMapper = $accessTokenMapper; @@ -71,6 +75,7 @@ class OauthApiController extends Controller { $this->secureRandom = $secureRandom; $this->time = $time; $this->throttler = $throttler; + $this->userManager = $userManager; } /** @@ -162,6 +167,45 @@ class OauthApiController extends Controller { $this->throttler->resetDelay($this->request->getRemoteAddress(), 'login', ['user' => $appToken->getUID()]); + // 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 + + // Create token header as a JSON string + $header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']); + + // We need the user to fill in name and email in the id_token + $user = $this->userManager->get($appToken->getUID()); + + // Create token payload as a JSON string + $payload = json_encode([ + // required for OIDC + 'iss' => \OC::$server->getURLGenerator()->getBaseUrl(), + 'sub' => $appToken->getUID(), + 'aud' => $client_id, + 'exp' => $appToken->getExpires(), + 'iat' => $this->time->getTime(), + 'auth_time' => $this->time->getTime(), + + // optional, can be requested by claims, we don't support requesting claims as of now, so we just send them always + 'email' => $user->getEMailAddress(), + 'name' => $user->getDisplayName(), + + ]); + + // Encode Header to Base64Url String + $base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header)); + + // Encode Payload to Base64Url String + $base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload)); + + // Create Signature Hash + $signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $client->getSecret(), true); + + // Encode Signature to Base64Url String + $base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature)); + + // Create JWT + $jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; + return new JSONResponse( [ 'access_token' => $newToken, @@ -169,6 +213,7 @@ class OauthApiController extends Controller { 'expires_in' => 3600, 'refresh_token' => $newCode, 'user_id' => $appToken->getUID(), + 'id_token' => $jwt, ] ); } diff --git a/apps/oauth2/tests/Controller/OauthApiControllerTest.php b/apps/oauth2/tests/Controller/OauthApiControllerTest.php index f5a8138fa2..a735087f1f 100644 --- a/apps/oauth2/tests/Controller/OauthApiControllerTest.php +++ b/apps/oauth2/tests/Controller/OauthApiControllerTest.php @@ -37,6 +37,8 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; +use OCP\IUser; +use OCP\IUserManager; use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; use Test\TestCase; @@ -58,6 +60,8 @@ class OauthApiControllerTest extends TestCase { private $time; /** @var Throttler|\PHPUnit_Framework_MockObject_MockObject */ private $throttler; + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + private $userManager; /** @var OauthApiController */ private $oauthApiController; @@ -72,6 +76,7 @@ class OauthApiControllerTest extends TestCase { $this->secureRandom = $this->createMock(ISecureRandom::class); $this->time = $this->createMock(ITimeFactory::class); $this->throttler = $this->createMock(Throttler::class); + $this->userManager = $this->createMock(IUserManager::class); $this->oauthApiController = new OauthApiController( 'oauth2', @@ -82,7 +87,8 @@ class OauthApiControllerTest extends TestCase { $this->tokenProvider, $this->secureRandom, $this->time, - $this->throttler + $this->throttler, + $this->userManager ); } @@ -287,6 +293,16 @@ class OauthApiControllerTest extends TestCase { 'expires_in' => 3600, 'refresh_token' => 'random128', 'user_id' => 'userId', + 'id_token' => $this->encodeJWT(json_encode([ + 'iss' => 'http://localhost', + 'sub' => 'userId', + 'aud' => 'clientId', + 'exp' => 4600, + 'iat' => 1000, + 'auth_time' => 1000, + 'email' => null, + 'name' => null + ]), 'clientSecret') ]); $this->request->method('getRemoteAddress') @@ -300,6 +316,13 @@ class OauthApiControllerTest extends TestCase { ['user' => 'userId'] ); + $user = $this->createMock(IUser::class);; + + $this->userManager->expects($this->once()) + ->method('get') + ->with('userId') + ->willReturn($user); + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret')); } @@ -379,6 +402,16 @@ class OauthApiControllerTest extends TestCase { 'expires_in' => 3600, 'refresh_token' => 'random128', 'user_id' => 'userId', + 'id_token' => $this->encodeJWT(json_encode([ + 'iss' => 'http://localhost', + 'sub' => 'userId', + 'aud' => 'clientId', + 'exp' => 4600, + 'iat' => 1000, + 'auth_time' => 1000, + 'email' => null, + 'name' => null + ]), 'clientSecret'), ]); $this->request->server['PHP_AUTH_USER'] = 'clientId'; @@ -395,6 +428,13 @@ class OauthApiControllerTest extends TestCase { ['user' => 'userId'] ); + $user = $this->createMock(IUser::class);; + + $this->userManager->expects($this->once()) + ->method('get') + ->with('userId') + ->willReturn($user); + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null)); } @@ -474,6 +514,16 @@ class OauthApiControllerTest extends TestCase { 'expires_in' => 3600, 'refresh_token' => 'random128', 'user_id' => 'userId', + 'id_token' => $this->encodeJWT(json_encode([ + 'iss' => 'http://localhost', + 'sub' => 'userId', + 'aud' => 'clientId', + 'exp' => 4600, + 'iat' => 1000, + 'auth_time' => 1000, + 'email' => null, + 'name' => null + ]), 'clientSecret'), ]); $this->request->method('getRemoteAddress') @@ -487,6 +537,33 @@ class OauthApiControllerTest extends TestCase { ['user' => 'userId'] ); + $user = $this->createMock(IUser::class);; + + $this->userManager->expects($this->once()) + ->method('get') + ->with('userId') + ->willReturn($user); + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret')); } + + private function encodeJWT($payload, $secret) { + // Create token header as a JSON string + $header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']); + + // Encode Header to Base64Url String + $base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header)); + + // Encode Payload to Base64Url String + $base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload)); + + // Create Signature Hash + $signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true); + + // Encode Signature to Base64Url String + $base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature)); + + // Create JWT + return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; + } } From 21eef41caaefaf3272f9c4bc42e12c4d995487b5 Mon Sep 17 00:00:00 2001 From: Markus Heberling Date: Fri, 15 Feb 2019 15:32:12 +0100 Subject: [PATCH 2/2] extract idToken generation into own function Signed-off-by: Markus Heberling --- .../lib/Controller/OauthApiController.php | 55 ++++++++++++++----- .../Controller/OauthApiControllerTest.php | 16 +++++- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/apps/oauth2/lib/Controller/OauthApiController.php b/apps/oauth2/lib/Controller/OauthApiController.php index e3feeadb51..84ade899c6 100644 --- a/apps/oauth2/lib/Controller/OauthApiController.php +++ b/apps/oauth2/lib/Controller/OauthApiController.php @@ -35,6 +35,7 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; +use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; @@ -54,8 +55,10 @@ class OauthApiController extends Controller { private $time; /** @var Throttler */ private $throttler; - /** @var IUserManager */ + /** @var IUserManager */ private $userManager; + /** @var IURLGenerator */ + private $urlGenerator; public function __construct(string $appName, IRequest $request, @@ -66,7 +69,8 @@ class OauthApiController extends Controller { ISecureRandom $secureRandom, ITimeFactory $time, Throttler $throttler, - IUserManager $userManager) { + IUserManager $userManager, + IURLGenerator $urlGenerator) { parent::__construct($appName, $request); $this->crypto = $crypto; $this->accessTokenMapper = $accessTokenMapper; @@ -76,6 +80,7 @@ class OauthApiController extends Controller { $this->time = $time; $this->throttler = $throttler; $this->userManager = $userManager; + $this->urlGenerator = $urlGenerator; } /** @@ -166,7 +171,28 @@ class OauthApiController extends Controller { $this->accessTokenMapper->update($accessToken); $this->throttler->resetDelay($this->request->getRemoteAddress(), 'login', ['user' => $appToken->getUID()]); + $jwt = $this->getIdToken($client_id, $appToken, $client); + return new JSONResponse( + [ + 'access_token' => $newToken, + 'token_type' => 'Bearer', + 'expires_in' => 3600, + 'refresh_token' => $newCode, + 'user_id' => $appToken->getUID(), + 'id_token' => $jwt, + ] + ); + } + + /** + * @param $client_id + * @param \OC\Authentication\Token\IToken $appToken + * @param \OCA\OAuth2\Db\Client $client + * @return string + */ + private function getIdToken($client_id, \OC\Authentication\Token\IToken $appToken, \OCA\OAuth2\Db\Client $client) + { // 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 // Create token header as a JSON string @@ -177,16 +203,25 @@ class OauthApiController extends Controller { // Create token payload as a JSON string $payload = json_encode([ - // required for OIDC - 'iss' => \OC::$server->getURLGenerator()->getBaseUrl(), + // required for OIDC, see https://openid.net/specs/openid-connect-core-1_0.html#IDToken + // Issuer Identifier for the Issuer of the response. + 'iss' => $this->urlGenerator->getBaseUrl(), + // Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client 'sub' => $appToken->getUID(), + // Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. 'aud' => $client_id, + // Expiration time on or after which the ID Token MUST NOT be accepted for processing. 'exp' => $appToken->getExpires(), + // Time at which the JWT was issued. 'iat' => $this->time->getTime(), + // Time when the End-User authentication occurred. 'auth_time' => $this->time->getTime(), // optional, can be requested by claims, we don't support requesting claims as of now, so we just send them always + // see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + // End-User's preferred e-mail address. 'email' => $user->getEMailAddress(), + // End-User's full name in displayable form including all name parts, possibly including titles and suffixes 'name' => $user->getDisplayName(), ]); @@ -205,16 +240,6 @@ class OauthApiController extends Controller { // Create JWT $jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; - - return new JSONResponse( - [ - 'access_token' => $newToken, - 'token_type' => 'Bearer', - 'expires_in' => 3600, - 'refresh_token' => $newCode, - 'user_id' => $appToken->getUID(), - 'id_token' => $jwt, - ] - ); + return $jwt; } } diff --git a/apps/oauth2/tests/Controller/OauthApiControllerTest.php b/apps/oauth2/tests/Controller/OauthApiControllerTest.php index a735087f1f..704cfe8f75 100644 --- a/apps/oauth2/tests/Controller/OauthApiControllerTest.php +++ b/apps/oauth2/tests/Controller/OauthApiControllerTest.php @@ -37,6 +37,7 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\Security\ICrypto; @@ -62,6 +63,8 @@ class OauthApiControllerTest extends TestCase { private $throttler; /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ private $userManager; + /** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */ + private $urlGenerator; /** @var OauthApiController */ private $oauthApiController; @@ -77,6 +80,7 @@ class OauthApiControllerTest extends TestCase { $this->time = $this->createMock(ITimeFactory::class); $this->throttler = $this->createMock(Throttler::class); $this->userManager = $this->createMock(IUserManager::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->oauthApiController = new OauthApiController( 'oauth2', @@ -88,7 +92,8 @@ class OauthApiControllerTest extends TestCase { $this->secureRandom, $this->time, $this->throttler, - $this->userManager + $this->userManager, + $this->urlGenerator ); } @@ -287,6 +292,9 @@ class OauthApiControllerTest extends TestCase { }) ); + $this->urlGenerator->method('getBaseUrl') + ->willReturn('http://localhost'); + $expected = new JSONResponse([ 'access_token' => 'random72', 'token_type' => 'Bearer', @@ -396,6 +404,9 @@ class OauthApiControllerTest extends TestCase { }) ); + $this->urlGenerator->method('getBaseUrl') + ->willReturn('http://localhost'); + $expected = new JSONResponse([ 'access_token' => 'random72', 'token_type' => 'Bearer', @@ -508,6 +519,9 @@ class OauthApiControllerTest extends TestCase { }) ); + $this->urlGenerator->method('getBaseUrl') + ->willReturn('http://localhost'); + $expected = new JSONResponse([ 'access_token' => 'random72', 'token_type' => 'Bearer',