From 5b9aed6ce65a65947c5803f2d13f9f274bf82287 Mon Sep 17 00:00:00 2001 From: dakriy Date: Sat, 22 Feb 2020 17:53:02 -0800 Subject: [PATCH] Fix tons of bugs and actually get auth workin for saml and everything else --- simple-saml-idp/config/config.php | 2 +- simple-saml-idp/metadata/saml20-sp-remote.php | 12 +- simple-saml/config/authsources.php | 2 +- simple-saml/metadata/saml20-idp-remote.php | 8 +- .../app/Http/Controllers/AuthController.php | 64 +++++++-- src/web/backend/app/Http/Kernel.php | 1 + .../app/Http/Middleware/EncryptCookies.php | 1 - .../app/Providers/AppServiceProvider.php | 2 + src/web/backend/config/saml.php | 58 +++++++++ .../2014_10_12_000000_create_users_table.php | 2 +- src/web/backend/routes/api.php | 2 + src/web/backend/routes/web.php | 7 + src/web/backend/src/Entities/SamlUser.php | 70 ++++++++++ src/web/backend/src/Entities/User.php | 28 ++-- .../Gateways/Saml/InMemorySamlRepository.php | 60 +++++++++ .../src/Gateways/Saml/SamlRepository.php | 40 ++++++ .../Saml/SamlRepositoryServiceProvider.php | 52 ++++++++ .../Saml/SimpleSamlPhpSamlRepository.php | 121 ++++++++++++++++++ .../Tokens/DatabaseTokensRepository.php | 58 ++++++--- .../Tokens/InMemoryTokensRepository.php | 9 ++ .../src/Gateways/Tokens/TokensRepository.php | 5 + .../Users/DatabaseUsersRepository.php | 59 ++++++--- .../Users/InMemoryUsersRepository.php | 13 ++ .../src/Gateways/Users/UsersRepository.php | 8 ++ .../Users/Authenticate/APIPresenter.php | 10 +- .../Users/Authenticate/Authenticate.php | 70 +++++++++- .../Authenticate/AuthenticateUseCase.php | 25 ++++ .../AuthenticateUseCaseServiceProvider.php | 7 +- .../Authenticate/UserCreationException.php | 14 ++ 29 files changed, 731 insertions(+), 79 deletions(-) create mode 100644 src/web/backend/config/saml.php create mode 100644 src/web/backend/src/Entities/SamlUser.php create mode 100644 src/web/backend/src/Gateways/Saml/InMemorySamlRepository.php create mode 100644 src/web/backend/src/Gateways/Saml/SamlRepository.php create mode 100644 src/web/backend/src/Gateways/Saml/SamlRepositoryServiceProvider.php create mode 100644 src/web/backend/src/Gateways/Saml/SimpleSamlPhpSamlRepository.php create mode 100644 src/web/backend/src/UseCases/Users/Authenticate/UserCreationException.php diff --git a/simple-saml-idp/config/config.php b/simple-saml-idp/config/config.php index b95366f2..3d60ee8d 100644 --- a/simple-saml-idp/config/config.php +++ b/simple-saml-idp/config/config.php @@ -27,7 +27,7 @@ $config = [ * external url, no matter where you come from (direct access or via the * reverse proxy). */ - 'baseurlpath' => 'http://localhost:8080/simplesaml-idp/', + 'baseurlpath' => 'https://localhost:8080/simplesaml-idp/', /* * The 'application' configuration array groups a set configuration options diff --git a/simple-saml-idp/metadata/saml20-sp-remote.php b/simple-saml-idp/metadata/saml20-sp-remote.php index 91e80755..8457070b 100644 --- a/simple-saml-idp/metadata/saml20-sp-remote.php +++ b/simple-saml-idp/metadata/saml20-sp-remote.php @@ -6,13 +6,13 @@ * See: https://simplesamlphp.org/docs/stable/simplesamlphp-reference-sp-remote */ -$metadata['http://localhost:8080/simplesaml/module.php/saml/sp/metadata.php/default-sp'] = array ( +$metadata['https://localhost:8080/simplesaml/module.php/saml/sp/metadata.php/default-sp'] = array ( 'SingleLogoutService' => array ( 0 => array ( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - 'Location' => 'http://localhost:8080/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp', + 'Location' => 'https://localhost:8080/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp', ), ), 'AssertionConsumerService' => @@ -21,25 +21,25 @@ $metadata['http://localhost:8080/simplesaml/module.php/saml/sp/metadata.php/defa array ( 'index' => 0, 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - 'Location' => 'http://localhost:8080/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', + 'Location' => 'https://localhost:8080/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', ), 1 => array ( 'index' => 1, 'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post', - 'Location' => 'http://localhost:8080/simplesaml/module.php/saml/sp/saml1-acs.php/default-sp', + 'Location' => 'https://localhost:8080/simplesaml/module.php/saml/sp/saml1-acs.php/default-sp', ), 2 => array ( 'index' => 2, 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact', - 'Location' => 'http://localhost:8080/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', + 'Location' => 'https://localhost:8080/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', ), 3 => array ( 'index' => 3, 'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01', - 'Location' => 'http://localhost:8080/simplesaml/module.php/saml/sp/saml1-acs.php/default-sp/artifact', + 'Location' => 'https://localhost:8080/simplesaml/module.php/saml/sp/saml1-acs.php/default-sp/artifact', ), ), 'certData' => 'MIIEwTCCAymgAwIBAgIUOBK2NqzhcX63Vj6AIoGNzzRp8WgwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xFjAUBgNVBAcMDUNvbGxlZ2UgUGxhY2UxHzAdBgNVBAoMFldhbGxhIFdhbGxhIFVuaXZlcnNpdHkxEzARBgNVBAsMCktyZXRzY2htYXIwHhcNMjAwMTIxMTAyODE1WhcNMzAwMTIwMTAyODE1WjBwMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEWMBQGA1UEBwwNQ29sbGVnZSBQbGFjZTEfMB0GA1UECgwWV2FsbGEgV2FsbGEgVW5pdmVyc2l0eTETMBEGA1UECwwKS3JldHNjaG1hcjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKSU6Xayh+chaCvKHwMuvKZcxt6QrtSfjxwHOd+xsGIrQXW/8LB9yAIalb/aKavfw6sQ+Ftt1rRXJM5QFFZiADrZuds/IHIaV+YhFH2JsBY6kmiGWzYsI0Cyq+wRW9vcuCSyYZb4r7xQw++kILD1Wg2rW7jNcq4E7Nm16tgVcQwztN4I3G7fTso9AHTv1yZRzpDzGAjiVRXWScoMx7k49QOcXrfJQzoR+jQxa4M+p4ofAIAqECJ1VlYNOIcIyT2TJXMWpwocpKG+ukxHsN/az7DmbRErn8Aw7va9FzMGaiJ2xM52j+1qNzqUOn939W3UM05fnOz/rYyJh0jTiLJv6zLj+Y5L0CCASCMOF90+jrKD0EBP0NzTy85cNHqxtisEyxio54UO3LFjKR7EecrIrmDnn1xnQ8cLIMoeU6jqw8hKtnt0aAd6Xhplnmr59Mkoc/sRRAqgM5OHbE2V5H0KwJq78a2nHTR7TWL9iVDqos07FjOzJv7zc2510DT93fCKrQIDAQABo1MwUTAdBgNVHQ4EFgQUiVs1Pj0+Hf67NHYkh5QAQ8pk7vcwHwYDVR0jBBgwFoAUiVs1Pj0+Hf67NHYkh5QAQ8pk7vcwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAYEASF3ITaDiuuSLmDBzFQpVZcOQfiKu1UC4cLSdz8E5pWHdm+ZPsjQFlrGt4fz+QAG5IcaCmBmpm+uuJkY8kPCrHlX5GYIDCr17RYAfTBiergdTLrH0UN+r7s0S9bvw33a4WAeq8sviE4oE742hCgHfxhDHcux2mAULeYQr7YWubfWLvVrJ7/ibIT/cLscRBmBEDQwxSIFpnQlcmmvEquVfec15twKyk6GB4/gIVylBlWixdYP1qIbMsUClXNNJY9kSERKcUxicX609n0L5eJ5AcScPuuNewKxy5vO0FsK9YrrITwJ1HPyv3JfIm71Dw/zQwl+pk97IF5Yz9oTQpsXD62PtxU+0HUZQhQZRUOyqoGlp9fRe95/mOV+ljdUa+n7PfiVL4eIocmGXAGgxeaeARGCB3lYUO9dfxoK4l2AtWgnPq5jMexm+m5DimLkC+kLViYkrN/dzLMgYREBoyY6cB9HjYI1qd3KKk6wUDwKtQp729pKLY7Mnw26U0AHCPYBn', diff --git a/simple-saml/config/authsources.php b/simple-saml/config/authsources.php index 032317c3..d6d34138 100644 --- a/simple-saml/config/authsources.php +++ b/simple-saml/config/authsources.php @@ -25,7 +25,7 @@ $config = [ // The entity ID of the IdP this SP should contact. // Can be NULL/unset, in which case the user will be shown a list of available IdPs. - 'idp' => 'http://localhost:8080/simplesaml-idp/saml2/idp/metadata.php', + 'idp' => 'https://localhost:8080/simplesaml-idp/saml2/idp/metadata.php', // The URL to the discovery service. // Can be NULL/unset, in which case a builtin discovery service will be used. diff --git a/simple-saml/metadata/saml20-idp-remote.php b/simple-saml/metadata/saml20-idp-remote.php index 0dcbf8d5..eaf4edf9 100644 --- a/simple-saml/metadata/saml20-idp-remote.php +++ b/simple-saml/metadata/saml20-idp-remote.php @@ -8,15 +8,15 @@ * See: https://simplesamlphp.org/docs/stable/simplesamlphp-reference-idp-remote */ -$metadata['http://localhost:8080/simplesaml-idp/saml2/idp/metadata.php'] = array ( +$metadata['https://localhost:8080/simplesaml-idp/saml2/idp/metadata.php'] = array ( 'metadata-set' => 'saml20-idp-remote', - 'entityid' => 'http://localhost:8080/simplesaml-idp/saml2/idp/metadata.php', + 'entityid' => 'https://localhost:8080/simplesaml-idp/saml2/idp/metadata.php', 'SingleSignOnService' => array ( 0 => array ( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - 'Location' => 'http://localhost:8080/simplesaml-idp/saml2/idp/SSOService.php', + 'Location' => 'https://localhost:8080/simplesaml-idp/saml2/idp/SSOService.php', ), ), 'SingleLogoutService' => @@ -24,7 +24,7 @@ $metadata['http://localhost:8080/simplesaml-idp/saml2/idp/metadata.php'] = array 0 => array ( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - 'Location' => 'http://localhost:8080/simplesaml-idp/saml2/idp/SingleLogoutService.php', + 'Location' => 'https://localhost:8080/simplesaml-idp/saml2/idp/SingleLogoutService.php', ), ), 'certData' => 'MIIEwTCCAymgAwIBAgIUWlixu/uHDLux2Txl3HGBngrYZxQwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xFjAUBgNVBAcMDUNvbGxlZ2UgUGxhY2UxHzAdBgNVBAoMFldhbGxhIFdhbGxhIFVuaXZlcnNpdHkxEzARBgNVBAsMCktyZXRzY2htYXIwHhcNMjAwMTIxMTAyNjQxWhcNMzAwMTIwMTAyNjQxWjBwMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjEWMBQGA1UEBwwNQ29sbGVnZSBQbGFjZTEfMB0GA1UECgwWV2FsbGEgV2FsbGEgVW5pdmVyc2l0eTETMBEGA1UECwwKS3JldHNjaG1hcjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBALhJUyYFwJRwiYzXnuR4dGkEUF6hjnbXxKnkwfuRO0Apy7G3RGtkqq+vGOTn2MymUpYOxXNCsz6cAwrrADjnW9cRUERkqR0KSnrTsATzF6rvnodM27hREysQEKVW+dcKIklrXxOaSRJuCoYXV+QuSK/Qph6qDYimxdLl4CWTuWtu2Pytr5ABcewYoawf816ErcNVw2pP2gxAB+OxyoERlo2E+6b4yk6e1V/StFdRgeABuMAPAgip49PPd7u0hUkay0fplB5fqg7xnuwMsJRffBJRRd5bZjMRz7M3OHL8kvhjCAWn54ERf8zYJJLaG+D96TPT7fNbyfylDzWS64wwwdo/iX3R9cwpPatAmh6ke2MGgypE1Xv2EUmqTPnlTIgxRxX4Y+N3sQibDtat1KhDIWZFk1YAkJpOarWaXolzUt/7wsYnocA51/REEDoIlVwa0xLDpGAMVHpzCmOMk62C047ptUVAKwPRKwVTRo44wyK2OOOvTWef3oeGLjQU0ICX2wIDAQABo1MwUTAdBgNVHQ4EFgQUGwOm6LVLEeLxGMOiqVHjcehg1YQwHwYDVR0jBBgwFoAUGwOm6LVLEeLxGMOiqVHjcehg1YQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAYEAfrz2N8cMRnDpD8H6o63Sa6aVJc0soSy5dzlSc/MLh9TvOuztWSovaDmfsshMTBH6ChZQ+XifZWwKJaf+fFaYGgcFDFwQVslzGjoSY2VXYSKmcfSIOpb4jkKvc4mjuxLxhi/WcKga7IVb7/xNw5uqWfJI+ndtz45AwJ/zpzQjvAMipZwjtwAgryXhcAdBlzhSRNdysPPCDCxjQWqaI+SSWMa0Ud/frXgYeOP9ID73qOf9rKrSjftJKXCYpXsjGykkv9GrCjJxe+usRSHXw6ddrO7aYfl7mXjsXQh+OlhuKog8MGUOQMa2I14qn8qTPKNmMl62Qu06pYFgDez9oLPM40mEilRpNQHO/lDqwS8J6x8Ir/Ub8a38s+VkIWGHsnvLDR/tHW0VI+RGsL10hsUbV2geIQ0CkDOJMo9kohlLqBf0WKr+sSRH7n/M/Kd0bccERTnr9NYl76Wuddo3v4JcIf48vYE8pUFDmCTGRwnTN6vCnNZLvWa/+WwYLpN8KhYW', diff --git a/src/web/backend/app/Http/Controllers/AuthController.php b/src/web/backend/app/Http/Controllers/AuthController.php index 615224ce..69abf430 100644 --- a/src/web/backend/app/Http/Controllers/AuthController.php +++ b/src/web/backend/app/Http/Controllers/AuthController.php @@ -3,37 +3,34 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use Illuminate\Cookie\CookieJar; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Support\Facades\Cookie; use Illuminate\Auth\AuthenticationException; use Source\Exceptions\AuthorizationException; -use Illuminate\Validation\ValidationException; use Source\Exceptions\EntityNotFoundException; use Source\UseCases\Users\Authenticate\APIPresenter; use Source\UseCases\Users\Authenticate\AuthenticateUseCase; +use Source\UseCases\Users\Authenticate\UserCreationException; class AuthController extends ApiController { protected Request $request; - public function __construct(Request $request) { + protected CookieJar $cookieJar; + + public function __construct(Request $request, CookieJar $cookieJar) { $this->request = $request; + $this->cookieJar = $cookieJar; } /** * @param AuthenticateUseCase $authenticateUseCase * @return JsonResponse - * @throws ValidationException * @throws AuthenticationException * @throws EntityNotFoundException */ public function login(AuthenticateUseCase $authenticateUseCase): JsonResponse { - $this->validate( - $this->request, - [ - 'email' => 'required', - 'password' => 'required', - ] - ); - $presenter = new APIPresenter(); try { @@ -42,10 +39,53 @@ class AuthController extends ApiController { throw new AuthenticationException(); } - return $this->respondWithData($presenter->getViewModel())->cookie( + return $this->respondWithData($presenter->getViewModel())->withCookie( + cookie( + 'api_token', + $presenter->getViewModel()['token']['value'], + $presenter->getViewModel()['token']['minutes'] + ) + ); + } + + public function samlLogin(AuthenticateUseCase $authenticateUseCase): RedirectResponse { + return redirect()->to($authenticateUseCase->handToSaml()); + } + + /** + * @param AuthenticateUseCase $authenticateUseCase + * @return mixed + * @throws EntityNotFoundException + */ + public function handle(AuthenticateUseCase $authenticateUseCase) { + $presenter = new APIPresenter(); + + try { + $authenticateUseCase->handleSamlLogin($presenter); + } catch (UserCreationException $e) { + return $this->respondWithError( + 'There was an error authenticating the user. Please contact an administrator.' + ); + } + + return redirect()->intended(url(config('saml.home_page')))->cookie( 'api_token', $presenter->getViewModel()['token']['value'], $presenter->getViewModel()['token']['minutes'] ); } + + /** + * @param AuthenticateUseCase $authenticateUseCase + * @return RedirectResponse + */ + public function samlLogout(AuthenticateUseCase $authenticateUseCase): RedirectResponse { + Cookie::queue($this->cookieJar->forget('api_token')); + + return redirect()->to( + $authenticateUseCase->samlLogout( + $this->request->cookie('api_token') + ) + ); + } } diff --git a/src/web/backend/app/Http/Kernel.php b/src/web/backend/app/Http/Kernel.php index 0dcca7c4..ba8ebfe5 100644 --- a/src/web/backend/app/Http/Kernel.php +++ b/src/web/backend/app/Http/Kernel.php @@ -59,6 +59,7 @@ class Kernel extends HttpKernel ], 'api' => [ + EncryptCookies::class, 'throttle:60,1', 'bindings', ], diff --git a/src/web/backend/app/Http/Middleware/EncryptCookies.php b/src/web/backend/app/Http/Middleware/EncryptCookies.php index 2c0d327f..50d0fe79 100644 --- a/src/web/backend/app/Http/Middleware/EncryptCookies.php +++ b/src/web/backend/app/Http/Middleware/EncryptCookies.php @@ -12,6 +12,5 @@ class EncryptCookies extends Middleware * @var array */ protected $except = [ - 'api_token', ]; } diff --git a/src/web/backend/app/Providers/AppServiceProvider.php b/src/web/backend/app/Providers/AppServiceProvider.php index 87672f3a..5619f1da 100644 --- a/src/web/backend/app/Providers/AppServiceProvider.php +++ b/src/web/backend/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Source\Gateways\Saml\SamlRepositoryServiceProvider; use Source\Gateways\Users\UsersRepositoryServiceProvider; use Source\Gateways\Doors\DoorsRepositoryServiceProvider; use Source\Gateways\Tokens\TokensRepositoryServiceProvider; @@ -22,6 +23,7 @@ class AppServiceProvider extends ServiceProvider * @var string[] */ protected array $gatewayProviders = [ + SamlRepositoryServiceProvider::class, UsersRepositoryServiceProvider::class, DoorsRepositoryServiceProvider::class, TokensRepositoryServiceProvider::class, diff --git a/src/web/backend/config/saml.php b/src/web/backend/config/saml.php new file mode 100644 index 00000000..7ee951dc --- /dev/null +++ b/src/web/backend/config/saml.php @@ -0,0 +1,58 @@ + env('FRONTEND_URL', '/'), + + /* + |-------------------------------------------------------------------------- + | Login Route + |-------------------------------------------------------------------------- + | + | Here you may set the route callback to go to + | after login. + | + */ + + 'login_route' => '/api/handle-login', + + /* + |-------------------------------------------------------------------------- + | Logout Route + |-------------------------------------------------------------------------- + | + | Here you may set the route name the will be used to redirect the user + | after logout. + | + */ + + 'logout_route' => '/', + + /* + |-------------------------------------------------------------------------- + | SimpleSAMLphp + |-------------------------------------------------------------------------- + | + | Here you may set the SimpleSAMLphp configuration. + | + | Note that the autoload file is relative to the + | base directory of this project. + | + */ + + 'simplesamlphp' => [ + 'autoload' => env('SAML_SIMPLESAMLPHP_AUTOLOAD'), + 'auth_source' => env('SAML_SIMPLESAMLPHP_AUTH_SOURCE', 'default-sp'), + ] +]; + diff --git a/src/web/backend/database/migrations/2014_10_12_000000_create_users_table.php b/src/web/backend/database/migrations/2014_10_12_000000_create_users_table.php index 7ca22c1f..2a2476a8 100644 --- a/src/web/backend/database/migrations/2014_10_12_000000_create_users_table.php +++ b/src/web/backend/database/migrations/2014_10_12_000000_create_users_table.php @@ -24,7 +24,7 @@ class CreateUsersTable extends Migration // hashed $table->string('password')->nullable()->default(null); // hashed - $table->string('doorcode'); + $table->string('doorcode')->nullable()->default(null); $table->timestamp('expires_at')->nullable(); $table->timestamps(); $table->softDeletes(); diff --git a/src/web/backend/routes/api.php b/src/web/backend/routes/api.php index 4d057cbe..211c5370 100644 --- a/src/web/backend/routes/api.php +++ b/src/web/backend/routes/api.php @@ -17,6 +17,8 @@ use App\Http\Controllers\UsersController; */ Route::post('login', [AuthController::class, 'login']); +Route::post('logout', [AuthController::class, 'logout']); + Route::group(['middleware' => 'auth:api'], static function () { Route::group( [ diff --git a/src/web/backend/routes/web.php b/src/web/backend/routes/web.php index 54e5b355..4ea86bf1 100644 --- a/src/web/backend/routes/web.php +++ b/src/web/backend/routes/web.php @@ -1,6 +1,7 @@ firstName = ucfirst($firstName); + $this->lastName = ucfirst($lastName); + $this->email = strtolower($email); + $this->emplid = $emplid; + } + + /** + * @return string + */ + public function getFirstName(): string { + return $this->firstName; + } + + /** + * @return string + */ + public function getLastName(): string { + return $this->lastName; + } + + /** + * @return string + */ + public function getEmplid(): string { + return $this->emplid; + } + + /** + * @return string + */ + public function getEmail(): string { + return $this->email; + } + + /** + * @return string + */ + public function getDisplayName(): string { + return $this->getFirstName() . ' ' . $this->getLastName(); + } +} diff --git a/src/web/backend/src/Entities/User.php b/src/web/backend/src/Entities/User.php index 9d51bca8..4e922449 100644 --- a/src/web/backend/src/Entities/User.php +++ b/src/web/backend/src/Entities/User.php @@ -43,9 +43,9 @@ class User { protected ?string $password; /** - * @var string + * @var string|null */ - protected string $doorcode; + protected ?string $doorcode; /** * @var Carbon|null @@ -69,8 +69,8 @@ class User { * @param string $displayName * @param string|null $emplid * @param string $email - * @param string $password - * @param string $doorcode + * @param string|null $password + * @param string|null $doorcode * @param Carbon|null $expiresAt * @param Carbon|null $createdAt * @param Carbon|null $updatedAt @@ -82,10 +82,10 @@ class User { ?string $emplid, string $email, ?string $password, - string $doorcode, - ?Carbon $expiresAt, - ?Carbon $createdAt, - ?Carbon $updatedAt) { + ?string $doorcode, + ?Carbon $expiresAt = null, + ?Carbon $createdAt = null, + ?Carbon $updatedAt = null) { $this->id = $id; $this->firstName = $firstName; $this->lastName = $lastName; @@ -149,9 +149,9 @@ class User { } /** - * @return string + * @return string|null */ - public function getDoorcode(): string { + public function getDoorcode(): ?string { return $this->doorcode; } @@ -208,6 +208,14 @@ class User { * @return bool */ public function hasDoorcodeOf(?string $doorcode): bool { + if (!$doorcode) { + return false; + } + return $this->getDoorcode() === $doorcode; } + + public function hasEmailOf(?string $email): bool { + return $this->getEmail() === strtolower($email); + } } diff --git a/src/web/backend/src/Gateways/Saml/InMemorySamlRepository.php b/src/web/backend/src/Gateways/Saml/InMemorySamlRepository.php new file mode 100644 index 00000000..27aae764 --- /dev/null +++ b/src/web/backend/src/Gateways/Saml/InMemorySamlRepository.php @@ -0,0 +1,60 @@ +loginUrl = $loginUrl; + $this->logoutUrl = $logoutUrl; + } + + public function setLoginUser(SamlUser $user): void { + $this->userToLogInAs = $user; + } + + /** + * @inheritDoc + */ + public function login(array $options = []): string { + $this->loggedInUser = $this->userToLogInAs; + return $this->loginUrl; + } + + /** + * @inheritDoc + */ + public function handleLogin(): ?SamlUser { + return $this->loggedInUser; + } + + /** + * @inheritDoc + */ + public function logout(): string { + $this->loggedInUser = null; + return $this->logoutUrl; + } + + /** + * @inheritDoc + */ + public function isAuthenticated(): bool { + return $this->loggedInUser !== null; + } +} diff --git a/src/web/backend/src/Gateways/Saml/SamlRepository.php b/src/web/backend/src/Gateways/Saml/SamlRepository.php new file mode 100644 index 00000000..6fd18246 --- /dev/null +++ b/src/web/backend/src/Gateways/Saml/SamlRepository.php @@ -0,0 +1,40 @@ +app->singleton(SamlRepository::class, static function (Application $app) { + if(env('APP_ENV') === 'testing') { + return new InMemorySamlRepository( + config('saml.login_route'), + config('saml.logout_route') + ); + } + + return new SimpleSamlPhpSamlRepository( + config('saml.login_route'), + config('saml.logout_route'), + config('saml.simplesamlphp.autoload'), + config('saml.simplesamlphp.auth_source') + ); + }); + } + + /** + * Bootstrap any application services. + * + * @return void + */ + public function boot(): void { + } + + /** + * @return array + */ + public function provides() { + return [SamlRepository::class]; + } +} diff --git a/src/web/backend/src/Gateways/Saml/SimpleSamlPhpSamlRepository.php b/src/web/backend/src/Gateways/Saml/SimpleSamlPhpSamlRepository.php new file mode 100644 index 00000000..3b6992a8 --- /dev/null +++ b/src/web/backend/src/Gateways/Saml/SimpleSamlPhpSamlRepository.php @@ -0,0 +1,121 @@ +loginUrl = $loginUrl; + $this->logoutUrl = $logoutUrl; + + require_once base_path($samlAutoloadPath); + + $this->saml = new SimpleSAML_Auth_Simple($authSource); + } + + /** + * @inheritDoc + */ + public function login(array $options = []): string { + return $this->saml->getloginURL($this->loginUrl); + } + + /** + * @inheritDoc + * @throws Exception + */ + public function handleLogin(): ?SamlUser { + return $this->makeUser(); + } + + /** + * @inheritDoc + */ + public function logout(): string { + return $this->saml->getLogoutURL($this->logoutUrl); + } + + public function isAuthenticated(): bool { + return $this->saml->isAuthenticated(); + } + + public function isGuest(): bool { + return !$this->isAuthenticated(); + } + + /** + * Verify required user attributes. + */ + protected function makeUser(): ?SamlUser { + if ($this->isGuest()) { + return null; + } + + $attributes = $this->saml->getAttributes(); + + $validator = Validator::make( + $attributes, + [ + 'emplid.0' => 'required|string|size:7', + 'first_name.0' => 'required|string', + 'last_name.0' => 'required|string', + 'email.0' => 'required|email', + ] + ); + + if ($validator->fails()) { + Log::error( + 'User does not have required attributes', + [$validator->errors()->all(), $attributes] + ); + return null; + } + + return new SamlUser( + $attributes['first_name'][0], + $attributes['last_name'][0], + $attributes['emplid'][0], + $attributes['email'][0] + ); + } +} diff --git a/src/web/backend/src/Gateways/Tokens/DatabaseTokensRepository.php b/src/web/backend/src/Gateways/Tokens/DatabaseTokensRepository.php index 8180ed1b..a0b787f1 100644 --- a/src/web/backend/src/Gateways/Tokens/DatabaseTokensRepository.php +++ b/src/web/backend/src/Gateways/Tokens/DatabaseTokensRepository.php @@ -7,10 +7,22 @@ namespace Source\Gateways\Tokens; use App\User; use Carbon\Carbon; use Source\Entities\Token; -use Basho\Riak\Node\Builder; +use Illuminate\Database\Eloquent\Builder; use Source\Exceptions\EntityNotFoundException; class DatabaseTokensRepository implements TokensRepository { + protected function dbTokenToToken(\App\Token $token): Token { + return new Token( + $token->id, + $token->user_id, + $token->api_token, + $token->name, + $token->expires_at, + $token->created_at, + $token->updated_at + ); + } + /** * @inheritDoc */ @@ -21,36 +33,46 @@ class DatabaseTokensRepository implements TokensRepository { throw new EntityNotFoundException('Cannot create token for non existent user'); } - return $user->tokens()->create( - [ - 'name' => $token->getName(), - 'api_token' => $token->getTokenString(), - 'expires_at' => $token->getExpiresAt(), - ] - ); + $dbToken = new \App\Token(); + $dbToken->name = $token->getName(); + $dbToken->api_token = $token->getTokenString(); + $dbToken->expires_at = $token->getExpiresAt(); + + $dbToken = $user->tokens()->save($dbToken); + + return $this->dbTokenToToken($dbToken); } /** * @inheritDoc */ public function findValidToken(string $token): ?Token { - $found = \App\Token::where('api_token', $token)->andWhere('expired_at', '<', Carbon::now())->orWhere( + $found = \App\Token::where('api_token', $token)->where('expires_at', '>', Carbon::now())->orWhere( static function (Builder $query) use ($token) { - $query->where('api_token', $token)->andWhere('expired_at', null); + $query->where('api_token', $token)->where('expires_at', null); } )->first(); + if (!$found) { return null; } - return new Token( - $found->id, - $found->user_id, - $found->name, - $found->expires_at, - $found->created_at, - $found->updated_at - ); + return $this->dbTokenToToken($found); + } + + /** + * @inheritDoc + */ + public function invalidateToken(string $token): void { + $found = \App\Token::where('api_token', $token) + ->where('expires_at', '<', Carbon::now()) + ->where('expires_at', '!=', null) + ->first(); + + if ($found) { + $found->expires_at = Carbon::now(); + $found->save(); + } } } diff --git a/src/web/backend/src/Gateways/Tokens/InMemoryTokensRepository.php b/src/web/backend/src/Gateways/Tokens/InMemoryTokensRepository.php index db9de552..43d4249e 100644 --- a/src/web/backend/src/Gateways/Tokens/InMemoryTokensRepository.php +++ b/src/web/backend/src/Gateways/Tokens/InMemoryTokensRepository.php @@ -33,4 +33,13 @@ class InMemoryTokensRepository implements TokensRepository { return null; } + + /** + * @inheritDoc + */ + public function invalidateToken(string $token): void { + $this->tokens = array_filter($this->tokens, static function (Token $t) use ($token) { + return !$t->matches($token); + }); + } } diff --git a/src/web/backend/src/Gateways/Tokens/TokensRepository.php b/src/web/backend/src/Gateways/Tokens/TokensRepository.php index eb5f8da5..39766543 100644 --- a/src/web/backend/src/Gateways/Tokens/TokensRepository.php +++ b/src/web/backend/src/Gateways/Tokens/TokensRepository.php @@ -20,4 +20,9 @@ interface TokensRepository { * @return Token|null */ public function findValidToken(string $token): ?Token; + + /** + * @param string $token + */ + public function invalidateToken(string $token): void; } diff --git a/src/web/backend/src/Gateways/Users/DatabaseUsersRepository.php b/src/web/backend/src/Gateways/Users/DatabaseUsersRepository.php index 06573412..daff9cb5 100644 --- a/src/web/backend/src/Gateways/Users/DatabaseUsersRepository.php +++ b/src/web/backend/src/Gateways/Users/DatabaseUsersRepository.php @@ -7,6 +7,19 @@ namespace Source\Gateways\Users; use Source\Entities\User; class DatabaseUsersRepository implements UsersRepository { + + protected function fillBasicUserAttrs(User $user, \App\User $dbUser): \App\User { + $dbUser->first_name = $user->getFirstName(); + $dbUser->last_name = $user->getLastName(); + $dbUser->display_name = $user->getDisplayName(); + $dbUser->emplid = $user->getEmplid(); + $dbUser->email = $user->getEmail(); + $dbUser->password = bcrypt($user->getPassword()); + $dbUser->doorcode = hash('sha256', $user->getDoorcode()); + $dbUser->expires_at = $user->getExpiresAt(); + return $dbUser; + } + /** * @inheritDoc */ @@ -36,7 +49,7 @@ class DatabaseUsersRepository implements UsersRepository { * @inheritDoc */ public function all(): array { - $users = \App\User::all(); + $users = \App\User::all()->values()->all(); return array_map( static function (\App\User $user) { @@ -62,18 +75,9 @@ class DatabaseUsersRepository implements UsersRepository { * @inheritDoc */ public function create(User $user): ?User { - $newUser = \App\User::create( - [ - 'first_name' => $user->getFirstName(), - 'last_name' => $user->getLastName(), - 'display_name' => $user->getDisplayName(), - 'emplid' => $user->getEmplid(), - 'email' => $user->getEmail(), - 'password' => bcrypt($user->getPassword()), - 'doorcode' => hash('sha256', $user->getDoorcode()), - 'expires_at' =>$user->getExpiresAt(), - ] - ); + $newUser = $this->fillBasicUserAttrs($user, new \App\User()); + + $newUser->save(); return new User( $newUser->id, @@ -100,14 +104,7 @@ class DatabaseUsersRepository implements UsersRepository { return null; } - $dbUser->first_name = $user->getFirstName(); - $dbUser->last_name = $user->getLastName(); - $dbUser->display_name = $user->getDisplayName(); - $dbUser->emplid = $user->getEmplid(); - $dbUser->email = $user->getEmail(); - $dbUser->password = bcrypt($user->getPassword()); - $dbUser->doorcode = hash('sha256', $user->getDoorcode()); - $dbUser->expires_at = $user->getExpiresAt(); + $dbUser = $this->fillBasicUserAttrs($user, $dbUser); $dbUser->save(); @@ -198,4 +195,24 @@ class DatabaseUsersRepository implements UsersRepository { } + /** + * @inheritDoc + */ + public function findByEmail(string $email): ?User { + $user = \App\User::where('email', strtolower($email))->first(); + + return new User( + $user->id, + $user->first_name, + $user->last_name, + $user->display_name, + $user->emplid, + $user->email, + $user->password, + $user->doorcode, + $user->expires_at, + $user->created_at, + $user->updated_at + ); + } } diff --git a/src/web/backend/src/Gateways/Users/InMemoryUsersRepository.php b/src/web/backend/src/Gateways/Users/InMemoryUsersRepository.php index a604ab6d..306837f6 100644 --- a/src/web/backend/src/Gateways/Users/InMemoryUsersRepository.php +++ b/src/web/backend/src/Gateways/Users/InMemoryUsersRepository.php @@ -118,4 +118,17 @@ class InMemoryUsersRepository implements UsersRepository { public function clear(): void { $this->users = []; } + + /** + * @inheritDoc + */ + public function findByEmail(string $email): ?User { + foreach ($this->users as $user) { + if ($user->hasEmailOf($email)) { + return $user; + } + } + + return null; + } } diff --git a/src/web/backend/src/Gateways/Users/UsersRepository.php b/src/web/backend/src/Gateways/Users/UsersRepository.php index 5af7d15f..1addecf1 100644 --- a/src/web/backend/src/Gateways/Users/UsersRepository.php +++ b/src/web/backend/src/Gateways/Users/UsersRepository.php @@ -59,4 +59,12 @@ interface UsersRepository { * @return User|null */ public function findByDoorcode(string $doorcode): ?User; + + /** + * Find a user by email + * + * @param string $email + * @return User|null + */ + public function findByEmail(string $email): ?User; } diff --git a/src/web/backend/src/UseCases/Users/Authenticate/APIPresenter.php b/src/web/backend/src/UseCases/Users/Authenticate/APIPresenter.php index 4bdd4452..a34ee1ff 100644 --- a/src/web/backend/src/UseCases/Users/Authenticate/APIPresenter.php +++ b/src/web/backend/src/UseCases/Users/Authenticate/APIPresenter.php @@ -12,12 +12,20 @@ class APIPresenter extends BasePresenter implements Presenter { public function present(ResponseModel $responseModel): void { $user = $responseModel->getUser(); $token = $responseModel->getToken(); + $expires = $token->getExpiresAt(); + + if (!$expires) { + $expires = 0; + } else { + $expires = $expires->diffInMinutes(Carbon::now()); + } + $this->viewModel['user'] = $this->formatUser($user); $this->viewModel['token'] = [ 'value' => $token->getTokenString(), 'expires_at' => $this->formatDateTime($token->getExpiresAt()), - 'minutes' => Carbon::now()->minutesUntil($token->getExpiresAt()), + 'minutes' => $expires, ]; } diff --git a/src/web/backend/src/UseCases/Users/Authenticate/Authenticate.php b/src/web/backend/src/UseCases/Users/Authenticate/Authenticate.php index 95304fd0..e502eeaf 100644 --- a/src/web/backend/src/UseCases/Users/Authenticate/Authenticate.php +++ b/src/web/backend/src/UseCases/Users/Authenticate/Authenticate.php @@ -3,8 +3,10 @@ namespace Source\UseCases\Users\Authenticate; use Carbon\Carbon; +use Source\Entities\User; use Source\Entities\Token; use Illuminate\Support\Str; +use Source\Gateways\Saml\SamlRepository; use Source\Gateways\Users\UsersRepository; use Source\Gateways\Tokens\TokensRepository; use Source\Exceptions\AuthorizationException; @@ -14,7 +16,10 @@ class Authenticate implements AuthenticateUseCase { protected TokensRepository $tokens; - public function __construct(UsersRepository $users, TokensRepository $tokens) { + protected SamlRepository $saml; + + public function __construct(UsersRepository $users, SamlRepository $saml, TokensRepository $tokens) { + $this->saml = $saml; $this->users = $users; $this->tokens = $tokens; } @@ -42,7 +47,7 @@ class Authenticate implements AuthenticateUseCase { $user->getId(), Str::random(60), null, - Carbon::now()->days(2) + Carbon::now()->addDays(2) ) ); @@ -50,4 +55,65 @@ class Authenticate implements AuthenticateUseCase { $presenter->present($response); } + + public function handToSaml(array $options = []): string { + return $this->saml->login($options); + } + + /** + * @inheritDoc + */ + public function handleSamlLogin(Presenter $presenter): void { + $user = $this->saml->handleLogin(); + + if (!$user) { + throw new UserCreationException(); + } + + // First check to see if the user exists in the database. + $user = $this->users->findByEmail($user->getEmail()); + + // If the user does not exist, create them. + if (!$user) { + $user = $this->users->create( + new User( + 0, + $user->getFirstName(), + $user->getLastName(), + $user->getDisplayName(), + $user->getEmplid(), + $user->getEmail(), + null, + null + ) + ); + } + + if (!$user) { + throw new UserCreationException(); + } + + $token = $this->tokens->create( + new Token( + 0, + $user->getId(), + Str::random(60), + null, + Carbon::now()->addDays(2) + ) + ); + + $response = new ResponseModel($user, $token); + + $presenter->present($response); + } + + /** + * @inheritDoc + */ + public function samlLogout(?string $token): string { + $this->tokens->invalidateToken($token); + + return $this->saml->logout(); + } } diff --git a/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCase.php b/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCase.php index c74589e0..aaea7f3c 100644 --- a/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCase.php +++ b/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCase.php @@ -17,4 +17,29 @@ interface AuthenticateUseCase { * @throws EntityNotFoundException */ public function attempt(Presenter $presenter, array $credentials): void; + + /** + * Returns url to redirect to to logout + * + * @param string|null $token + * @return string + */ + public function samlLogout(?string $token): string; + + /** + * Returns url to redirect user to for authentication + * + * @param array $options + * @return string + */ + public function handToSaml(array $options = []): string; + + /** + * Handle a saml callback. Returns URL to redirect to. + * + * @param Presenter $presenter + * @throws UserCreationException + * @throws EntityNotFoundException + */ + public function handleSamlLogin(Presenter $presenter): void; } diff --git a/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php b/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php index 8c6eaa40..f7ff0cfb 100644 --- a/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php +++ b/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php @@ -4,6 +4,7 @@ namespace Source\UseCases\Users\Authenticate; +use Source\Gateways\Saml\SamlRepository; use Source\Gateways\Users\UsersRepository; use Source\Gateways\Tokens\TokensRepository; use Illuminate\Contracts\Foundation\Application; @@ -21,7 +22,11 @@ class AuthenticateUseCaseServiceProvider extends ServiceProvider implements Defe */ public function register() { $this->app->bind(AuthenticateUseCase::class, static function (Application $app) { - return new Authenticate($app->make(UsersRepository::class), $app->make(TokensRepository::class)); + return new Authenticate( + $app->make(UsersRepository::class), + $app->make(SamlRepository::class), + $app->make(TokensRepository::class) + ); }); } diff --git a/src/web/backend/src/UseCases/Users/Authenticate/UserCreationException.php b/src/web/backend/src/UseCases/Users/Authenticate/UserCreationException.php new file mode 100644 index 00000000..d0c01efc --- /dev/null +++ b/src/web/backend/src/UseCases/Users/Authenticate/UserCreationException.php @@ -0,0 +1,14 @@ +