diff --git a/apps/oauth2/lib/Controller/OauthApiController.php b/apps/oauth2/lib/Controller/OauthApiController.php index 2fbaf45626..ad0266c395 100644 --- a/apps/oauth2/lib/Controller/OauthApiController.php +++ b/apps/oauth2/lib/Controller/OauthApiController.php @@ -41,6 +41,8 @@ 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; @@ -59,6 +61,10 @@ class OauthApiController extends Controller { private $time; /** @var Throttler */ private $throttler; + /** @var IUserManager */ + private $userManager; + /** @var IURLGenerator */ + private $urlGenerator; public function __construct(string $appName, IRequest $request, @@ -68,7 +74,9 @@ class OauthApiController extends Controller { TokenProvider $tokenProvider, ISecureRandom $secureRandom, ITimeFactory $time, - Throttler $throttler) { + Throttler $throttler, + IUserManager $userManager, + IURLGenerator $urlGenerator) { parent::__construct($appName, $request); $this->crypto = $crypto; $this->accessTokenMapper = $accessTokenMapper; @@ -77,6 +85,8 @@ class OauthApiController extends Controller { $this->secureRandom = $secureRandom; $this->time = $time; $this->throttler = $throttler; + $this->userManager = $userManager; + $this->urlGenerator = $urlGenerator; } /** @@ -167,6 +177,7 @@ 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( [ @@ -175,7 +186,66 @@ class OauthApiController extends Controller { '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 + $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, 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(), + + ]); + + // 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 $jwt; + } } diff --git a/apps/oauth2/tests/Controller/OauthApiControllerTest.php b/apps/oauth2/tests/Controller/OauthApiControllerTest.php index 414bcd3e43..83642b43ac 100644 --- a/apps/oauth2/tests/Controller/OauthApiControllerTest.php +++ b/apps/oauth2/tests/Controller/OauthApiControllerTest.php @@ -42,6 +42,9 @@ 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; use OCP\Security\ISecureRandom; use Test\TestCase; @@ -63,6 +66,10 @@ class OauthApiControllerTest extends TestCase { private $time; /** @var Throttler|\PHPUnit\Framework\MockObject\MockObject */ private $throttler; + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + private $userManager; + /** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */ + private $urlGenerator; /** @var OauthApiController */ private $oauthApiController; @@ -77,6 +84,8 @@ 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->urlGenerator = $this->createMock(IURLGenerator::class); $this->oauthApiController = new OauthApiController( 'oauth2', @@ -87,7 +96,9 @@ class OauthApiControllerTest extends TestCase { $this->tokenProvider, $this->secureRandom, $this->time, - $this->throttler + $this->throttler, + $this->userManager, + $this->urlGenerator ); } @@ -286,12 +297,25 @@ class OauthApiControllerTest extends TestCase { }) ); + $this->urlGenerator->method('getBaseUrl') + ->willReturn('http://localhost'); + $expected = new JSONResponse([ 'access_token' => 'random72', 'token_type' => 'Bearer', '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') @@ -305,6 +329,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')); } @@ -378,12 +409,25 @@ class OauthApiControllerTest extends TestCase { }) ); + $this->urlGenerator->method('getBaseUrl') + ->willReturn('http://localhost'); + $expected = new JSONResponse([ 'access_token' => 'random72', 'token_type' => 'Bearer', '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'; @@ -400,6 +444,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)); } @@ -473,12 +524,25 @@ class OauthApiControllerTest extends TestCase { }) ); + $this->urlGenerator->method('getBaseUrl') + ->willReturn('http://localhost'); + $expected = new JSONResponse([ 'access_token' => 'random72', 'token_type' => 'Bearer', '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') @@ -492,6 +556,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; + } }