diff --git a/.gitignore b/.gitignore index f70f2e112eb1ef6f2f6d9ac0bc3ad73689ff02d6..5d63beec995d5919657b6e8e5b4466fbabd4a318 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ logs/ vendor/ client-icons/ /nbproject/ +*.pem diff --git a/classes/CallbackHandler.php b/classes/CallbackHandler.php index dfd6c5d935ea772fb6bbe867ec4ad96615fbf7a4..5c5fbbee0b5b804c8d222ec6d8bbee9c299d7896 100644 --- a/classes/CallbackHandler.php +++ b/classes/CallbackHandler.php @@ -29,14 +29,10 @@ namespace RAP; */ class CallbackHandler { - private $dao; - private $basePath; - private $callbacks; - - public function __construct(DAO $dao, $basePath, $callbacks) { - $this->dao = $dao; - $this->basePath = $basePath; - $this->callbacks = $callbacks; + private $locator; + + public function __construct(Locator $locator) { + $this->locator = $locator; } /** @@ -91,8 +87,17 @@ class CallbackHandler { return null; } - public function manageLoginRedirect($user, SessionData $session) { - + public function manageLoginRedirect(User $user, SessionData $session) { + + if($session->getOAuth2Data() !== null) { + $session->user = $user; + $session->save(); + $redirectUrl = $this->locator->getOAuth2RequestHandler()->getCodeResponseUrl(); + $session->setOAuth2Data(null); + header('Location: ' . $redirectUrl); + die(); + } + if ($session->getCallbackURL() === null) { http_response_code(401); die("Unauthorized callback URL"); diff --git a/classes/DAO.php b/classes/DAO.php index 51b48f141ff15a6649111fc1ad135862b81adc41..54bc5ff6204aae74e7dbb8e9b0176846766b7a26 100644 --- a/classes/DAO.php +++ b/classes/DAO.php @@ -40,22 +40,20 @@ interface DAO { * @param type $token login token * @param type $userId */ - function createLoginToken($token, $userId); + function createAccessToken(string $token, string $code, string $userId): string; /** - * Retrieve the user ID from the login token. - * @param type $token - * @return type user ID + * Retrieve the access token value from the code. */ - function findLoginToken($token); + function findAccessToken(string $code): ?string; /** - * Delete a login token from the database. This happens when the caller + * Delete an access token from the database. This happens when the caller * application has received the token and used it for retrieving user * information from the token using the RAP REST web service. * @param type $token login token */ - function deleteLoginToken($token); + function deleteAccessToken(string $token): void; /** * Create a new identity. @@ -130,9 +128,9 @@ interface DAO { */ function getOAuth2Clients(); - function createOAuth2Client($client) : OAuth2Client; + function createOAuth2Client($client): OAuth2Client; - function updateOAuth2Client($client) : OAuth2Client; + function updateOAuth2Client($client): OAuth2Client; function deleteOAuth2Client($clientId); @@ -140,5 +138,5 @@ interface DAO { * Retrieve the client from the configured client id (the one associated to * the secret, not the database id). */ - function getOAuth2ClientByClientId($clientId) : ?OAuth2Client; + function getOAuth2ClientByClientId($clientId): ?OAuth2Client; } diff --git a/classes/JWKSHandler.php b/classes/JWKSHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..eb49951642fa183fb97563e663f1e88d2a0ce307 --- /dev/null +++ b/classes/JWKSHandler.php @@ -0,0 +1,14 @@ +<?php + +namespace RAP; + +/** + * Manages the JWT Key Sets. + */ +class JWKSHandler { + + public function generateKeyPair() { + + } + +} diff --git a/classes/Locator.php b/classes/Locator.php index d4a6883e3e61bb152667a240eb8c403c0b8229dd..bee9b493b244e1b0a27626ab47d59040685db9c5 100644 --- a/classes/Locator.php +++ b/classes/Locator.php @@ -39,7 +39,7 @@ class Locator { } public function getCallbackHandler(): CallbackHandler { - return new \RAP\CallbackHandler($dao, $this->getBasePath()); + return new \RAP\CallbackHandler($this); } public function getUserHandler(): UserHandler { @@ -50,6 +50,10 @@ class Locator { return new \RAP\MailSender($_SERVER['HTTP_HOST'], $this->getBasePath()); } + public function getOAuth2RequestHandler(): OAuth2RequestHandler { + return new \RAP\OAuth2RequestHandler($this); + } + /** * Retrieve the SessionData object from the $_SESSION PHP variable. Create a * new one if it is necessary. @@ -64,11 +68,11 @@ class Locator { return $this->session; } - public function getServiceLogger() { + public function getServiceLogger(): \Monolog\Logger { return $this->serviceLogger; } - public function getAuditLogger() { + public function getAuditLogger(): \Monolog\Logger { return $this->auditLogger; } diff --git a/classes/MySQLDAO.php b/classes/MySQLDAO.php index 6033582830ae93c5bbeea71f95ef0ca05e926c38..7dc0fd5c911d8d4f8d54280727273538bf378f17 100644 --- a/classes/MySQLDAO.php +++ b/classes/MySQLDAO.php @@ -45,13 +45,14 @@ class MySQLDAO implements DAO { return $dbh; } - public function createLoginToken($token, $userId) { + public function createAccessToken(string $token, string $code, string $userId): string { $dbh = $this->getDBHandler(); - $stmt = $dbh->prepare("INSERT INTO login_token (token, user_id) VALUES(:token, :user_id)"); + $stmt = $dbh->prepare("INSERT INTO access_token (token, code, user_id) VALUES(:token, :code, :user_id)"); $params = array( ':token' => $token, + ':code' => $code, ':user_id' => $userId ); @@ -63,27 +64,27 @@ class MySQLDAO implements DAO { } } - public function findLoginToken($token) { + public function findAccessToken(string $code): ?string { $dbh = $this->getDBHandler(); - $stmt = $dbh->prepare("SELECT user_id FROM login_token WHERE token = :token AND CURRENT_TIMESTAMP < TIMESTAMPADD(MINUTE,1,creation_time)"); - $stmt->bindParam(':token', $token); + $stmt = $dbh->prepare("SELECT token FROM access_token WHERE code = :code");// AND CURRENT_TIMESTAMP < TIMESTAMPADD(MINUTE,1,creation_time)"); + $stmt->bindParam(':code', $code); $stmt->execute(); foreach ($stmt->fetchAll() as $row) { - return $row['user_id']; + return $row['token']; } return null; } - public function deleteLoginToken($token) { + public function deleteAccessToken($token): void { $dbh = $this->getDBHandler(); - $stmt = $dbh->prepare("DELETE FROM login_token WHERE token = :token"); + $stmt = $dbh->prepare("DELETE FROM access_token WHERE token = :token"); $stmt->bindParam(':token', $token); $stmt->execute(); } @@ -373,7 +374,7 @@ class MySQLDAO implements DAO { return $clients; } - function createOAuth2Client($client) : OAuth2Client { + function createOAuth2Client($client): OAuth2Client { $dbh = $this->getDBHandler(); try { @@ -412,7 +413,7 @@ class MySQLDAO implements DAO { return $client; } - function updateOAuth2Client($client) : OAuth2Client { + function updateOAuth2Client($client): OAuth2Client { $dbh = $this->getDBHandler(); try { @@ -478,7 +479,7 @@ class MySQLDAO implements DAO { } } - function getOAuth2ClientByClientId($clientId) : ?OAuth2Client { + function getOAuth2ClientByClientId($clientId): ?OAuth2Client { $dbh = $this->getDBHandler(); // Load clients info diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php index e947368bf53c0ede8360ca2c69371ee262a7ac2c..de070dbd6477a04e329dc11f7e7dac2af4810a5e 100644 --- a/classes/OAuth2RequestHandler.php +++ b/classes/OAuth2RequestHandler.php @@ -61,4 +61,82 @@ class OAuth2RequestHandler { $session->setOAuth2Data($oauth2Data); } + public function getCodeResponseUrl(): string { + + $session = $this->locator->getSession(); + + $code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64))); + $accessToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); + $state = $session->getOAuth2Data()->state; + + $userId = $session->user->id; + + $this->locator->getDAO()->createAccessToken($accessToken, $code, $userId); + + $redirectUrl = $session->getOAuth2Data()->redirectUrl + . '?code=' . $code . '&scope=profile&state=' . $state; + + return $redirectUrl; + } + + public function handleAccessTokenRequest(): array { + + $this->validateAccessTokenRequest(); + + $code = filter_input(INPUT_POST, 'code', FILTER_SANITIZE_STRING); + $accessToken = $this->locator->getDAO()->findAccessToken($code); + + $this->validateParametersMatching(); + + $token = []; + $token['access_token'] = $accessToken; + $token['token_type'] = 'bearer'; + $token['expires_in'] = 300; + + return $token; + } + + private function validateAccessTokenRequest() { + + if (!isset($_POST['grant_type'])) { + throw new BadRequestException("Client id is required"); + } else if ($_POST['grant_type'] !== 'authorization_code') { + throw new BadRequestException("grant_type must be authorization_code"); + } + + if (!isset($_POST['code'])) { + throw new BadRequestException("Client id is required"); + } + + if (!isset($_POST['redirect_uri'])) { + throw new BadRequestException("Redirect URI is required"); + } + + // Note: theorically the standard wants also the client_id here, + // however some clients don't send it + } + + private function validateParametersMatching() { + + } + + public function handleCheckTokenRequest(): array { + + if (!isset($_POST['token'])) { + throw new BadRequestException("Access token id is required"); + } + + $accessToken = filter_input(INPUT_POST, 'token', FILTER_SANITIZE_STRING); + + //if($accessToken) + + $result = []; + $result['exp'] = 3600; + $result['user_name'] = "test"; + $result['client_id'] = "gms"; + $result['scope'] = "profile"; + + return $result; + } + } diff --git a/classes/SessionData.php b/classes/SessionData.php index 878459407f3ab67fbde751dcd905b8c7c919cba9..ee0e0bc000ce46c56e58dafbecc0d28c37a4871b 100644 --- a/classes/SessionData.php +++ b/classes/SessionData.php @@ -60,7 +60,7 @@ class SessionData { } } - public function setOAuth2Data(OAuth2Data $oauth2Data) { + public function setOAuth2Data(?OAuth2Data $oauth2Data) { $this->oauth2Data = $oauth2Data; $this->save(); } diff --git a/classes/UserHandler.php b/classes/UserHandler.php index 4614a8f17b5004b7552c16e2a60eda7d56f8a76b..ec0002910d5c447a683dd73558934ab1d9f03003 100644 --- a/classes/UserHandler.php +++ b/classes/UserHandler.php @@ -30,11 +30,9 @@ namespace RAP; class UserHandler { private $dao; - private $grouperConfig; - public function __construct(DAO $dao, $grouperConfig) { + public function __construct(DAO $dao) { $this->dao = $dao; - $this->grouperConfig = $grouperConfig; } /** diff --git a/classes/social/GoogleLogin.php b/classes/social/GoogleLogin.php new file mode 100644 index 0000000000000000000000000000000000000000..f2174d092a40fef77e5648783b5032d7d218feaa --- /dev/null +++ b/classes/social/GoogleLogin.php @@ -0,0 +1,97 @@ +<?php + +namespace RAP; + +class GoogleLogin { + + protected $locator; + + public function __construct(Locator $locator) { + $this->locator = $locator; + } + + public function call() { + // Retrieve Google configuration + + $Google = $this->locator->config->authenticationMethods->Google; + + $client = new \Google_Client(array( + 'client_id' => $Google->id, + 'client_secret' => $Google->secret, + 'redirect_uri' => $this->locator->getBasePath() . $Google->callback, + )); + + // Ask permission to obtain user email and profile information + $client->setScopes(array(\Google_Service_People::USERINFO_EMAIL, \Google_Service_People::USERINFO_PROFILE)); + + if (isset($_REQUEST['logout'])) { + // Reset the access token stored into the session + unset($_SESSION['access_token']); + } + + if (isset($_GET['code'])) { + // An access token has been returned from the auth URL. + $client->authenticate($_GET['code']); + $_SESSION['access_token'] = $client->getAccessToken(); + } + + if ($client->getAccessToken()) { + + // Query web service for retrieving user information + $service = new \Google_Service_People($client); + + try { + $res = $service->people->get('people/me', array('requestMask.includeField' => 'person.names,person.email_addresses')); + } catch (Google_Service_Exception $e) { + echo '<p>' . json_encode($e->getErrors()) . '</p>'; + $thisPage = $PROTOCOL . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']; + echo '<p><a href="' . $thisPage . '?logout">Click here to unset the access token</a></p>'; + } + + $name = $res->getNames()[0]->getGivenName(); + $surname = $res->getNames()[0]->getFamilyName(); + + $emailAddresses = []; + foreach ($res->getEmailAddresses() as $addr) { + array_push($emailAddresses, $addr->value); + } + + $typedId = explode('/', $res->getResourceName())[1]; + + // Search if the user is already registered into RAP using the Google ID. + $user = $this->locator->getUserHandler()->findUserByIdentity(Identity::GOOGLE, $typedId); + + $session = $this->locator->getSession(); + + if ($user === null) { + // Create new user + $user = new \RAP\User(); + + $identity = new Identity(Identity::GOOGLE); + $identity->email = $emailAddresses[0]; + $identity->name = $name; + $identity->surname = $surname; + $identity->typedId = $typedId; + + $user->addIdentity($identity); + + $session->userToLogin = $user; + $session->save(); + + header('Location: ' . $this->locator->getBasePath() . '/tou-check'); + die(); + } + + $this->locator->getAuditLogger()->info("LOGIN,Google," . $user->id); + $this->locator->getCallbackHandler()->manageLoginRedirect($user, $session); + + die(); + } else { + // Redirect to Google authorization URL for obtaining an access token + $authUrl = $client->createAuthUrl(); + header('Location: ' . $authUrl); + die(); + } + } + +} diff --git a/config-example.json b/config-example.json index 7cf6ab4b58b1124f84a479c8314d5e30c08fc54c..f73a89ee9e4d51dc28b64c5226584609c11c6e9d 100644 --- a/config-example.json +++ b/config-example.json @@ -17,7 +17,7 @@ "Google": { "id": "XXXXXX", "secret": "XXXXXX", - "callback": "/auth/social/google_token.php" + "callback": "/auth/social/google" }, "Facebook": { "id": "XXXXXX", diff --git a/include/front-controller.php b/include/front-controller.php index 0c2d79e14032daf4e1e57788cee28ac47fb95df9..8ab6e953554a3b273ac026101eab13b78409ca9a 100644 --- a/include/front-controller.php +++ b/include/front-controller.php @@ -22,7 +22,7 @@ function setCallback($callback) { * services list if a valid callback is not found */ Flight::route('/', function() { - + session_start(); global $locator; @@ -36,6 +36,7 @@ Flight::route('/', function() { renderMainPage($authPageModel); break; default: + session_destroy(); Flight::render('services-list.php', array('title' => 'RAP', 'version' => $locator->getVersion(), 'action' => $locator->getBasePath() . '/')); @@ -49,7 +50,7 @@ function renderMainPage(RAP\AuthPageModel $authPageModel) { 'version' => $locator->getVersion(), 'model' => $authPageModel)); } -Flight::route('/oauth2/authorize', function() { +Flight::route('GET /auth/oauth2/authorize', function() { session_start(); global $locator; @@ -60,10 +61,34 @@ Flight::route('/oauth2/authorize', function() { Flight::redirect('/?action=oaut2client'); }); -Flight::route('GET /admin', function() { +Flight::route('POST /auth/oauth2/token', function() { - session_start(); global $locator; + + $requestHandler = new \RAP\OAuth2RequestHandler($locator); + $token = $requestHandler->handleAccessTokenRequest(); + + Flight::json($token); +}); + +Flight::route('POST /auth/oauth2/check_token', function() { + + global $locator; + + $requestHandler = new \RAP\OAuth2RequestHandler($locator); + $token = $requestHandler->handleCheckTokenRequest(); + + Flight::json($token); +}); + +Flight::route('GET /auth/oidc/jwks', function() { + + global $locator; + + $requestHandler = new \RAP\OAuth2RequestHandler($locator); + $token = $requestHandler->handleCheckTokenRequest(); + + Flight::json($token); }); Flight::route('GET /logout', function() { @@ -80,8 +105,11 @@ function sendAuthRedirect($url) { Flight::redirect($url); } -Flight::route('/google', function() { - sendAuthRedirect('/auth/social/google_token.php'); +Flight::route('/auth/social/google', function() { + session_start(); + global $locator; + $googleLogin = new \RAP\GoogleLogin($locator); + $googleLogin->call(); }); Flight::route('/facebook', function() { diff --git a/include/init.php b/include/init.php index b366300b9c2c1283b3de92a6ebbfae1eab3ea0ff..ca98a295f8055497aca32ec23799ff15ee54a2e1 100644 --- a/include/init.php +++ b/include/init.php @@ -28,12 +28,21 @@ define('ROOT', dirname(dirname(__FILE__))); // Defining autoload for RAP classes -spl_autoload_register(function ($class_name) { +spl_autoload_register(function ($class) { $prefix = "RAP\\"; $len = strlen($prefix); - if (strncmp($prefix, $class_name, $len) === 0) { - $classpath = ROOT . '/classes/' . substr($class_name, $len, strlen($class_name) - $len) . '.php'; - require $classpath; + + if (strncmp($prefix, $class, $len) === 0) { + + $fileName = substr($class, $len, strlen($class) - $len); + + $classDirectories = ['/', '/social/', '/exceptions/']; + foreach ($classDirectories as $directory) { + $classpath = ROOT . '/classes' . $directory . $fileName . '.php'; + if (file_exists($classpath)) { + require_once $classpath; + } + } } }); diff --git a/index.php b/index.php index 754f719c6ba324aeb4dd06185284f4094c3d769c..777c43d366a87a7484aa56c5f065e1e1a748014b 100644 --- a/index.php +++ b/index.php @@ -30,3 +30,14 @@ include './include/rest-web-service.php'; // Starting Flight framework Flight::start(); + +// Error handling +Flight::map('error', function(Exception $ex) { + if ($ex instanceof \RAP\BadRequestException) { + http_response_code(400); + echo "Bad request: " . $ex->message; + } else { + echo $ex->getTraceAsString(); + } +}); + diff --git a/sql/setup-database.sql b/sql/setup-database.sql index c4faac8a8d7fdb6cf21fc46ecb74d4757e1aff6d..11f927025053b39e7f559d800680e860ab05f59a 100644 --- a/sql/setup-database.sql +++ b/sql/setup-database.sql @@ -42,10 +42,11 @@ SET FOREIGN_KEY_CHECKS=0; ALTER TABLE `user` ADD FOREIGN KEY (`primary_identity`) REFERENCES `identity`(`id`); SET FOREIGN_KEY_CHECKS=1; -CREATE TABLE `login_token` ( +CREATE TABLE `access_token` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, - `token` varchar(255) NOT NULL, - `user_id` text, + `token` text NOT NULL, + `user_id` text NOT NULL, + `code` text NOT NULL, `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/views/main-page.php b/views/main-page.php index 36bc2d36b2711f5c0da4cde9c7a34c6ab994f0ce..1aa6c049ce5423e885c593ff03f9598fd9a19919 100644 --- a/views/main-page.php +++ b/views/main-page.php @@ -33,7 +33,7 @@ include 'include/header.php'; <div class="home-box"> <div class="img-wrapper"> <?php if ($model->google) { ?> - <a href="google" class="animated pulse"> + <a href="auth/social/google" class="animated pulse"> <img src="img/google-60.png" alt="Google Logo" /> </a> <?php } ?>