diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 00000000..278532f0 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "public/bower_assets", + "interactive": false +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a727b5d7..a6f153c1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ ChangeLog doc/build *.egg *.egg-info +public/bower_assets +public/bower_assets/* \ No newline at end of file diff --git a/app/config/app.php b/app/config/app.php index 78e8a57c..6f1ecb64 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -26,7 +26,7 @@ return array( | */ - 'url' => 'http://localhost', + 'url' => '', /* |-------------------------------------------------------------------------- @@ -65,7 +65,7 @@ return array( | */ - 'key' => '2mikPhsqqnw9qfEtp3XxHmtkZqoTTvWl', + 'key' => '', /* |-------------------------------------------------------------------------- diff --git a/app/config/dev/mail.php b/app/config/dev/mail.php deleted file mode 100644 index ac813553..00000000 --- a/app/config/dev/mail.php +++ /dev/null @@ -1,124 +0,0 @@ - 'mail', - - /* - |-------------------------------------------------------------------------- - | SMTP Host Address - |-------------------------------------------------------------------------- - | - | Here you may provide the host address of the SMTP server used by your - | applications. A default option is provided that is compatible with - | the Postmark mail service, which will provide reliable delivery. - | - */ - - 'host' => 'smtp.mailgun.org', - - /* - |-------------------------------------------------------------------------- - | SMTP Host Port - |-------------------------------------------------------------------------- - | - | This is the SMTP port used by your application to delivery e-mails to - | users of your application. Like the host we have set this value to - | stay compatible with the Postmark e-mail application by default. - | - */ - - 'port' => 587, - - /* - |-------------------------------------------------------------------------- - | Global "From" Address - |-------------------------------------------------------------------------- - | - | You may wish for all e-mails sent by your application to be sent from - | the same address. Here, you may specify a name and address that is - | used globally for all e-mails that are sent by your application. - | - */ - - 'from' => array('address' => null, 'name' => null), - - /* - |-------------------------------------------------------------------------- - | E-Mail Encryption Protocol - |-------------------------------------------------------------------------- - | - | Here you may specify the encryption protocol that should be used when - | the application send e-mail messages. A sensible default using the - | transport layer security protocol should provide great security. - | - */ - - 'encryption' => 'tls', - - /* - |-------------------------------------------------------------------------- - | SMTP Server Username - |-------------------------------------------------------------------------- - | - | If your SMTP server requires a username for authentication, you should - | set it here. This will get used to authenticate with your server on - | connection. You may also set the "password" value below this one. - | - */ - - 'username' => null, - - /* - |-------------------------------------------------------------------------- - | SMTP Server Password - |-------------------------------------------------------------------------- - | - | Here you may set the password required by your SMTP server to send out - | messages from your application. This will be given to the server on - | connection so that the application will be able to send messages. - | - */ - - 'password' => null, - - /* - |-------------------------------------------------------------------------- - | Sendmail System Path - |-------------------------------------------------------------------------- - | - | When using the "sendmail" driver to send e-mails, we will need to know - | the path to where Sendmail lives on this server. A default path has - | been provided here, which will work well on most of your systems. - | - */ - - 'sendmail' => '/usr/sbin/sendmail -bs', - - /* - |-------------------------------------------------------------------------- - | Mail "Pretend" - |-------------------------------------------------------------------------- - | - | When this option is enabled, e-mail will not actually be sent over the - | web and will instead be written to your application's logs files so - | you may inspect the message. This is great for local development. - | - */ - - 'pretend' => true, - -); \ No newline at end of file diff --git a/app/config/local/app.php b/app/config/local/app.php deleted file mode 100644 index a7f7d174..00000000 --- a/app/config/local/app.php +++ /dev/null @@ -1,187 +0,0 @@ - true, - - /* - |-------------------------------------------------------------------------- - | Application URL - |-------------------------------------------------------------------------- - | - | This URL is used by the console to properly generate URLs when using - | the Artisan command line tool. You should set this to the root of - | your application so that it is used when running Artisan tasks. - | - */ - - 'url' => 'https://local.openstackid.openstack.org', - - /* - |-------------------------------------------------------------------------- - | Application Timezone - |-------------------------------------------------------------------------- - | - | Here you may specify the default timezone for your application, which - | will be used by the PHP date and date-time functions. We have gone - | ahead and set this to a sensible default for you out of the box. - | - */ - - 'timezone' => 'UTC', - - /* - |-------------------------------------------------------------------------- - | Application Locale Configuration - |-------------------------------------------------------------------------- - | - | The application locale determines the default locale that will be used - | by the translation service provider. You are free to set this value - | to any of the locales which will be supported by the application. - | - */ - - 'locale' => 'en', - - /* - |-------------------------------------------------------------------------- - | Encryption Key - |-------------------------------------------------------------------------- - | - | This key is used by the Illuminate encrypter service and should be set - | to a random, 32 character string, otherwise these encrypted strings - | will not be safe. Please do this before deploying an application! - | - */ - - 'key' => '2mikPhsqqnw9qfEtp3XxHmtkZqoTTvWl', - - /* - |-------------------------------------------------------------------------- - | Autoloaded Service Providers - |-------------------------------------------------------------------------- - | - | The service providers listed here will be automatically loaded on the - | request to your application. Feel free to add your own services to - | this array to grant expanded functionality to your applications. - | - */ - - 'providers' => array( - 'Illuminate\Foundation\Providers\ArtisanServiceProvider', - 'Illuminate\Auth\AuthServiceProvider', - 'Illuminate\Cache\CacheServiceProvider', - 'Illuminate\Session\CommandsServiceProvider', - 'Illuminate\Foundation\Providers\ConsoleSupportServiceProvider', - 'Illuminate\Routing\ControllerServiceProvider', - 'Illuminate\Cookie\CookieServiceProvider', - 'Illuminate\Database\DatabaseServiceProvider', - 'Illuminate\Encryption\EncryptionServiceProvider', - 'Illuminate\Filesystem\FilesystemServiceProvider', - 'Illuminate\Hashing\HashServiceProvider', - 'Illuminate\Html\HtmlServiceProvider', - 'Illuminate\Log\LogServiceProvider', - 'Illuminate\Mail\MailServiceProvider', - 'Illuminate\Database\MigrationServiceProvider', - 'Illuminate\Pagination\PaginationServiceProvider', - 'Illuminate\Queue\QueueServiceProvider', - 'Illuminate\Remote\RemoteServiceProvider', - 'Illuminate\Auth\Reminders\ReminderServiceProvider', - 'Illuminate\Database\SeedServiceProvider', - 'Illuminate\Session\SessionServiceProvider', - 'Illuminate\Translation\TranslationServiceProvider', - 'Illuminate\Validation\ValidationServiceProvider', - 'Illuminate\View\ViewServiceProvider', - 'Illuminate\Workbench\WorkbenchServiceProvider', - 'Illuminate\Redis\RedisServiceProvider', - 'services\utils\UtilsProvider', - 'repositories\RepositoriesProvider', - 'services\oauth2\OAuth2ServiceProvider', - 'services\openid\OpenIdProvider', - 'auth\AuthenticationServiceProvider', - 'services\ServicesProvider', - 'strategies\StrategyProvider', - 'oauth2\OAuth2ServiceProvider', - 'openid\OpenIdServiceProvider', - 'Greggilbert\Recaptcha\RecaptchaServiceProvider', - 'services\oauth2\CORS\CORSProvider', - ), - - /* - |-------------------------------------------------------------------------- - | Service Provider Manifest - |-------------------------------------------------------------------------- - | - | The service provider manifest is used by Laravel to lazy load service - | providers which are not needed for each request, as well to keep a - | list of all of the services. Here, you may set its storage spot. - | - */ - - 'manifest' => storage_path().'/meta', - - /* - |-------------------------------------------------------------------------- - | Class Aliases - |-------------------------------------------------------------------------- - | - | This array of class aliases will be registered when this application - | is started. However, feel free to register as many as you wish as - | the aliases are "lazy" loaded so they don't hinder performance. - | - */ - - 'aliases' => array( - - 'App' => 'Illuminate\Support\Facades\App', - 'Artisan' => 'Illuminate\Support\Facades\Artisan', - 'Auth' => 'Illuminate\Support\Facades\Auth', - 'Blade' => 'Illuminate\Support\Facades\Blade', - 'Cache' => 'Illuminate\Support\Facades\Cache', - 'ClassLoader' => 'Illuminate\Support\ClassLoader', - 'Config' => 'Illuminate\Support\Facades\Config', - 'Controller' => 'Illuminate\Routing\Controller', - 'Cookie' => 'Illuminate\Support\Facades\Cookie', - 'Crypt' => 'Illuminate\Support\Facades\Crypt', - 'DB' => 'Illuminate\Support\Facades\DB', - 'Eloquent' => 'Illuminate\Database\Eloquent\Model', - 'Event' => 'Illuminate\Support\Facades\Event', - 'File' => 'Illuminate\Support\Facades\File', - 'Form' => 'Illuminate\Support\Facades\Form', - 'Hash' => 'Illuminate\Support\Facades\Hash', - 'HTML' => 'Illuminate\Support\Facades\HTML', - 'Input' => 'Illuminate\Support\Facades\Input', - 'Lang' => 'Illuminate\Support\Facades\Lang', - 'Log' => 'Illuminate\Support\Facades\Log', - 'Mail' => 'Illuminate\Support\Facades\Mail', - 'Paginator' => 'Illuminate\Support\Facades\Paginator', - 'Password' => 'Illuminate\Support\Facades\Password', - 'Queue' => 'Illuminate\Support\Facades\Queue', - 'Redirect' => 'Illuminate\Support\Facades\Redirect', - 'Request' => 'Illuminate\Support\Facades\Request', - 'Response' => 'Illuminate\Support\Facades\Response', - 'Route' => 'Illuminate\Support\Facades\Route', - 'Schema' => 'Illuminate\Support\Facades\Schema', - 'Seeder' => 'Illuminate\Database\Seeder', - 'Session' => 'Illuminate\Support\Facades\Session', - 'SSH' => 'Illuminate\Support\Facades\SSH', - 'Str' => 'Illuminate\Support\Str', - 'URL' => 'Illuminate\Support\Facades\URL', - 'Validator' => 'Illuminate\Support\Facades\Validator', - 'View' => 'Illuminate\Support\Facades\View', - 'RedisLV4' => 'Illuminate\Support\Facades\Redis', - - ), - -); diff --git a/app/config/local/mail.php b/app/config/local/mail.php deleted file mode 100644 index ac813553..00000000 --- a/app/config/local/mail.php +++ /dev/null @@ -1,124 +0,0 @@ - 'mail', - - /* - |-------------------------------------------------------------------------- - | SMTP Host Address - |-------------------------------------------------------------------------- - | - | Here you may provide the host address of the SMTP server used by your - | applications. A default option is provided that is compatible with - | the Postmark mail service, which will provide reliable delivery. - | - */ - - 'host' => 'smtp.mailgun.org', - - /* - |-------------------------------------------------------------------------- - | SMTP Host Port - |-------------------------------------------------------------------------- - | - | This is the SMTP port used by your application to delivery e-mails to - | users of your application. Like the host we have set this value to - | stay compatible with the Postmark e-mail application by default. - | - */ - - 'port' => 587, - - /* - |-------------------------------------------------------------------------- - | Global "From" Address - |-------------------------------------------------------------------------- - | - | You may wish for all e-mails sent by your application to be sent from - | the same address. Here, you may specify a name and address that is - | used globally for all e-mails that are sent by your application. - | - */ - - 'from' => array('address' => null, 'name' => null), - - /* - |-------------------------------------------------------------------------- - | E-Mail Encryption Protocol - |-------------------------------------------------------------------------- - | - | Here you may specify the encryption protocol that should be used when - | the application send e-mail messages. A sensible default using the - | transport layer security protocol should provide great security. - | - */ - - 'encryption' => 'tls', - - /* - |-------------------------------------------------------------------------- - | SMTP Server Username - |-------------------------------------------------------------------------- - | - | If your SMTP server requires a username for authentication, you should - | set it here. This will get used to authenticate with your server on - | connection. You may also set the "password" value below this one. - | - */ - - 'username' => null, - - /* - |-------------------------------------------------------------------------- - | SMTP Server Password - |-------------------------------------------------------------------------- - | - | Here you may set the password required by your SMTP server to send out - | messages from your application. This will be given to the server on - | connection so that the application will be able to send messages. - | - */ - - 'password' => null, - - /* - |-------------------------------------------------------------------------- - | Sendmail System Path - |-------------------------------------------------------------------------- - | - | When using the "sendmail" driver to send e-mails, we will need to know - | the path to where Sendmail lives on this server. A default path has - | been provided here, which will work well on most of your systems. - | - */ - - 'sendmail' => '/usr/sbin/sendmail -bs', - - /* - |-------------------------------------------------------------------------- - | Mail "Pretend" - |-------------------------------------------------------------------------- - | - | When this option is enabled, e-mail will not actually be sent over the - | web and will instead be written to your application's logs files so - | you may inspect the message. This is great for local development. - | - */ - - 'pretend' => true, - -); \ No newline at end of file diff --git a/app/config/server.php b/app/config/server.php index 6e2fa4e0..e2c7f322 100644 --- a/app/config/server.php +++ b/app/config/server.php @@ -17,6 +17,7 @@ return array( */ 'BlacklistSecurityPolicy_BannedIpLifeTimeSeconds' => 21600, 'BlacklistSecurityPolicy_MinutesWithoutExceptions' => 5, + 'BlacklistSecurityPolicy_MaxReplayAttackExceptionAttempts' => 3, 'BlacklistSecurityPolicy_ReplayAttackExceptionInitialDelay' => 10, 'BlacklistSecurityPolicy_MaxInvalidNonceAttempts' => 10, 'BlacklistSecurityPolicy_InvalidNonceInitialDelay' => 10, @@ -41,6 +42,8 @@ return array( //oauth2 default config values 'OAuth2_AuthorizationCode_Lifetime' => 240, 'OAuth2_AccessToken_Lifetime' => 3600, + // in seconds , should be equal to session.lifetime (120 minutes) + 'OAuth2_IdToken_Lifetime' => 7200, 'OAuth2_RefreshToken_Lifetime' => 0, 'OAuth2_Enable' => true, //oauth2 security policy configuration @@ -49,4 +52,5 @@ return array( 'OAuth2SecurityPolicy_MaxInvalidClientExceptionAttempts' => 10, 'OAuth2SecurityPolicy_MaxInvalidRedeemAuthCodeAttempts' => 10, 'OAuth2SecurityPolicy_MaxInvalidInvalidClientCredentialsAttempts' => 5, + 'Banning_Enable' => true, ); \ No newline at end of file diff --git a/app/config/testing/session.php b/app/config/testing/session.php index a18c1b9f..d92f8515 100644 --- a/app/config/testing/session.php +++ b/app/config/testing/session.php @@ -16,6 +16,6 @@ return array( | */ - 'driver' => 'array', + 'driver' => 'redis', ); \ No newline at end of file diff --git a/app/controllers/AdminController.php b/app/controllers/AdminController.php index 100b829e..449a35c6 100644 --- a/app/controllers/AdminController.php +++ b/app/controllers/AdminController.php @@ -9,23 +9,64 @@ use oauth2\services\IApiEndpointService; use utils\services\IAuthService; use openid\services\IUserService; use utils\services\IServerConfigurationService; -use \utils\services\IBannedIPService; +use utils\services\IBannedIPService; +use oauth2\repositories\IServerPrivateKeyRepository; +use oauth2\repositories\IApiScopeGroupRepository; +use auth\User; + /** * Class AdminController */ class AdminController extends BaseController { + /** + * @var IClientService + */ private $client_service; + /** + * @var IApiScopeService + */ private $scope_service; + /** + * @var ITokenService + */ private $token_service; + /** + * @var IResourceServerService + */ private $resource_server_service; + /** + * @var IApiService + */ private $api_service; + /** + * @var IApiEndpointService + */ private $endpoint_service; + /** + * @var IAuthService + */ private $auth_service; + /** + * @var IUserService + */ private $user_service; + /** + * @var IServerConfigurationService + */ private $configuration_service; + /** + * @var IBannedIPService + */ private $banned_ips_service; + private $private_keys_repository; + + /** + * @var IApiScopeGroupRepository + */ + private $group_repository; + public function __construct( IClientService $client_service, IApiScopeService $scope_service, ITokenService $token_service, @@ -35,7 +76,10 @@ class AdminController extends BaseController { IAuthService $auth_service, IUserService $user_service, IServerConfigurationService $configuration_service, - IBannedIPService $banned_ips_service){ + IBannedIPService $banned_ips_service, + IServerPrivateKeyRepository $private_keys_repository, + IApiScopeGroupRepository $group_repository) + { $this->client_service = $client_service; $this->scope_service = $scope_service; @@ -47,6 +91,8 @@ class AdminController extends BaseController { $this->user_service = $user_service; $this->configuration_service = $configuration_service; $this->banned_ips_service = $banned_ips_service; + $this->private_keys_repository = $private_keys_repository; + $this->group_repository = $group_repository; } public function editRegisteredClient($id) @@ -59,8 +105,6 @@ class AdminController extends BaseController { return View::make("404"); } - $allowed_uris = $client->getClientRegisteredUris(); - $allowed_origins = $client->getClientAllowedOrigins(); $selected_scopes = $client->getClientScopes(); $aux_scopes = array(); @@ -68,7 +112,8 @@ class AdminController extends BaseController { array_push($aux_scopes, $scope->id); } - $scopes = $this->scope_service->getAvailableScopes($user->canUseSystemScopes()); + $scopes = $this->scope_service->getAvailableScopes(); + $group_scopes = $user->getGroupScopes(); $access_tokens = $this->token_service->getAccessTokenByClient($client->client_id); @@ -86,32 +131,69 @@ class AdminController extends BaseController { return View::make("oauth2.profile.edit-client", array( - 'client' => $client, - 'allowed_uris' => $allowed_uris, - 'allowed_origins' => $allowed_origins, - 'selected_scopes' => $aux_scopes, - 'scopes' => $scopes, - 'access_tokens' => $access_tokens, - "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), + 'client' => $client, + 'selected_scopes' => $aux_scopes, + 'scopes' => array_merge($scopes, $group_scopes), + 'access_tokens' => $access_tokens, + "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), "is_openstackid_admin" => $user->isOpenstackIdAdmin(), - "use_system_scopes" => $user->canUseSystemScopes(), - 'refresh_tokens' => $refresh_tokens, + "use_system_scopes" => $user->canUseSystemScopes(), + 'refresh_tokens' => $refresh_tokens, )); } - public function listResourceServers() { + // Api Scope Groups + + public function listApiScopeGroups() + { + $user = $this->auth_service->getCurrentUser(); + $groups = $this->group_repository->getAll(1,1000); + $non_selected_scopes = $this->scope_service->getAssignedByGroups(); + $non_selected_users = User::where('active', '=', true)->get(); + return View::make("oauth2.profile.admin.api-scope-groups",array + ( + "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), + "is_openstackid_admin" => $user->isOpenstackIdAdmin(), + 'groups' => $groups, + 'non_selected_scopes' => $non_selected_scopes, + 'non_selected_users' => $non_selected_users, + )); + } + + public function editApiScopeGroup($id){ + $group = $this->group_repository->get($id); + + if(is_null($group)) + return Response::view('404', array(), 404); $user = $this->auth_service->getCurrentUser(); + $non_selected_scopes = $this->scope_service->getAssignedByGroups(); + $non_selected_users = User::where('active', '=', true)->get(); + return View::make("oauth2.profile.admin.edit-api-scope-group", + array + ( + 'is_oauth2_admin' => $user->isOAuth2ServerAdmin(), + 'is_openstackid_admin' => $user->isOpenstackIdAdmin(), + 'group' => $group, + 'non_selected_scopes' => $non_selected_scopes, + 'non_selected_users' => $non_selected_users, + ) + ); + } + + // Resource servers + public function listResourceServers() { + $user = $this->auth_service->getCurrentUser(); $resource_servers = $this->resource_server_service->getAll(1,1000); return View::make("oauth2.profile.admin.resource-servers",array( "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), "is_openstackid_admin" => $user->isOpenstackIdAdmin(), - 'resource_servers'=>$resource_servers)); + 'resource_servers' => $resource_servers)); } public function editResourceServer($id){ $resource_server = $this->resource_server_service->get($id); if(is_null($resource_server)) - return View::make('404'); + return Response::view('404', array(), 404); $user = $this->auth_service->getCurrentUser(); return View::make("oauth2.profile.admin.edit-resource-server",array( "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), @@ -123,7 +205,7 @@ class AdminController extends BaseController { public function editApi($id){ $api = $this->api_service->get($id); if(is_null($api)) - return View::make('404'); + return Response::view('404', array(), 404); $user = $this->auth_service->getCurrentUser(); return View::make("oauth2.profile.admin.edit-api",array( "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), @@ -134,7 +216,7 @@ class AdminController extends BaseController { public function editScope($id){ $scope = $this->scope_service->get($id); if(is_null($scope)) - return View::make('404'); + return Response::view('404', array(), 404); $user = $this->auth_service->getCurrentUser(); return View::make("oauth2.profile.admin.edit-scope",array( "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), @@ -145,7 +227,7 @@ class AdminController extends BaseController { public function editEndpoint($id){ $endpoint = $this->endpoint_service->get($id); if(is_null($endpoint)) - return View::make('404'); + return Response::view('404', array(), 404); $user = $this->auth_service->getCurrentUser(); $selected_scopes = array(); $list = $endpoint->scopes()->get(array('id')); @@ -235,31 +317,32 @@ class AdminController extends BaseController { )); } - - public function listServerConfig(){ $user = $this->auth_service->getCurrentUser(); $config_values = array(); - $config_values['MaxFailed.Login.Attempts'] = $this->configuration_service->getConfigValue('MaxFailed.Login.Attempts'); + $config_values['MaxFailed.Login.Attempts'] = $this->configuration_service->getConfigValue('MaxFailed.Login.Attempts'); $config_values['MaxFailed.LoginAttempts.2ShowCaptcha'] = $this->configuration_service->getConfigValue('MaxFailed.LoginAttempts.2ShowCaptcha'); - $config_values['OpenId.Private.Association.Lifetime'] = $this->configuration_service->getConfigValue('OpenId.Private.Association.Lifetime'); - $config_values['OpenId.Session.Association.Lifetime'] = $this->configuration_service->getConfigValue('OpenId.Session.Association.Lifetime'); - $config_values['OpenId.Nonce.Lifetime'] = $this->configuration_service->getConfigValue('OpenId.Nonce.Lifetime'); + $config_values['OpenId.Private.Association.Lifetime'] = $this->configuration_service->getConfigValue('OpenId.Private.Association.Lifetime'); + $config_values['OpenId.Session.Association.Lifetime'] = $this->configuration_service->getConfigValue('OpenId.Session.Association.Lifetime'); + $config_values['OpenId.Nonce.Lifetime'] = $this->configuration_service->getConfigValue('OpenId.Nonce.Lifetime'); - $config_values['OAuth2.AuthorizationCode.Lifetime'] = $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'); - $config_values['OAuth2.AccessToken.Lifetime'] = $this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime'); - $config_values['OAuth2.RefreshToken.Lifetime'] = $this->configuration_service->getConfigValue('OAuth2.RefreshToken.Lifetime'); + $config_values['OAuth2.AuthorizationCode.Lifetime'] = $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'); + $config_values['OAuth2.AccessToken.Lifetime'] = $this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime'); + $config_values['OAuth2.IdToken.Lifetime'] = $this->configuration_service->getConfigValue('OAuth2.IdToken.Lifetime'); + $config_values['OAuth2.RefreshToken.Lifetime'] = $this->configuration_service->getConfigValue('OAuth2.RefreshToken.Lifetime'); - return View::make("admin.server-config", array( - "username" => $user->getFullName(), - "user_id" => $user->getId(), - "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), - "is_openstackid_admin" => $user->isOpenstackIdAdmin(), - 'config_values' => $config_values, - )); + return View::make("admin.server-config", array + ( + "username" => $user->getFullName(), + "user_id" => $user->getId(), + "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), + "is_openstackid_admin" => $user->isOpenstackIdAdmin(), + 'config_values' => $config_values, + ) + ); } public function saveServerConfig(){ @@ -275,6 +358,7 @@ class AdminController extends BaseController { 'oauth2-auth-code-lifetime' => 'required|integer', 'oauth2-refresh-token-lifetime' => 'required|integer', 'oauth2-access-token-lifetime' => 'required|integer', + 'oauth2-id-token-lifetime' => 'required|integer', ); $dictionary = array( @@ -285,6 +369,7 @@ class AdminController extends BaseController { 'openid-nonce-lifetime' => 'OpenId.Nonce.Lifetime', 'oauth2-auth-code-lifetime' => 'OAuth2.AuthorizationCode.Lifetime', 'oauth2-access-token-lifetime' => 'OAuth2.AccessToken.Lifetime', + 'oauth2-id-token-lifetime' => 'OAuth2.IdToken.Lifetime', 'oauth2-refresh-token-lifetime' => 'OAuth2.RefreshToken.Lifetime', ); @@ -314,4 +399,15 @@ class AdminController extends BaseController { "ips" =>$ips )); } + + public function listServerPrivateKeys(){ + + $user = $this->auth_service->getCurrentUser(); + + return View::make("oauth2.profile.admin.server-private-keys", array( + 'private_keys' => $this->private_keys_repository->getAll(1,4294967296), + "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), + "is_openstackid_admin" => $user->isOpenstackIdAdmin(), + )); + } } \ No newline at end of file diff --git a/app/controllers/apis/AbstractRESTController.php b/app/controllers/apis/AbstractRESTController.php index daa4c726..3375481b 100644 --- a/app/controllers/apis/AbstractRESTController.php +++ b/app/controllers/apis/AbstractRESTController.php @@ -2,23 +2,30 @@ use utils\services\ILogService; -abstract class AbstractRESTController extends JsonController { +/** + * Class AbstractRESTController + */ +abstract class AbstractRESTController extends JsonController +{ + protected $allowed_filter_fields; protected $allowed_projection_fields; - private $filter_delimiter; - private $field_delimiter; + protected $filter_delimiter; + protected $field_delimiter; - public function __construct(ILogService $log_service){ + public function __construct(ILogService $log_service) + { parent::__construct($log_service); $this->filter_delimiter = '+'; $this->field_delimiter = ','; } - protected function getProjection($fields){ + protected function getProjection($fields) + { if(!is_string($fields)) return array('*'); if(empty($fields)) return array('*'); $fields_args = explode($this->field_delimiter,$fields); @@ -33,7 +40,8 @@ abstract class AbstractRESTController extends JsonController { return $res; } - protected function getFilters($filters){ + protected function getFilters($filters) + { if(!is_array($filters)) return array(); $res = array(); foreach($filters as $fieldname=>$value){ diff --git a/app/controllers/apis/ApiResourceServerController.php b/app/controllers/apis/ApiResourceServerController.php index 78e0c79e..9400744f 100644 --- a/app/controllers/apis/ApiResourceServerController.php +++ b/app/controllers/apis/ApiResourceServerController.php @@ -1,8 +1,9 @@ resource_server_service = $resource_server_service; - $this->allowed_filter_fields = array(''); + $this->allowed_filter_fields = array(''); $this->allowed_projection_fields = array('*'); } @@ -29,18 +30,20 @@ class ApiResourceServerController extends AbstractRESTController implements ICRU return $this->error404(array('error' => 'resource server not found')); } - $data = $resource_server->toArray(); - $apis = $resource_server->apis()->get(array('id','name')); + $data = $resource_server->toArray(); + $apis = $resource_server->apis()->get(array('id', 'name')); $data['apis'] = $apis->toArray(); - $client = $resource_server->getClient(); - if(!is_null($client)){ - $data['client_id'] = $client->getClientId(); - $data['client_secret'] = $client->getClientSecret(); + $client = $resource_server->getClient(); + if (!is_null($client)) { + $data['client_id'] = $client->getClientId(); + $data['client_secret'] = $client->getClientSecret(); } + return $this->ok($data); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -48,22 +51,24 @@ class ApiResourceServerController extends AbstractRESTController implements ICRU public function getByPage() { try { - $fields = $this->getProjection(Input::get('fields',null)); - $filters = $this->getFilters(Input::except('fields','limit','offset')); - $page_nbr = intval(Input::get('offset',1)); - $page_size = intval(Input::get('limit',10)); + $fields = $this->getProjection(Input::get('fields', null)); + $filters = $this->getFilters(Input::except('fields', 'limit', 'offset')); + $page_nbr = intval(Input::get('offset', 1)); + $page_size = intval(Input::get('limit', 10)); - $list = $this->resource_server_service->getAll($page_nbr, $page_size,$filters,$fields); + $list = $this->resource_server_service->getAll($page_nbr, $page_size, $filters, $fields); $items = array(); foreach ($list->getItems() as $rs) { array_push($items, $rs->toArray()); } - return $this->ok( array( + + return $this->ok(array( 'page' => $items, 'total_items' => $list->getTotal() )); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -74,17 +79,18 @@ class ApiResourceServerController extends AbstractRESTController implements ICRU $values = Input::all(); $rules = array( - 'host' => 'required|host|max:255', - 'ip' => 'required|ip|max:16', + 'host' => 'required|host|max:255', + 'ip' => 'required|ip|max:16', 'friendly_name' => 'required|text|max:512', - 'active' => 'required|boolean', + 'active' => 'required|boolean', ); // Creates a Validator instance and validates the data. $validation = Validator::make($values, $rules); if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error'=>'validation','messages' => $messages)); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); } $new_resource_server_model = $this->resource_server_service->add( @@ -94,13 +100,13 @@ class ApiResourceServerController extends AbstractRESTController implements ICRU $values['active']); return $this->created(array('resource_server_id' => $new_resource_server_model->id)); - } - catch(InvalidResourceServer $ex1){ + } catch (InvalidResourceServer $ex1) { $this->log_service->error($ex1); - return $this->error400(array('error'=>$ex1->getMessage())); - } - catch (Exception $ex) { + + return $this->error400(array('error' => $ex1->getMessage())); + } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -109,9 +115,11 @@ class ApiResourceServerController extends AbstractRESTController implements ICRU { try { $res = $this->resource_server_service->delete($id); - return $res?$this->deleted():$this->error404(array('error'=>'operation failed')); + + return $res ? $this->deleted() : $this->error404(array('error' => 'operation failed')); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -120,9 +128,11 @@ class ApiResourceServerController extends AbstractRESTController implements ICRU { try { $res = $this->resource_server_service->regenerateClientSecret($id); - return !is_null($res)?$this->ok(array('new_secret'=>$res)):$this->error404(array('error'=>'operation failed')); + + return !is_null($res) ? $this->ok(array('new_secret' => $res)) : $this->error404(array('error' => 'operation failed')); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -134,48 +144,55 @@ class ApiResourceServerController extends AbstractRESTController implements ICRU $values = Input::all(); $rules = array( - 'id' => 'required|integer', - 'host' => 'sometimes|required|host|max:255', - 'ip' => 'sometimes|required|ip|max:16', + 'id' => 'required|integer', + 'host' => 'sometimes|required|host|max:255', + 'ip' => 'sometimes|required|ip|max:16', 'friendly_name' => 'sometimes|required|text|max:512', ); // Creates a Validator instance and validates the data. $validation = Validator::make($values, $rules); if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error'=>'validation','messages' => $messages)); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); } - $res = $this->resource_server_service->update(intval($values['id']),$values); - return $res?$this->ok():$this->error400(array('error'=>'operation failed')); - } - catch(InvalidResourceServer $ex1){ + $res = $this->resource_server_service->update(intval($values['id']), $values); + + return $res ? $this->ok() : $this->error400(array('error' => 'operation failed')); + } catch (InvalidResourceServer $ex1) { $this->log_service->error($ex1); - return $this->error404(array('error'=>$ex1->getMessage())); - } - catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); - } - } - - public function activate($id){ - try { - $res = $this->resource_server_service->setStatus($id,true); - return $res?$this->ok():$this->error400(array('error'=>'operation failed')); + return $this->error404(array('error' => $ex1->getMessage())); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } - public function deactivate($id){ - try { - $res = $this->resource_server_service->setStatus($id,false); - return $res?$this->ok():$this->error400(array('error'=>'operation failed')); - } catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); - } - } + public function activate($id) + { + try { + $res = $this->resource_server_service->setStatus($id, true); + + return $res ? $this->ok() : $this->error400(array('error' => 'operation failed')); + } catch (Exception $ex) { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } + + public function deactivate($id) + { + try { + $res = $this->resource_server_service->setStatus($id, false); + + return $res ? $this->ok() : $this->error400(array('error' => 'operation failed')); + } catch (Exception $ex) { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } } \ No newline at end of file diff --git a/app/controllers/apis/ApiScopeController.php b/app/controllers/apis/ApiScopeController.php index 78cb2f0f..306290b0 100644 --- a/app/controllers/apis/ApiScopeController.php +++ b/app/controllers/apis/ApiScopeController.php @@ -10,6 +10,9 @@ use oauth2\exceptions\InvalidApiScope; */ class ApiScopeController extends AbstractRESTController implements ICRUDController { + /** + * @var IApiScopeService + */ private $api_scope_service; public function __construct(IApiScopeService $api_scope_service, ILogService $log_service) @@ -74,6 +77,7 @@ class ApiScopeController extends AbstractRESTController implements ICRUDControll 'default' => 'required|boolean', 'system' => 'required|boolean', 'api_id' => 'required|integer', + 'assigned_by_groups' => 'required|boolean', ); // Creates a Validator instance and validates the data. @@ -91,7 +95,8 @@ class ApiScopeController extends AbstractRESTController implements ICRUDControll $values['active'], $values['default'], $values['system'], - $values['api_id'] + $values['api_id'], + $values['assigned_by_groups'] ); return $this->created(array('scope_id' => $new_scope->id)); @@ -140,6 +145,7 @@ class ApiScopeController extends AbstractRESTController implements ICRUDControll 'active' => 'sometimes|required|boolean', 'system' => 'sometimes|required|boolean', 'default' => 'sometimes|required|boolean', + 'assigned_by_groups' => 'sometimes|boolean', ); // Creates a Validator instance and validates the data. diff --git a/app/controllers/apis/ApiScopeGroupController.php b/app/controllers/apis/ApiScopeGroupController.php new file mode 100644 index 00000000..36842ff3 --- /dev/null +++ b/app/controllers/apis/ApiScopeGroupController.php @@ -0,0 +1,278 @@ +repository = $repository; + $this->user_repository = $user_repository; + $this->scope_service = $scope_service; + $this->service = $service; + $this->allowed_filter_fields = array(''); + $this->allowed_projection_fields = array('*'); + } + + /** + * @param $id + * @return mixed + */ + public function get($id) + { + // TODO: Implement get() method. + } + + /** + * @return mixed + */ + public function create() + { + try + { + $values = Input::all(); + + $rules = array + ( + 'name' => 'required|text|max:512', + 'active' => 'required|boolean', + 'scopes' => 'required', + 'users' => 'required', + ); + // Creates a Validator instance and validates the data. + $validation = Validator::make($values, $rules); + + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); + } + + $new_group = $this->service->register + ( + $values['name'], + $values['active'], + $values['scopes'], + $values['users'] + ); + + return $this->created(array('group_id' => $new_group->id)); + } catch (InvalidApiScopeGroup $ex1) { + $this->log_service->error($ex1); + + return $this->error400(array('error' => $ex1->getMessage())); + } catch (Exception $ex) { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } + + /** + * @return mixed + */ + public function getByPage() + { + try + { + $fields = $this->getProjection(Input::get('fields', null)); + $filters = $this->getFilters(Input::except('fields', 'limit', 'offset')); + $page_nbr = intval(Input::get('offset', 1)); + $page_size = intval(Input::get('limit', 10)); + + $list = $this->repository->getAll($page_nbr, $page_size, $filters, $fields); + $items = array(); + + foreach ($list->getItems() as $g) + { + array_push($items, $g->toArray()); + } + + return $this->ok( + array + ( + 'page' => $items, + 'total_items' => $list->getTotal() + ) + ); + } catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + /** + * @param $id + * @return mixed + */ + public function delete($id) + { + try { + $group = $this->repository->get(intval($id)); + if(is_null($group)) return $this->error404(); + foreach($group->users()->get() as $user) + { + foreach($user->clients()->get() as $client) + { + foreach($group->scopes()->get() as $scope) + $client->scopes()->detach(intval($scope->id)); + } + } + $this->repository->delete($group); + return $this->deleted(); + } + catch (Exception $ex) + { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } + + /** + * @return mixed + */ + public function update() + { + try { + + $values = Input::all(); + + $rules = array + ( + 'id' => 'required|integer', + 'name' => 'required|text|max:512', + 'active' => 'required|boolean', + 'scopes' => 'required', + 'users' => 'required', + ); + // Creates a Validator instance and validates the data. + $validation = Validator::make($values, $rules); + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); + } + + $res = $this->service->update(intval($values['id']), $values); + + return $res ? $this->ok() : $this->error400(array('error' => 'operation failed')); + } + catch (InvalidApiScopeGroup $ex1) + { + $this->log_service->error($ex1); + return $this->error404(array('error' => $ex1->getMessage())); + } + catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + public function activate($id){ + try + { + $res = $this->service->setStatus($id, true); + return $res?$this->ok():$this->error400(array('error'=>'operation failed')); + } + catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + public function deactivate($id){ + try + { + $res = $this->service->setStatus($id, false); + return $res?$this->ok():$this->error400(array('error'=>'operation failed')); + } + catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + public function fetchUsers() + { + $values = Input::all(); + if(!isset($values['t'])) return $this->error404(); + $term = $values['t']; + $users = $this->user_repository->getByEmailOrName($term); + if(count($users) > 0) + { + $list = array(); + foreach($users as $u) + { + array_push($list, array + ( + 'id' => $u->id, + 'value' => sprintf('%s (%s)', $u->getFullName(), $u->getEmail()) + ) + ); + } + return $this->ok($list); + } + return $this->updated(); + } +} \ No newline at end of file diff --git a/app/controllers/apis/AssymetricKeyApiController.php b/app/controllers/apis/AssymetricKeyApiController.php new file mode 100644 index 00000000..6ecb87d6 --- /dev/null +++ b/app/controllers/apis/AssymetricKeyApiController.php @@ -0,0 +1,131 @@ +repository = $repository; + $this->service = $service; + //set filters allowed values + $this->allowed_filter_fields = array('*'); + $this->allowed_projection_fields = array('*'); + } + + /** + * @param $id + * @return mixed + */ + protected function _delete($id) + { + try { + $res = $this->service->delete($id); + + return $res ? $this->deleted() : $this->error404(array('error' => 'operation failed')); + } catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + + protected function _update($id) + { + try { + + $values = Input::all(); + + $rules = array( + 'id' => 'required|integer', + 'active' => 'required|boolean', + ); + + // Creates a Validator instance and validates the data. + $validation = Validator::make($values, $rules); + + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); + } + + $res = $this->service->update(intval($id), $values); + + return $res ? $this->ok() : $this->error400(array('error' => 'operation failed')); + + } catch (AbsentClientException $ex1) { + $this->log_service->error($ex1); + + return $this->error404(array('error' => $ex1->getMessage())); + } catch (Exception $ex) { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + /** + * @return mixed + */ + protected function _getByPage() + { + try { + //check for optional filters param on querystring + $fields = $this->getProjection(Input::get('fields', null)); + $filters = $this->getFilters(Input::except('fields', 'limit', 'offset')); + $page_nbr = intval(Input::get('offset', 1)); + $page_size = intval(Input::get('limit', 10)); + + $list = $this->repository->getAll($page_nbr, $page_size, $filters, $fields); + $items = array(); + foreach ($list->getItems() as $private_key) { + $data = $private_key->toArray(); + $data['sha_256'] = $private_key->getSHA_256_Thumbprint(); + array_push($items, $data); + } + + return $this->ok(array( + 'page' => $items, + 'total_items' => $list->getTotal() + )); + } catch (Exception $ex) { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } + +} \ No newline at end of file diff --git a/app/controllers/apis/ClientApiController.php b/app/controllers/apis/ClientApiController.php index 319181b0..be5a1167 100644 --- a/app/controllers/apis/ClientApiController.php +++ b/app/controllers/apis/ClientApiController.php @@ -1,21 +1,32 @@ client_service = $client_service; - $this->scope_service = $scope_service; - $this->token_service = $token_service; + $this->scope_service = $scope_service; + $this->token_service = $token_service; //set filters allowed values - $this->allowed_filter_fields = array('user_id'); + $this->allowed_filter_fields = array('user_id'); $this->allowed_projection_fields = array('*'); } + public function get($id) + { + try { + $client = $this->client_service->get($id); + if (is_null($client)) + { + return $this->error404(array('error' => 'client not found')); + } + $data = $client->toArray(); + + return $this->ok($data); + } catch (Exception $ex) { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } + /** * Deletes an existing client - * @param $id client id + * @param $id * @return mixed */ public function delete($id) { try { $res = $this->client_service->deleteClientByIdentifier($id); + return $res ? $this->deleted() : $this->error404(array('error' => 'operation failed')); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -65,11 +101,11 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl // Build the validation constraint set. $rules = array( - 'user_id' => 'required|integer', - 'app_name' => 'required|alpha_dash|max:255', - 'app_description' => 'required|freetext', - 'website' => 'required|url', - 'application_type' => 'required|applicationtype', + 'user_id' => 'required|integer', + 'app_name' => 'required|alpha_dash|max:255', + 'app_description' => 'required|freetext', + 'website' => 'url', + 'application_type' => 'required|applicationtype', ); // Create a new validator instance. @@ -77,41 +113,26 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error'=>'validation','messages' => $messages)); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); } if ($this->client_service->existClientAppName($values['app_name'])) { return $this->error400(array('error' => 'application Name already exists!.')); } - $new_client = $this->client_service->addClient($values['application_type'], intval($values['user_id']), trim($values['app_name']), trim($values['app_description']), trim($values['website'])); + $new_client = $this->client_service->addClient($values['application_type'], intval($values['user_id']), + trim($values['app_name']), trim($values['app_description']), trim($values['website'])); return $this->created(array('client_id' => $new_client->id)); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } - /** - * @param $id - * @return mixed - */ - public function get($id) - { - try { - $client = $this->client_service->get($id); - if (is_null($client)) { - return $this->error404(array('error' => 'client not found')); - } - $data = $client->toArray(); - return $this->ok($data); - } catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); - } - } /** * @return mixed @@ -120,24 +141,26 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl { try { //check for optional filters param on querystring - $fields = $this->getProjection(Input::get('fields',null)); - $filters = $this->getFilters(Input::except('fields','limit','offset')); - $page_nbr = intval(Input::get('offset',1)); - $page_size = intval(Input::get('limit',10)); + $fields = $this->getProjection(Input::get('fields', null)); + $filters = $this->getFilters(Input::except('fields', 'limit', 'offset')); + $page_nbr = intval(Input::get('offset', 1)); + $page_size = intval(Input::get('limit', 10)); - $list = $this->client_service->getAll($page_nbr, $page_size,$filters,$fields); + $list = $this->client_service->getAll($page_nbr, $page_size, $filters, $fields); $items = array(); foreach ($list->getItems() as $client) { $data = $client->toArray(); $data['application_type'] = $client->getFriendlyApplicationType(); array_push($items, $data); } + return $this->ok(array( 'page' => $items, 'total_items' => $list->getTotal() )); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -152,14 +175,38 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $values = Input::all(); $rules = array( - 'id' => 'required|integer', - 'app_name' => 'sometimes|required|alpha_dash|max:255', - 'app_description' => 'sometimes|required|freetext', - 'website' => 'sometimes|required|url', - 'active' => 'sometimes|required|boolean', - 'locked' => 'sometimes|required|boolean', - 'use_refresh_token' => 'sometimes|required|boolean', + 'id' => 'required|integer', + 'application_type' =>'required|application_type', + 'app_name' => 'sometimes|required|alpha_dash|max:255', + 'app_description' => 'sometimes|required|freetext', + 'website' => 'url', + 'active' => 'sometimes|required|boolean', + 'locked' => 'sometimes|required|boolean', + 'use_refresh_token' => 'sometimes|required|boolean', 'rotate_refresh_token' => 'sometimes|required|boolean', + 'contacts' => 'email_set', + 'logo_uri' => 'url', + 'tos_uri' => 'url', + 'redirect_uris' => 'custom_url_set:application_type', + 'post_logout_redirect_uris' => 'ssl_url_set', + 'allowed_origins' => 'ssl_url_set', + 'logout_uri' => 'url', + 'logout_session_required' => 'sometimes|required|boolean', + 'logout_use_iframe' => 'sometimes|required|boolean', + 'policy_uri' => 'url', + 'jwks_uri' => 'url', + 'default_max_age' => 'sometimes|required|integer', + 'logout_use_iframe' => 'sometimes|required|boolean', + 'require_auth_time' => 'sometimes|required|boolean', + 'token_endpoint_auth_method' => 'sometimes|required|token_endpoint_auth_method', + 'token_endpoint_auth_signing_alg' => 'sometimes|required|signing_alg', + 'subject_type' => 'sometimes|required|subject_type', + 'userinfo_signed_response_alg' => 'sometimes|required|signing_alg', + 'userinfo_encrypted_response_alg' => 'sometimes|required|encrypted_alg', + 'userinfo_encrypted_response_enc' => 'sometimes|required|encrypted_enc', + 'id_token_signed_response_alg' => 'sometimes|required|signing_alg', + 'id_token_encrypted_response_alg' => 'sometimes|required|encrypted_alg', + 'id_token_encrypted_response_enc' => 'sometimes|required|encrypted_enc', ); // Creates a Validator instance and validates the data. @@ -167,115 +214,70 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error'=>'validation','messages' => $messages)); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); } $res = $this->client_service->update(intval($values['id']), $values); return $res ? $this->ok() : $this->error400(array('error' => 'operation failed')); - } catch (AbsentClientException $ex1) { + } + catch (AbsentClientException $ex1) + { $this->log_service->error($ex1); return $this->error404(array('error' => $ex1->getMessage())); - } catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); } - } - - /** - * @param $id - * @return mixed - */ - public function getRegisteredUris($id) - { - try { - $client = $this->client_service->getClientByIdentifier($id); - $allowed_uris = $client->authorized_uris()->get(array('id', 'uri')); - - $data = array(); - foreach ($allowed_uris as $uri) { - array_push($data, $uri->toArray()); - } - - return $this->ok(array('allowed_uris' => $data)); - } catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); - } - } - - /** - * @param $id - * @return mixed - */ - public function addAllowedRedirectUri($id) - { - try { - $values = Input::All(); - // Build the validation constraint set. - $rules = array( - 'redirect_uri' => 'sslurl|required', - ); - // Creates a Validator instance and validates the data. - $validation = Validator::make($values, $rules); - if ($validation->fails()) { - $messages = $validation->messages()->toArray(); - return $this->error400(array('error'=>'validation','messages' => $messages)); - } - $res = $this->client_service->addClientAllowedUri($id, $values['redirect_uri']); - return $res ? $this->ok(): $this->error404(array('error' => 'operation failed')); - } catch (AllowedClientUriAlreadyExistsException $ex1) { - $this->log_service->error($ex1); - return $this->error400(array('error' => $ex1->getMessage())); - } catch (AbsentClientException $ex2) { + catch(ValidationException $ex2) + { $this->log_service->error($ex2); - return $this->error404(array('error' => $ex2->getMessage())); - } catch (Exception $ex) { + return $this->error412(array($ex2->getMessage())); + } + catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } - /** - * @param $id - * @param $uri_id - * @return mixed - */ - public function deleteClientAllowedUri($id, $uri_id) + + public function addAllowedScope($id, $scope_id) { - try { - $res = $this->client_service->deleteClientAllowedUri($id, $uri_id); - return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); - } catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); - } - } - - - public function addAllowedScope($id,$scope_id){ - try { + try + { $this->client_service->addClientScope($id, $scope_id); return $this->ok(); - } catch (AbsentClientException $ex1) { + } + catch (EntityNotFoundException $ex1) + { $this->log_service->error($ex1); return $this->error404(array('error' => $ex1->getMessage())); - } catch (Exception $ex) { + } + catch (InvalidApiScope $ex2) + { + $this->log_service->error($ex2); + return $this->error412(array('messages' => $ex2->getMessage())); + } + catch (Exception $ex) + { $this->log_service->error($ex); return $this->error500($ex); } } - public function removeAllowedScope($id,$scope_id){ - try { + public function removeAllowedScope($id, $scope_id) + { + try + { $res = $this->client_service->deleteClientScope($id, $scope_id); return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); } catch (AbsentClientException $ex1) { $this->log_service->error($ex1); + return $this->error404(array('error' => $ex1->getMessage())); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -285,39 +287,56 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl { try { $res = $this->client_service->activateClient($id, true); + return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); } catch (AbsentClientException $ex1) { $this->log_service->error($ex1); + return $this->error404(array('error' => $ex1->getMessage())); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } - public function deactivate($id) - { - try { - $res = $this->client_service->activateClient($id, false); - return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); - } catch (AbsentClientException $ex1) { - $this->log_service->error($ex1); - return $this->error404(array('error' => $ex1->getMessage())); - } catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); - } - } - - - public function regenerateClientSecret($id) + public function deactivate($id) { try { - $res = $this->client_service->regenerateClientSecret($id); - return !empty($res) ? - $this->ok(array('new_secret' => $res)): $this->error404(array('error' => 'operation failed')); + $res = $this->client_service->activateClient($id, false); + + return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); + } catch (AbsentClientException $ex1) { + $this->log_service->error($ex1); + + return $this->error404(array('error' => $ex1->getMessage())); } catch (Exception $ex) { $this->log_service->error($ex); + + return $this->error500($ex); + } + } + + + public function regenerateClientSecret($id) + { + try + { + $client = $this->client_service->regenerateClientSecret($id); + + return !is_null($client) ? + $this->ok + ( + array + ( + 'new_secret' => $client->getClientSecret(), + 'new_expiration_date' => $client->getClientSecretExpiration(), + ) + ) : $this->error404(array('error' => 'operation failed')); + } + catch (Exception $ex) + { + $this->log_service->error($ex); return $this->error500($ex); } } @@ -336,7 +355,8 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $validation = Validator::make($values, $rules); if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error'=>'validation','messages' => $messages)); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); } $res = $this->client_service->setRefreshTokenUsage($id, $values['use_refresh_token']); @@ -345,9 +365,11 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl } catch (AbsentClientException $ex1) { $this->log_service->error($ex1); + return $this->error404(array('error' => $ex1->getMessage())); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -365,16 +387,20 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $validation = Validator::make($values, $rules); if ($validation->fails()) { $messages = $validation->messages()->toArray(); - return $this->error400(array('error'=>'validation','messages' => $messages)); + + return $this->error400(array('error' => 'validation', 'messages' => $messages)); } $res = $this->client_service->setRotateRefreshTokenPolicy($id, $values['rotate_refresh_token']); + return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); } catch (AbsentClientException $ex1) { $this->log_service->error($ex1); + return $this->error404(array('error' => $ex1->getMessage())); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -385,26 +411,28 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $res = false; $client = $this->client_service->getClientByIdentifier($id); switch ($hint) { - case 'access-token': - { - $token = $this->token_service->getAccessToken($value,true); + case 'access-token': { + $token = $this->token_service->getAccessToken($value, true); if (is_null($token)) { return $this->error404(array('error' => sprintf('access token %s does not exists!', $value))); } if ($token->getClientId() !== $client->client_id) { - return $this->error404(array('error' => sprintf('access token %s does not belongs to client id !', $value, $id))); + return $this->error404(array( + 'error' => sprintf('access token %s does not belongs to client id !', $value, $id) + )); } $res = $this->token_service->revokeAccessToken($value, true); } break; - case 'refresh-token': - { - $token = $this->token_service->getRefreshToken($value,true); + case 'refresh-token': { + $token = $this->token_service->getRefreshToken($value, true); if (is_null($token)) { return $this->error404(array('error' => sprintf('refresh token %s does not exists!', $value))); } if ($token->getClientId() !== $client->client_id) { - return $this->error404(array('error' => sprintf('refresh token %s does not belongs to client id !', $value, $id))); + return $this->error404(array( + 'error' => sprintf('refresh token %s does not belongs to client id !', $value, $id) + )); } $res = $this->token_service->revokeRefreshToken($value, true); } @@ -416,6 +444,7 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -427,17 +456,18 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $access_tokens = $this->token_service->getAccessTokenByClient($client->client_id); $res = array(); foreach ($access_tokens as $token) { - $friendly_scopes = $this->scope_service->getFriendlyScopesByName(explode(' ', $token->scope)); array_push($res, array( - 'value' => $token->value, - 'scope' => implode(',', $friendly_scopes), + 'value' => $token->value, + 'scope' => $token->scope, 'lifetime' => $token->getRemainingLifetime(), - 'issued' => $token->created_at->format('Y-m-d H:i:s') + 'issued' => $token->created_at->format('Y-m-d H:i:s') )); } + return $this->ok(array('access_tokens' => $res)); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -449,17 +479,18 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl $refresh_tokens = $this->token_service->getRefreshTokenByClient($client->client_id); $res = array(); foreach ($refresh_tokens as $token) { - $friendly_scopes = $this->scope_service->getFriendlyScopesByName(explode(' ', $token->scope)); array_push($res, array( 'value' => $token->value, - 'scope' => implode(',', $friendly_scopes), + 'scope' => $token->scope, 'lifetime' => $token->getRemainingLifetime(), 'issued' => $token->created_at->format('Y-m-d H:i:s') )); } + return $this->ok(array('refresh_tokens' => $res)); } catch (Exception $ex) { $this->log_service->error($ex); + return $this->error500($ex); } } @@ -468,87 +499,19 @@ class ClientApiController extends AbstractRESTController implements ICRUDControl * @param $id * @return mixed */ - public function unlock($id){ + public function unlock($id) + { try { $res = $this->client_service->unlockClient($id); + return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); - } - catch (AbsentClientException $ex1) { + } catch (AbsentClientException $ex1) { $this->log_service->error($ex1); + return $this->error404(array('error' => $ex1->getMessage())); - } - catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); - } - } - - - - /** - * @param $id - * @return mixed - */ - public function geAllowedOrigins($id) - { - try { - $client = $this->client_service->getClientByIdentifier($id); - $allowed_origins = $client->allowed_origins()->get(array('id', 'allowed_origin')); - $data = array(); - foreach ($allowed_origins as $origin) { - array_push($data, $origin->toArray()); - } - return $this->ok(array('allowed_origins' => $data)); } catch (Exception $ex) { $this->log_service->error($ex); - return $this->error500($ex); - } - } - /** - * @param $id - * @return mixed - */ - public function addAllowedOrigin($id) - { - try { - $values = Input::All(); - // Build the validation constraint set. - $rules = array( - 'origin' => 'sslorigin|required', - ); - // Creates a Validator instance and validates the data. - $validation = Validator::make($values, $rules); - if ($validation->fails()) { - $messages = $validation->messages()->toArray(); - return $this->error400(array('error'=>'validation','messages' => $messages)); - } - $res = $this->client_service->addClientAllowedOrigin($id, $values['origin']); - return $res ? $this->ok(): $this->error404(array('error' => 'operation failed')); - } catch (AllowedClientUriAlreadyExistsException $ex1) { - $this->log_service->error($ex1); - return $this->error400(array('error' => $ex1->getMessage())); - } catch (AbsentClientException $ex2) { - $this->log_service->error($ex2); - return $this->error404(array('error' => $ex2->getMessage())); - } catch (Exception $ex) { - $this->log_service->error($ex); - return $this->error500($ex); - } - } - - /** - * @param $id - * @param $origin_id - * @return mixed - */ - public function deleteClientAllowedOrigin($id, $origin_id) - { - try { - $res = $this->client_service->deleteClientAllowedOrigin($id, $origin_id); - return $res ? $this->ok() : $this->error404(array('error' => 'operation failed')); - } catch (Exception $ex) { - $this->log_service->error($ex); return $this->error500($ex); } } diff --git a/app/controllers/apis/ClientPublicKeyApiController.php b/app/controllers/apis/ClientPublicKeyApiController.php new file mode 100644 index 00000000..3a9c1bf5 --- /dev/null +++ b/app/controllers/apis/ClientPublicKeyApiController.php @@ -0,0 +1,154 @@ +error404(); + } + + /** + * @param int $client_id + * @return mixed + */ + public function create($client_id) + { + try + { + + $values = Input::All(); + $values['client_id'] = $client_id; + // Build the validation constraint set. + $rules = array( + 'client_id' => 'required|integer', + 'kid' => 'required|text|max:255', + 'active' => 'required|boolean', + 'valid_from' => 'date_format:m/d/Y', + 'valid_to' => 'date_format:m/d/Y|after:valid_from', + 'pem_content' => 'required|public_key_pem|public_key_pem_length', + 'usage' => 'required|public_key_usage', + 'type' => 'required|public_key_type', + 'alg' => 'required|key_alg:usage', + ); + + // Create a new validator instance. + $validation = Validator::make($values, $rules); + + if ($validation->fails()) + { + $messages = $validation->messages()->toArray(); + return $this->error400(array('error' => 'validation', 'messages' => $messages)); + } + + $public_key = $this->service->register($values); + + return $this->created(array('id' => $public_key->getId())); + + } + catch(ValidationException $ex1) + { + return $this->error400(array('error' => $ex1->getMessage())); + } + catch (Exception $ex) + { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } + + + /** + * @return mixed + */ + public function getByPage($client_id) + { + try { + //check for optional filters param on querystring + $fields = $this->getProjection(Input::get('fields', null)); + $filters = $this->getFilters(Input::except('fields', 'limit', 'offset')); + $page_nbr = intval(Input::get('offset', 1)); + $page_size = intval(Input::get('limit', 10)); + array_push($filters, array + ( + 'name' => 'oauth2_client_id', + 'op' => '=', + 'value' => $client_id + ) + ); + $list = $this->repository->getAll($page_nbr, $page_size, $filters, $fields); + $items = array(); + foreach ($list->getItems() as $private_key) { + $data = $private_key->toArray(); + $data['sha_256'] = $private_key->getSHA_256_Thumbprint(); + array_push($items, $data); + } + + return $this->ok(array( + 'page' => $items, + 'total_items' => $list->getTotal() + )); + } catch (Exception $ex) { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } + + /** + * @param int $client_id + * @param int $public_key_id + * @return mixed + */ + public function update($client_id, $public_key_id) + { + return $this->_update($public_key_id); + } + + /** + * @param int $client_id + * @param int $public_key_id + * @return mixed + */ + public function delete($client_id, $public_key_id){ + return $this->_delete($public_key_id); + } + +} \ No newline at end of file diff --git a/app/controllers/apis/ICRUDController.php b/app/controllers/apis/ICRUDController.php index 51bb25dc..bfc96b6c 100644 --- a/app/controllers/apis/ICRUDController.php +++ b/app/controllers/apis/ICRUDController.php @@ -1,12 +1,35 @@ log_service->error($ex); - return Response::json(array('message' => 'server error'), 500); + return Response::json(array( 'error' => 'server error'), 500); } protected function created($data='ok'){ @@ -27,6 +27,15 @@ abstract class JsonController extends BaseController { return $res; } + protected function updated() + { + $res = Response::json($data, 204); + //jsonp + if(Input::has('callback')) + $res->setCallback(Input::get('callback')); + return $res; + } + protected function deleted($data='ok'){ $res = Response::json($data, 204); //jsonp @@ -35,7 +44,7 @@ abstract class JsonController extends BaseController { return $res; } - protected function ok($data='ok'){ + protected function ok($data = 'ok'){ $res = Response::json($data, 200); //jsonp if(Input::has('callback')) @@ -67,6 +76,6 @@ abstract class JsonController extends BaseController { */ protected function error412($messages){ - return Response::json(array('message' => 'Validation Failed', 'errors' => $messages), 412); + return Response::json(array('error'=>'validation' , 'messages' => $messages), 412); } } \ No newline at end of file diff --git a/app/controllers/apis/ServerPrivateKeyApiController.php b/app/controllers/apis/ServerPrivateKeyApiController.php new file mode 100644 index 00000000..5e3a786c --- /dev/null +++ b/app/controllers/apis/ServerPrivateKeyApiController.php @@ -0,0 +1,110 @@ + 'required|text|min:5|max:255', + 'active' => 'required|boolean', + 'valid_from' => 'date_format:m/d/Y', + 'valid_to' => 'date_format:m/d/Y|after:valid_from', + 'pem_content' => 'sometimes|required|private_key_pem:password|private_key_pem_length:password', + 'usage' => 'required|public_key_usage', + 'type' => 'required|public_key_type', + 'alg' => 'required|key_alg:usage', + 'password' => 'min:5|max:255|private_key_password:pem_content', + ); + + // Create a new validator instance. + $validation = Validator::make($values, $rules); + + if ($validation->fails()) + { + $messages = $validation->messages()->toArray(); + return $this->error400(array('error' => 'validation', 'messages' => $messages)); + } + + $private_key = $this->service->register($values); + + return $this->created(array('id' => $private_key->getId())); + + } + catch(ValidationException $ex1) + { + return $this->error400(array('error' => $ex1->getMessage())); + } + catch (Exception $ex) + { + $this->log_service->error($ex); + + return $this->error500($ex); + } + } + + public function getByPage() + { + return $this->_getByPage(); + } + + /** + * @param int $id + * @return mixed + */ + public function update($id) + { + return $this->_update($id); + } + + /** + * @param int $id + * @return mixed + */ + public function delete($id) + { + return $this->_delete($id); + } + +} \ No newline at end of file diff --git a/app/controllers/apis/protected/OAuth2ProtectedController.php b/app/controllers/apis/protected/OAuth2ProtectedController.php index 3e813285..8ad212ce 100644 --- a/app/controllers/apis/protected/OAuth2ProtectedController.php +++ b/app/controllers/apis/protected/OAuth2ProtectedController.php @@ -7,13 +7,24 @@ use utils\services\ILogService; * Class OAuth2ProtectedController * OAuth2 Protected Base API */ -abstract class OAuth2ProtectedController extends JsonController { +abstract class OAuth2ProtectedController extends JsonController +{ + /** + * @var IResourceServerContext + */ protected $resource_server_context; + /** + * @var + */ protected $repository; - public function __construct(IResourceServerContext $resource_server_context, ILogService $log_service) + public function __construct + ( + IResourceServerContext $resource_server_context, + ILogService $log_service + ) { parent::__construct($log_service); $this->resource_server_context = $resource_server_context; diff --git a/app/controllers/apis/protected/OAuth2UserApiController.php b/app/controllers/apis/protected/OAuth2UserApiController.php index f48894e5..3e9b1c2e 100644 --- a/app/controllers/apis/protected/OAuth2UserApiController.php +++ b/app/controllers/apis/protected/OAuth2UserApiController.php @@ -3,30 +3,111 @@ use oauth2\IResourceServerContext; use utils\services\ILogService; use oauth2\resource_server\IUserService; +use oauth2\services\IClientService; +use oauth2\heuristics\SigningKeyFinder; +use oauth2\heuristics\EncryptionKeyFinder; +use oauth2\builders\IdTokenBuilder; +use utils\http\HttpContentType; /** * Class OAuth2UserApiController * OAUTH2 Protected User REST API */ -class OAuth2UserApiController extends OAuth2ProtectedController { +class OAuth2UserApiController extends OAuth2ProtectedController +{ + /** + * @var IUserService + */ + private $user_service; - public function __construct (IUserService $user_service, IResourceServerContext $resource_server_context, ILogService $log_service){ + /** + * @var IClientService + */ + private $client_service; + + /** + * @var IdTokenBuilder + */ + private $id_token_builder; + + /** + * @param IUserService $user_service + * @param IResourceServerContext $resource_server_context + * @param ILogService $log_service + * @param IClientService $client_service + * @param IdTokenBuilder $id_token_builder + */ + public function __construct + ( + IUserService $user_service, + IResourceServerContext $resource_server_context, + ILogService $log_service, + IClientService $client_service, + IdTokenBuilder $id_token_builder + ) + { parent::__construct($resource_server_context,$log_service); - $this->user_service = $user_service; + + $this->user_service = $user_service; + $this->client_service = $client_service; + $this->id_token_builder = $id_token_builder; } /** * Gets User Basic Info * @return mixed */ - public function me(){ - try{ + public function me() + { + try + { $data = $this->user_service->getCurrentUserInfo(); return $this->ok($data); } - catch(Exception $ex){ + catch(Exception $ex) + { $this->log_service->error($ex); return $this->error500($ex); } } + + public function userInfo() + { + try + { + $claims = $this->user_service->getCurrentUserInfoClaims(); + $client_id = $this->resource_server_context->getCurrentClientId(); + $client = $this->client_service->getClientById($client_id); + + // The UserInfo Claims MUST be returned as the members of a JSON object unless a signed or encrypted response + // was requested during Client Registration. + $user_info_response_info = $client->getUserInfoResponseInfo(); + + $sig_alg = $user_info_response_info->getSigningAlgorithm(); + $enc_alg = $user_info_response_info->getEncryptionKeyAlgorithm(); + $enc = $user_info_response_info->getEncryptionContentAlgorithm(); + + if($sig_alg || ($enc_alg && $enc) ) + { + $jwt = $this->id_token_builder->buildJWT($claims, $user_info_response_info, $client); + $http_response = Response::make($jwt->toCompactSerialization(), 200); + $http_response->header('Content-Type', HttpContentType::JWT); + $http_response->header('Cache-Control','no-cache, no-store, max-age=0, must-revalidate'); + $http_response->header('Pragma','no-cache'); + return $http_response; + } + else + { + // return plain json + return $this->ok( $claims->toArray() ); + } + } + catch(Exception $ex) + { + $this->log_service->error($ex); + return $this->error500($ex); + } + } + + } \ No newline at end of file diff --git a/app/controllers/oauth2/OAuth2ProviderController.php b/app/controllers/oauth2/OAuth2ProviderController.php index c70a6e88..b8ef288b 100644 --- a/app/controllers/oauth2/OAuth2ProviderController.php +++ b/app/controllers/oauth2/OAuth2ProviderController.php @@ -1,56 +1,132 @@ oauth2_protocol = $oauth2_protocol; $this->memento_service = $memento_service; + $this->auth_service = $auth_service; + $this->client_service = $client_service; } /** * Authorize HTTP Endpoint + * The authorization server MUST support the use of the HTTP "GET" + * method [RFC2616] for the authorization endpoint and MAY support the + * use of the "POST" method as well. * @return mixed */ - public function authorize(){ - $request = $this->memento_service->getCurrentAuthorizationRequest(); - $response = $this->oauth2_protocol->authorize($request); - $reflector = new ReflectionClass($response); - if ($reflector->isSubclassOf('oauth2\\responses\\OAuth2Response')) { - $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($response); - return $strategy->handle($response); + public function authorize() + { + try + { + $msg = new OAuth2Message(Input::all()); + + if ($this->memento_service->exists()) { + $msg = OAuth2Message::buildFromMemento($this->memento_service->load()); + } + + $request = OAuth2AuthorizationRequestFactory::getInstance()->build($msg); + + $response = $this->oauth2_protocol->authorize($request); + + if ($response instanceof OAuth2Response) { + $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($request, $response); + + return $strategy->handle($response); + } + + return $response; + } + catch(UriNotAllowedException $ex1) + { + return Response::view + ( + '400', + array + ( + 'error_code' => $ex1->getError(), + 'error_description' => $ex1->getMessage() + ), + 400 + ); } - return $response; } /** * Token HTTP Endpoint * @return mixed */ - public function token(){ - $response = $this->oauth2_protocol->token(new OAuth2TokenRequest(new OAuth2Message(Input::all()))); - $reflector = new ReflectionClass($response); - if ($reflector->isSubclassOf('oauth2\\responses\\OAuth2Response')) { - $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($response); + public function token() + { + + $request = new OAuth2TokenRequest + ( + new OAuth2Message + ( + Input::all() + ) + ); + + $response = $this->oauth2_protocol->token($request); + + if ($response instanceof OAuth2Response) + { + $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($request, $response); return $strategy->handle($response); } + return $response; } @@ -58,13 +134,24 @@ class OAuth2ProviderController extends BaseController { * Revoke Token HTTP Endpoint * @return mixed */ - public function revoke(){ - $response = $this->oauth2_protocol->revoke(new OAuth2TokenRevocationRequest(new OAuth2Message(Input::all()))); - $reflector = new ReflectionClass($response); - if ($reflector->isSubclassOf('oauth2\\responses\\OAuth2Response')) { - $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($response); + public function revoke() + { + $request = new OAuth2TokenRevocationRequest + ( + new OAuth2Message + ( + Input::all() + ) + ); + + $response = $this->oauth2_protocol->revoke($request); + + if ($response instanceof OAuth2Response) + { + $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($request, $response); return $strategy->handle($response); } + return $response; } @@ -73,13 +160,124 @@ class OAuth2ProviderController extends BaseController { * Introspection Token HTTP Endpoint * @return mixed */ - public function introspection(){ - $response = $this->oauth2_protocol->introspection(new OAuth2AccessTokenValidationRequest(new OAuth2Message(Input::all()))); - $reflector = new ReflectionClass($response); - if ($reflector->isSubclassOf('oauth2\\responses\\OAuth2Response')) { - $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($response); + public function introspection() + { + $request = new OAuth2AccessTokenValidationRequest + ( + new OAuth2Message + ( + Input::all() + ) + ); + + $response = $this->oauth2_protocol->introspection($request); + + if ($response instanceof OAuth2Response) + { + $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($request, $response); return $strategy->handle($response); } + return $response; } + + /** + * OP's JSON Web Key Set [JWK] document. + * @return string + */ + public function certs() + { + + $doc = $this->oauth2_protocol->getJWKSDocument(); + $response = Response::make($doc, 200); + $response->header('Content-Type', HttpContentType::Json); + + return $response; + } + + public function discovery() + { + + $doc = $this->oauth2_protocol->getDiscoveryDocument(); + $response = Response::make($doc, 200); + $response->header('Content-Type', HttpContentType::Json); + + return $response; + } + + /** + * http://openid.net/specs/openid-connect-session-1_0.html#OPiframe + */ + public function checkSessionIFrame() + { + $data = array(); + return View::make("oauth2.session.check-session", $data); + } + + /** + * http://openid.net/specs/openid-connect-session-1_0.html#RPLogout + */ + public function endSession() + { + if(!$this->auth_service->isUserLogged()) + return Response::view('404', array(), 404); + + $request = new OAuth2LogoutRequest + ( + new OAuth2Message + ( + Input::all() + ) + ); + + if(!$request->isValid()) + { + Log::error('invalid OAuth2LogoutRequest!'); + return Response::view('404', array(), 404); + } + + if(Request::isMethod('get') ) + { + $rps = $this->auth_service->getLoggedRPs(); + $clients = array(); + foreach($this->auth_service->getLoggedRPs() as $client_id) + { + $client = $this->client_service->getClientById($client_id); + if(!is_null($client)) array_push($clients, $client); + } + + // At the logout endpoint, the OP SHOULD ask the End-User whether he wants to log out of the OP as well. + // If the End-User says "yes", then the OP MUST log out the End-User. + return View::make('oauth2.session.session-logout', array + ( + 'clients' => $clients, + 'id_token_hint' => $request->getIdTokenHint(), + 'post_logout_redirect_uri' => $request->getPostLogoutRedirectUri(), + 'state' => $request->getState(), + )); + } + + $consent = Input::get('oidc_endsession_consent'); + + if($consent === '1') + { + $response = $this->oauth2_protocol->endSession($request); + + if (!is_null($response) && $response instanceof OAuth2Response) { + $strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($request, $response); + + return $strategy->handle($response); + } + + return View::make('oauth2.session.session-ended'); + } + + Log::error('invalid consent response!'); + return Response::view('404', array(), 404); + } + + public function cancelLogout() + { + return Redirect::action('HomeController@index'); + } } \ No newline at end of file diff --git a/app/controllers/openid/OpenIdProviderController.php b/app/controllers/openid/OpenIdProviderController.php index ae1de420..3a36f653 100644 --- a/app/controllers/openid/OpenIdProviderController.php +++ b/app/controllers/openid/OpenIdProviderController.php @@ -3,34 +3,54 @@ use openid\exceptions\InvalidOpenIdMessageException; use openid\helpers\OpenIdErrorMessages; use openid\IOpenIdProtocol; -use openid\services\IMementoOpenIdRequestService; +use openid\services\IMementoOpenIdSerializerService; use openid\strategies\OpenIdResponseStrategyFactoryMethod; - +use openid\OpenIdMessage; +use openid\responses\OpenIdResponse; /** * Class OpenIdProviderController */ class OpenIdProviderController extends BaseController { + /** + * @var IOpenIdProtocol + */ private $openid_protocol; + /** + * @var IMementoOpenIdSerializerService + */ private $memento_service; - public function __construct(IOpenIdProtocol $openid_protocol, IMementoOpenIdRequestService $memento_service) + /** + * @param IOpenIdProtocol $openid_protocol + * @param IMementoOpenIdSerializerService $memento_service + */ + public function __construct(IOpenIdProtocol $openid_protocol, IMementoOpenIdSerializerService $memento_service) { $this->openid_protocol = $openid_protocol; $this->memento_service = $memento_service; } + /** + * @return OpenIdResponse + * @throws Exception + * @throws InvalidOpenIdMessageException + */ public function endpoint() { - $input = Input::all(); - Log::debug(print_r($input, true)); - $msg = $this->memento_service->getCurrentRequest(); - if (is_null($msg) || !$msg->isValid()) + $msg = new OpenIdMessage( Input::all() ); + + if($this->memento_service->exists()){ + $msg = OpenIdMessage::buildFromMemento( $this->memento_service->load()); + } + + if (!$msg->isValid()) throw new InvalidOpenIdMessageException(OpenIdErrorMessages::InvalidOpenIdMessage); + //get response and manage it taking in consideration its type (direct or indirect) $response = $this->openid_protocol->handleOpenIdMessage($msg); - $reflector = new ReflectionClass($response); - if ($reflector->isSubclassOf('openid\\responses\\OpenIdResponse')) { + + if ($response instanceof OpenIdResponse) { $strategy = OpenIdResponseStrategyFactoryMethod::buildStrategy($response); return $strategy->handle($response); } diff --git a/app/controllers/openid/UserController.php b/app/controllers/openid/UserController.php index 0672397f..4dfbdead 100644 --- a/app/controllers/openid/UserController.php +++ b/app/controllers/openid/UserController.php @@ -2,24 +2,24 @@ use oauth2\services\IApiScopeService; use oauth2\services\IClientService; -use oauth2\services\IMementoOAuth2AuthenticationRequestService; -use oauth2\services\ITokenService; use oauth2\services\IResourceServerService; -use openid\services\IMementoOpenIdRequestService; +use oauth2\services\ITokenService; +use openid\requests\OpenIdAuthenticationRequest; +use openid\services\IMementoOpenIdSerializerService; +use openid\services\IServerConfigurationService; use openid\services\ITrustedSitesService; use openid\services\IUserService; -use openid\services\IServerConfigurationService; -use openid\requests\OpenIdAuthenticationRequest; -use openid\XRDS\XRDSDocumentBuilder; -use strategies\OpenIdLoginStrategy; -use strategies\OpenIdConsentStrategy; -use utils\IPHelper; use services\IUserActionService; use strategies\DefaultLoginStrategy; use strategies\OAuth2ConsentStrategy; use strategies\OAuth2LoginStrategy; +use strategies\OpenIdConsentStrategy; +use strategies\OpenIdLoginStrategy; +use utils\IPHelper; use utils\services\IAuthService; use utils\services\IServerConfigurationService as IUtilsServerConfigurationService; +use oauth2\services\IMementoOAuth2SerializerService; +use oauth2\services\ISecurityContextService; /** * Class UserController @@ -27,36 +27,97 @@ use utils\services\IServerConfigurationService as IUtilsServerConfigurationServi class UserController extends OpenIdController { + /** + * @var IMementoOpenIdSerializerService + */ private $openid_memento_service; + /** + * @var IMementoOAuth2SerializerService + */ private $oauth2_memento_service; + /** + * @var IAuthService + */ private $auth_service; + /** + * @var IServerConfigurationService + */ private $server_configuration_service; + /** + * @var DiscoveryController + */ private $discovery; + /** + * @var IUserService + */ private $user_service; + /** + * @var IUserActionService + */ private $user_action_service; + /** + * @var DefaultLoginStrategy + */ private $login_strategy; + /** + * @var null + */ private $consent_strategy; + /** + * @var IClientService + */ private $client_service; + /** + * @var IApiScopeService + */ private $scope_service; + /** + * @var ITokenService + */ private $token_service; + /** + * @var IResourceServerService + */ private $resource_server_service; - private $utils_configuration_service; + /** + * @var IUtilsServerConfigurationService + */ + private $utils_configuration_service; - public function __construct(IMementoOpenIdRequestService $openid_memento_service, - IMementoOAuth2AuthenticationRequestService $oauth2_memento_service, - IAuthService $auth_service, - IServerConfigurationService $server_configuration_service, - ITrustedSitesService $trusted_sites_service, - DiscoveryController $discovery, - IUserService $user_service, - IUserActionService $user_action_service, - IClientService $client_service, - IApiScopeService $scope_service, - ITokenService $token_service, - IResourceServerService $resource_server_service, - IUtilsServerConfigurationService $utils_configuration_service - ) + /** + * @param IMementoOpenIdSerializerService $openid_memento_service + * @param IMementoOAuth2SerializerService $oauth2_memento_service + * @param IAuthService $auth_service + * @param IServerConfigurationService $server_configuration_service + * @param ITrustedSitesService $trusted_sites_service + * @param DiscoveryController $discovery + * @param IUserService $user_service + * @param IUserActionService $user_action_service + * @param IClientService $client_service + * @param IApiScopeService $scope_service + * @param ITokenService $token_service + * @param IResourceServerService $resource_server_service + * @param IUtilsServerConfigurationService $utils_configuration_service + */ + public function __construct + ( + IMementoOpenIdSerializerService $openid_memento_service, + IMementoOAuth2SerializerService $oauth2_memento_service, + IAuthService $auth_service, + IServerConfigurationService $server_configuration_service, + ITrustedSitesService $trusted_sites_service, + DiscoveryController $discovery, + IUserService $user_service, + IUserActionService $user_action_service, + IClientService $client_service, + IApiScopeService $scope_service, + ITokenService $token_service, + IResourceServerService $resource_server_service, + IUtilsServerConfigurationService $utils_configuration_service, + ISecurityContextService $security_context_service + ) { + $this->openid_memento_service = $openid_memento_service; $this->oauth2_memento_service = $oauth2_memento_service; $this->auth_service = $auth_service; @@ -69,25 +130,50 @@ class UserController extends OpenIdController $this->scope_service = $scope_service; $this->token_service = $token_service; $this->resource_server_service = $resource_server_service; - $this->utils_configuration_service = $utils_configuration_service; + $this->utils_configuration_service = $utils_configuration_service; //filters $this->beforeFilter('csrf', array('only' => array('postLogin', 'postConsent'))); - $openid_msg = $this->openid_memento_service->getCurrentRequest(); - $oauth2_msg = $this->oauth2_memento_service->getCurrentAuthorizationRequest(); - - if (!is_null($openid_msg) && $openid_msg->isValid() && OpenIdAuthenticationRequest::IsOpenIdAuthenticationRequest($openid_msg)) { + if ($this->openid_memento_service->exists()) + { //openid stuff - $this->beforeFilter('openid.save.request'); - $this->beforeFilter('openid.needs.auth.request', array('only' => array('getConsent'))); - $this->login_strategy = new OpenIdLoginStrategy($openid_memento_service, $user_action_service, $auth_service); - $this->consent_strategy = new OpenIdConsentStrategy($openid_memento_service, $auth_service, $server_configuration_service, $user_action_service); - } else if (!is_null($oauth2_msg) && $oauth2_msg->isValid()) { - $this->beforeFilter('oauth2.save.request'); - $this->beforeFilter('oauth2.needs.auth.request', array('only' => array('getConsent'))); - $this->login_strategy = new OAuth2LoginStrategy($auth_service, $oauth2_memento_service ,$user_action_service); - $this->consent_strategy = new OAuth2ConsentStrategy($auth_service, $oauth2_memento_service, $scope_service, $client_service); - } else { + $this->login_strategy = new OpenIdLoginStrategy + ( + $openid_memento_service, + $user_action_service, + $auth_service + ); + + $this->consent_strategy = new OpenIdConsentStrategy + ( + $openid_memento_service, + $auth_service, + $server_configuration_service, + $user_action_service + ); + + } + else if ($this->oauth2_memento_service->exists()) + { + + $this->login_strategy = new OAuth2LoginStrategy + ( + $auth_service, + $oauth2_memento_service, + $user_action_service, + $security_context_service + ); + + $this->consent_strategy = new OAuth2ConsentStrategy + ( + $auth_service, + $oauth2_memento_service, + $scope_service, + $client_service + ); + } + else + { //default stuff $this->login_strategy = new DefaultLoginStrategy($user_action_service, $auth_service); $this->consent_strategy = null; @@ -106,46 +192,54 @@ class UserController extends OpenIdController public function postLogin() { - try { + try + { $max_login_attempts_2_show_captcha = $this->server_configuration_service->getConfigValue("MaxFailed.LoginAttempts.2ShowCaptcha"); $data = Input::all(); $login_attempts = intval(Input::get('login_attempts')); // Build the validation constraint set. - $rules = array( + $rules = array + ( 'username' => 'required|email', 'password' => 'required', ); - if ($login_attempts >= $max_login_attempts_2_show_captcha) { - $rules['recaptcha_response_field'] = 'required|recaptcha'; + if ($login_attempts >= $max_login_attempts_2_show_captcha) + { + $rules['g-recaptcha-response'] = 'required|recaptcha'; } // Create a new validator instance. $validator = Validator::make($data, $rules); - - - if ($validator->passes()) { - $username = Input::get("username"); - $password = Input::get("password"); - $remember = Input::get("remember"); + if ($validator->passes()) + { + $username = Input::get("username"); + $password = Input::get("password"); + $remember = Input::get("remember"); $remember = !is_null($remember); - if ($this->auth_service->login($username, $password, $remember)) { + if ($this->auth_service->login($username, $password, $remember)) + { return $this->login_strategy->postLogin(); } //failed login attempt... $user = $this->auth_service->getUserByUsername($username); - if ($user) { + if ($user) + { $login_attempts = $user->login_failed_attempt; } + return Redirect::action('UserController@getLogin') - ->with('max_login_attempts_2_show_captcha', $max_login_attempts_2_show_captcha) - ->with('login_attempts', $login_attempts) - ->with('username',$username) - ->with('flash_notice', "We're sorry, your username or password does not match an existing record."); + ->with('max_login_attempts_2_show_captcha', $max_login_attempts_2_show_captcha) + ->with('login_attempts', $login_attempts) + ->with('username', $username) + ->with('flash_notice', "We're sorry, your username or password does not match an existing record."); } + return Redirect::action('UserController@getLogin') - ->withErrors($validator); - } catch (Exception $ex) { + ->withErrors($validator); + } + catch (Exception $ex) + { Log::error($ex); return Redirect::action('UserController@getLogin'); } @@ -154,19 +248,26 @@ class UserController extends OpenIdController public function getConsent() { if (is_null($this->consent_strategy)) + { return View::make("404"); + } + return $this->consent_strategy->getConsent(); } public function postConsent() { - try { + try + { $trust_action = input::get("trust"); - if (!is_null($trust_action) && !is_null($this->consent_strategy)) { + if (!is_null($trust_action) && !is_null($this->consent_strategy)) + { return $this->consent_strategy->postConsent($trust_action); } return Redirect::action('UserController@getConsent'); - } catch (Exception $ex) { + } + catch (Exception $ex) + { Log::error($ex); return Redirect::action('UserController@getConsent'); } @@ -174,12 +275,16 @@ class UserController extends OpenIdController public function getIdentity($identifier) { - try { + try + { $user = $this->auth_service->getUserByOpenId($identifier); if (is_null($user)) + { return View::make("404"); + } - if ($this->isDiscoveryRequest()) { + if ($this->isDiscoveryRequest()) + { /* * If the Claimed Identifier was not previously discovered by the Relying Party * (the "openid.identity" in the request was "http://specs.openid.net/auth/2.0/identifier_select" @@ -191,15 +296,17 @@ class UserController extends OpenIdController } $current_user = $this->auth_service->getCurrentUser(); $another_user = false; - if ($current_user && $current_user->getIdentifier() != $user->getIdentifier()) { + if ($current_user && $current_user->getIdentifier() != $user->getIdentifier()) + { $another_user = true; } - $assets_url = $this->utils_configuration_service->getConfigValue("Assets.Url"); - $pic_url = $user->getPic(); - $pic_url = str_contains($pic_url,'http')?$pic_url:$assets_url.$pic_url; + $assets_url = $this->utils_configuration_service->getConfigValue("Assets.Url"); + $pic_url = $user->getPic(); + $pic_url = str_contains($pic_url, 'http') ? $pic_url : $assets_url . $pic_url; - $params = array( + $params = array + ( 'show_fullname' => $user->getShowProfileFullName(), 'username' => $user->getFullName(), 'show_email' => $user->getShowProfileEmail(), @@ -209,8 +316,11 @@ class UserController extends OpenIdController 'pic' => $pic_url, 'another_user' => $another_user, ); + return View::make("identity", $params); - } catch (Exception $ex) { + } + catch (Exception $ex) + { Log::error($ex); return View::make("404"); } @@ -218,8 +328,15 @@ class UserController extends OpenIdController public function logout() { - $this->user_action_service->addUserAction($this->auth_service->getCurrentUser(), IPHelper::getUserIp(), IUserActionService::LogoutAction); - Auth::logout(); + $this->user_action_service->addUserAction + ( + $this->auth_service->getCurrentUser(), + IPHelper::getUserIp(), + IUserActionService::LogoutAction + ); + + $this->auth_service->logout(); + return Redirect::action("UserController@getLogin"); } @@ -229,7 +346,8 @@ class UserController extends OpenIdController $sites = $user->getTrustedSites(); $actions = $user->getActions(); - return View::make("profile", array( + return View::make("profile", array + ( "username" => $user->getFullName(), "user_id" => $user->getId(), "is_oauth2_admin" => $user->isOAuth2ServerAdmin(), @@ -252,13 +370,15 @@ class UserController extends OpenIdController $show_pic = Input::get("show_pic"); $user = $this->auth_service->getCurrentUser(); $this->user_service->saveProfileInfo($user->getId(), $show_pic, $show_full_name, $show_email); + return Redirect::action("UserController@getProfile"); } public function deleteTrustedSite($id) { $this->trusted_sites_service->delTrustedSite($id); + return Redirect::action("UserController@getProfile"); } -} +} \ No newline at end of file diff --git a/app/database/migrations/2015_03_19_190534_insert_marketplace_api_endpoints_scopes.php b/app/database/migrations/2015_03_19_190534_insert_marketplace_api_endpoints_scopes.php index a1873273..0318d9a4 100644 --- a/app/database/migrations/2015_03_19_190534_insert_marketplace_api_endpoints_scopes.php +++ b/app/database/migrations/2015_03_19_190534_insert_marketplace_api_endpoints_scopes.php @@ -24,7 +24,7 @@ class InsertMarketplaceApiEndpointsScopes extends Migration { 'active' => true, 'Description' => 'Marketplace Public Clouds', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); // private clouds @@ -35,7 +35,7 @@ class InsertMarketplaceApiEndpointsScopes extends Migration { 'active' => true, 'Description' => 'Marketplace Private Clouds', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); // consultants @@ -46,7 +46,7 @@ class InsertMarketplaceApiEndpointsScopes extends Migration { 'active' => true, 'Description' => 'Marketplace Consultants', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); diff --git a/app/database/migrations/2015_06_30_192410_update_oauth2_client_oidc.php b/app/database/migrations/2015_06_30_192410_update_oauth2_client_oidc.php new file mode 100644 index 00000000..a52256a2 --- /dev/null +++ b/app/database/migrations/2015_06_30_192410_update_oauth2_client_oidc.php @@ -0,0 +1,95 @@ +dateTime('client_secret_expires_at')->nullable(); + $table->text('contacts')->nullable(); + $table->text('allowed_origins')->nullable(); + $table->text('redirect_uris')->nullable(); + $table->string('logo_uri')->nullable(); + $table->string('tos_uri')->nullable(); + // http://openid.net/specs/openid-connect-session-1_0.html#ClientMetadata + $table->text('post_logout_redirect_uris')->nullable(); + + $table->text('logout_uri')->nullable(); + $table->boolean('logout_session_required')->default(false); + $table->boolean('logout_use_iframe')->default(false); + + $table->string('policy_uri')->nullable(); + $table->string('jwks_uri')->nullable(); + $table->integer('default_max_age')->default(-1); + $table->boolean('require_auth_time')->default(false); + // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + $table->enum('token_endpoint_auth_method', array_merge(OAuth2Protocol::$token_endpoint_auth_methods, array ( + OAuth2Protocol::TokenEndpoint_AuthMethod_None)))->default(OAuth2Protocol::TokenEndpoint_AuthMethod_None); + $table->enum("token_endpoint_auth_signing_alg", OAuth2Protocol::$supported_signing_algorithms)->default(JSONWebSignatureAndEncryptionAlgorithms::None); + $table->enum('subject_type', Client::$valid_subject_types)->default(IClient::SubjectType_Public); + + $table->enum("userinfo_signed_response_alg", OAuth2Protocol::$supported_signing_algorithms)->default(JSONWebSignatureAndEncryptionAlgorithms::None); + // encryption + $table->enum("userinfo_encrypted_response_alg", OAuth2Protocol::$supported_key_management_algorithms)->default(JSONWebSignatureAndEncryptionAlgorithms::None); + $table->enum("userinfo_encrypted_response_enc", OAuth2Protocol::$supported_content_encryption_algorithms)->default(JSONWebSignatureAndEncryptionAlgorithms::None); + + $table->enum("id_token_signed_response_alg", OAuth2Protocol::$supported_signing_algorithms)->default(JSONWebSignatureAndEncryptionAlgorithms::None); + // encryption + $table->enum("id_token_encrypted_response_alg", OAuth2Protocol::$supported_key_management_algorithms)->default(JSONWebSignatureAndEncryptionAlgorithms::None); + $table->enum("id_token_encrypted_response_enc", OAuth2Protocol::$supported_content_encryption_algorithms)->default(JSONWebSignatureAndEncryptionAlgorithms::None); + + }); + + DB::statement("ALTER TABLE oauth2_client MODIFY COLUMN client_type ENUM('PUBLIC','CONFIDENTIAL') default 'CONFIDENTIAL';"); + DB::statement("ALTER TABLE oauth2_client MODIFY COLUMN application_type ENUM('WEB_APPLICATION','JS_CLIENT','SERVICE', 'NATIVE') default 'WEB_APPLICATION';"); + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + Schema::table('oauth2_client', function($table) { + $table->dropColumn('client_secret_expires_at'); + $table->dropColumn('contacts'); + $table->dropColumn('allowed_origins'); + $table->dropColumn('redirect_uris'); + $table->dropColumn('logo_uri'); + $table->dropColumn('tos_uri'); + $table->dropColumn('post_logout_redirect_uris'); + $table->dropColumn('logout_uri'); + $table->dropColumn('logout_session_required'); + $table->dropColumn('logout_use_iframe'); + $table->dropColumn('policy_uri'); + $table->dropColumn('jwks_uri'); + $table->dropColumn('default_max_age'); + $table->dropColumn('require_auth_time'); + $table->dropColumn('token_endpoint_auth_method'); + $table->dropColumn('token_endpoint_auth_signing_alg'); + $table->dropColumn('subject_type'); + $table->dropColumn('userinfo_signed_response_alg'); + $table->dropColumn('userinfo_encrypted_response_alg'); + $table->dropColumn('userinfo_encrypted_response_enc'); + $table->dropColumn('id_token_signed_response_alg'); + $table->dropColumn('id_token_encrypted_response_alg'); + $table->dropColumn('id_token_encrypted_response_enc'); + }); + + DB::statement("ALTER TABLE oauth2_client MODIFY COLUMN application_type ENUM('WEB_APPLICATION','JS_CLIENT','SERVICE') default 'WEB_APPLICATION';"); + } + +} diff --git a/app/database/migrations/2015_07_09_044730_create_assymetric_keys.php b/app/database/migrations/2015_07_09_044730_create_assymetric_keys.php new file mode 100644 index 00000000..05886e0b --- /dev/null +++ b/app/database/migrations/2015_07_09_044730_create_assymetric_keys.php @@ -0,0 +1,87 @@ +bigIncrements('id'); + $table->timestamps(); + $table->text('pem_content'); + $table->string('kid'); + $table->boolean('active')->default(true); + + $table->enum + ( + 'usage', + JSONWebKeyPublicKeyUseValues::$valid_uses + )->default(JSONWebKeyPublicKeyUseValues::Signature); + + $table->enum('class_name', array('ClientPublicKey', 'ServerPrivateKey'))->default('ClientPublicKey'); + + $table->enum( + 'type', + array + ( + JSONWebKeyTypes::RSA, + JSONWebKeyTypes::EllipticCurve + ) + )->default(JSONWebKeyTypes::RSA); + + $table->dateTime('last_use')->nullable(); + $table->text('password')->nullable(); + $table->dateTime('valid_from'); + $table->dateTime('valid_to'); + + $table->enum + ( + 'alg', + array_merge + ( + OAuth2Protocol::$supported_signing_algorithms_rsa, + OAuth2Protocol::$supported_key_management_algorithms + ) + ) + ->default + ( + JSONWebSignatureAndEncryptionAlgorithms::None + ); + + // FK + $table->bigInteger("oauth2_client_id")->unsigned()->nullable(); + $table->index('oauth2_client_id'); + $table->foreign('oauth2_client_id') + ->references('id') + ->on('oauth2_client') + ->onDelete('cascade') + ->onUpdate('no action'); + }); + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + Schema::table('oauth2_assymetric_keys', function ($table) { + $table->dropForeign('oauth2_client_id'); + }); + Schema::dropIfExists('oauth2_assymetric_keys'); + } + +} diff --git a/app/database/migrations/2015_07_23_151507_migrate_redirect_uris.php b/app/database/migrations/2015_07_23_151507_migrate_redirect_uris.php new file mode 100644 index 00000000..7da2b292 --- /dev/null +++ b/app/database/migrations/2015_07_23_151507_migrate_redirect_uris.php @@ -0,0 +1,54 @@ +orderBy('client_id', 'desc')->get(); + foreach($uris as $uri) + { + $client = Client::find($uri->client_id); + $redirect_uris = $client->redirect_uris; + if(!empty($redirect_uris)) + $redirect_uris = $redirect_uris.','.$uri->uri; + else + $redirect_uris = $uri->uri; + + $client->redirect_uris = $redirect_uris; + + $client->save(); + } + + $uris = DB::table('oauth2_client_allowed_origin')->orderBy('client_id', 'desc')->get(); + foreach($uris as $uri) + { + $client = Client::find($uri->client_id); + $allowed_origins = $client->allowed_origins; + if(!empty($allowed_origins)) + $allowed_origins = $redirect_uris.','.$uri->allowed_origin; + else + $allowed_origins = $uri->allowed_origin; + + $client->allowed_origins = $allowed_origins; + + $client->save(); + } + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + // + } + +} diff --git a/app/database/migrations/2015_07_23_151520_add_new_default_scopes.php b/app/database/migrations/2015_07_23_151520_add_new_default_scopes.php new file mode 100644 index 00000000..b9680e83 --- /dev/null +++ b/app/database/migrations/2015_07_23_151520_add_new_default_scopes.php @@ -0,0 +1,50 @@ + OAuth2Protocol::OpenIdConnect_Scope, + 'short_description' => 'OpenId Connect Protocol', + 'description' => 'OpenId Connect Protocol', + 'api_id' => null, + 'system' => true, + 'default' => true, + 'active' => true, + ) + ); + + ApiScope::create( + array( + 'name' => OAuth2Protocol::OfflineAccess_Scope, + 'short_description' => 'allow to emit refresh tokens (offline access without user presence)', + 'description' => 'allow to emit refresh tokens (offline access without user presence)', + 'api_id' => null, + 'system' => true, + 'default' => true, + 'active' => true, + ) + ); + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + ApiScope::where('name', '=', OAuth2Protocol::OpenIdConnect_Scope)->first()-delete(); + ApiScope::where('name', '=', OAuth2Protocol::OfflineAccess_Scope)->first()-delete(); + } + +} diff --git a/app/database/migrations/2015_07_23_151541_update_oauth2_client_scopes.php b/app/database/migrations/2015_07_23_151541_update_oauth2_client_scopes.php new file mode 100644 index 00000000..3f75ba9a --- /dev/null +++ b/app/database/migrations/2015_07_23_151541_update_oauth2_client_scopes.php @@ -0,0 +1,43 @@ +first(); + $scope_offline = ApiScope::where('name', '=', OAuth2Protocol::OfflineAccess_Scope)->first(); + + foreach($clients as $client) + { + $client->scopes()->attach($scope_openid->id); + if($client->application_type === IClient::ApplicationType_Native || $client->application_type === IClient::ApplicationType_Web_App) + $client->scopes()->attach($scope_offline->id); + + if ($client->client_type === IClient::ClientType_Confidential) + { + $client->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic; + $client->save(); + } + } + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + // + } + +} diff --git a/app/database/migrations/2015_07_30_221758_oauth2_clients_update_secret_expiration_date.php b/app/database/migrations/2015_07_30_221758_oauth2_clients_update_secret_expiration_date.php new file mode 100644 index 00000000..47ba9498 --- /dev/null +++ b/app/database/migrations/2015_07_30_221758_oauth2_clients_update_secret_expiration_date.php @@ -0,0 +1,36 @@ +client_type !== IClient::ClientType_Confidential) continue; + // default 6 months + $client->client_secret_expires_at = $now->add(new \DateInterval('P6M')); + $client->save(); + } + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + // + } + +} diff --git a/app/database/migrations/2015_08_10_205534_add_user_info_OIDC_endpoint.php b/app/database/migrations/2015_08_10_205534_add_user_info_OIDC_endpoint.php new file mode 100644 index 00000000..a5617c38 --- /dev/null +++ b/app/database/migrations/2015_08_10_205534_add_user_info_OIDC_endpoint.php @@ -0,0 +1,66 @@ +first(); + + if(is_null($users)) return; + + ApiEndpoint::create( + array( + 'name' => 'get-user-claims-get', + 'active' => true, + 'api_id' => $users->id, + 'route' => '/api/v1/users/info', + 'http_method' => 'GET', + 'allow_cors' => true, + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-user-claims-post', + 'active' => true, + 'api_id' => $users->id, + 'route' => '/api/v1/users/info', + 'http_method' => 'POST', + 'allow_cors' => true, + ) + ); + + $profile_scope = ApiScope::where('name', '=', 'profile')->first(); + $email_scope = ApiScope::where('name', '=', 'email')->first(); + $address_scope = ApiScope::where('name', '=', 'address')->first(); + + + $get_user_info_endpoint = ApiEndpoint::where('name', '=', 'get-user-claims-get')->first(); + $get_user_info_endpoint->scopes()->attach($profile_scope->id); + $get_user_info_endpoint->scopes()->attach($email_scope->id); + $get_user_info_endpoint->scopes()->attach($address_scope->id); + + $get_user_info_endpoint = ApiEndpoint::where('name', '=', 'get-user-claims-post')->first(); + $get_user_info_endpoint->scopes()->attach($profile_scope->id); + $get_user_info_endpoint->scopes()->attach($email_scope->id); + $get_user_info_endpoint->scopes()->attach($address_scope->id); + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + // + } + +} diff --git a/app/database/migrations/2015_12_14_203852_add_api_scopes_groups.php b/app/database/migrations/2015_12_14_203852_add_api_scopes_groups.php new file mode 100644 index 00000000..9495ef16 --- /dev/null +++ b/app/database/migrations/2015_12_14_203852_add_api_scopes_groups.php @@ -0,0 +1,75 @@ +bigIncrements('id')->unsigned(); + $table->string('name', 512); + $table->text('description'); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + + Schema::create('oauth2_api_scope_group_scope', function($table) + { + $table->timestamps(); + + $table->bigInteger("group_id")->unsigned(); + $table->index('group_id'); + $table->foreign('group_id') + ->references('id') + ->on('oauth2_api_scope_group') + ->onDelete('cascade') + ->onUpdate('no action'); ; + + $table->bigInteger("scope_id")->unsigned(); + $table->index('scope_id'); + $table->foreign('scope_id') + ->references('id') + ->on('oauth2_api_scope') + ->onDelete('cascade') + ->onUpdate('no action'); + }); + + Schema::create('oauth2_api_scope_group_users', function($table) + { + $table->timestamps(); + + $table->bigInteger("group_id")->unsigned(); + $table->index('group_id'); + $table->foreign('group_id') + ->references('id') + ->on('oauth2_api_scope_group') + ->onDelete('cascade') + ->onUpdate('no action'); ; + + $table->bigInteger("user_id")->unsigned(); + $table->index('user_id'); + $table->foreign('user_id') + ->references('id') + ->on('openid_users') + ->onDelete('cascade') + ->onUpdate('no action'); + }); + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + Schema::dropIfExists('oauth2_api_scope_group'); + Schema::dropIfExists('oauth2_api_scope_group_scope'); + Schema::dropIfExists('oauth2_api_scope_group_users'); + } +} diff --git a/app/database/migrations/2015_12_15_201400_update_api_scope.php b/app/database/migrations/2015_12_15_201400_update_api_scope.php new file mode 100644 index 00000000..cd77f7ec --- /dev/null +++ b/app/database/migrations/2015_12_15_201400_update_api_scope.php @@ -0,0 +1,26 @@ +boolean('assigned_by_groups')->default(false); + }); + } + + /** + * Reverse the migrations. + * @return void + */ + public function down() + { + Schema::table('oauth2_api_scope', function ($table) { + $table->dropColumn('assigned_by_groups'); + }); + } + +} diff --git a/app/database/seeds/ApiEndpointSeeder.php b/app/database/seeds/ApiEndpointSeeder.php index 03ef0c52..b03a3c96 100644 --- a/app/database/seeds/ApiEndpointSeeder.php +++ b/app/database/seeds/ApiEndpointSeeder.php @@ -37,6 +37,36 @@ class ApiEndpointSeeder extends Seeder $get_user_info_endpoint->scopes()->attach($profile_scope->id); $get_user_info_endpoint->scopes()->attach($email_scope->id); $get_user_info_endpoint->scopes()->attach($address_scope->id); + + ApiEndpoint::create( + array( + 'name' => 'get-user-claims-get', + 'active' => true, + 'api_id' => $users->id, + 'route' => '/api/v1/users/info', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-user-claims-post', + 'active' => true, + 'api_id' => $users->id, + 'route' => '/api/v1/users/info', + 'http_method' => 'POST' + ) + ); + + $get_user_info_endpoint = ApiEndpoint::where('name','=','get-user-claims-get')->first(); + $get_user_info_endpoint->scopes()->attach($profile_scope->id); + $get_user_info_endpoint->scopes()->attach($email_scope->id); + $get_user_info_endpoint->scopes()->attach($address_scope->id); + + $get_user_info_endpoint = ApiEndpoint::where('name','=','get-user-claims-post')->first(); + $get_user_info_endpoint->scopes()->attach($profile_scope->id); + $get_user_info_endpoint->scopes()->attach($email_scope->id); + $get_user_info_endpoint->scopes()->attach($address_scope->id); } } \ No newline at end of file diff --git a/app/database/seeds/ApiScopeSeeder.php b/app/database/seeds/ApiScopeSeeder.php index 7e7ac270..7d320b96 100644 --- a/app/database/seeds/ApiScopeSeeder.php +++ b/app/database/seeds/ApiScopeSeeder.php @@ -1,5 +1,7 @@ OAuth2Protocol::OpenIdConnect_Scope, + 'short_description' => 'OpenId Connect Protocol', + 'description' => 'OpenId Connect Protocol', + 'api_id' => null, + 'system' => true, + 'default' => true, + 'active' => true, + ) + ); + + ApiScope::create( + array( + 'name' => OAuth2Protocol::OfflineAccess_Scope, + 'short_description' => 'allow to emit refresh tokens (offline access without user presence)', + 'description' => 'allow to emit refresh tokens (offline access without user presence)', + 'api_id' => null, + 'system' => true, + 'default' => true, + 'active' => true, + ) + ); + } } \ No newline at end of file diff --git a/app/database/seeds/ApiSeeder.php b/app/database/seeds/ApiSeeder.php index afea2c1c..4fc4d81f 100644 --- a/app/database/seeds/ApiSeeder.php +++ b/app/database/seeds/ApiSeeder.php @@ -22,7 +22,7 @@ class ApiSeeder extends Seeder { 'active' => true, 'Description' => 'User Info', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); } diff --git a/app/database/seeds/TestSeeder.php b/app/database/seeds/TestSeeder.php index 5d4cc395..46145fac 100644 --- a/app/database/seeds/TestSeeder.php +++ b/app/database/seeds/TestSeeder.php @@ -3,17 +3,248 @@ use oauth2\models\IClient; use auth\User; use utils\services\IAuthService; +use \jwk\JSONWebKeyPublicKeyUseValues; +use \jwk\JSONWebKeyTypes; +use \oauth2\OAuth2Protocol; +use \jwa\JSONWebSignatureAndEncryptionAlgorithms; /** * Class OAuth2ApplicationSeeder * This seeder is only for testing purposes */ class TestSeeder extends Seeder { +static $client_private_key_1 = <<statement($member_table); + + Member::create( + array( + 'ID' => 1, + 'FirstName' => 'Sebastian', + 'Surname' => 'Marcet', + 'Email' => 'sebastian@tipit.net', + 'Password' => '1qaz2wsx', + 'PasswordEncryption' => 'none', + 'Salt' => 'none', + 'Gender' => 'male', + 'Address' => 'Av. Siempre Viva 111', + 'Suburb' => 'Lanus Este', + 'State' => 'Buenos Aires', + 'City' => 'Lanus', + 'Postcode' => '1824', + 'Country' => 'AR', + 'Locale' => 'ESP', + ) + ); + + Member::create( + array( + 'ID' => 2, + 'FirstName' => 'Sebastian', + 'Surname' => 'Marcet', + 'Email' => 'sebastian+1@tipit.net', + 'Password' => '1qaz2wsx', + 'PasswordEncryption' => 'none', + 'Salt' => 'none', + 'Gender' => 'male', + 'Address' => 'Av. Siempre Viva 111', + 'Suburb' => 'Lanus Este', + 'State' => 'Buenos Aires', + 'City' => 'Lanus', + 'Postcode' => '1824', + 'Country' => 'AR', + 'Locale' => 'ESP', + ) + ); + + Member::create( + array( + 'ID' => 3, + 'FirstName' => 'Sebastian', + 'Surname' => 'Marcet', + 'Email' => 'sebastian+2@tipit.net', + 'Password' => '1qaz2wsx', + 'PasswordEncryption' => 'none', + 'Salt' => 'none', + 'Gender' => 'male', + 'Address' => 'Av. Siempre Viva 111', + 'Suburb' => 'Lanus Este', + 'State' => 'Buenos Aires', + 'City' => 'Lanus', + 'Postcode' => '1824', + 'Country' => 'AR', + 'Locale' => 'ESP', + ) + ); + DB::table('banned_ips')->delete(); DB::table('user_exceptions_trail')->delete(); DB::table('server_configuration')->delete(); @@ -23,10 +254,12 @@ class TestSeeder extends Seeder { DB::table('oauth2_client_authorized_uri')->delete(); DB::table('oauth2_access_token')->delete(); DB::table('oauth2_refresh_token')->delete(); + DB::table('oauth2_assymetric_keys')->delete(); DB::table('oauth2_client')->delete(); DB::table('openid_trusted_sites')->delete(); DB::table('openid_associations')->delete(); + DB::table('user_actions')->delete(); DB::table('openid_users')->delete(); DB::table('oauth2_api_endpoint_api_scope')->delete(); @@ -53,6 +286,31 @@ class TestSeeder extends Seeder { $this->seedApis(); //scopes + + ApiScope::create( + array( + 'name' => OAuth2Protocol::OpenIdConnect_Scope, + 'short_description' => 'OIDC', + 'description' => 'OIDC', + 'api_id' => null, + 'system' => true, + 'default' => true, + 'active' => true, + ) + ); + + ApiScope::create( + array( + 'name' => OAuth2Protocol::OfflineAccess_Scope, + 'short_description' => 'allow to emit refresh tokens (offline access without user presence)', + 'description' => 'allow to emit refresh tokens (offline access without user presence)', + 'api_id' => null, + 'system' => true, + 'default' => true, + 'active' => true, + ) + ); + $this->seedResourceServerScopes(); $this->seedApiScopes(); $this->seedApiEndpointScopes(); @@ -308,7 +566,7 @@ class TestSeeder extends Seeder { User::create( array( 'identifier' => 'sebastian.marcet', - 'external_identifier' => 13867, + 'external_identifier' => 1, 'last_login_date' => gmdate("Y-m-d H:i:s", time()) ) ); @@ -323,18 +581,72 @@ class TestSeeder extends Seeder { ) ); + $now = new \DateTime(); + Client::create( array( 'app_name' => 'oauth2_test_app', 'app_description' => 'oauth2_test_app', 'app_logo' => null, 'client_id' => 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client', - 'client_secret' => 'ITc/6Y5N7kOtGKhg', + 'client_secret' => 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg', 'client_type' => IClient::ClientType_Confidential, 'application_type' => IClient::ApplicationType_Web_App, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic, 'user_id' => $user->id, 'rotate_refresh_token' => true, - 'use_refresh_token' => true + 'use_refresh_token' => true, + 'redirect_uris' => 'https://www.test.com/oauth2,https://op.certification.openid.net:60393/authz_cb', + 'id_token_signed_response_alg' => JSONWebSignatureAndEncryptionAlgorithms::HS512, + 'id_token_encrypted_response_alg' => JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP_256, + 'id_token_encrypted_response_enc' => JSONWebSignatureAndEncryptionAlgorithms::A256CBC_HS512, + 'client_secret_expires_at' => $now->add(new \DateInterval('P6M')), + ) + ); + + Client::create( + array + ( + 'app_name' => 'oauth2_test_app2', + 'app_description' => 'oauth2_test_app2', + 'app_logo' => null, + 'client_id' => 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x2.openstack.client', + 'client_secret' => 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg', + 'client_type' => IClient::ClientType_Confidential, + 'application_type' => IClient::ApplicationType_Web_App, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretJwt, + 'token_endpoint_auth_signing_alg' => JSONWebSignatureAndEncryptionAlgorithms::HS512, + 'subject_type' => IClient::SubjectType_Pairwise, + 'user_id' => $user->id, + 'rotate_refresh_token' => true, + 'use_refresh_token' => true, + 'redirect_uris' => 'https://www.test.com/oauth2', + 'id_token_signed_response_alg' => JSONWebSignatureAndEncryptionAlgorithms::HS512, + 'id_token_encrypted_response_alg' => JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP_256, + 'id_token_encrypted_response_enc' => JSONWebSignatureAndEncryptionAlgorithms::A256CBC_HS512, + + 'client_secret_expires_at' => $now->add(new \DateInterval('P6M')), + ) + ); + + Client::create + ( + array( + 'app_name' => 'oauth2_test_app3', + 'app_description' => 'oauth2_test_app3', + 'app_logo' => null, + 'client_id' => 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ33.openstack.client', + 'client_secret' => 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N585OtGKhg55', + 'client_type' => IClient::ClientType_Confidential, + 'application_type' => IClient::ApplicationType_Web_App, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic, + 'user_id' => $user->id, + 'rotate_refresh_token' => true, + 'use_refresh_token' => true, + 'redirect_uris' => 'https://www.test.com/oauth2', + 'id_token_signed_response_alg' => JSONWebSignatureAndEncryptionAlgorithms::HS512, + 'userinfo_signed_response_alg' => JSONWebSignatureAndEncryptionAlgorithms::RS512, + 'client_secret_expires_at' => $now->add(new \DateInterval('P6M')), ) ); @@ -344,12 +656,15 @@ class TestSeeder extends Seeder { 'app_description' => 'oauth2.service', 'app_logo' => null, 'client_id' => '11z87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client', - 'client_secret' => '11c/6Y5N7kOtGKhg', + 'client_secret' => '11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg', 'client_type' => IClient::ClientType_Confidential, 'application_type' => IClient::ApplicationType_Service, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic, 'user_id' => $user->id, 'rotate_refresh_token' => true, - 'use_refresh_token' => true + 'use_refresh_token' => true, + 'redirect_uris' => 'https://www.test.com/oauth2', + 'client_secret_expires_at' => $now->add(new \DateInterval('P6M')), ) ); @@ -360,11 +675,49 @@ class TestSeeder extends Seeder { 'app_logo' => null, 'client_id' => 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.openstack.client', 'client_secret' => null, - 'client_type' => IClient::ClientType_Public, 'application_type' => IClient::ApplicationType_JS_Client, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_PrivateKeyJwt, + 'token_endpoint_auth_signing_alg' => JSONWebSignatureAndEncryptionAlgorithms::RS512, 'user_id' => $user->id, 'rotate_refresh_token' => false, - 'use_refresh_token' => false + 'use_refresh_token' => false, + 'redirect_uris' => 'https://www.test.com/oauth2', + + ) + ); + + Client::create( + array( + 'app_name' => 'oauth2_native_app', + 'app_description' => 'oauth2_native_app', + 'app_logo' => null, + 'client_id' => 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.android.openstack.client', + 'client_secret' => '11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhgfdfdfdf', + 'client_type' => IClient::ClientType_Confidential, + 'application_type' => IClient::ApplicationType_Native, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic, + 'user_id' => $user->id, + 'rotate_refresh_token' => true, + 'use_refresh_token' => true, + 'redirect_uris' => 'androipapp://oidc_endpoint_callback', + ) + ); + + Client::create( + array( + 'app_name' => 'oauth2_native_app2', + 'app_description' => 'oauth2_native_app2', + 'app_logo' => null, + 'client_id' => 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.android2.openstack.client', + 'client_secret' => '11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhgfdfdfdf', + 'client_type' => IClient::ClientType_Confidential, + 'application_type' => IClient::ApplicationType_Native, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_PrivateKeyJwt, + 'token_endpoint_auth_signing_alg' => JSONWebSignatureAndEncryptionAlgorithms::RS512, + 'user_id' => $user->id, + 'rotate_refresh_token' => true, + 'use_refresh_token' => true, + 'redirect_uris' => 'androipapp://oidc_endpoint_callback2', ) ); @@ -379,7 +732,8 @@ class TestSeeder extends Seeder { 'application_type' => IClient::ApplicationType_JS_Client, 'user_id' => $user->id, 'rotate_refresh_token' => false, - 'use_refresh_token' => false + 'use_refresh_token' => false, + 'redirect_uris' => 'https://www.test.com/oauth2' ) ); @@ -389,47 +743,158 @@ class TestSeeder extends Seeder { 'app_description' => 'resource_server_client', 'app_logo' => null, 'client_id' => 'resource.server.1.openstack.client', - 'client_secret' => '123456789', + 'client_secret' => '123456789123456789123456789123456789123456789', 'client_type' => IClient::ClientType_Confidential, 'application_type' => IClient::ApplicationType_Service, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic, 'resource_server_id' => $resource_server->id, 'rotate_refresh_token' => false, - 'use_refresh_token' => false + 'use_refresh_token' => false, + 'client_secret_expires_at' => $now->add(new \DateInterval('P6M')), ) ); - $client_confidential = Client::where('app_name','=','oauth2_test_app')->first(); - $client_public = Client::where('app_name','=','oauth2_test_app_public')->first(); - $client_service = Client::where('app_name','=','oauth2.service')->first(); - //attach scopes + $client_confidential = Client::where('app_name','=','oauth2_test_app')->first(); + $client_confidential2 = Client::where('app_name','=','oauth2_test_app2')->first(); + $client_confidential3 = Client::where('app_name','=','oauth2_test_app3')->first(); + $client_public = Client::where('app_name','=','oauth2_test_app_public')->first(); + $client_service = Client::where('app_name','=','oauth2.service')->first(); + $client_native = Client::where('app_name','=','oauth2_native_app')->first(); + $client_native2 = Client::where('app_name','=','oauth2_native_app2')->first(); + + //attach all scopes $scopes = ApiScope::get(); - foreach($scopes as $scope){ + foreach($scopes as $scope) + { $client_confidential->scopes()->attach($scope->id); + $client_confidential2->scopes()->attach($scope->id); + $client_confidential3->scopes()->attach($scope->id); $client_public->scopes()->attach($scope->id); $client_service->scopes()->attach($scope->id); + $client_native->scopes()->attach($scope->id); + $client_native2->scopes()->attach($scope->id); } - //add uris - ClientAuthorizedUri::create( - array( - 'uri' => 'https://www.test.com/oauth2', - 'client_id' => $client_confidential->id - ) + + $now = new \DateTime('now'); + $to = new \DateTime('now'); + $to->add(new \DateInterval('P31D')); + + $public_key_1 = ClientPublicKey::buildFromPEM( + 'public_key_1', + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Encryption, + self::$client_public_key_1, + JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP_256, + true, + $now, + $to ); - //add uris - ClientAllowedOrigin::create( - array( - 'allowed_origin' => 'https://www.test.com/oauth2', - 'client_id' => $client_confidential->id - ) + $public_key_1->oauth2_client_id = $client_confidential->id; + $public_key_1->save(); + + $public_key_2 = ClientPublicKey::buildFromPEM( + 'public_key_2', + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Signature, + self::$client_public_key_2, + JSONWebSignatureAndEncryptionAlgorithms::RS512, + true, + $now, + $to ); - ClientAuthorizedUri::create( - array( - 'uri'=>'https://www.test.com/oauth2', - 'client_id'=>$client_public->id - ) + $public_key_2->oauth2_client_id = $client_confidential->id; + $public_key_2->save(); + + // confidential client 2 + $public_key_11 = ClientPublicKey::buildFromPEM( + 'public_key_1', + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Encryption, + self::$client_public_key_1, + JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP_256, + true, + $now, + $to ); + + $public_key_11->oauth2_client_id = $client_confidential2->id; + $public_key_11->save(); + + $public_key_22 = ClientPublicKey::buildFromPEM( + 'public_key_2', + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Signature, + self::$client_public_key_2, + JSONWebSignatureAndEncryptionAlgorithms::RS512, + true, + $now, + $to + ); + + $public_key_22->oauth2_client_id = $client_confidential2->id; + $public_key_22->save(); + + // public native client + $public_key_33 = ClientPublicKey::buildFromPEM( + 'public_key_33', + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Encryption, + self::$client_public_key_1, + JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP_256, + true, + $now, + $to + ); + + $public_key_33->oauth2_client_id = $client_native2->id; + $public_key_33->save(); + + $public_key_44 = ClientPublicKey::buildFromPEM( + 'public_key_44', + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Signature, + self::$client_public_key_2, + JSONWebSignatureAndEncryptionAlgorithms::RS512, + true, + $now, + $to + ); + + $public_key_44->oauth2_client_id = $client_native2->id; + $public_key_44->save(); + + // server private keys + + $pkey_1 = ServerPrivateKey::build + ( + 'server_key_enc', + $now, + $to, + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Encryption, + JSONWebSignatureAndEncryptionAlgorithms::RSA1_5, + true, + TestKeys::$private_key_pem + ); + + $pkey_1->save(); + + + $pkey_2 = ServerPrivateKey::build + ( + 'server_key_sig', + $now, + $to, + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Signature, + JSONWebSignatureAndEncryptionAlgorithms::RS512, + true, + TestKeys::$private_key_pem + ); + + $pkey_2->save(); } private function seedApis(){ @@ -442,7 +907,7 @@ class TestSeeder extends Seeder { 'active' => true, 'Description' => 'Resource Server CRUD operations', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); @@ -453,7 +918,7 @@ class TestSeeder extends Seeder { 'active' => true, 'Description' => 'Api CRUD operations', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); @@ -465,7 +930,7 @@ class TestSeeder extends Seeder { 'active' => true, 'Description' => 'Api Endpoints CRUD operations', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); @@ -476,7 +941,7 @@ class TestSeeder extends Seeder { 'active' => true, 'Description' => 'Api Scopes CRUD operations', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); @@ -487,7 +952,7 @@ class TestSeeder extends Seeder { 'active' => true, 'Description' => 'User Info', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); @@ -498,7 +963,7 @@ class TestSeeder extends Seeder { 'active' => true, 'Description' => 'Marketplace Public Clouds', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); @@ -509,7 +974,7 @@ class TestSeeder extends Seeder { 'active' => true, 'Description' => 'Marketplace Private Clouds', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); @@ -520,7 +985,7 @@ class TestSeeder extends Seeder { 'active' => true, 'Description' => 'Marketplace Consultants', 'resource_server_id' => $resource_server->id, - 'logo' => asset('img/apis/server.png') + 'logo' => asset('/assets/img/apis/server.png') ) ); @@ -667,6 +1132,7 @@ class TestSeeder extends Seeder { ) ); + } private function seedApiEndpointScopes(){ @@ -1363,6 +1829,27 @@ class TestSeeder extends Seeder { 'http_method' => 'GET' ) ); + + ApiEndpoint::create( + array( + 'name' => 'get-user-claims-get', + 'active' => true, + 'api_id' => $users->id, + 'route' => '/api/v1/users/info', + 'http_method' => 'GET' + ) + ); + + ApiEndpoint::create( + array( + 'name' => 'get-user-claims-post', + 'active' => true, + 'api_id' => $users->id, + 'route' => '/api/v1/users/info', + 'http_method' => 'POST' + ) + ); + $profile_scope = ApiScope::where('name','=','profile')->first(); $email_scope = ApiScope::where('name','=','email')->first(); $address_scope = ApiScope::where('name','=','address')->first(); @@ -1371,6 +1858,16 @@ class TestSeeder extends Seeder { $get_user_info_endpoint->scopes()->attach($profile_scope->id); $get_user_info_endpoint->scopes()->attach($email_scope->id); $get_user_info_endpoint->scopes()->attach($address_scope->id); + + $get_user_info_endpoint = ApiEndpoint::where('name','=','get-user-claims-get')->first(); + $get_user_info_endpoint->scopes()->attach($profile_scope->id); + $get_user_info_endpoint->scopes()->attach($email_scope->id); + $get_user_info_endpoint->scopes()->attach($address_scope->id); + + $get_user_info_endpoint = ApiEndpoint::where('name','=','get-user-claims-post')->first(); + $get_user_info_endpoint->scopes()->attach($profile_scope->id); + $get_user_info_endpoint->scopes()->attach($email_scope->id); + $get_user_info_endpoint->scopes()->attach($address_scope->id); } private function seedPublicCloudsEndpoints(){ diff --git a/app/filters.php b/app/filters.php index ce811342..1e9af545 100644 --- a/app/filters.php +++ b/app/filters.php @@ -1,4 +1,5 @@ getService(UtilsServiceCatalog::CheckPointService); - if (!$checkpoint_service->check()) { - return View::make('404'); + + ClientAuthContextValidatorFactory::setTokenEndpointUrl + ( + URL::action("OAuth2ProviderController@token") + ); + + ClientAuthContextValidatorFactory::setJWKSetReader + ( + App::make('oauth2\services\IClientJWKSetReader') + ); + + if(Config::get('server.Banning_Enable', true)) + { + try { + //checkpoint security pattern entry point + $checkpoint_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::CheckPointService); + if (!$checkpoint_service->check()) { + return Response::view('404', array(), 404); + } + } catch (Exception $ex) { + Log::error($ex); + + return Response::view('404', array(), 404); } - } catch (Exception $ex) { - Log::error($ex); - return View::make('404'); } $cors = ServiceLocator::getInstance()->getService('CORSMiddleware'); @@ -39,15 +58,6 @@ App::after(function($request, $response){ // https://www.owasp.org/index.php/List_of_useful_HTTP_headers $response->headers->set('X-content-type-options','nosniff'); $response->headers->set('X-xss-protection','1; mode=block'); - // http://tools.ietf.org/html/rfc6797 - /** - * The HSTS header field below stipulates that the HSTS Policy is to - * remain in effect for one year (there are approximately 31536000 - * seconds in a year) - * applies to the domain of the issuing HSTS Host and all of its - * subdomains: - */ - $response->headers->set('Strict-Transport-Security','max-age=31536000; includeSubDomains'); //cache $response->headers->set('pragma','no-cache'); $response->headers->set('Expires','-1'); @@ -70,11 +80,13 @@ App::after(function($request, $response){ Route::filter('auth', function () { if (Auth::guest()) { Session::put('url.intended', URL::full()); + Session::save(); return Redirect::action('HomeController@index'); } $redirect = Session::get('url.intended'); if (!empty($redirect)) { Session::forget('url.intended'); + Session::save(); return Redirect::to($redirect); } }); @@ -120,57 +132,15 @@ Route::filter('ajax', function() if (!Request::ajax()) App::abort(404); }); - -Route::filter("openid.needs.auth.request", function () { - - $memento_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::MementoService); - $openid_message = $memento_service->getCurrentRequest(); - - if ($openid_message == null || !$openid_message->isValid()) - throw new InvalidOpenIdMessageException(); - $configuration_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::ServerConfigurationService); - $auth_request = new OpenIdAuthenticationRequest($openid_message, $configuration_service->getUserIdentityEndpointURL('@identifier')); - if (!$auth_request->isValid()) - throw new InvalidOpenIdMessageException(); -}); - -Route::filter("openid.save.request", function () { - - $memento_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::MementoService); - $memento_service->saveCurrentRequest(); - -}); - -Route::filter("oauth2.save.request", function () { - - $memento_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::MementoService); - $memento_service->saveCurrentAuthorizationRequest(); -}); - -Route::filter("oauth2.needs.auth.request", function () { - - $memento_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::MementoService); - $oauth2_message = $memento_service->getCurrentAuthorizationRequest(); - - if ($oauth2_message == null || !$oauth2_message->isValid()) - throw new InvalidAuthorizationRequestException(); -}); - Route::filter("ssl", function () { if ((!Request::secure()) && (ServerConfigurationService::getConfigValue("SSL.Enable"))) { - $openid_memento_service = ServiceLocator::getInstance()->getService(OpenIdServiceCatalog::MementoService); - $openid_memento_service->saveCurrentRequest(); - - $oauth2_memento_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::MementoService); - $oauth2_memento_service->saveCurrentAuthorizationRequest(); - return Redirect::secure(Request::getRequestUri()); } }); Route::filter("oauth2.enabled",function(){ if(!ServerConfigurationService::getConfigValue("OAuth2.Enable")){ - return View::make('404'); + return Response::view('404', array(), 404); } }); @@ -179,6 +149,13 @@ Route::filter('user.owns.client.policy',function($route, $request){ $authentication_service = ServiceLocator::getInstance()->getService(UtilsServiceCatalog::AuthenticationService); $client_service = ServiceLocator::getInstance()->getService(OAuth2ServiceCatalog::ClientService); $client_id = $route->getParameter('id'); + + if(is_null($client_id)) + $client_id = $route->getParameter('client_id'); + + if(is_null($client_id)) + $client_id =Input::get('client_id',null);; + $client = $client_service->getClientByIdentifier($client_id); $user = $authentication_service->getCurrentUser(); if (is_null($client) || intval($client->getUserId()) !== intval($user->getId())) @@ -215,8 +192,6 @@ Route::filter('is.current.user',function($route, $request){ }); - - // filter to protect an api endpoint with oauth2 Route::filter('oauth2.protected.endpoint','OAuth2BearerAccessTokenRequestValidator'); @@ -235,10 +210,10 @@ Route::filter('oauth2.server.admin.json',function(){ Route::filter('oauth2.server.admin',function(){ if (Auth::guest()) { - return View::make('404'); + return Response::view('404', array(), 404); } if(!Auth::user()->isOAuth2ServerAdmin()){ - return View::make('404'); + return Response::view('404', array(), 404); } }); @@ -257,9 +232,9 @@ Route::filter('openstackid.server.admin.json',function(){ Route::filter('openstackid.server.admin',function(){ if (Auth::guest()) { - return View::make('404'); + return Response::view('404', array(), 404); } if(!Auth::user()->isOpenstackIdAdmin()){ - return View::make('404'); + return Response::view('404', array(), 404); } -}); +}); \ No newline at end of file diff --git a/app/filters/OAuth2RequestAccessTokenValidator.php b/app/filters/OAuth2RequestAccessTokenValidator.php index 2e1511ff..37b22244 100644 --- a/app/filters/OAuth2RequestAccessTokenValidator.php +++ b/app/filters/OAuth2RequestAccessTokenValidator.php @@ -13,6 +13,8 @@ use utils\services\ICheckPointService; use oauth2\IResourceServerContext; use oauth2\services\IClientService; use oauth2\models\IClient; +use utils\http\HttpContentType; +use oauth2\exceptions\RevokedAccessTokenException; /** * Class OAuth2BearerAccessTokenRequestValidator @@ -20,8 +22,7 @@ use oauth2\models\IClient; * http://tools.ietf.org/html/rfc6750 * http://tools.ietf.org/html/rfc6749#section-7 */ -class OAuth2BearerAccessTokenRequestValidator { - +final class OAuth2BearerAccessTokenRequestValidator { protected function getHeaders() { @@ -49,15 +50,53 @@ class OAuth2BearerAccessTokenRequestValidator { return $headers; } + /** + * @var IApiEndpointService + */ private $api_endpoint_service; + /** + * @var ITokenService + */ private $token_service; + /** + * @var ILogService + */ private $log_service; + /** + * @var ICheckPointService + */ private $checkpoint_service; + /** + * @var IResourceServerContext + */ private $resource_server_context; + /** + * @var array + */ private $headers; + /** + * @var IClientService + */ private $client_service; - public function __construct(IResourceServerContext $resource_server_context, IApiEndpointService $api_endpoint_service, ITokenService $token_service, ILogService $log_service, ICheckPointService $checkpoint_service, IClientService $client_service){ + /** + * @param IResourceServerContext $resource_server_context + * @param IApiEndpointService $api_endpoint_service + * @param ITokenService $token_service + * @param ILogService $log_service + * @param ICheckPointService $checkpoint_service + * @param IClientService $client_service + */ + public function __construct + ( + IResourceServerContext $resource_server_context, + IApiEndpointService $api_endpoint_service, + ITokenService $token_service, + ILogService $log_service, + ICheckPointService $checkpoint_service, + IClientService $client_service + ) + { $this->api_endpoint_service = $api_endpoint_service; $this->token_service = $token_service; $this->log_service = $log_service; @@ -75,7 +114,8 @@ class OAuth2BearerAccessTokenRequestValidator { { $url = $route->getPath(); - if(strpos($url, '/') != 0){ + if(strpos($url, '/') != 0) + { $url = '/'.$url; } $method = $request->getMethod(); @@ -83,28 +123,54 @@ class OAuth2BearerAccessTokenRequestValidator { // http://tools.ietf.org/id/draft-abarth-origin-03.html $origin = $request->headers->has('Origin') ? $request->headers->get('Origin') : null; - try{ + try + { $endpoint = $this->api_endpoint_service->getApiEndpointByUrlAndMethod($url, $method); //api endpoint must be registered on db and active - if(is_null($endpoint) || !$endpoint->isActive()){ - throw new OAuth2ResourceServerException(400,OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest,sprintf('API endpoint does not exits! (%s:%s)',$url,$method)); + if(is_null($endpoint) || !$endpoint->isActive()) + { + throw new OAuth2ResourceServerException + ( + 400, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + sprintf + ( + 'API endpoint does not exits! (%s:%s)', + $url, + $method + ) + ); } //check first http basic auth header $auth_header = isset($this->headers['authorization'])?$this->headers['authorization']:null; + if(!is_null($auth_header) && !empty($auth_header)) $access_token_value = BearerAccessTokenAuthorizationHeaderParser::getInstance()->parse($auth_header); - else{ + else + { // http://tools.ietf.org/html/rfc6750#section-2- 2 // if access token is not on authorization header check on POST/GET params + if($request->headers->get('content-type') !== HttpContentType::Form) + throw new OAuth2ResourceServerException + ( + 400, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + 'invalid content-type' + ); $access_token_value = Input::get(OAuth2Protocol::OAuth2Protocol_AccessToken, ''); } if(is_null($access_token_value) || empty($access_token_value)) { //if access token value is not set, then error - throw new OAuth2ResourceServerException(400,OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest,'missing access token'); + throw new OAuth2ResourceServerException + ( + 400, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + 'missing access token' + ); } // get access token from service @@ -112,36 +178,68 @@ class OAuth2BearerAccessTokenRequestValidator { if(is_null($access_token)) throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $access_token_value)); //check token audience - $audience = explode(' ',$access_token->getAudience()); - if((!in_array($realm,$audience))) - throw new OAuth2ResourceServerException(401,OAuth2Protocol::OAuth2Protocol_Error_InvalidToken,'access token audience does not match'); + $audience = explode(' ', $access_token->getAudience()); + + if((!in_array($realm , $audience))) + throw new OAuth2ResourceServerException + ( + 401, + OAuth2Protocol::OAuth2Protocol_Error_InvalidToken, + sprintf('access token audience does not match - current_realm %s - access token audience %s',$realm, $access_token->getAudience()) + ); //check client existence $client_id = $access_token->getClientId(); $client = $this->client_service->getClientById($client_id); if(is_null($client)) - throw new OAuth2ResourceServerException(400,OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, 'invalid client'); + throw new OAuth2ResourceServerException + ( + 400, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + 'invalid client' + ); //if js client , then check if the origin is allowed .... - if($client->getApplicationType() == IClient::ApplicationType_JS_Client){ + if($client->getApplicationType() == IClient::ApplicationType_JS_Client) + { if(!$client->isOriginAllowed($origin)) - throw new OAuth2ResourceServerException(403,OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, 'invalid origin'); + throw new OAuth2ResourceServerException + ( + 403, + OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, + 'invalid origin' + ); } //check scopes $endpoint_scopes = explode(' ',$endpoint->getScope()); $token_scopes = explode(' ',$access_token->getScope()); + //check token available scopes vs. endpoint scopes if (count(array_intersect($endpoint_scopes, $token_scopes)) == 0) { - $this->log_service->error_msg(sprintf('access token scopes (%s) does not allow to access to api url %s , needed scopes %s',$access_token->getScope(),$url,implode(' OR ',$endpoint_scopes) )); + $this->log_service->error_msg + ( + sprintf + ( + 'access token scopes (%s) does not allow to access to api url %s , needed scopes %s', + $access_token->getScope(), + $url, + implode(' OR ',$endpoint_scopes) + ) + ); - throw new OAuth2ResourceServerException(403,OAuth2Protocol::OAuth2Protocol_Error_InsufficientScope, + throw new OAuth2ResourceServerException + ( + 403, + OAuth2Protocol::OAuth2Protocol_Error_InsufficientScope, 'the request requires higher privileges than provided by the access token', - implode(' ',$endpoint_scopes)); + implode(' ',$endpoint_scopes) + ); } - $context = array( + $context = array + ( 'access_token' => $access_token_value, 'expires_in' => $access_token->getRemainingLifetime(), 'client_id' => $client_id, @@ -154,7 +252,8 @@ class OAuth2BearerAccessTokenRequestValidator { $this->resource_server_context->setAuthorizationContext($context); } - catch(OAuth2ResourceServerException $ex1){ + catch(OAuth2ResourceServerException $ex1) + { $this->log_service->error($ex1); $this->checkpoint_service->trackException($ex1); $response = new OAuth2WWWAuthenticateErrorResponse($realm, @@ -167,7 +266,8 @@ class OAuth2BearerAccessTokenRequestValidator { $http_response->header('WWW-Authenticate',$response->getWWWAuthenticateHeaderValue()); return $http_response; } - catch(InvalidGrantTypeException $ex2){ + catch(InvalidGrantTypeException $ex2) + { $this->log_service->error($ex2); $this->checkpoint_service->trackException($ex2); $response = new OAuth2WWWAuthenticateErrorResponse($realm, @@ -180,7 +280,8 @@ class OAuth2BearerAccessTokenRequestValidator { $http_response->header('WWW-Authenticate',$response->getWWWAuthenticateHeaderValue()); return $http_response; } - catch(ExpiredAccessTokenException $ex3){ + catch(ExpiredAccessTokenException $ex3) + { $this->log_service->error($ex3); $this->checkpoint_service->trackException($ex3); $response = new OAuth2WWWAuthenticateErrorResponse($realm, @@ -193,7 +294,22 @@ class OAuth2BearerAccessTokenRequestValidator { $http_response->header('WWW-Authenticate',$response->getWWWAuthenticateHeaderValue()); return $http_response; } - catch(Exception $ex){ + catch(RevokedAccessTokenException $ex4) + { + $this->log_service->error($ex4); + $this->checkpoint_service->trackException($ex4); + $response = new OAuth2WWWAuthenticateErrorResponse($realm, + OAuth2Protocol::OAuth2Protocol_Error_InvalidToken, + 'the access token provided is expired, revoked, malformed, or invalid for other reasons.', + null, + 401 + ); + $http_response = Response::json($response->getContent(), $response->getHttpCode()); + $http_response->header('WWW-Authenticate',$response->getWWWAuthenticateHeaderValue()); + return $http_response; + } + catch(Exception $ex) + { $this->log_service->error($ex); $this->checkpoint_service->trackException($ex); $response = new OAuth2WWWAuthenticateErrorResponse($realm, diff --git a/app/lang/en/validation.php b/app/lang/en/validation.php index 23bc2a16..bc024339 100644 --- a/app/lang/en/validation.php +++ b/app/lang/en/validation.php @@ -2,107 +2,113 @@ return array( - /* - |-------------------------------------------------------------------------- - | Validation Language Lines - |-------------------------------------------------------------------------- - | - | The following language lines contain the default error messages used by - | the validator class. Some of these rules have multiple versions such - | such as the size rules. Feel free to tweak each of these messages. - | - */ + /* + |-------------------------------------------------------------------------- + | Validation Language Lines + |-------------------------------------------------------------------------- + | + | The following language lines contain the default error messages used by + | the validator class. Some of these rules have multiple versions such + | such as the size rules. Feel free to tweak each of these messages. + | + */ - "accepted" => "The :attribute must be accepted.", - "active_url" => "The :attribute is not a valid URL.", - "after" => "The :attribute must be a date after :date.", - "alpha" => "The :attribute may only contain letters.", - "alpha_dash" => "The :attribute may only contain letters, numbers, and dashes.", - "alpha_num" => "The :attribute may only contain letters and numbers.", - "array" => "The :attribute must be an array.", - "before" => "The :attribute must be a date before :date.", - "between" => array( - "numeric" => "The :attribute must be between :min - :max.", - "file" => "The :attribute must be between :min - :max kilobytes.", - "string" => "The :attribute must be between :min - :max characters.", - "array" => "The :attribute must have between :min - :max items.", - ), - "confirmed" => "The :attribute confirmation does not match.", - "date" => "The :attribute is not a valid date.", - "date_format" => "The :attribute does not match the format :format.", - "different" => "The :attribute and :other must be different.", - "digits" => "The :attribute must be :digits digits.", - "digits_between" => "The :attribute must be between :min and :max digits.", - "email" => "The :attribute format is invalid.", - "exists" => "The selected :attribute is invalid.", - "image" => "The :attribute must be an image.", - "in" => "The selected :attribute is invalid.", - "integer" => "The :attribute must be an integer.", - "ip" => "The :attribute must be a valid IP address.", - "max" => array( - "numeric" => "The :attribute may not be greater than :max.", - "file" => "The :attribute may not be greater than :max kilobytes.", - "string" => "The :attribute may not be greater than :max characters.", - "array" => "The :attribute may not have more than :max items.", - ), - "mimes" => "The :attribute must be a file of type: :values.", - "min" => array( - "numeric" => "The :attribute must be at least :min.", - "file" => "The :attribute must be at least :min kilobytes.", - "string" => "The :attribute must be at least :min characters.", - "array" => "The :attribute must have at least :min items.", - ), - "not_in" => "The selected :attribute is invalid.", - "numeric" => "The :attribute must be a number.", - "regex" => "The :attribute format is invalid.", - "required" => "The :attribute field is required.", - "required_if" => "The :attribute field is required when :other is :value.", - "required_with" => "The :attribute field is required when :values is present.", - "required_without" => "The :attribute field is required when :values is not present.", - "same" => "The :attribute and :other must match.", - "size" => array( - "numeric" => "The :attribute must be :size.", - "file" => "The :attribute must be :size kilobytes.", - "string" => "The :attribute must be :size characters.", - "array" => "The :attribute must contain :size items.", - ), - "unique" => "The :attribute has already been taken.", - "url" => "The :attribute format is invalid.", + "accepted" => "The :attribute must be accepted.", + "active_url" => "The :attribute is not a valid URL.", + "after" => "The :attribute must be a date after :date.", + "alpha" => "The :attribute may only contain letters.", + "alpha_dash" => "The :attribute may only contain letters, numbers, and dashes.", + "alpha_num" => "The :attribute may only contain letters and numbers.", + "array" => "The :attribute must be an array.", + "before" => "The :attribute must be a date before :date.", + "between" => array( + "numeric" => "The :attribute must be between :min - :max.", + "file" => "The :attribute must be between :min - :max kilobytes.", + "string" => "The :attribute must be between :min - :max characters.", + "array" => "The :attribute must have between :min - :max items.", + ), + "confirmed" => "The :attribute confirmation does not match.", + "date" => "The :attribute is not a valid date.", + "date_format" => "The :attribute does not match the format :format.", + "different" => "The :attribute and :other must be different.", + "digits" => "The :attribute must be :digits digits.", + "digits_between" => "The :attribute must be between :min and :max digits.", + "email" => "The :attribute format is invalid.", + "exists" => "The selected :attribute is invalid.", + "image" => "The :attribute must be an image.", + "in" => "The selected :attribute is invalid.", + "integer" => "The :attribute must be an integer.", + "ip" => "The :attribute must be a valid IP address.", + "max" => array( + "numeric" => "The :attribute may not be greater than :max.", + "file" => "The :attribute may not be greater than :max kilobytes.", + "string" => "The :attribute may not be greater than :max characters.", + "array" => "The :attribute may not have more than :max items.", + ), + "mimes" => "The :attribute must be a file of type: :values.", + "min" => array( + "numeric" => "The :attribute must be at least :min.", + "file" => "The :attribute must be at least :min kilobytes.", + "string" => "The :attribute must be at least :min characters.", + "array" => "The :attribute must have at least :min items.", + ), + "not_in" => "The selected :attribute is invalid.", + "numeric" => "The :attribute must be a number.", + "regex" => "The :attribute format is invalid.", + "required" => "The :attribute field is required.", + "required_if" => "The :attribute field is required when :other is :value.", + "required_with" => "The :attribute field is required when :values is present.", + "required_without" => "The :attribute field is required when :values is not present.", + "same" => "The :attribute and :other must match.", + "size" => array( + "numeric" => "The :attribute must be :size.", + "file" => "The :attribute must be :size kilobytes.", + "string" => "The :attribute must be :size characters.", + "array" => "The :attribute must contain :size items.", + ), + "unique" => "The :attribute has already been taken.", + "url" => "The :attribute format is invalid.", + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ - /* - |-------------------------------------------------------------------------- - | Custom Validation Language Lines - |-------------------------------------------------------------------------- - | - | Here you may specify custom validation messages for attributes using the - | convention "attribute.rule" to name the lines. This makes it quick to - | specify a specific custom language line for a given attribute rule. - | - */ + 'custom' => array(), + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ - 'custom' => array(), - - /* - |-------------------------------------------------------------------------- - | Custom Validation Attributes - |-------------------------------------------------------------------------- - | - | The following language lines are used to swap attribute place-holders - | with something more reader friendly such as E-Mail Address instead - | of "email". This simply helps us make messages a little cleaner. - | - */ - - 'attributes' => array(), + 'attributes' => array(), //custom messages - 'boolean' => "The :attribute must be a boolean.", - 'text' => "The :attribute may only contain text.", - 'httpmethod' => "The :attribute must be one of the following values 'GET', 'HEAD','POST','PUT','DELETE','TRACE','CONNECT' OR 'OPTIONS'.", - 'route' => "The :attribute may be a valid http route.", - 'host' => "The :attribute may be a valid host name.", - 'scopename' => "The :attribute may be a valid scope name.", + 'boolean' => "The :attribute must be a boolean.", + 'text' => "The :attribute may only contain text.", + 'httpmethod' => "The :attribute must be one of the following values 'GET', 'HEAD','POST','PUT','DELETE','TRACE','CONNECT' OR 'OPTIONS'.", + 'route' => "The :attribute may be a valid http route.", + 'host' => "The :attribute may be a valid host name.", + 'scopename' => "The :attribute may be a valid scope name.", 'applicationtype' => "The :attribute may be a valid application type.", - 'sslurl' => "The :attribute may be a valid URL under ssl schema.", - 'sslorigin' => "The :attribute may be a valid HTTP origin under ssl schema.", - 'freetext' => "The :attribute may only contain text." + 'sslurl' => "The :attribute may be a valid URL under ssl schema.", + 'sslorigin' => "The :attribute may be a valid HTTP origin under ssl schema.", + 'freetext' => "The :attribute may only contain text.", + 'public_key_pem' => "The :attribute has not a valid PCK#1/PCK#8 public key format.", + 'private_key_pem' => "The :attribute has not a valid PCK#1/PCK#8 private key format.", + 'public_key_usage' => "The :attribute has not a valid key usage (enc, sig).", + 'public_key_type'=> "The :attribute has not a valid key type (RSA).", + 'private_key_password' => "The :attribute is not the right password of the PEM private key.", + 'private_key_pem_length'=> "The :attribute has not a valid private key length ( at least 2048 bits ).", + 'public_key_pem_length'=> "The :attribute has not a valid public key length ( at least 2048 bits ).", + 'key_alg' => "The :attribute has not a valid alg parameter for the key usage.", ); diff --git a/app/libs/auth/AuthService.php b/app/libs/auth/AuthService.php index b894f076..37e9f9fe 100644 --- a/app/libs/auth/AuthService.php +++ b/app/libs/auth/AuthService.php @@ -3,42 +3,69 @@ namespace auth; use Auth; +use Config; +use Crypt; +use Cookie; +use jwe\compression_algorithms\CompressionAlgorithms_Registry; +use jwe\compression_algorithms\CompressionAlgorithmsNames; use Member; +use oauth2\models\IClient; +use oauth2\services\IPrincipalService; +use openid\model\IOpenIdUser; +use openid\services\IUserService; use Session; +use utils\Base64UrlRepresentation; use utils\services\IAuthService; +use utils\services\ICacheService; -class AuthService implements IAuthService +/** + * Class AuthService + * @package auth + */ +final class AuthService implements IAuthService { + /** + * @var IPrincipalService + */ + private $principal_service; + + /** + * @var IUserService + */ + private $user_service; + + + /** + * @var ICacheService + */ + private $cache_service; + + public function __construct + ( + IPrincipalService $principal_service, + IUserService $user_service, + ICacheService $cache_service + ) + { + $this->principal_service = $principal_service; + $this->user_service = $user_service; + $this->cache_service = $cache_service; + } /** * @return mixed */ public function isUserLogged() { - $res = Auth::check(); - if ($res) { - $user = $this->getCurrentUser(); - if (!$user->hasAssociatedMember()) { - $this->logout(); - $res = false; - } - } - - return $res; + return Auth::check(); } /** - * @return mixed + * @return IOpenIdUser */ public function getCurrentUser() { - $user = Auth::user(); - if (!is_null($user) && !$user->hasAssociatedMember()) { - $this->logout(); - $user = null; - } - - return $user; + return Auth::user(); } /** @@ -49,12 +76,26 @@ class AuthService implements IAuthService */ public function login($username, $password, $remember_me) { - return Auth::attempt(array('username' => $username, 'password' => $password), $remember_me); + $res = Auth::attempt(array('username' => $username, 'password' => $password), $remember_me); + + if ($res) + { + $this->principal_service->clear(); + $this->principal_service->register + ( + $this->getCurrentUser()->getId(), + time() + ); + } + + return $res; } public function logout() { Auth::logout(); + $this->principal_service->clear(); + Cookie::queue('rps', null, $minutes = -2628000, $path = '/', $domain = null, $secure = false, $httpOnly = false); } /** @@ -62,7 +103,8 @@ class AuthService implements IAuthService */ public function getUserAuthorizationResponse() { - if (Session::has("openid.authorization.response")) { + if (Session::has("openid.authorization.response")) + { $value = Session::get("openid.authorization.response"); return $value; @@ -73,16 +115,23 @@ class AuthService implements IAuthService public function clearUserAuthorizationResponse() { - if (Session::has("openid.authorization.response")) { + if (Session::has("openid.authorization.response")) + { Session::remove("openid.authorization.response"); + Session::save(); } } public function setUserAuthorizationResponse($auth_response) { Session::set("openid.authorization.response", $auth_response); + Session::save(); } + /** + * @param string $openid + * @return IOpenIdUser + */ public function getUserByOpenId($openid) { $user = User::where('identifier', '=', $openid)->first(); @@ -90,16 +139,32 @@ class AuthService implements IAuthService return $user; } + /** + * @param string $username + * @return bool|IOpenIdUser + */ public function getUserByUsername($username) { $member = Member::where('Email', '=', $username)->first(); - if (!is_null($member)) { - return User::where('external_identifier', '=', $member->ID)->first(); + if (!is_null($member)) + { + $user = User::where('external_identifier', '=', $member->ID)->first(); + + if(!$user) + { + $user = $this->user_service->buildUser($member); + } + + return $user; } return false; } + /** + * @param int $id + * @return IOpenIdUser + */ public function getUserById($id) { return User::find($id); @@ -111,22 +176,164 @@ class AuthService implements IAuthService { if (Session::has("openstackid.authentication.response")) { $value = Session::get("openstackid.authentication.response"); - return $value; } - return IAuthService::AuthenticationResponse_None; } public function setUserAuthenticationResponse($auth_response) { Session::set("openstackid.authentication.response", $auth_response); + Session::save(); } public function clearUserAuthenticationResponse() { - if (Session::has("openstackid.authentication.response")) { + if (Session::has("openstackid.authentication.response")) + { Session::remove("openstackid.authentication.response"); + Session::save(); + } + } + + /** + * @param int $user_id + * @return string + */ + public function unwrapUserId($user_id) + { + $user = $this->getUserByExternaldId($user_id); + if(!is_null($user)) + return $user_id; + + $unwrapped_name = $this->decrypt($user_id); + $parts = explode(':', $unwrapped_name); + return intval($parts[1]); + } + + /** + * @param int $user_id + * @param IClient $client + * @return string + */ + public function wrapUserId($user_id, IClient $client) + { + if($client->getSubjectType() === IClient::SubjectType_Public) + return $user_id; + else + { + $wrapped_name = sprintf('%s:%s', $client->getClientId(), $user_id); + return $this->encrypt($wrapped_name); + } + } + + /** + * @param string $value + * @return String + */ + private function encrypt($value) + { + return base64_encode(Crypt::encrypt($value)); + } + + /** + * @param string $value + * @return String + */ + private function decrypt($value) + { + $value = base64_decode($value); + return Crypt::decrypt($value); + } + + /** + * @param int $external_id + * @return IOpenIdUser + */ + public function getUserByExternaldId($external_id) + { + $member = Member::where('ID', '=', $external_id)->first(); + if (!is_null($member)) + { + $user = User::where('external_identifier', '=', $member->ID)->first(); + + if(!$user) + { + $user = $this->user_service->buildUser($member); + } + + return $user; + } + + return false; + } + + /** + * @return string + */ + public function getSessionId() + { + return Session::getId(); + } + + /** + * @param $client_id + * @return void + */ + public function registerRPLogin($client_id) + { + $rps = Cookie::get('rps'); + $zlib = CompressionAlgorithms_Registry::getInstance()->get(CompressionAlgorithmsNames::ZLib); + + if(!empty($rps)) + { + $rps = $this->decrypt($rps); + $rps = $zlib->uncompress($rps); + $rps .= '|'; + } + + if(!str_contains($rps, $client_id)) + $rps .= $client_id; + + $rps = $zlib->compress($rps); + $rps = $this->encrypt($rps); + + Cookie::queue('rps', $rps, $minutes = 2628000, $path = '/', $domain = null, $secure = false, $httpOnly = false); + } + + /** + * @return string[] + */ + public function getLoggedRPs() + { + $rps = Cookie::get('rps'); + $zlib = CompressionAlgorithms_Registry::getInstance()->get(CompressionAlgorithmsNames::ZLib); + + if(!empty($rps)) + { + $rps = $this->decrypt($rps); + $rps = $zlib->uncompress($rps); + return explode('|', $rps); + } + return null; + } + + /** + * @param string $jti + * @return void + */ + public function reloadSession($jti) + { + $session_id = $this->cache_service->getSingleValue($jti); + if(empty($session_id)) throw new \Exception('session not found!'); + Session::setId(Crypt::decrypt($session_id)); + Session::start(); + if(!Auth::check()) + { + $user_id = $this->principal_service->get()->getUserId(); + $user = $this->getUserById($user_id); + if(is_null($user)) throw new \Exception('user not found!'); + Auth::login($user); } } } \ No newline at end of file diff --git a/app/libs/auth/AuthenticationServiceProvider.php b/app/libs/auth/AuthenticationServiceProvider.php index 112b23a5..7bf13808 100644 --- a/app/libs/auth/AuthenticationServiceProvider.php +++ b/app/libs/auth/AuthenticationServiceProvider.php @@ -2,9 +2,9 @@ namespace auth; +use App; use Illuminate\Support\ServiceProvider; use utils\services\UtilsServiceCatalog; -use App; class AuthenticationServiceProvider extends ServiceProvider { @@ -16,7 +16,7 @@ class AuthenticationServiceProvider extends ServiceProvider public function register() { App::singleton(UtilsServiceCatalog::AuthenticationService, 'auth\\AuthService'); - App::singleton('auth\\IAuthenticationExtensionService', 'auth\\AuthenticationExtensionService'); + App::singleton('auth\\IAuthenticationExtensionService', 'auth\\AuthenticationExtensionService'); } public function provides() diff --git a/app/libs/auth/CustomAuthProvider.php b/app/libs/auth/CustomAuthProvider.php index 4572fb57..e42e8b7b 100644 --- a/app/libs/auth/CustomAuthProvider.php +++ b/app/libs/auth/CustomAuthProvider.php @@ -86,11 +86,10 @@ class CustomAuthProvider implements UserProviderInterface { try { //here we do the manuel join between 2 DB, (openid and SS db) - $user = $this->user_repository->getByExternalId($identifier); + $user = $this->user_repository->getByExternalId($identifier); $member = $this->member_repository->get($identifier); - if (!is_null($member) && !is_null($user)) { + if (!is_null($member) && $member->canLogin() && !is_null($user)) { $user->setMember($member); - return $user; } @@ -110,27 +109,35 @@ class CustomAuthProvider implements UserProviderInterface */ public function retrieveByCredentials(array $credentials) { - $user_service = $this->user_service; + $user_service = $this->user_service; $auth_extension_service = $this->auth_extension_service; - $user_repository = $this->user_repository; - $member_repository = $this->member_repository; - - try { + $user_repository = $this->user_repository; + $member_repository = $this->member_repository; + $log_service = $this->log_service; + $checkpoint_service = $this->checkpoint_service; - $user = $this->tx_service->transaction(function () use ( - $credentials, - $user_repository, - $member_repository, - $user_service, - $auth_extension_service - ) { + return $this->tx_service->transaction(function () use ( + $credentials, + $user_repository, + $member_repository, + $user_service, + $auth_extension_service, + $log_service, + $checkpoint_service + ) { - if (!isset($credentials['username']) || !isset($credentials['password'])) { + $user = null; + + try + { + + if (!isset($credentials['username']) || !isset($credentials['password'])) + { throw new AuthenticationException("invalid crendentials"); } - $email = $credentials['username']; + $email = $credentials['username']; $password = $credentials['password']; //get SS member @@ -142,10 +149,15 @@ class CustomAuthProvider implements UserProviderInterface throw new AuthenticationException(sprintf("member %s does not exists!", $email)); } + if(!$member->canLogin()) + { + throw new AuthenticationException(sprintf("member %s does not exists!", $email)); + } $valid_password = $member->checkPassword($password); - if (!$valid_password) { + if (!$valid_password) + { throw new AuthenticationInvalidPasswordAttemptException($email, sprintf("invalid login attempt for user %s ", $email)); } @@ -153,54 +165,44 @@ class CustomAuthProvider implements UserProviderInterface $user = $user_repository->getByExternalId($member->ID); - //if user does not exists, then create it - if (is_null($user)) { - //create user - $user = new User(); - $user->external_identifier = $member->ID; - $user->identifier = $member->ID; - $user->last_login_date = gmdate("Y-m-d H:i:s", time()); - $user->active = true; - $user->lock = false; - $user->login_failed_attempt = 0; - $user_repository->add($user); + if (!$user) { + $user = $user_service->buildUser($member); } //check user status... if ($user->lock || !$user->active) { Log::warning(sprintf("user %s is on lock state", $email)); - throw new AuthenticationLockedUserLoginAttempt($email, sprintf("user %s is on lock state", $email)); + throw new AuthenticationLockedUserLoginAttempt($email, + sprintf("user %s is on lock state", $email)); } - - $user_name = strtolower($member->FirstName . "." . $member->Surname); - //do association between user and member - $user_service->associateUser($user, $user_name); - //update user fields - $user->last_login_date = gmdate("Y-m-d H:i:s", time()); + $user->last_login_date = gmdate("Y-m-d H:i:s", time()); $user->login_failed_attempt = 0; - $user->active = true; - $user->lock = false; + $user->active = true; + $user->lock = false; $user_repository->update($user); $user->setMember($member); $auth_extensions = $auth_extension_service->getExtensions(); - foreach ($auth_extensions as $auth_extension) { + foreach ($auth_extensions as $auth_extension) + { $auth_extension->process($user); } - return $user; - }); - } catch (Exception $ex) { - $this->checkpoint_service->trackException($ex); - $this->log_service->error($ex); - $user = null; - } + } + catch (Exception $ex) + { + $checkpoint_service->trackException($ex); + $log_service->error($ex); + $user = null; + } + + return $user; + }); - return $user; } @@ -217,12 +219,12 @@ class CustomAuthProvider implements UserProviderInterface throw new AuthenticationException("invalid crendentials"); } try { - $email = $credentials['username']; + $email = $credentials['username']; $password = $credentials['password']; - $member = $this->member_repository->getByEmail($email); + $member = $this->member_repository->getByEmail($email); - if (!$member || !$member->checkPassword($password)) { + if (!$member || !$member->canLogin() || !$member->checkPassword($password)) { return false; } @@ -248,7 +250,6 @@ class CustomAuthProvider implements UserProviderInterface */ public function retrieveByToken($identifier, $token) { - return $this->user_repository->getByToken($identifier, $token); } @@ -263,5 +264,5 @@ class CustomAuthProvider implements UserProviderInterface $user->setAttribute($user->getRememberTokenName(), $token); $user->save(); - } + } } \ No newline at end of file diff --git a/app/libs/auth/IMemberRepository.php b/app/libs/auth/IMemberRepository.php index 506298b4..7e6f68a3 100644 --- a/app/libs/auth/IMemberRepository.php +++ b/app/libs/auth/IMemberRepository.php @@ -1,17 +1,24 @@ member = $member; } - private function getAssociatedMember() { if (is_null($this->member)) { @@ -70,15 +72,6 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu return $this->member; } - /** - * @return bool - */ - public function hasAssociatedMember() - { - $this->getAssociatedMember(); - return !is_null($this->member); - } - /** * Get the unique identifier for the user. * the one that is saved as session id on vendor/laravel/framework/src/Illuminate/Auth/Guard.php @@ -110,10 +103,18 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu public function getEmail() { $this->getAssociatedMember(); - + if(is_null($this->member)) return false; return $this->member->Email; } + /** + * @return string + */ + public function getEmailAttribute() + { + return $this->getEmail(); + } + public function getFullName() { return $this->getFirstName() . " " . $this->getLastName(); @@ -175,7 +176,7 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu public function getId() { - return $this->id; + return (int)$this->id; } public function getShowProfileFullName() @@ -264,7 +265,11 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu { $this->getAssociatedMember(); - return sprintf("%s, %s ", $this->member->Address, $this->member->Suburb); + $street_address = $this->member->Address; + $suburb = $this->member->Suburb; + if(!empty($suburb)) + $street_address .= ', '.$suburb; + return $street_address; } public function getRegion() @@ -307,4 +312,73 @@ class User extends BaseModelEloquent implements UserInterface, IOpenIdUser, IOAu { return 'remember_token'; } + + /** + * @return int + */ + public function getExternalIdentifier() + { + return $this->getAuthIdentifier(); + } + + /** + * @return string + */ + public function getFormattedAddress() + { + $street = $this->getStreetAddress(); + $region = $this->getRegion(); + $city = $this->getLocality(); + $zip_code = $this->getPostalCode(); + $country = $this->getCountry(); + + $complete = $street; + + if(!empty($city)) + $complete .= ', '.$city; + + if(!empty($region)) + $complete .= ', '.$region; + + if(!empty($zip_code)) + $complete .= ', '.$zip_code; + + if(!empty($country)) + $complete .= ', '.$country; + + return $complete; + } + + /** + * @return IApiScopeGroup[] + */ + public function getGroups() + { + return $this->groups()->where('active','=',true)->get(); + } + + /** + * @return mixed + */ + public function groups() + { + return $this->belongsToMany('ApiScopeGroup','oauth2_api_scope_group_users','user_id', 'group_id'); + } + + /** + * @return IApiScope[] + */ + public function getGroupScopes() + { + $scopes = array(); + $map = array(); + foreach($this->groups()->where('active','=',true)->get() as $group){ + foreach($group->scopes()->get() as $scope) + { + if(!isset($map[$scope->id])) + array_push($scopes, $scope); + } + } + return $scopes; + } } \ No newline at end of file diff --git a/app/libs/oauth2/AddressClaim.php b/app/libs/oauth2/AddressClaim.php new file mode 100644 index 00000000..7ceb16d2 --- /dev/null +++ b/app/libs/oauth2/AddressClaim.php @@ -0,0 +1,66 @@ +container[$param])?$this->container[$param]:null; + return isset($this->container[$param])? $this->container[$param] : null; + } + + /** + * @param string $param + * @param mixed $value + * @return $this + */ + public function setParam($param, $value) + { + $this->container[$param] = $value; + return $this; + } + + /** + * @return OAuth2RequestMemento + */ + public function createMemento(){ + return OAuth2RequestMemento::buildFromRequest($this); + } + + /** + * @param OAuth2RequestMemento $memento + * @return $this + */ + public function setMemento(OAuth2RequestMemento $memento){ + $this->container = $memento->getState(); + return $this; + } + + /** + * @param OAuth2RequestMemento $memento + * @return OAuth2Message + */ + static public function buildFromMemento(OAuth2RequestMemento $memento){ + $msg = new self; + $msg->setMemento($memento); + return $msg; } } \ No newline at end of file diff --git a/app/libs/oauth2/OAuth2Protocol.php b/app/libs/oauth2/OAuth2Protocol.php index 3f60a365..1c847de7 100644 --- a/app/libs/oauth2/OAuth2Protocol.php +++ b/app/libs/oauth2/OAuth2Protocol.php @@ -2,56 +2,53 @@ namespace oauth2; -//endpoints +use Exception; +use jwa\JSONWebSignatureAndEncryptionAlgorithms; +use jwk\impl\JWKSet; +use jwk\impl\RSAJWKFactory; +use jwk\impl\RSAJWKPEMPrivateKeySpecification; +use jwk\JSONWebKeyVisibility; +use jws\IJWS; +use jwt\IJWT; +use jwt\impl\UnsecuredJWT; +use oauth2\discovery\DiscoveryDocumentBuilder; +use oauth2\discovery\IOpenIDProviderConfigurationService; use oauth2\endpoints\AuthorizationEndpoint; use oauth2\endpoints\TokenEndpoint; use oauth2\endpoints\TokenIntrospectionEndpoint; use oauth2\endpoints\TokenRevocationEndpoint; - -//exceptions -use Exception; -use oauth2\exceptions\AccessDeniedException; -use oauth2\exceptions\BearerTokenDisclosureAttemptException; -use oauth2\exceptions\ExpiredAuthorizationCodeException; -use oauth2\exceptions\InvalidAccessTokenException; -use oauth2\exceptions\InvalidApplicationType; -use oauth2\exceptions\InvalidAuthorizationCodeException; -use oauth2\exceptions\InvalidClientException; -use oauth2\exceptions\InvalidClientType; -use oauth2\exceptions\InvalidGrantTypeException; -use oauth2\exceptions\InvalidOAuth2Request; -use oauth2\exceptions\LockedClientException; -use oauth2\exceptions\MissingClientIdParam; -use oauth2\exceptions\OAuth2GenericException; -use oauth2\exceptions\ReplayAttackException; -use oauth2\exceptions\ScopeNotAllowedException; -use oauth2\exceptions\UnAuthorizedClientException; -use oauth2\exceptions\UnsupportedResponseTypeException; -use oauth2\exceptions\UriNotAllowedException; -use oauth2\exceptions\MissingClientAuthorizationInfo; -use oauth2\exceptions\InvalidRedeemAuthCodeException; -use oauth2\exceptions\InvalidClientCredentials; use oauth2\exceptions\ExpiredAccessTokenException; - -//grant types +use oauth2\exceptions\InvalidClientException; +use oauth2\exceptions\InvalidOAuth2Request; +use oauth2\exceptions\OAuth2BaseException; +use oauth2\exceptions\UriNotAllowedException; use oauth2\grant_types\AuthorizationCodeGrantType; +use oauth2\grant_types\ClientCredentialsGrantType; +use oauth2\grant_types\HybridGrantType; use oauth2\grant_types\ImplicitGrantType; use oauth2\grant_types\RefreshBearerTokenGrantType; -use oauth2\grant_types\ClientCredentialsGrantType; - +use oauth2\models\IClient; +use oauth2\repositories\IServerPrivateKeyRepository; +use oauth2\requests\OAuth2EndSessionRequest; +use oauth2\requests\OAuth2LogoutRequest; use oauth2\requests\OAuth2Request; - +use oauth2\resource_server\IUserService; use oauth2\responses\OAuth2DirectErrorResponse; use oauth2\responses\OAuth2IndirectErrorResponse; +use oauth2\responses\OAuth2LogoutResponse; use oauth2\responses\OAuth2TokenRevocationResponse; - use oauth2\services\IApiScopeService; +use oauth2\services\IClientJWKSetReader; use oauth2\services\IClientService; -use oauth2\services\IMementoOAuth2AuthenticationRequestService; +use oauth2\services\IMementoOAuth2SerializerService; +use oauth2\services\IPrincipalService; +use oauth2\services\ISecurityContextService; use oauth2\services\ITokenService; +use oauth2\services\IUserConsentService; use oauth2\strategies\IOAuth2AuthenticationStrategy; use oauth2\strategies\OAuth2IndirectErrorResponseFactoryMethod; -use oauth2\services\IUserConsentService; +use utils\ArrayUtils; +use utils\factories\BasicJWTFactory; use utils\services\IAuthService; use utils\services\ICheckPointService; use utils\services\ILogService; @@ -61,40 +58,459 @@ use utils\services\ILogService; * Implementation of http://tools.ietf.org/html/rfc6749 * @package oauth2 */ -class OAuth2Protocol implements IOAuth2Protocol +final class OAuth2Protocol implements IOAuth2Protocol { - const OAuth2Protocol_GrantType_AuthCode = 'authorization_code'; - const OAuth2Protocol_GrantType_Implicit = 'implicit'; + const OAuth2Protocol_Scope_Delimiter = ' '; + const OAuth2Protocol_ResponseType_Delimiter = ' '; + + const OAuth2Protocol_GrantType_AuthCode = 'authorization_code'; + const OAuth2Protocol_GrantType_Implicit = 'implicit'; + const OAuth2Protocol_GrantType_Hybrid = 'hybrid'; + const OAuth2Protocol_GrantType_ResourceOwner_Password = 'password'; - const OAuth2Protocol_GrantType_ClientCredentials = 'client_credentials'; - const OAuth2Protocol_GrantType_RefreshToken = 'refresh_token'; - const OAuth2Protocol_ResponseType_Code = 'code'; - const OAuth2Protocol_ResponseType_Token = 'token'; + const OAuth2Protocol_GrantType_ClientCredentials = 'client_credentials'; + const OAuth2Protocol_GrantType_RefreshToken = 'refresh_token'; + + const OAuth2Protocol_ResponseType_Code = 'code'; + const OAuth2Protocol_ResponseType_Token = 'token'; + const OAuth2Protocol_ResponseType_IdToken = 'id_token'; + const OAuth2Protocol_ResponseType_None = 'none'; + + /** + * The OAuth 2.0 specification allows for registration of space-separated response_type parameter values. If a + * Response Type contains one of more space characters (%20), it is compared as a space-delimited list of values + * in which the order of values does not matter. + */ const OAuth2Protocol_ResponseType = 'response_type'; - const OAuth2Protocol_ClientId = 'client_id'; - const OAuth2Protocol_UserId = 'user_id'; + /** + * http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes + * Informs the Authorization Server of the mechanism to be used for returning Authorization Response parameters from + * the Authorization Endpoint. This use of this parameter is NOT RECOMMENDED with a value that specifies the same + * Response Mode as the default Response Mode for the Response Type used. + */ + const OAuth2Protocol_ResponseMode = 'response_mode'; + + /** + * In this mode, Authorization Response parameters are encoded in the query string added to the redirect_uri when + * redirecting back to the Client. + */ + const OAuth2Protocol_ResponseMode_Query = 'query'; + + /** + * In this mode, Authorization Response parameters are encoded in the fragment added to the redirect_uri when + * redirecting back to the Client. + */ + const OAuth2Protocol_ResponseMode_Fragment = 'fragment'; + + /** + * http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html + * In this mode, Authorization Response parameters are encoded as HTML form values that are auto-submitted in the + * User Agent, and thus are transmitted via the HTTP POST method to the Client, with the result parameters being + * encoded in the body using the application/x-www-form-urlencoded format. The action attribute of the form MUST be + * the Client's Redirection URI. The method of the form attribute MUST be POST. Because the Authorization Response + * is intended to be used only once, the Authorization Server MUST instruct the User Agent (and any intermediaries) + * not to store or reuse the content of the response. + */ + const OAuth2Protocol_ResponseMode_FormPost = 'form_post'; + + const OAuth2Protocol_ResponseMode_Direct = 'direct'; + + + static public $valid_response_modes = array + ( + self::OAuth2Protocol_ResponseMode_Query, + self::OAuth2Protocol_ResponseMode_Fragment, + self::OAuth2Protocol_ResponseMode_FormPost, + self::OAuth2Protocol_ResponseMode_Direct + ); + + /** + * http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes + * + * Each Response Type value also defines a default Response Mode mechanism to be used, + * if no Response Mode is specified using the request parameter. + * For purposes of this specification, the default Response Mode for the OAuth 2.0 code Response Type is the query + * encoding. For purposes of this specification, the default Response Mode for the OAuth 2.0 token Response Type is + * the fragment encoding. + * + * @param array $response_type + * @return string + */ + static public function getDefaultResponseMode(array $response_type) + { + + if(count(array_diff($response_type, array(self::OAuth2Protocol_ResponseType_Code))) === 0) + return self::OAuth2Protocol_ResponseMode_Query; + + if(count(array_diff($response_type, array(self::OAuth2Protocol_ResponseType_Token))) === 0) + return self::OAuth2Protocol_ResponseMode_Fragment; + // http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations + if(count(array_diff($response_type, array + ( + self::OAuth2Protocol_ResponseType_Code, + self::OAuth2Protocol_ResponseType_Token + ) + )) === 0) + return self::OAuth2Protocol_ResponseMode_Fragment; + + if(count(array_diff($response_type, array + ( + self::OAuth2Protocol_ResponseType_Code, + self::OAuth2Protocol_ResponseType_IdToken + ) + )) === 0) + return self::OAuth2Protocol_ResponseMode_Fragment; + + if(count(array_diff($response_type, array + ( + self::OAuth2Protocol_ResponseType_Token, + self::OAuth2Protocol_ResponseType_IdToken + ) + )) === 0) + return self::OAuth2Protocol_ResponseMode_Fragment; + + if(count(array_diff($response_type, array + ( + self::OAuth2Protocol_ResponseType_Code, + self::OAuth2Protocol_ResponseType_Token, + self::OAuth2Protocol_ResponseType_IdToken + ) + )) === 0) + return self::OAuth2Protocol_ResponseMode_Fragment; + } + + + const OAuth2Protocol_ClientId = 'client_id'; + const OAuth2Protocol_UserId = 'user_id'; const OAuth2Protocol_ClientSecret = 'client_secret'; - const OAuth2Protocol_Token = 'token'; - const OAuth2Protocol_TokenType = 'token_type'; - //http://tools.ietf.org/html/rfc7009#section-2.1 - const OAuth2Protocol_TokenType_Hint = 'token_type_hint'; + const OAuth2Protocol_Token = 'token'; + const OAuth2Protocol_TokenType = 'token_type'; + + // http://tools.ietf.org/html/rfc7009#section-2.1 + const OAuth2Protocol_TokenType_Hint = 'token_type_hint'; const OAuth2Protocol_AccessToken_ExpiresIn = 'expires_in'; - const OAuth2Protocol_RefreshToken = 'refresh_token'; - const OAuth2Protocol_AccessToken = 'access_token'; - const OAuth2Protocol_RedirectUri = 'redirect_uri'; - const OAuth2Protocol_Scope = 'scope'; - const OAuth2Protocol_Audience = 'audience'; - const OAuth2Protocol_State = 'state'; + const OAuth2Protocol_RefreshToken = 'refresh_token'; + const OAuth2Protocol_AccessToken = 'access_token'; + const OAuth2Protocol_RedirectUri = 'redirect_uri'; + const OAuth2Protocol_Scope = 'scope'; + const OAuth2Protocol_Audience = 'audience'; + const OAuth2Protocol_State = 'state'; + + /** + * http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions + * In OpenID Connect, the session at the RP typically starts when the RP validates the End-User's ID Token. Refer + * to the OpenID Connect Core 1.0 [OpenID.Core] specification to find out how to obtain an ID Token and validate it. + * When the OP supports session management, it MUST also return the Session State as an additional session_state + * parameter in the Authentication Response. The OpenID Connect Authentication Response is specified in + * Section 3.1.2.5 of OpenID Connect Core 1.0. + * JSON string that represents the End-User's login state at the OP. It MUST NOT contain the space (" ") character. + * This value is opaque to the RP. This is REQUIRED if session management is supported. + */ + const OAuth2Protocol_Session_State = 'session_state'; + // http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse + // ID Token value associated with the authenticated session. + const OAuth2Protocol_IdToken = 'id_token'; + + // http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + const OAuth2Protocol_Nonce = 'nonce'; + + /** + * Time when the End-User authentication occurred. Its value is a JSON number representing the number of seconds + * from 1970-01-01T0:0:0Z as measured in UTC until the date/time. When a max_age request is made or when auth_time + * is requested as an Essential Claim, then this Claim is REQUIRED; otherwise, its inclusion is OPTIONAL. + * (The auth_time Claim semantically corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] auth_time response parameter.) + */ + const OAuth2Protocol_AuthTime = 'auth_time'; + + /** + * Access Token hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of + * the ASCII representation of the access_token value, where the hash algorithm used is the hash algorithm used in + * the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the access_token + * value with SHA-256, then take the left-most 128 bits and base64url encode them. The at_hash value is a case + * sensitive string. + */ + const OAuth2Protocol_AccessToken_Hash = 'at_hash'; + + + + + /** + * Code hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII + * representation of the code value, where the hash algorithm used is the hash algorithm used in the alg Header + * Parameter of the ID Token's JOSE Header. For instance, if the alg is HS512, hash the code value with SHA-512, + * then take the left-most 256 bits and base64url encode them. The c_hash value is a case sensitive string. + * If the ID Token is issued from the Authorization Endpoint with a code, which is the case for the response_type + * values code id_token and code id_token token, this is REQUIRED; otherwise, its inclusion is OPTIONAL. + */ + const OAuth2Protocol_AuthCode_Hash = 'c_hash'; + + /** + * Specifies how the Authorization Server displays the authentication and consent user interface pages to + * the End-User. + */ + const OAuth2Protocol_Display ='display'; + /** + * The Authorization Server SHOULD display the authentication and consent UI consistent with a full User Agent page + * view. If the display parameter is not specified, this is the default display mode. + * The Authorization Server MAY also attempt to detect the capabilities of the User Agent and present an + * appropriate display. + */ + const OAuth2Protocol_Display_Page ='page'; + /** + * The Authorization Server SHOULD display the authentication and consent UI consistent with a popup User Agent + * window. The popup User Agent window should be of an appropriate size for a login-focused dialog and should not + * obscure the entire window that it is popping up over. + */ + const OAuth2Protocol_Display_PopUp ='popup'; + /** + * The Authorization Server SHOULD display the authentication and consent UI consistent with a device that leverages + * a touch interface. + */ + const OAuth2Protocol_Display_Touch ='touch'; + /** + * The Authorization Server SHOULD display the authentication and consent UI consistent with a "feature phone" + * type display. + */ + const OAuth2Protocol_Display_Wap ='wap'; + + /** + * @var array + */ + static public $valid_display_values = array + ( + self::OAuth2Protocol_Display_Page, + self::OAuth2Protocol_Display_PopUp, + self::OAuth2Protocol_Display_Touch, + self::OAuth2Protocol_Display_Wap + ); + + /** + * Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. + * The prompt parameter can be used by the Client to make sure that the End-User is still present for the current + * session or to bring attention to the request. If this parameter contains none with any other value, an error is + * returned. + */ + const OAuth2Protocol_Prompt = 'prompt'; + + /** + * The Authorization Server MUST NOT display any authentication or consent user interface pages. An error is + * returned if an End-User is not already authenticated or the Client does not have pre-configured consent for the + * requested Claims or does not fulfill other conditions for processing the request. The error code will typically + * be login_required, interaction_required, or another code defined in Section 3.1.2.6. This can be used as a method + * to check for existing authentication and/or consent. + */ + const OAuth2Protocol_Prompt_None = 'none'; + + /** + * The Authorization Server SHOULD prompt the End-User for reauthentication. If it cannot reauthenticate the + * End-User, it MUST return an error, typically login_required. + */ + const OAuth2Protocol_Prompt_Login = 'login'; + + /** + * The Authorization Server SHOULD prompt the End-User for consent before returning information to the Client. + * If it cannot obtain consent, it MUST return an error, typically consent_required. + */ + const OAuth2Protocol_Prompt_Consent = 'consent'; + + /** + * The Authorization Server SHOULD prompt the End-User to select a user account. This enables an End-User who has + * multiple accounts at the Authorization Server to select amongst the multiple accounts that they might have + * current sessions for. If it cannot obtain an account selection choice made by the End-User, it MUST return an + * error, typically account_selection_required. + */ + const OAuth2Protocol_Prompt_SelectAccount = 'select_account'; + + /** + * @var array + */ + static public $valid_prompt_values = array + ( + self::OAuth2Protocol_Prompt_None, + self::OAuth2Protocol_Prompt_Login, + self::OAuth2Protocol_Prompt_Consent, + self::OAuth2Protocol_Prompt_SelectAccount + ); + + /** + * @param string $flow + * @return array + */ + static public function getValidResponseTypes($flow = 'all') + { + $code_flow = array + ( + //OAuth2 / OIDC + array + ( + self::OAuth2Protocol_ResponseType_Code + ) + ); + + $implicit_flow = array + ( + // only for OAuth2 + array + ( + self::OAuth2Protocol_ResponseType_Token + ), + // OIDC + array + ( + self::OAuth2Protocol_ResponseType_IdToken + ), + array + ( + self::OAuth2Protocol_ResponseType_IdToken , + self::OAuth2Protocol_ResponseType_Token + ) + ); + + $hybrid_flow = array + ( + array + ( + self::OAuth2Protocol_ResponseType_Code, + self::OAuth2Protocol_ResponseType_IdToken + ), + array + ( + self::OAuth2Protocol_ResponseType_Code, + self::OAuth2Protocol_ResponseType_Token + ), + array + ( + self::OAuth2Protocol_ResponseType_Code , + self::OAuth2Protocol_ResponseType_IdToken, + self::OAuth2Protocol_ResponseType_Token + ) + ); + + if($flow === 'all') + return array_merge + ( + $code_flow, + $implicit_flow, + $hybrid_flow + ); + + if($flow === OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode) + return $code_flow; + + if($flow === OAuth2Protocol::OAuth2Protocol_GrantType_Implicit) + return $implicit_flow; + + if($flow === OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid) + return $hybrid_flow; + } + + /** + * http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Terminology + * + * The OAuth 2.0 specification allows for registration of space-separated response_type parameter values. If a + * Response Type contains one of more space characters (%20), it is compared as a space-delimited list of values in + * which the order of values does not matter. + * + * @param array $response_type + * @param string $flow + * @return bool + */ + static public function responseTypeBelongsToFlow(array $response_type, $flow = 'all') + { + if + ( + !in_array + ( + $flow, array + ( + OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + OAuth2Protocol::OAuth2Protocol_GrantType_Implicit, + OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid, + 'all' + ) + ) + ) + return false; + + $flow_response_types = self::getValidResponseTypes($flow); + + foreach($flow_response_types as $rt) + { + if(count($rt) !== count($response_type)) continue; + $diff = array_diff($rt, $response_type); + if(count($diff) === 0) return true; + } + return false; + } + + /** + * Maximum Authentication Age. Specifies the allowable elapsed time in seconds since the last time the End-User was + * actively authenticated by the OP. If the elapsed time is greater than this value, the OP MUST attempt to actively + * re-authenticate the End-User. (The max_age request parameter corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] + * max_auth_age request parameter.) When max_age is used, the ID Token returned MUST include an auth_time Claim + * Value. + */ + const OAuth2Protocol_MaxAge = 'max_age'; + + /** + * End-User's preferred languages and scripts for the user interface, represented as a space-separated list + * of BCP47 [RFC5646] language tag values, ordered by preference. For instance, the value "fr-CA fr en" represents + * a preference for French as spoken in Canada, then French (without a region designation), followed by English + * (without a region designation). An error SHOULD NOT result if some or all of the requested locales are not + * supported by the OpenID Provider. + */ + const OAuth2Protocol_UILocales = 'ui_locales'; + + /** + * ID Token previously issued by the Authorization Server being passed as a hint about the End-User's current or + * past authenticated session with the Client. If the End-User identified by the ID Token is logged in or is logged + * in by the request, then the Authorization Server returns a positive response; otherwise, it SHOULD return an + * error, such as login_required. When possible, an id_token_hint SHOULD be present when prompt=none is used and an + * invalid_request error MAY be returned if it is not; however, the server SHOULD respond successfully when + * possible, even if it is not present. The Authorization Server need not be listed as an audience of the ID Token + * when it is used as an id_token_hint value. + * If the ID Token received by the RP from the OP is encrypted, to use it as an id_token_hint, the Client MUST + * decrypt the signed ID Token contained within the encrypted ID Token. The Client MAY re-encrypt the signed ID + * token to the Authentication Server using a key that enables the server to decrypt the ID Token, and use the + * re-encrypted ID token as the id_token_hint value. + */ + const OAuth2Protocol_IDTokenHint = 'id_token_hint'; + + /** + * Hint to the Authorization Server about the login identifier the End-User might use to log in (if necessary). + * This hint can be used by an RP if it first asks the End-User for their e-mail address (or other identifier) + * and then wants to pass that value as a hint to the discovered authorization service. It is RECOMMENDED that the + * hint value match the value used for discovery. This value MAY also be a phone number in the format specified for + * the phone_number Claim. The use of this parameter is left to the OP's discretion. + */ + const OAuth2Protocol_LoginHint = 'login_hint'; + + /** + * Requested Authentication Context Class Reference values. Space-separated string that specifies the acr values + * that the Authorization Server is being requested to use for processing this Authentication Request, with the + * values appearing in order of preference. The Authentication Context Class satisfied by the authentication + * performed is returned as the acr Claim Value, as specified in Section 2. The acr Claim is requested as a + * Voluntary Claim by this parameter. + */ + const OAuth2Protocol_ACRValues = 'acr_values'; + + /** + * OPTIONAL. URL to which the RP is requesting that the End-User's User Agent be redirected after a logout has been + * performed. The value MUST have been previously registered with the OP, either using the post_logout_redirect_uris + * Registration parameter or via another mechanism. If supplied, the OP SHOULD honor this request following the logout. + */ + const OAuth2Protocol_PostLogoutRedirectUri = 'post_logout_redirect_uri'; + /** * Indicates whether the user should be re-prompted for consent. The default is auto, * so a given user should only see the consent page for a given set of scopes the first time * through the sequence. If the value is force, then the user sees a consent page even if they * previously gave consent to your application for a given set of scopes. */ - const OAuth2Protocol_Approval_Prompt = 'approval_prompt'; + const OAuth2Protocol_Approval_Prompt = 'approval_prompt'; const OAuth2Protocol_Approval_Prompt_Force = 'force'; - const OAuth2Protocol_Approval_Prompt_Auto = 'auto'; + const OAuth2Protocol_Approval_Prompt_Auto = 'auto'; /** * Indicates whether your application needs to access an API when the user is not present at @@ -102,8 +518,8 @@ class OAuth2Protocol implements IOAuth2Protocol * when the user is not present at the browser, then use offline. This will result in your application * obtaining a refresh token the first time your application exchanges an authorization code for a user. */ - const OAuth2Protocol_AccessType = 'access_type'; - const OAuth2Protocol_AccessType_Online = 'online'; + const OAuth2Protocol_AccessType = 'access_type'; + const OAuth2Protocol_AccessType_Online = 'online'; const OAuth2Protocol_AccessType_Offline = 'offline'; const OAuth2Protocol_GrantType = 'grant_type'; @@ -112,6 +528,7 @@ class OAuth2Protocol implements IOAuth2Protocol const OAuth2Protocol_ErrorUri = 'error_uri'; const OAuth2Protocol_Error_InvalidRequest = 'invalid_request'; const OAuth2Protocol_Error_UnauthorizedClient = 'unauthorized_client'; + const OAuth2Protocol_Error_RedirectUriMisMatch = 'redirect_uri_mismatch'; const OAuth2Protocol_Error_AccessDenied = 'access_denied'; const OAuth2Protocol_Error_UnsupportedResponseType = 'unsupported_response_type'; const OAuth2Protocol_Error_InvalidScope = 'invalid_scope'; @@ -126,18 +543,145 @@ class OAuth2Protocol implements IOAuth2Protocol const OAuth2Protocol_Error_InvalidToken = 'invalid_token'; const OAuth2Protocol_Error_InsufficientScope = 'insufficient_scope'; - public static $valid_responses_types = array( - self::OAuth2Protocol_ResponseType_Code => self::OAuth2Protocol_ResponseType_Code, + // http://openid.net/specs/openid-connect-core-1_0.html#AuthError + + /** + * The Authorization Server requires End-User interaction of some form to proceed. This error MAY be returned when + * the prompt parameter value in the Authentication Request is none, but the Authentication Request cannot be + * completed without displaying a user interface for End-User interaction. + */ + const OAuth2Protocol_Error_Interaction_Required = 'interaction_required'; + + /** + * The Authorization Server requires End-User authentication. This error MAY be returned when the prompt parameter + * value in the Authentication Request is none, but the Authentication Request cannot be completed without + * displaying a user interface for End-User authentication. + */ + const OAuth2Protocol_Error_Login_Required = 'login_required'; + + /** + * The End-User is REQUIRED to select a session at the Authorization Server. The End-User MAY be authenticated at + * the Authorization Server with different associated accounts, but the End-User did not select a session. + * This error MAY be returned when the prompt parameter value in the Authentication Request is none, but the + * Authentication Request cannot be completed without displaying a user interface to prompt for a session to use. + */ + const OAuth2Protocol_Error_Account_Selection_Required = 'account_selection_required'; + + /** + * The Authorization Server requires End-User consent. This error MAY be returned when the prompt parameter value + * in the Authentication Request is none, but the Authentication Request cannot be completed without displaying a + * user interface for End-User consent. + */ + const OAuth2Protocol_Error_Consent_Required = 'consent_required'; + + /** + * The request_uri in the Authorization Request returns an error or contains invalid data + */ + const OAuth2Protocol_Error_Invalid_RequestUri = 'invalid_request_uri'; + + /** + * The request parameter contains an invalid Request Object. + */ + const OAuth2Protocol_Error_Invalid_RequestObject = 'invalid_request_object'; + + /** + * The OP does not support use of the request parameter defined in Section 6. + */ + const OAuth2Protocol_Error_Request_Not_Supported = 'request_not_supported'; + /** + * The OP does not support use of the request_uri parameter defined in Section 6. + */ + const OAuth2Protocol_Error_Request_Uri_Not_Supported = 'request_uri_not_supported'; + + /** + * The OP does not support use of the registration parameter defined in Section 7.2.1. + */ + const OAuth2Protocol_Error_Registration_Not_Supported = 'registration_not_supported'; + + + const OAuth2Protocol_Error_Invalid_Recipient_Keys = 'invalid_recipient_keys'; + const OAuth2Protocol_Error_Invalid_Server_Keys = 'invalid_server_keys'; + + + public static $valid_responses_types = array + ( + self::OAuth2Protocol_ResponseType_Code => self::OAuth2Protocol_ResponseType_Code, self::OAuth2Protocol_ResponseType_Token => self::OAuth2Protocol_ResponseType_Token ); - public static $protocol_definition = array( - self::OAuth2Protocol_ResponseType => self::OAuth2Protocol_ResponseType, - self::OAuth2Protocol_ClientId => self::OAuth2Protocol_ClientId, - self::OAuth2Protocol_RedirectUri => self::OAuth2Protocol_RedirectUri, - self::OAuth2Protocol_Scope => self::OAuth2Protocol_Scope, - self::OAuth2Protocol_State => self::OAuth2Protocol_State + + // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + + const TokenEndpoint_AuthMethod_ClientSecretBasic = 'client_secret_basic'; + const TokenEndpoint_AuthMethod_ClientSecretPost = 'client_secret_post'; + const TokenEndpoint_AuthMethod_ClientSecretJwt = 'client_secret_jwt'; + const TokenEndpoint_AuthMethod_PrivateKeyJwt = 'private_key_jwt'; + const TokenEndpoint_AuthMethod_None = 'none'; + + const OAuth2Protocol_ClientAssertionType = 'client_assertion_type'; + const OAuth2Protocol_ClientAssertion = 'client_assertion'; + + public static $token_endpoint_auth_methods = array + ( + self::TokenEndpoint_AuthMethod_ClientSecretBasic, + self::TokenEndpoint_AuthMethod_ClientSecretPost, + self::TokenEndpoint_AuthMethod_ClientSecretJwt, + self::TokenEndpoint_AuthMethod_PrivateKeyJwt, ); + const OpenIdConnect_Scope = 'openid'; + const OfflineAccess_Scope = 'offline_access'; + + public static $supported_signing_algorithms = array + ( + // MAC SHA2 + JSONWebSignatureAndEncryptionAlgorithms::HS256, + JSONWebSignatureAndEncryptionAlgorithms::HS384, + JSONWebSignatureAndEncryptionAlgorithms::HS512, + // RSA + JSONWebSignatureAndEncryptionAlgorithms::RS256, + JSONWebSignatureAndEncryptionAlgorithms::RS384, + JSONWebSignatureAndEncryptionAlgorithms::RS512, + JSONWebSignatureAndEncryptionAlgorithms::PS256, + JSONWebSignatureAndEncryptionAlgorithms::PS384, + JSONWebSignatureAndEncryptionAlgorithms::PS512, + JSONWebSignatureAndEncryptionAlgorithms::None + ); + + public static $supported_signing_algorithms_hmac_sha2 = array + ( + JSONWebSignatureAndEncryptionAlgorithms::HS256, + JSONWebSignatureAndEncryptionAlgorithms::HS384, + JSONWebSignatureAndEncryptionAlgorithms::HS512, + ); + + public static $supported_signing_algorithms_rsa = array + ( + JSONWebSignatureAndEncryptionAlgorithms::RS256, + JSONWebSignatureAndEncryptionAlgorithms::RS384, + JSONWebSignatureAndEncryptionAlgorithms::RS512, + JSONWebSignatureAndEncryptionAlgorithms::PS256, + JSONWebSignatureAndEncryptionAlgorithms::PS384, + JSONWebSignatureAndEncryptionAlgorithms::PS512, + ); + + // https://tools.ietf.org/html/rfc7518#page-12 + public static $supported_key_management_algorithms = array + ( + JSONWebSignatureAndEncryptionAlgorithms::RSA1_5, + JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP, + JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP_256, + JSONWebSignatureAndEncryptionAlgorithms::Dir, + JSONWebSignatureAndEncryptionAlgorithms::None, + ); + + // https://tools.ietf.org/html/rfc7518#page-22 + public static $supported_content_encryption_algorithms = array + ( + JSONWebSignatureAndEncryptionAlgorithms::A128CBC_HS256, + JSONWebSignatureAndEncryptionAlgorithms::A192CBC_HS384, + JSONWebSignatureAndEncryptionAlgorithms::A256CBC_HS512, + JSONWebSignatureAndEncryptionAlgorithms::None, + ); /** * http://tools.ietf.org/html/rfc6749#appendix-A @@ -146,50 +690,188 @@ class OAuth2Protocol implements IOAuth2Protocol const VsChar = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_~'; //services + /** + * @var ILogService + */ private $log_service; + /** + * @var ICheckPointService + */ private $checkpoint_service; + /** + * @var IClientService + */ private $client_service; + /** + * @var IAuthService + */ + private $auth_service; + + /** + * @var IPrincipalService + */ + private $principal_service; + + /** + * @var ITokenService + */ + private $token_service; + //endpoints + /** + * @var AuthorizationEndpoint + */ private $authorize_endpoint; + /** + * @var TokenEndpoint + */ private $token_endpoint; + /** + * @var TokenRevocationEndpoint + */ private $revoke_endpoint; + /** + * @var TokenIntrospectionEndpoint + */ private $introspection_endpoint; - //grant types + /** + * grant types + * @var array + */ private $grant_types = array(); - public function __construct( + /** + * @var IServerPrivateKeyRepository + */ + private $server_private_keys_repository; + + /** + * @var IOpenIDProviderConfigurationService + */ + private $oidc_provider_configuration_service; + + /** + * @param ILogService $log_service + * @param IClientService $client_service + * @param ITokenService $token_service + * @param IAuthService $auth_service + * @param IOAuth2AuthenticationStrategy $auth_strategy + * @param ICheckPointService $checkpoint_service + * @param IApiScopeService $scope_service + * @param IUserConsentService $user_consent_service + * @param IServerPrivateKeyRepository $server_private_keys_repository + * @param IOpenIDProviderConfigurationService $oidc_provider_configuration_service + * @param IMementoOAuth2SerializerService $memento_service + * @param ISecurityContextService $security_context_service + * @param IPrincipalService $principal_service + * @param IServerPrivateKeyRepository $server_private_key_repository + * @param IClientJWKSetReader $jwk_set_reader_service + */ + public function __construct + ( ILogService $log_service, IClientService $client_service, ITokenService $token_service, IAuthService $auth_service, - IMementoOAuth2AuthenticationRequestService $memento_service, IOAuth2AuthenticationStrategy $auth_strategy, ICheckPointService $checkpoint_service, IApiScopeService $scope_service, - IUserConsentService $user_consent_service) + IUserConsentService $user_consent_service, + IServerPrivateKeyRepository $server_private_keys_repository, + IOpenIDProviderConfigurationService $oidc_provider_configuration_service, + IMementoOAuth2SerializerService $memento_service, + ISecurityContextService $security_context_service, + IPrincipalService $principal_service, + IServerPrivateKeyRepository $server_private_key_repository, + IClientJWKSetReader $jwk_set_reader_service + ) { - $authorization_code_grant_type = new AuthorizationCodeGrantType($scope_service, $client_service, $token_service, $auth_service, $memento_service, $auth_strategy, $log_service,$user_consent_service); - $implicit_grant_type = new ImplicitGrantType($scope_service, $client_service, $token_service, $auth_service, $memento_service, $auth_strategy, $log_service,$user_consent_service); - $refresh_bearer_token_grant_type = new RefreshBearerTokenGrantType($client_service, $token_service, $log_service); - $client_credential_grant_type = new ClientCredentialsGrantType($scope_service,$client_service, $token_service, $log_service); + $this->server_private_keys_repository = $server_private_keys_repository; + $this->oidc_provider_configuration_service = $oidc_provider_configuration_service; + + $authorization_code_grant_type = new AuthorizationCodeGrantType + ( + $scope_service, + $client_service, + $token_service, + $auth_service, + $auth_strategy, + $log_service, + $user_consent_service, + $memento_service, + $security_context_service, + $principal_service, + $server_private_key_repository, + $jwk_set_reader_service + ); + + $implicit_grant_type = new ImplicitGrantType + ( + $scope_service, + $client_service, + $token_service, + $auth_service, + $auth_strategy, + $log_service, + $user_consent_service, + $memento_service, + $security_context_service, + $principal_service, + $server_private_key_repository, + $jwk_set_reader_service + ); + + $hybrid_grant_type = new HybridGrantType + ( + $scope_service, + $client_service, + $token_service, + $auth_service, + $auth_strategy, + $log_service, + $user_consent_service, + $memento_service, + $security_context_service, + $principal_service, + $server_private_key_repository, + $jwk_set_reader_service + ); + + $refresh_bearer_token_grant_type = new RefreshBearerTokenGrantType + ( + $client_service, + $token_service, + $log_service + ); + + $client_credential_grant_type = new ClientCredentialsGrantType + ( + $scope_service, + $client_service, + $token_service, + $log_service + ); $this->grant_types[$authorization_code_grant_type->getType()] = $authorization_code_grant_type; $this->grant_types[$implicit_grant_type->getType()] = $implicit_grant_type; $this->grant_types[$refresh_bearer_token_grant_type->getType()] = $refresh_bearer_token_grant_type; $this->grant_types[$client_credential_grant_type->getType()] = $client_credential_grant_type; - + $this->grant_types[$hybrid_grant_type->getType()] = $hybrid_grant_type; $this->log_service = $log_service; $this->checkpoint_service = $checkpoint_service; $this->client_service = $client_service; + $this->auth_service = $auth_service; + $this->principal_service = $principal_service; + $this->token_service = $token_service; $this->authorize_endpoint = new AuthorizationEndpoint($this); $this->token_endpoint = new TokenEndpoint($this); $this->revoke_endpoint = new TokenRevocationEndpoint($this,$client_service, $token_service, $log_service); - $this->introspection_endpoint = new TokenIntrospectionEndpoint($this,$client_service, $token_service, $log_service); + $this->introspection_endpoint = new TokenIntrospectionEndpoint($this, $client_service, $token_service, $auth_service, $log_service); } /** @@ -200,119 +882,39 @@ class OAuth2Protocol implements IOAuth2Protocol */ public function authorize(OAuth2Request $request = null) { - try { + try + { if (is_null($request) || !$request->isValid()) throw new InvalidOAuth2Request; + return $this->authorize_endpoint->handle($request); - } catch (InvalidOAuth2Request $ex1) { + } + catch (UriNotAllowedException $ex1) + { $this->log_service->error($ex1); $this->checkpoint_service->trackException($ex1); - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex1; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, $redirect_uri); - } catch (UnsupportedResponseTypeException $ex2) { + throw $ex1; + } + catch(OAuth2BaseException $ex2) + { $this->log_service->error($ex2); $this->checkpoint_service->trackException($ex2); $redirect_uri = $this->validateRedirectUri($request); + if (is_null($redirect_uri)) throw $ex2; - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_UnsupportedResponseType, $redirect_uri); - } catch (InvalidClientException $ex3) { - $this->log_service->error($ex3); - $this->checkpoint_service->trackException($ex3); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex3; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, $redirect_uri); - } catch (UriNotAllowedException $ex4) { - $this->log_service->error($ex4); - $this->checkpoint_service->trackException($ex4); - throw $ex4; - } catch (ScopeNotAllowedException $ex5) { - - $this->log_service->error($ex5); - $this->checkpoint_service->trackException($ex5); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex5; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_InvalidScope, $redirect_uri); - } catch (UnAuthorizedClientException $ex6) { - $this->log_service->error($ex6); - $this->checkpoint_service->trackException($ex6); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex6; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, $redirect_uri); - } catch (AccessDeniedException $ex7) { - $this->log_service->error($ex7); - $this->checkpoint_service->trackException($ex7); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex7; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_AccessDenied, $redirect_uri); - } catch (OAuth2GenericException $ex8) { - $this->log_service->error($ex8); - $this->checkpoint_service->trackException($ex8); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex8; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_ServerError, $redirect_uri); + return OAuth2IndirectErrorResponseFactoryMethod::buildResponse + ( + $request, + $ex2->getError(), + $ex2->getMessage(), + $redirect_uri + ); } - catch(InvalidApplicationType $ex9){ - $this->log_service->error($ex9); - $this->checkpoint_service->trackException($ex9); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex9; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, $redirect_uri); - } - catch(LockedClientException $ex10){ - $this->log_service->error($ex10); - $this->checkpoint_service->trackException($ex10); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex10; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, $redirect_uri); - } - catch(MissingClientIdParam $ex11){ - $this->log_service->error($ex11); - $this->checkpoint_service->trackException($ex11); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex11; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, $redirect_uri); - } - catch(InvalidClientType $ex12){ - $this->log_service->error($ex12); - $this->checkpoint_service->trackException($ex12); - - $redirect_uri = $this->validateRedirectUri($request); - if (is_null($redirect_uri)) - throw $ex12; - - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient, $redirect_uri); - } - catch (Exception $ex) { + catch (Exception $ex) + { $this->log_service->error($ex); $this->checkpoint_service->trackException($ex); @@ -320,7 +922,13 @@ class OAuth2Protocol implements IOAuth2Protocol if (is_null($redirect_uri)) throw $ex; - return OAuth2IndirectErrorResponseFactoryMethod::buildResponse($request, OAuth2Protocol::OAuth2Protocol_Error_ServerError, $redirect_uri); + return OAuth2IndirectErrorResponseFactoryMethod::buildResponse + ( + $request, + OAuth2Protocol::OAuth2Protocol_Error_ServerError, + $ex->getMessage(), + $redirect_uri + ); } } @@ -348,95 +956,29 @@ class OAuth2Protocol implements IOAuth2Protocol */ public function token(OAuth2Request $request = null) { - try { + try + { if (is_null($request) || !$request->isValid()) throw new InvalidOAuth2Request; return $this->token_endpoint->handle($request); - } catch (InvalidOAuth2Request $ex1) { + } + catch(OAuth2BaseException $ex1) + { $this->log_service->error($ex1); $this->checkpoint_service->trackException($ex1); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest); - } catch (InvalidAuthorizationCodeException $ex2) { - $this->log_service->error($ex2); - $this->checkpoint_service->trackException($ex2); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } catch (InvalidClientException $ex3) { - $this->log_service->error($ex3); - $this->checkpoint_service->trackException($ex3); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } catch (UriNotAllowedException $ex4) { - $this->log_service->error($ex4); - $this->checkpoint_service->trackException($ex4); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } catch (UnAuthorizedClientException $ex5) { - $this->log_service->error($ex5); - $this->checkpoint_service->trackException($ex5); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } catch (ExpiredAuthorizationCodeException $ex6) { - $this->log_service->error($ex6); - $this->checkpoint_service->trackException($ex6); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest); - } catch (ReplayAttackException $ex7) { - $this->log_service->error($ex7); - $this->checkpoint_service->trackException($ex7); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest); - } catch (InvalidAccessTokenException $ex8) { - $this->log_service->error($ex8); - $this->checkpoint_service->trackException($ex8); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant); - } catch (InvalidGrantTypeException $ex9) { - $this->log_service->error($ex9); - $this->checkpoint_service->trackException($ex9); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant); - } catch (BearerTokenDisclosureAttemptException $ex10) { - $this->log_service->error($ex10); - $this->checkpoint_service->trackException($ex10); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant); + + return new OAuth2DirectErrorResponse($ex1->getError(), $ex1->getMessage());; } - catch(ScopeNotAllowedException $ex11){ - $this->log_service->error($ex11); - $this->checkpoint_service->trackException($ex11); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidScope); - } - catch(InvalidApplicationType $ex12){ - $this->log_service->error($ex12); - $this->checkpoint_service->trackException($ex12); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } - catch(LockedClientException $ex13){ - $this->log_service->error($ex13); - $this->checkpoint_service->trackException($ex13); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } - catch(MissingClientIdParam $ex14){ - $this->log_service->error($ex14); - $this->checkpoint_service->trackException($ex14); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } - catch(InvalidClientType $ex15){ - $this->log_service->error($ex15); - $this->checkpoint_service->trackException($ex15); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } - catch(MissingClientAuthorizationInfo $ex16){ - $this->log_service->error($ex16); - $this->checkpoint_service->trackException($ex16); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } - catch(InvalidRedeemAuthCodeException $ex17){ - $this->log_service->error($ex17); - $this->checkpoint_service->trackException($ex17); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } - catch(InvalidClientCredentials $ex18){ - $this->log_service->error($ex18); - $this->checkpoint_service->trackException($ex18); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } - catch (Exception $ex) { + catch (Exception $ex) + { $this->log_service->error($ex); $this->checkpoint_service->trackException($ex); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_ServerError); + + return new OAuth2DirectErrorResponse + ( + OAuth2Protocol::OAuth2Protocol_Error_ServerError, + $ex->getMessage() + ); } } @@ -467,37 +1009,36 @@ class OAuth2Protocol implements IOAuth2Protocol * @param OAuth2Request $request * @return mixed */ - public function introspection(OAuth2Request $request = null){ - - try { + public function introspection(OAuth2Request $request = null) + { + try + { if (is_null($request) || !$request->isValid()) throw new InvalidOAuth2Request; + return $this->introspection_endpoint->handle($request); } - catch(UnAuthorizedClientException $ex1){ + catch(ExpiredAccessTokenException $ex1) + { $this->log_service->error($ex1); - $this->checkpoint_service->trackException($ex1); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); + return new OAuth2DirectErrorResponse($ex1->getError(), $ex1->getMessage()); } - catch(BearerTokenDisclosureAttemptException $ex2){ + catch(OAuth2BaseException $ex2) + { $this->log_service->error($ex2); $this->checkpoint_service->trackException($ex2); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant); + return new OAuth2DirectErrorResponse($ex2->getError(), $ex2->getMessage()); } - catch(InvalidClientCredentials $ex3){ - $this->log_service->error($ex3); - $this->checkpoint_service->trackException($ex3); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); - } - catch(ExpiredAccessTokenException $ex4){ - $this->log_service->warning($ex4); - $this->checkpoint_service->trackException($ex4); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidToken); - } - catch (Exception $ex) { + catch (Exception $ex) + { $this->log_service->error($ex); $this->checkpoint_service->trackException($ex); - return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest); + + return new OAuth2DirectErrorResponse + ( + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest, + OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest + ); } } @@ -505,4 +1046,319 @@ class OAuth2Protocol implements IOAuth2Protocol { return $this->grant_types; } + + /** + * @param IClient $client + * @return bool + */ + static public function isClientAllowedToUseTokenEndpointAuth(IClient $client) + { + return $client->client_type === IClient::ClientType_Confidential || + $client->application_type === IClient::ApplicationType_Native; + } + + static public function getTokenEndpointAuthMethodsPerClientType(IClient $client) + { + if($client->getClientType() == IClient::ClientType_Public) + { + return ArrayUtils::convert2Assoc + ( + array + ( + self::TokenEndpoint_AuthMethod_PrivateKeyJwt, + self::TokenEndpoint_AuthMethod_None + ) + ); + } + + return ArrayUtils::convert2Assoc + ( + array_merge + ( + self::$token_endpoint_auth_methods, + array + ( + self::TokenEndpoint_AuthMethod_None + ) + ) + ); + } + + /** + * @param IClient $client + * @return array + */ + static public function getSigningAlgorithmsPerClientType(IClient $client) + { + if($client->getClientType() == IClient::ClientType_Public) + { + return ArrayUtils::convert2Assoc + ( + array_merge + ( + self::$supported_signing_algorithms_rsa, + array + ( + JSONWebSignatureAndEncryptionAlgorithms::None + ) + ) + ); + } + return ArrayUtils::convert2Assoc + ( + array_merge + ( + self::$supported_signing_algorithms_hmac_sha2, + self::$supported_signing_algorithms_rsa, + array + ( + JSONWebSignatureAndEncryptionAlgorithms::None + ) + ) + ); + } + + + /** + * @param IClient $client + * @return array + */ + static public function getKeyManagementAlgorithmsPerClientType(IClient $client) + { + if($client->getClientType() == IClient::ClientType_Public) + { + return ArrayUtils::convert2Assoc + ( + array_diff + ( + self::$supported_key_management_algorithms, + array + ( + JSONWebSignatureAndEncryptionAlgorithms::Dir + ) + ) + ); + } + return ArrayUtils::convert2Assoc + ( + self::$supported_key_management_algorithms + ); + } + + + /** + * @return string + */ + public function getJWKSDocument() + { + $keys = $this->server_private_keys_repository->getActives(); + $set = array(); + + foreach($keys as $private_key) + { + $jwk = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + $private_key->getPEM(), + $private_key->getPassword() + ) + ); + + $jwk->setVisibility(JSONWebKeyVisibility::PublicOnly); + + $jwk + ->setId($private_key->getKeyId()) + ->setKeyUse($private_key->getUse()) + ->setType($private_key->getType()) + ->setAlgorithm($private_key->getAlg()->getName()); + + array_push($set, $jwk); + } + + $jkws = new JWKSet($set); + return $jkws->toJson(); + } + + /** + * http://openid.net/specs/openid-connect-discovery-1_0.html + * @return string + */ + public function getDiscoveryDocument() + { + $builder = new DiscoveryDocumentBuilder(); + + return $builder + ->setIssuer($this->oidc_provider_configuration_service->getIssuerUrl()) + ->setAuthEndpoint($this->oidc_provider_configuration_service->getAuthEndpoint()) + ->setTokenEndpoint($this->oidc_provider_configuration_service->getTokenEndpoint()) + ->setUserInfoEndpoint($this->oidc_provider_configuration_service->getUserInfoEndpoint()) + ->setJWKSUrl($this->oidc_provider_configuration_service->getJWKSUrl()) + ->setRevocationEndpoint($this->oidc_provider_configuration_service->getRevocationEndpoint()) + ->setIntrospectionEndpoint($this->oidc_provider_configuration_service->getIntrospectionEndpoint()) + // session management http://openid.net/specs/openid-connect-session-1_0.html + ->setEndSessionEndpoint($this->oidc_provider_configuration_service->getEndSessionEndpoint()) + ->setCheckSessionIframe($this->oidc_provider_configuration_service->getCheckSessionIFrame()) + // response types + ->addResponseTypeSupported('code') + ->addResponseTypeSupported('token') + ->addResponseTypeSupported('code token') + ->addResponseTypeSupported('token id_token') + ->addResponseTypeSupported('code token id_token') + // claims + ->addClaimSupported('aud') + ->addClaimSupported('exp') + ->addClaimSupported('iat') + ->addClaimSupported('iss') + ->addClaimSupported('sub') + ->addClaimSupported(StandardClaims::Email) + ->addClaimSupported(StandardClaims::EmailVerified) + ->addClaimSupported(StandardClaims::Name) + ->addClaimSupported(StandardClaims::GivenName) + ->addClaimSupported(StandardClaims::FamilyName) + ->addClaimSupported(StandardClaims::NickName) + ->addClaimSupported(StandardClaims::Picture) + ->addClaimSupported(StandardClaims::Birthdate) + ->addClaimSupported(StandardClaims::Locale) + ->addClaimSupported(StandardClaims::Gender) + ->addClaimSupported(StandardClaims::Address) + // scopes + ->addScopeSupported(self::OpenIdConnect_Scope) + ->addScopeSupported(IUserService::UserProfileScope_Address) + ->addScopeSupported(IUserService::UserProfileScope_Email) + ->addScopeSupported(IUserService::UserProfileScope_Profile) + // id token signing alg + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::HS256) + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::HS384) + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::HS512) + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RS256) + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RS384) + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RS512) + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::PS256) + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::PS384) + ->addIdTokenSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::PS512) + // id token enc alg + ->addIdTokenEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RSA1_5) + ->addIdTokenEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP) + ->addIdTokenEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP_256) + ->addIdTokenEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::Dir) + // id token enc enc + ->addIdTokenEncryptionEncSupported(JSONWebSignatureAndEncryptionAlgorithms::A128CBC_HS256) + ->addIdTokenEncryptionEncSupported(JSONWebSignatureAndEncryptionAlgorithms::A192CBC_HS384) + ->addIdTokenEncryptionEncSupported(JSONWebSignatureAndEncryptionAlgorithms::A256CBC_HS512) + // user info signing alg + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::HS256) + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::HS384) + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::HS512) + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RS256) + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RS384) + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RS512) + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::PS256) + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::PS384) + ->addUserInfoSigningAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::PS512) + // user info enc alg + ->addUserInfoEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RSA1_5) + ->addUserInfoEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP) + ->addUserInfoEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::RSA_OAEP_256) + ->addUserInfoEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::Dir) + // user info enc enc + ->addUserInfoEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::A128CBC_HS256) + ->addUserInfoEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::A192CBC_HS384) + ->addUserInfoEncryptionAlgSupported(JSONWebSignatureAndEncryptionAlgorithms::A256CBC_HS512) + ->addSubjectTypeSupported(IClient::SubjectType_Public) + ->addSubjectTypeSupported(IClient::SubjectType_Pairwise) + ->addTokenEndpointAuthMethodSupported(self::TokenEndpoint_AuthMethod_ClientSecretBasic) + ->addTokenEndpointAuthMethodSupported(self::TokenEndpoint_AuthMethod_ClientSecretPost) + ->addTokenEndpointAuthMethodSupported(self::TokenEndpoint_AuthMethod_PrivateKeyJwt) + ->addTokenEndpointAuthMethodSupported(self::TokenEndpoint_AuthMethod_ClientSecretJwt) + ->addResponseModeSupported(self::OAuth2Protocol_ResponseMode_FormPost) + ->addResponseModeSupported(self::OAuth2Protocol_ResponseMode_Query) + ->addResponseModeSupported(self::OAuth2Protocol_ResponseMode_Fragment) + ->render(); + } + + /** + * http://openid.net/specs/openid-connect-session-1_0.html#RPLogout + */ + public function endSession(OAuth2Request $request = null) + { + try + { + if (is_null($request) || !$request->isValid()) + throw new InvalidOAuth2Request; + + if(! $request instanceof OAuth2LogoutRequest) throw new InvalidOAuth2Request; + + $id_token_hint = $request->getIdTokenHint(); + + $jwt = BasicJWTFactory::build($id_token_hint); + + if((!$jwt instanceof IJWT)) + throw new InvalidOAuth2Request('invalid id_token_hint!'); + + $client_id = $jwt->getClaimSet()->getAudience(); + + if(is_null($client_id)) throw new InvalidClientException('claim aud not set on id_token_hint!'); + + $client = $this->client_service->getClientById($client_id->getString()); + + if(is_null($client)) throw new InvalidClientException('client not found!'); + + $redirect_logout_uri = $request->getPostLogoutRedirectUri(); + + $state = $request->getState(); + + + if(!empty($redirect_logout_uri) && !$client->isPostLogoutUriAllowed($redirect_logout_uri)) + throw new InvalidOAuth2Request('post_logout_redirect_uri not allowed!'); + + $user_id = $jwt->getClaimSet()->getSubject(); + + if(is_null($user_id)) throw new InvalidOAuth2Request('claim sub not set on id_token_hint!'); + + $user_id = $this->auth_service->unwrapUserId(intval($user_id->getString())); + + $user = $this->auth_service->getUserByExternaldId($user_id); + + if(is_null($user)) throw new InvalidOAuth2Request('user not found!'); + + if($this->principal_service->get()->getUserId() !== $user->getId()) + throw new InvalidOAuth2Request('user does not match with current session!'); + + $this->auth_service->logout(); + + if(!empty($redirect_logout_uri)) + { + return new OAuth2LogoutResponse($redirect_logout_uri, $state); + } + + return null; + } + + catch(OAuth2BaseException $ex1) + { + $this->log_service->error($ex1); + $this->checkpoint_service->trackException($ex1); + + return new OAuth2DirectErrorResponse($ex1->getError(), $ex1->getMessage());; + } + catch (UriNotAllowedException $ex2) + { + $this->log_service->error($ex2); + $this->checkpoint_service->trackException($ex2); + + return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient); + } + catch (Exception $ex) + { + $this->log_service->error($ex); + $this->checkpoint_service->trackException($ex); + + return new OAuth2DirectErrorResponse + ( + OAuth2Protocol::OAuth2Protocol_Error_ServerError, + $ex->getMessage() + ); + } + } } \ No newline at end of file diff --git a/app/libs/oauth2/StandardClaims.php b/app/libs/oauth2/StandardClaims.php new file mode 100644 index 00000000..f3c0112b --- /dev/null +++ b/app/libs/oauth2/StandardClaims.php @@ -0,0 +1,157 @@ +set[OpenIDProviderMetadata::Issuer] = $issuer; + return $this; + } + + /** + * @param string $auth_endpoint + * @return $this + */ + public function setAuthEndpoint($auth_endpoint) + { + $this->set[OpenIDProviderMetadata::AuthEndpoint] = $auth_endpoint; + return $this; + } + + /** + * @param string $token_endpoint + * @return $this + */ + public function setTokenEndpoint($token_endpoint) + { + $this->set[OpenIDProviderMetadata::TokenEndpoint] = $token_endpoint; + return $this; + } + + /** + * @param string $user_info_endpoint + * @return $this + */ + public function setUserInfoEndpoint($user_info_endpoint) + { + $this->set[OpenIDProviderMetadata::UserInfoEndpoint] = $user_info_endpoint; + return $this; + } + + /** + * @param string $end_session_endpoint + * @return $this + */ + public function setEndSessionEndpoint($end_session_endpoint) + { + $this->set[OpenIDProviderMetadata::EndSessionEndPoint] = $end_session_endpoint; + return $this; + } + + /** + * @param string $check_session_iframe + * @return $this + */ + public function setCheckSessionIframe($check_session_iframe) + { + $this->set[OpenIDProviderMetadata::CheckSessionIFrame] = $check_session_iframe; + return $this; + } + + /** + * @param string $jwks_url + * @return $this + */ + public function setJWKSUrl($jwks_url) + { + $this->set[OpenIDProviderMetadata::JWKSUrl] = $jwks_url; + return $this; + } + + /** + * @param string $revocation_endpoint + * @return $this + */ + public function setRevocationEndpoint($revocation_endpoint) + { + $this->set[OpenIDProviderMetadata::RevocationEndpoint] = $revocation_endpoint; + return $this; + } + + /** + * @param string $introspection_endpoint + * @return $this + */ + public function setIntrospectionEndpoint($introspection_endpoint) + { + $this->set[OpenIDProviderMetadata::IntrospectionEndpoint] = $introspection_endpoint; + return $this; + } + + /** + * @param string $key + * @param string $value + */ + private function addArrayValue($key, $value) + { + + if( !isset($this->set[$key])) + $this->set[$key] = array(); + + array_push($this->set[$key], $value); + } + + /** + * @param string $response_type + * @return $this + */ + public function addResponseTypeSupported($response_type) + { + $this->addArrayValue(OpenIDProviderMetadata::ResponseTypesSupported, $response_type); + return $this; + } + + + /** + * @param string $response_mode + * @return $this + */ + public function addResponseModeSupported($response_mode) + { + $this->addArrayValue(OpenIDProviderMetadata::ResponseModesSupported, $response_mode); + return $this; + } + + /** + * @param string $subject_type + * @return $this + */ + public function addSubjectTypeSupported($subject_type) + { + $this->addArrayValue(OpenIDProviderMetadata::SubjectTypesSupported, $subject_type); + return $this; + } + + /** + * @param string $scope + * @return $this + */ + public function addScopeSupported($scope){ + $this->addArrayValue(OpenIDProviderMetadata::ScopesSupported, $scope); + return $this; + } + + /** + * @param string $claim + * @return $this + */ + public function addClaimSupported($claim) + { + $this->addArrayValue(OpenIDProviderMetadata::ClaimsSupported, $claim); + return $this; + } + + /** + * @param string $auth_method + * @return $this + */ + public function addTokenEndpointAuthMethodSupported($auth_method) + { + $this->addArrayValue(OpenIDProviderMetadata::TokenEndpointAuthMethodsSupported, $auth_method); + return $this; + } + + /** + * @param string $alg + * @return $this + */ + public function addIdTokenSigningAlgSupported($alg) + { + $this->addArrayValue(OpenIDProviderMetadata::IdTokenSigningAlgValuesSupported, $alg); + return $this; + } + + /** + * @param string $alg + * @return $this + */ + public function addIdTokenEncryptionAlgSupported($alg) + { + $this->addArrayValue(OpenIDProviderMetadata::IdTokenEncryptionAlgValuesSupported, $alg); + return $this; + } + + /** + * @param string $enc + * @return $this + */ + public function addIdTokenEncryptionEncSupported($enc) + { + $this->addArrayValue(OpenIDProviderMetadata::IdTokenEncryptionEncValuesSupported, $enc); + return $this; + } + + /** + * @param string $alg + * @return $this + */ + public function addUserInfoSigningAlgSupported($alg) + { + $this->addArrayValue(OpenIDProviderMetadata::UserInfoSigningAlgValuesSupported, $alg); + return $this; + } + + /** + * @param string $alg + * @return $this + */ + public function addUserInfoEncryptionAlgSupported($alg) + { + $this->addArrayValue(OpenIDProviderMetadata::UserInfoEncryptionAlgValuesSupported, $alg); + return $this; + } + + /** + * @param string $enc + * @return $this + */ + public function addUserInfoEncryptionEncSupported($enc) + { + $this->addArrayValue(OpenIDProviderMetadata::UserInfoEncryptionEncValuesSupported, $enc); + return $this; + } + + /** + * @return string + */ + public function render() + { + $json = json_encode($this->set); + $json = str_replace('\/','/', $json); + return $json; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/discovery/IOpenIDProviderConfigurationService.php b/app/libs/oauth2/discovery/IOpenIDProviderConfigurationService.php new file mode 100644 index 00000000..98064171 --- /dev/null +++ b/app/libs/oauth2/discovery/IOpenIDProviderConfigurationService.php @@ -0,0 +1,67 @@ +protocol = $protocol; } diff --git a/app/libs/oauth2/endpoints/IOAuth2Endpoint.php b/app/libs/oauth2/endpoints/IOAuth2Endpoint.php index 3da74c13..e485a697 100644 --- a/app/libs/oauth2/endpoints/IOAuth2Endpoint.php +++ b/app/libs/oauth2/endpoints/IOAuth2Endpoint.php @@ -8,6 +8,11 @@ use oauth2\requests\OAuth2Request; * Interface IOAuth2Endpoint * @package oauth2\endpoints */ -interface IOAuth2Endpoint { +interface IOAuth2Endpoint +{ + /** + * @param OAuth2Request $request + * @return mixed + */ public function handle(OAuth2Request $request); } \ No newline at end of file diff --git a/app/libs/oauth2/endpoints/TokenEndpoint.php b/app/libs/oauth2/endpoints/TokenEndpoint.php index 85ee0bfd..e6a8138b 100644 --- a/app/libs/oauth2/endpoints/TokenEndpoint.php +++ b/app/libs/oauth2/endpoints/TokenEndpoint.php @@ -20,13 +20,24 @@ use oauth2\requests\OAuth2Request; class TokenEndpoint implements IOAuth2Endpoint { + /** + * @var IOAuth2Protocol + */ private $protocol; + /** + * @param IOAuth2Protocol $protocol + */ public function __construct(IOAuth2Protocol $protocol) { $this->protocol = $protocol; } + /** + * @param OAuth2Request $request + * @return mixed + * @throws InvalidGrantTypeException + */ public function handle(OAuth2Request $request) { foreach ($this->protocol->getAvailableGrants() as $key => $grant) { diff --git a/app/libs/oauth2/endpoints/TokenIntrospectionEndpoint.php b/app/libs/oauth2/endpoints/TokenIntrospectionEndpoint.php index a151e1ac..bb20e979 100644 --- a/app/libs/oauth2/endpoints/TokenIntrospectionEndpoint.php +++ b/app/libs/oauth2/endpoints/TokenIntrospectionEndpoint.php @@ -6,22 +6,58 @@ use oauth2\requests\OAuth2Request; use oauth2\IOAuth2Protocol; use oauth2\services\IClientService; use oauth2\services\ITokenService; +use utils\services\IAuthService; use utils\services\ILogService; use oauth2\grant_types\ValidateBearerTokenGrantType; +use oauth2\exceptions\InvalidOAuth2Request; +/** + * Class TokenIntrospectionEndpoint + * @package oauth2\endpoints + */ +class TokenIntrospectionEndpoint implements IOAuth2Endpoint +{ -class TokenIntrospectionEndpoint implements IOAuth2Endpoint { - + /** + * @var IOAuth2Protocol + */ private $protocol; + /** + * @var ValidateBearerTokenGrantType + */ private $grant_type; - public function __construct(IOAuth2Protocol $protocol, IClientService $client_service, ITokenService $token_service, ILogService $log_service) + /** + * @param IOAuth2Protocol $protocol + * @param IClientService $client_service + * @param ITokenService $token_service + * @param IAuthService $auth_service + * @param ILogService $log_service + */ + public function __construct + ( + IOAuth2Protocol $protocol, + IClientService $client_service, + ITokenService $token_service, + IAuthService $auth_service, + ILogService $log_service + ) { $this->protocol = $protocol; - $this->grant_type = new ValidateBearerTokenGrantType($client_service, $token_service, $log_service); + $this->grant_type = new ValidateBearerTokenGrantType($client_service, $token_service, $auth_service, $log_service); } + /** + * @param OAuth2Request $request + * @return mixed|\oauth2\responses\OAuth2AccessTokenValidationResponse|void + * @throws InvalidOAuth2Request + * @throws \oauth2\exceptions\BearerTokenDisclosureAttemptException + * @throws \oauth2\exceptions\ExpiredAccessTokenException + * @throws \oauth2\exceptions\InvalidApplicationType + * @throws \oauth2\exceptions\InvalidOAuth2Request + * @throws \oauth2\exceptions\LockedClientException + */ public function handle(OAuth2Request $request) { if($this->grant_type->canHandle($request)) diff --git a/app/libs/oauth2/endpoints/TokenRevocationEndpoint.php b/app/libs/oauth2/endpoints/TokenRevocationEndpoint.php index be6d4e35..b1ad29fb 100644 --- a/app/libs/oauth2/endpoints/TokenRevocationEndpoint.php +++ b/app/libs/oauth2/endpoints/TokenRevocationEndpoint.php @@ -10,18 +10,48 @@ use oauth2\services\ITokenService; use utils\services\ILogService; use oauth2\grant_types\RevokeBearerTokenGrantType; -class TokenRevocationEndpoint implements IOAuth2Endpoint { +/** + * Class TokenRevocationEndpoint + * @package oauth2\endpoints + */ +class TokenRevocationEndpoint implements IOAuth2Endpoint +{ + /** + * @var IOAuth2Protocol + */ private $protocol; + /** + * @var RevokeBearerTokenGrantType + */ private $grant_type; - public function __construct(IOAuth2Protocol $protocol, IClientService $client_service, ITokenService $token_service, ILogService $log_service) + /** + * @param IOAuth2Protocol $protocol + * @param IClientService $client_service + * @param ITokenService $token_service + * @param ILogService $log_service + */ + public function __construct( + IOAuth2Protocol $protocol, + IClientService $client_service, + ITokenService $token_service, + ILogService $log_service + ) { $this->protocol = $protocol; $this->grant_type = new RevokeBearerTokenGrantType($client_service, $token_service, $log_service); } - + /** + * @param OAuth2Request $request + * @return \oauth2\responses\OAuth2TokenRevocationResponse + * @throws InvalidOAuth2Request + * @throws \Exception + * @throws \oauth2\exceptions\BearerTokenDisclosureAttemptException + * @throws \oauth2\exceptions\ExpiredAccessTokenException + * @throws \oauth2\exceptions\UnAuthorizedClientException + */ public function handle(OAuth2Request $request) { if($this->grant_type->canHandle($request)) diff --git a/app/libs/oauth2/exceptions/AbsentCurrentUserException.php b/app/libs/oauth2/exceptions/AbsentCurrentUserException.php new file mode 100644 index 00000000..552087d1 --- /dev/null +++ b/app/libs/oauth2/exceptions/AbsentCurrentUserException.php @@ -0,0 +1,32 @@ +http_code = $http_code; @@ -21,19 +44,23 @@ class OAuth2ResourceServerException extends Exception{ parent::__construct($message, 0, null); } - public function getError(){ + public function getError() + { return $this->error; } - public function getErrorDescription(){ + public function getErrorDescription() + { return $this->error_description; } - public function getScope(){ + public function getScope() + { return $this->scope; } - public function getHttpCode(){ + public function getHttpCode() + { return $this->http_code; } } \ No newline at end of file diff --git a/app/libs/oauth2/exceptions/RecipientKeyNotFoundException.php b/app/libs/oauth2/exceptions/RecipientKeyNotFoundException.php new file mode 100644 index 00000000..c87fd2bd --- /dev/null +++ b/app/libs/oauth2/exceptions/RecipientKeyNotFoundException.php @@ -0,0 +1,32 @@ +auth_code; - } - - public function __construct($auth_code,$message = "") + /** + * @param null|string $code + * @param null $description + */ + public function __construct($token, $description = null) { - $this->auth_code = $auth_code; - $message = "Possible Replay Attack : " . $message; - parent::__construct($message, 0, null); + $this->token = $token; + parent::__construct($description); + } + /** + * @var string + */ + private $token; + + /** + * @return string + */ + public function getToken() + { + return $this->token; + } + /** + * @return string + */ + public function getError() + { + return OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant; } } \ No newline at end of file diff --git a/app/libs/oauth2/exceptions/RevokedAccessTokenException.php b/app/libs/oauth2/exceptions/RevokedAccessTokenException.php new file mode 100644 index 00000000..2c6233b2 --- /dev/null +++ b/app/libs/oauth2/exceptions/RevokedAccessTokenException.php @@ -0,0 +1,32 @@ +getResponseType(false))) + { + $access_token = $token_service->createAccessTokenFromParams + ( + $request->getClientId(), + $request->getScope(), + $audience, + $user->getId() + ); + } + + if(in_array(OAuth2Protocol::OAuth2Protocol_ResponseType_IdToken, $request->getResponseType(false))) { + + $id_token = $token_service->createIdToken + ( + $request->getNonce(), + $request->getClientId(), + $access_token + ); + } + + return new OAuth2IDTokenFragmentResponse + ( + $request->getRedirectUri(), + is_null($access_token) ? null : $access_token->getValue(), + is_null($access_token) ? null : $access_token->getLifetime(), + is_null($access_token) ? null : $request->getScope(), + $request->getState(), + $session_state, + is_null($id_token) ? null : $id_token->toCompactSerialization() + ); + } + + if($request instanceof OAuth2AuthorizationRequest) + { + $access_token = $token_service->createAccessTokenFromParams + ( + $request->getClientId(), + $request->getScope(), + $audience, + $user->getId() + ); + + return new OAuth2AccessTokenFragmentResponse + ( + $request->getRedirectUri(), + $access_token->getValue(), + $access_token->getLifetime(), + $request->getScope(), + $request->getState() + ); + } + + throw new InvalidOAuth2Request; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/factories/OAuth2AccessTokenResponseFactory.php b/app/libs/oauth2/factories/OAuth2AccessTokenResponseFactory.php new file mode 100644 index 00000000..64c6cb4e --- /dev/null +++ b/app/libs/oauth2/factories/OAuth2AccessTokenResponseFactory.php @@ -0,0 +1,140 @@ +getResponseType() + ); + + $is_hybrid_flow = OAuth2Protocol::responseTypeBelongsToFlow + ( + $response_type, + OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid + ); + + if($is_hybrid_flow) + { + + if(in_array(OAuth2Protocol::OAuth2Protocol_ResponseType_Token, $response_type)) + { + + $access_token = $token_service->createAccessToken($auth_code, $request->getRedirectUri()); + } + + // check if should emmit id token + + if(in_array(OAuth2Protocol::OAuth2Protocol_ResponseType_IdToken, $response_type)) + { + + $id_token = $token_service->createIdToken + ( + $auth_code->getNonce(), + $auth_code->getClientId(), + $access_token + ); + } + + if(is_null($id_token) && is_null($access_token)) throw new InvalidOAuth2Request; + } + else + { + $access_token = $token_service->createAccessToken($auth_code, $request->getRedirectUri()); + + $id_token = $token_service->createIdToken + ( + $auth_code->getNonce(), + $auth_code->getClientId(), + $access_token + ); + } + + if(!is_null($access_token)) + $refresh_token = $access_token->getRefreshToken(); + + $response = new OAuth2IdTokenResponse + ( + is_null($access_token) ? null : $access_token->getValue(), + is_null($access_token) ? null : $access_token->getLifetime(), + is_null($id_token) ? null : $id_token->toCompactSerialization(), + is_null($refresh_token) ? null : $refresh_token->getValue() + ); + } + else // normal oauth2.0 code flow + { + $access_token = $token_service->createAccessToken($auth_code, $request->getRedirectUri()); + $refresh_token = $access_token->getRefreshToken(); + + $response = new OAuth2AccessTokenResponse + ( + $access_token->getValue(), + $access_token->getLifetime(), + is_null($refresh_token) ? null : $refresh_token->getValue() + ); + } + + return $response; + } + + /** + * @param AuthorizationCode $auth_code + * @return bool + */ + static public function authCodewasIssuedForOIDC(AuthorizationCode $auth_code) + { + return str_contains($auth_code->getScope(), OAuth2Protocol::OpenIdConnect_Scope); + } + +} \ No newline at end of file diff --git a/app/libs/oauth2/factories/OAuth2AuthorizationRequestFactory.php b/app/libs/oauth2/factories/OAuth2AuthorizationRequestFactory.php new file mode 100644 index 00000000..bd77eb64 --- /dev/null +++ b/app/libs/oauth2/factories/OAuth2AuthorizationRequestFactory.php @@ -0,0 +1,71 @@ +getScope(), OAuth2Protocol::OpenIdConnect_Scope) ) { + $auth_request = new OAuth2AuthenticationRequest($auth_request); + + } + return $auth_request; + } + + /** + * @var OAuth2AuthorizationRequestFactory + */ + private static $instance; + + private function __construct(){} + + private function __clone(){} + + /** + * @return OAuth2AuthorizationRequestFactory + */ + public static function getInstance() + { + if(!is_object(self::$instance)) + { + self::$instance = new OAuth2AuthorizationRequestFactory(); + } + return self::$instance; + } + +} \ No newline at end of file diff --git a/app/libs/oauth2/grant_types/AbstractGrantType.php b/app/libs/oauth2/grant_types/AbstractGrantType.php index 8437e212..e34416e1 100644 --- a/app/libs/oauth2/grant_types/AbstractGrantType.php +++ b/app/libs/oauth2/grant_types/AbstractGrantType.php @@ -2,32 +2,61 @@ namespace oauth2\grant_types; -use oauth2\exceptions\InvalidClientException; -use oauth2\exceptions\MissingClientIdParam; -use oauth2\exceptions\LockedClientException; use oauth2\exceptions\InvalidClientCredentials; - +use oauth2\exceptions\InvalidClientException; +use oauth2\exceptions\LockedClientException; +use oauth2\exceptions\MissingClientIdParam; +use oauth2\models\ClientAuthenticationContext; use oauth2\models\IClient; use oauth2\requests\OAuth2Request; use oauth2\services\IClientService; - use oauth2\services\ITokenService; - +use oauth2\strategies\ClientAuthContextValidatorFactory; use utils\services\ILogService; +/** + * Class AbstractGrantType + * @package oauth2\grant_types + */ abstract class AbstractGrantType implements IGrantType { + /** + * @var ClientAuthenticationContext + */ + protected $client_auth_context; + /** + * @var IClient + */ + protected $current_client; + + /** + * @var IClientService + */ protected $client_service; + /** + * @var ITokenService + */ protected $token_service; - //authorization info - protected $current_client_id; - protected $current_client_secret; - protected $current_client; + /** + * @var ILogService + */ protected $log_service; - public function __construct(IClientService $client_service, ITokenService $token_service, ILogService $log_service) + + /** + * @param IClientService $client_service + * @param ITokenService $token_service + * @param ILogService $log_service + */ + public function __construct + ( + IClientService $client_service, + ITokenService $token_service, + ILogService $log_service + + ) { $this->client_service = $client_service; $this->token_service = $token_service; @@ -37,33 +66,52 @@ abstract class AbstractGrantType implements IGrantType /** * @param OAuth2Request $request * @return mixed|void - * @throws \oauth2\exceptions\MissingClientIdParam - * @throws \oauth2\exceptions\InvalidClientCredentials - * @throws \oauth2\exceptions\InvalidClientException - * @throws \oauth2\exceptions\LockedClientException + * @throws MissingClientIdParam + * @throws InvalidClientCredentials + * @throws InvalidClientException + * @throws LockedClientException */ public function completeFlow(OAuth2Request $request) { //get client credentials from request.. - list($this->current_client_id, $this->current_client_secret) = $this->client_service->getCurrentClientAuthInfo(); + $this->client_auth_context = $this->client_service->getCurrentClientAuthInfo(); - //check if we have at least a client id - if (empty($this->current_client_id)) - throw new MissingClientIdParam(); //retrieve client from storage.. - $this->current_client = $this->client_service->getClientById($this->current_client_id); + $this->current_client = $this->client_service->getClientById($this->client_auth_context->getId()); if (is_null($this->current_client)) - throw new InvalidClientException($this->current_client_id,sprintf("client id %s does not exists!",$this->current_client_id)); + throw new InvalidClientException + ( + sprintf + ( + "client id %s does not exists!", + $this->client_auth_context->getId() + ) + ); if (!$this->current_client->isActive() || $this->current_client->isLocked()) { - throw new LockedClientException($this->current_client_id, sprintf('client id %s',$this->current_client_id)); + throw new LockedClientException + ( + sprintf + ( + 'client id %s is locked.', + $this->client_auth_context->getId() + ) + ); } - //verify client credentials (only for confidential clients ) - if ($this->current_client->getClientType() == IClient::ClientType_Confidential && $this->current_client->getClientSecret() !== $this->current_client_secret) - throw new InvalidClientCredentials($this->current_client_id, sprintf('client id %s',$this->current_client_id)); + $this->client_auth_context->setClient($this->current_client); + if(!ClientAuthContextValidatorFactory::build($this->client_auth_context)->validate($this->client_auth_context)) + throw new InvalidClientCredentials + ( + sprintf + ( + 'invalid credentials for client id %s.', + $this->client_auth_context->getId() + ) + ); } + } \ No newline at end of file diff --git a/app/libs/oauth2/grant_types/AuthorizationCodeGrantType.php b/app/libs/oauth2/grant_types/AuthorizationCodeGrantType.php index f3e5d9a4..53b17226 100644 --- a/app/libs/oauth2/grant_types/AuthorizationCodeGrantType.php +++ b/app/libs/oauth2/grant_types/AuthorizationCodeGrantType.php @@ -4,34 +4,47 @@ namespace oauth2\grant_types; use Exception; use oauth2\exceptions\AccessDeniedException; +use oauth2\exceptions\InvalidApplicationType; use oauth2\exceptions\InvalidAuthorizationCodeException; use oauth2\exceptions\InvalidClientException; +use oauth2\exceptions\InvalidClientType; +use oauth2\exceptions\InvalidLoginHint; use oauth2\exceptions\InvalidOAuth2Request; +use oauth2\exceptions\InvalidRedeemAuthCodeException; use oauth2\exceptions\LockedClientException; use oauth2\exceptions\OAuth2GenericException; use oauth2\exceptions\ScopeNotAllowedException; -use oauth2\exceptions\InvalidRedeemAuthCodeException; use oauth2\exceptions\UnsupportedResponseTypeException; -use oauth2\exceptions\InvalidApplicationType; - use oauth2\exceptions\UriNotAllowedException; +use oauth2\factories\OAuth2AccessTokenResponseFactory; +use oauth2\models\AuthorizationCode; use oauth2\models\IClient; use oauth2\OAuth2Protocol; +use oauth2\repositories\IServerPrivateKeyRepository; use oauth2\requests\OAuth2AccessTokenRequestAuthCode; +use oauth2\requests\OAuth2AuthenticationRequest; +use oauth2\requests\OAuth2AuthorizationRequest; use oauth2\requests\OAuth2Request; +use oauth2\requests\OAuth2TokenRequest; use oauth2\responses\OAuth2AccessTokenResponse; - use oauth2\responses\OAuth2AuthorizationResponse; -use oauth2\services\IClientService; -use oauth2\services\IMementoOAuth2AuthenticationRequestService; -use oauth2\services\ITokenService; -use oauth2\strategies\IOAuth2AuthenticationStrategy; +use oauth2\responses\OAuth2IdTokenResponse; +use oauth2\responses\OAuth2Response; use oauth2\services\IApiScopeService; - -use ReflectionClass; +use oauth2\services\IClientJWKSetReader; +use oauth2\services\IClientService; +use oauth2\services\IMementoOAuth2SerializerService; +use oauth2\services\IPrincipalService; +use oauth2\services\ISecurityContextService; +use oauth2\services\ITokenService; +use oauth2\services\IUserConsentService; +use oauth2\strategies\IOAuth2AuthenticationStrategy; +use openid\model\IOpenIdUser; use utils\services\IAuthService; use utils\services\ILogService; -use oauth2\services\IUserConsentService; +use oauth2\exceptions\InteractionRequiredException; +use oauth2\exceptions\LoginRequiredException; +use oauth2\exceptions\ConsentRequiredException; /** * Class AuthorizationCodeGrantType @@ -45,13 +58,56 @@ use oauth2\services\IUserConsentService; * http://tools.ietf.org/html/rfc6749#section-4.1 * @package oauth2\grant_types */ -class AuthorizationCodeGrantType extends AbstractGrantType +class AuthorizationCodeGrantType extends InteractiveGrantType { - private $auth_service; - private $auth_strategy; - private $memento_service; - private $scope_service; - private $user_consent_service; + + /** + * @param IApiScopeService $scope_service + * @param IClientService $client_service + * @param ITokenService $token_service + * @param IAuthService $auth_service + * @param IOAuth2AuthenticationStrategy $auth_strategy + * @param ILogService $log_service + * @param IUserConsentService $user_consent_service + * @param IMementoOAuth2SerializerService $memento_service + * @param ISecurityContextService $security_context_service + * @param IPrincipalService $principal_service + * @param IServerPrivateKeyRepository $server_private_key_repository + * @param IClientJWKSetReader $jwk_set_reader_service + */ + public function __construct + ( + IApiScopeService $scope_service, + IClientService $client_service, + ITokenService $token_service, + IAuthService $auth_service, + IOAuth2AuthenticationStrategy $auth_strategy, + ILogService $log_service, + IUserConsentService $user_consent_service, + IMementoOAuth2SerializerService $memento_service, + ISecurityContextService $security_context_service, + IPrincipalService $principal_service, + IServerPrivateKeyRepository $server_private_key_repository, + IClientJWKSetReader $jwk_set_reader_service + ) + { + + parent::__construct + ( + $client_service, + $token_service, + $log_service, + $security_context_service, + $principal_service, + $auth_service, + $user_consent_service, + $scope_service, + $auth_strategy, + $memento_service, + $server_private_key_repository, + $jwk_set_reader_service + ); + } /** * @param OAuth2Request $request @@ -59,31 +115,30 @@ class AuthorizationCodeGrantType extends AbstractGrantType */ public function canHandle(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - return - ($class_name == 'oauth2\requests\OAuth2AuthorizationRequest' && $request->isValid() && $request->getResponseType() == $this->getResponseType()) || - ($class_name == 'oauth2\requests\OAuth2TokenRequest' && $request->isValid() && $request->getGrantType() == $this->getType()); - } + if + ( + $request instanceof OAuth2AuthorizationRequest && + $request->isValid() && + OAuth2Protocol::responseTypeBelongsToFlow + ( + $request->getResponseType(false), + OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode + ) + ) + { + return true; + } + if + ( + $request instanceof OAuth2TokenRequest && + $request->isValid() && + $request->getGrantType() == $this->getType() + ) + { + return true; + } - /** - * @param IApiScopeService $scope_service - * @param IClientService $client_service - * @param ITokenService $token_service - * @param IAuthService $auth_service - * @param IMementoOAuth2AuthenticationRequestService $memento_service - * @param IOAuth2AuthenticationStrategy $auth_strategy - * @param ILogService $log_service - * @param IUserConsentService $user_consent_service - */ - public function __construct(IApiScopeService $scope_service ,IClientService $client_service, ITokenService $token_service, IAuthService $auth_service, IMementoOAuth2AuthenticationRequestService $memento_service, IOAuth2AuthenticationStrategy $auth_strategy, ILogService $log_service, IUserConsentService $user_consent_service) - { - parent::__construct($client_service, $token_service,$log_service); - $this->user_consent_service = $user_consent_service; - $this->scope_service = $scope_service; - $this->auth_service = $auth_service; - $this->memento_service = $memento_service; - $this->auth_strategy = $auth_strategy; + return false; } /** @@ -94,119 +149,12 @@ class AuthorizationCodeGrantType extends AbstractGrantType return OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode; } - /** Implements first request processing for Authorization code (Authorization Request processing) - * http://tools.ietf.org/html/rfc6749#section-4.1.1 and - * http://tools.ietf.org/html/rfc6749#section-4.1.2 - * @param OAuth2Request $request - * @return mixed|OAuth2AuthorizationResponse - * @throws \oauth2\exceptions\UnsupportedResponseTypeException - * @throws \oauth2\exceptions\LockedClientException - * @throws \oauth2\exceptions\InvalidClientException - * @throws \oauth2\exceptions\ScopeNotAllowedException - * @throws \oauth2\exceptions\OAuth2GenericException - * @throws \oauth2\exceptions\InvalidApplicationType - * @throws \oauth2\exceptions\AccessDeniedException - * @throws \oauth2\exceptions\UriNotAllowedException - * @throws \oauth2\exceptions\InvalidOAuth2Request - */ - public function handle(OAuth2Request $request) - { - - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2AuthorizationRequest') { - - $client_id = $request->getClientId(); - - $response_type = $request->getResponseType(); - - if ($response_type !== $this->getResponseType()) - throw new UnsupportedResponseTypeException(sprintf("response_type %s", $response_type)); - - $client = $this->client_service->getClientById($client_id); - if (is_null($client)) - throw new InvalidClientException($client_id, sprintf("client_id %s does not exists!", $client_id)); - - if (!$client->isActive() || $client->isLocked()) { - throw new LockedClientException(sprintf($client,'client id %s is locked',$client)); - } - - if ($client->getApplicationType() != IClient::ApplicationType_Web_App) - throw new InvalidApplicationType($client_id,sprintf("client id %s - Application type must be WEB_APPLICATION",$client_id)); - - //check redirect uri - $redirect_uri = $request->getRedirectUri(); - if (!$client->isUriAllowed($redirect_uri)) - throw new UriNotAllowedException(sprintf("redirect_to %s", $redirect_uri)); - - //check requested scope - $scope = $request->getScope(); - if (!$client->isScopeAllowed($scope)) - throw new ScopeNotAllowedException(sprintf("scope %s", $scope)); - - $state = $request->getState(); - - $authentication_response = $this->auth_service->getUserAuthenticationResponse(); - - if($authentication_response == IAuthService::AuthenticationResponse_Cancel){ - //clear saved data ... - $this->memento_service->clearCurrentRequest(); - $this->auth_service->clearUserAuthenticationResponse(); - $this->auth_service->clearUserAuthorizationResponse(); - throw new AccessDeniedException; - } - - //check user logged - if (!$this->auth_service->isUserLogged()) { - $this->memento_service->saveCurrentAuthorizationRequest(); - return $this->auth_strategy->doLogin($this->memento_service->getCurrentAuthorizationRequest()); - } - - $approval_prompt = $request->getApprovalPrompt(); - $access_type = $request->getAccessType(); - $user = $this->auth_service->getCurrentUser(); - - if(is_null($user)) - throw new OAuth2GenericException("Invalid Current User"); - - $authorization_response = $this->auth_service->getUserAuthorizationResponse(); - //check for former user consents - $former_user_consent = $this->user_consent_service->get($user->getId(),$client->getId(),$scope); - - if( !(!is_null($former_user_consent) && $approval_prompt == OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto)){ - if ($authorization_response == IAuthService::AuthorizationResponse_None) { - $this->memento_service->saveCurrentAuthorizationRequest(); - return $this->auth_strategy->doConsent($this->memento_service->getCurrentAuthorizationRequest()); - } - else if ($authorization_response == IAuthService::AuthorizationResponse_DenyOnce) { - throw new AccessDeniedException; - } - //save possitive consent - if(is_null($former_user_consent)){ - $this->user_consent_service->add($user->getId(),$client->getId(),$scope); - } - } - // build current audience ... - $audience = $this->scope_service->getStrAudienceByScopeNames(explode(' ',$scope)); - - $auth_code = $this->token_service->createAuthorizationCode($user->getId(), $client_id, $scope, $audience, $redirect_uri,$access_type,$approval_prompt,!is_null($former_user_consent)); - - if (is_null($auth_code)) - throw new OAuth2GenericException("Invalid Auth Code"); - // clear save data ... - $this->auth_service->clearUserAuthorizationResponse(); - $this->memento_service->clearCurrentRequest(); - return new OAuth2AuthorizationResponse($redirect_uri, $auth_code->getValue() , $scope, $state); - } - throw new InvalidOAuth2Request; - } - /** - * @return mixed|string + * @return array */ public function getResponseType() { - return OAuth2Protocol::OAuth2Protocol_ResponseType_Code; + return OAuth2Protocol::getValidResponseTypes(OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode); } /** @@ -225,59 +173,103 @@ class AuthorizationCodeGrantType extends AbstractGrantType public function completeFlow(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - try{ - if ($class_name == 'oauth2\requests\OAuth2AccessTokenRequestAuthCode') { + if (!($request instanceof OAuth2AccessTokenRequestAuthCode)) + { + throw new InvalidOAuth2Request; + } - parent::completeFlow($request); + try + { + parent::completeFlow($request); - //only confidential clients could use this grant type + $this->checkClientTypeAccess($this->client_auth_context->getClient()); - if ($this->current_client->getApplicationType() != IClient::ApplicationType_Web_App) - throw new InvalidApplicationType($this->current_client_id,sprintf("client id %s - Application type must be WEB_APPLICATION",$this->current_client_id)); - - $current_redirect_uri = $request->getRedirectUri(); - //verify redirect uri - if (!$this->current_client->isUriAllowed($current_redirect_uri)) - throw new UriNotAllowedException(sprintf('redirect url %s is not allowed for cliend id %s',$current_redirect_uri,$this->current_client_id)); - - $code = $request->getCode(); - // verify that the authorization code is valid - // The client MUST NOT use the authorization code - // more than once. If an authorization code is used more than - // once, the authorization server MUST deny the request and SHOULD - // revoke (when possible) all tokens previously issued based on - // that authorization code. The authorization code is bound to - // the client identifier and redirection URI. - $auth_code = $this->token_service->getAuthorizationCode($code); - - $client_id = $auth_code->getClientId(); - - //ensure that the authorization code was issued to the authenticated - //confidential client, or if the client is public, ensure that the - //code was issued to "client_id" in the request - if ($client_id != $this->current_client_id) - throw new InvalidRedeemAuthCodeException($this->current_client_id,sprintf("auth code was issued for another client id!.")); - - // ensure that the "redirect_uri" parameter is present if the - // "redirect_uri" parameter was included in the initial authorization - // and if included ensure that their values are identical. - $redirect_uri = $auth_code->getRedirectUri(); - if (!empty($redirect_uri) && $redirect_uri !== $current_redirect_uri) - throw new UriNotAllowedException(); - - $access_token = $this->token_service->createAccessToken($auth_code, $current_redirect_uri); - $refresh_token = $access_token->getRefreshToken(); - $response = new OAuth2AccessTokenResponse($access_token->getValue(), $access_token->getLifetime(), !is_null($refresh_token) ? $refresh_token->getValue() : null); - return $response; + $current_redirect_uri = $request->getRedirectUri(); + //verify redirect uri + if (!$this->current_client->isUriAllowed($current_redirect_uri)) + { + throw new UriNotAllowedException + ( + $redirect_uri + ); } + + $code = $request->getCode(); + // verify that the authorization code is valid + // The client MUST NOT use the authorization code + // more than once. If an authorization code is used more than + // once, the authorization server MUST deny the request and SHOULD + // revoke (when possible) all tokens previously issued based on + // that authorization code. The authorization code is bound to + // the client identifier and redirection URI. + $auth_code = $this->token_service->getAuthorizationCode($code); + + // reload session state + $client_id = $auth_code->getClientId(); + + $this->security_context_service->save + ( + $this->security_context_service->get() + ->setAuthTimeRequired + ( + $auth_code->isAuthTimeRequested() + ) + ->setRequestedUserId + ( + $auth_code->getUserId() + ) + ); + + $this->principal_service->register + ( + $auth_code->getUserId(), + $auth_code->getAuthTime() + ); + + //ensure that the authorization code was issued to the authenticated + //confidential client, or if the client is public, ensure that the + //code was issued to "client_id" in the request + if ($client_id != $this->client_auth_context->getId()) + { + throw new InvalidRedeemAuthCodeException + ( + sprintf + ( + "auth code was issued for another client id!." + ) + ); + } + + // ensure that the "redirect_uri" parameter is present if the + // "redirect_uri" parameter was included in the initial authorization + // and if included ensure that their values are identical. + $redirect_uri = $auth_code->getRedirectUri(); + + if (!empty($redirect_uri) && $redirect_uri !== $current_redirect_uri) + { + throw new UriNotAllowedException($current_redirect_uri); + } + + $response = OAuth2AccessTokenResponseFactory::build + ( + $this->token_service, + $auth_code, + $request + ); + + $this->security_context_service->clear(); + + return $response; } - catch(InvalidAuthorizationCodeException $ex){ + catch (InvalidAuthorizationCodeException $ex) + { $this->log_service->error($ex); - throw new InvalidRedeemAuthCodeException($this->current_client_id,$ex->getMessage()); + $this->security_context_service->clear(); + throw new InvalidRedeemAuthCodeException + ( + $ex->getMessage() + ); } - throw new InvalidOAuth2Request; } /** @@ -286,13 +278,114 @@ class AuthorizationCodeGrantType extends AbstractGrantType */ public function buildTokenRequest(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2TokenRequest') { + if ($request instanceof OAuth2TokenRequest) + { if ($request->getGrantType() !== $this->getType()) + { return null; + } return new OAuth2AccessTokenRequestAuthCode($request->getMessage()); } return null; } + + /** + * @param IClient $client + * @throws InvalidApplicationType + * @throws InvalidClientType + * @return void + */ + protected function checkClientTypeAccess(IClient $client) + { + if + ( + !( + $client->getClientType() === IClient::ClientType_Confidential || + $client->getApplicationType() === IClient::ApplicationType_Native + ) + ) + { + throw new InvalidApplicationType + ( + sprintf + ( + "client id %s - Application type must be %s or %s", + $client->getClientId(), + IClient::ClientType_Confidential, + IClient::ApplicationType_Native + ) + ); + } + } + + /** + * @param OAuth2AuthorizationRequest $request + * @param bool $has_former_consent + * @return OAuth2AuthorizationResponse + * @throws OAuth2GenericException + */ + protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent) + { + $user = $this->auth_service->getCurrentUser(); + $client = $this->client_service->getClientById($request->getClientId()); + + // build current audience ... + $audience = $this->scope_service->getStrAudienceByScopeNames + ( + explode + ( + OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, + $request->getScope() + ) + ); + + $nonce = null; + + if($request instanceof OAuth2AuthenticationRequest) + { + $nonce = $request->getNonce(); + } + + $auth_code = $this->token_service->createAuthorizationCode + ( + $user->getId(), + $request->getClientId(), + $request->getScope(), + $audience, + $request->getRedirectUri(), + $request->getAccessType(), + $request->getApprovalPrompt(), + $has_former_consent, + $request->getState(), + $nonce, + $request->getResponseType() + ); + + if (is_null($auth_code)) + { + throw new OAuth2GenericException("Invalid Auth Code"); + } + // http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions + $session_state = self::getSessionState + ( + self::getOrigin + ( + $request->getRedirectUri() + ), + $request->getClientId(), + + $this->principal_service->get()->getOPBrowserState() + ); + + return new OAuth2AuthorizationResponse + ( + $request->getRedirectUri(), + $auth_code->getValue(), + $request->getScope(), + $request->getState(), + $session_state + ); + } + + } \ No newline at end of file diff --git a/app/libs/oauth2/grant_types/ClientCredentialsGrantType.php b/app/libs/oauth2/grant_types/ClientCredentialsGrantType.php index c9c89de5..3b874f6f 100644 --- a/app/libs/oauth2/grant_types/ClientCredentialsGrantType.php +++ b/app/libs/oauth2/grant_types/ClientCredentialsGrantType.php @@ -2,22 +2,19 @@ namespace oauth2\grant_types; +use oauth2\exceptions\InvalidApplicationType; use oauth2\exceptions\InvalidGrantTypeException; use oauth2\exceptions\InvalidOAuth2Request; use oauth2\exceptions\ScopeNotAllowedException; -use oauth2\exceptions\InvalidApplicationType; - use oauth2\models\IClient; use oauth2\OAuth2Protocol; use oauth2\requests\OAuth2AccessTokenRequestClientCredentials; - use oauth2\requests\OAuth2Request; +use oauth2\requests\OAuth2TokenRequest; use oauth2\responses\OAuth2AccessTokenResponse; use oauth2\services\IApiScopeService; use oauth2\services\IClientService; - use oauth2\services\ITokenService; -use ReflectionClass; use utils\services\ILogService; /** @@ -34,12 +31,27 @@ use utils\services\ILogService; class ClientCredentialsGrantType extends AbstractGrantType { - + /** + * @var IApiScopeService + */ private $scope_service; - public function __construct(IApiScopeService $scope_service, IClientService $client_service, ITokenService $token_service, ILogService $log_service) + /** + * @param IApiScopeService $scope_service + * @param IClientService $client_service + * @param ITokenService $token_service + * @param ILogService $log_service + */ + public function __construct + ( + IApiScopeService $scope_service, + IClientService $client_service, + ITokenService $token_service, + ILogService $log_service + ) { parent::__construct($client_service, $token_service, $log_service); + $this->scope_service = $scope_service; } @@ -49,10 +61,7 @@ class ClientCredentialsGrantType extends AbstractGrantType */ public function canHandle(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - return - ($class_name == 'oauth2\requests\OAuth2TokenRequest' && $request->isValid() && $request->getGrantType() == $this->getType()); + return $request instanceof OAuth2TokenRequest && $request->isValid() && $request->getGrantType() == $this->getType(); } @@ -86,34 +95,45 @@ class ClientCredentialsGrantType extends AbstractGrantType */ public function completeFlow(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2AccessTokenRequestClientCredentials') { - - if($request->getGrantType()!=$this->getType()) - throw new InvalidGrantTypeException; - - parent::completeFlow($request); - - //only confidential clients could use this grant type - if ($this->current_client->getApplicationType() != IClient::ApplicationType_Service) - throw new InvalidApplicationType($this->current_client_id,sprintf('client id %s client type must be SERVICE',$this->current_client_id)); - - //check requested scope - $scope = $request->getScope(); - if (is_null($scope) || empty($scope) || !$this->current_client->isScopeAllowed($scope)) - throw new ScopeNotAllowedException(sprintf("scope %s", $scope)); - - // build current audience ... - $audience = $this->scope_service->getStrAudienceByScopeNames(explode(' ', $scope)); - - //build access token - $access_token = $this->token_service->createAccessTokenFromParams($this->current_client_id,$scope, $audience); - - $response = new OAuth2AccessTokenResponse($access_token->getValue(), $access_token->getLifetime(), null); - return $response; + if (!($request instanceof OAuth2AccessTokenRequestClientCredentials)) { + throw new InvalidOAuth2Request; } - throw new InvalidOAuth2Request; + + if ($request->getGrantType() != $this->getType()) { + throw new InvalidGrantTypeException; + } + + parent::completeFlow($request); + + //only confidential clients could use this grant type + if ($this->current_client->getApplicationType() != IClient::ApplicationType_Service) { + throw new InvalidApplicationType + ( + sprintf + ( + 'client id %s client type must be %s', + $this->client_auth_context->getId(), + IClient::ApplicationType_Service + ) + ); + } + + //check requested scope + $scope = $request->getScope(); + if (is_null($scope) || empty($scope) || !$this->current_client->isScopeAllowed($scope)) { + throw new ScopeNotAllowedException(sprintf("scope %s", $scope)); + } + + // build current audience ... + $audience = $this->scope_service->getStrAudienceByScopeNames(explode(' ', $scope)); + + //build access token + $access_token = $this->token_service->createAccessTokenFromParams($this->client_auth_context->getId(), $scope, $audience); + + $response = new OAuth2AccessTokenResponse($access_token->getValue(), $access_token->getLifetime(), null); + + return $response; + } /** builds specific Token request @@ -122,13 +142,16 @@ class ClientCredentialsGrantType extends AbstractGrantType */ public function buildTokenRequest(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2TokenRequest') { + + if ($request instanceof OAuth2TokenRequest) + { if ($request->getGrantType() !== $this->getType()) + { return null; + } return new OAuth2AccessTokenRequestClientCredentials($request->getMessage()); } + return null; } diff --git a/app/libs/oauth2/grant_types/HybridGrantType.php b/app/libs/oauth2/grant_types/HybridGrantType.php new file mode 100644 index 00000000..2b5b9bf5 --- /dev/null +++ b/app/libs/oauth2/grant_types/HybridGrantType.php @@ -0,0 +1,258 @@ +isValid() && + OAuth2Protocol::responseTypeBelongsToFlow + ( + $request->getResponseType(false), + OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid + ) + ); + } + + /** + * get grant type + * @return mixed + */ + public function getType() + { + return OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid; + } + + /** + * get grant type response type + * @return array + */ + public function getResponseType() + { + return OAuth2Protocol::getValidResponseTypes(OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid); + } + + /** builds specific Token request + * @param OAuth2Request $request + * @return mixed + */ + public function buildTokenRequest(OAuth2Request $request) + { + throw new InvalidOAuth2Request('not implemented!'); + } + + /** + * @param OAuth2AuthorizationRequest $request + * @param bool $has_former_consent + * @return OAuth2Response + */ + protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent) + { + if (!($request instanceof OAuth2AuthenticationRequest)) { + throw new InvalidOAuth2Request; + } + + $user = $this->auth_service->getCurrentUser(); + + // build current audience ... + $audience = $this->scope_service->getStrAudienceByScopeNames + ( + explode + ( + OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, + $request->getScope() + ) + ); + + // http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions + $session_state = self::getSessionState + ( + self::getOrigin + ( + $request->getRedirectUri() + ), + $request->getClientId(), + + $this->principal_service->get()->getOPBrowserState() + ); + + $auth_code = $this->token_service->createAuthorizationCode + ( + $user->getId(), + $request->getClientId(), + $request->getScope(), + $audience, + $request->getRedirectUri(), + $request->getAccessType(), + $request->getApprovalPrompt(), + $has_former_consent, + $request->getState(), + $request->getNonce(), + $request->getResponseType() + ); + + if (is_null($auth_code)) { + throw new OAuth2GenericException("Invalid Auth Code"); + } + + $access_token = null; + $id_token = null; + + + if (in_array(OAuth2Protocol::OAuth2Protocol_ResponseType_Token, $request->getResponseType(false))) + { + $access_token = $this->token_service->createAccessToken + ( + $auth_code, + $request->getRedirectUri() + ); + } + + if (in_array(OAuth2Protocol::OAuth2Protocol_ResponseType_IdToken, $request->getResponseType(false))) + { + + $id_token = $this->token_service->createIdToken + ( + $request->getNonce(), + $request->getClientId(), + $access_token, + $auth_code + ); + } + + return new OAuth2HybridTokenFragmentResponse + ( + $request->getRedirectUri(), + $auth_code->getValue(), + is_null($access_token) ? null : $access_token->getValue(), + is_null($access_token) ? null : $access_token->getLifetime(), + is_null($access_token) ? null : $request->getScope(), + $request->getState(), + $session_state, + is_null($id_token) ? null : $id_token->toCompactSerialization() + ); + } + + /** + * @param IClient $client + * @throws InvalidApplicationType + * @throws InvalidClientType + * @return void + */ + protected function checkClientTypeAccess(IClient $client) + { + if + ( + !( + $client->getClientType() === IClient::ClientType_Confidential || + $client->getApplicationType() === IClient::ApplicationType_Native + ) + ) + { + throw new InvalidApplicationType + ( + sprintf + ( + "client id %s - Application type must be %s or %s", + $client->getClientId(), + IClient::ClientType_Confidential, + IClient::ApplicationType_Native + ) + ); + } + } +} \ No newline at end of file diff --git a/app/libs/oauth2/grant_types/IGrantType.php b/app/libs/oauth2/grant_types/IGrantType.php index 9ab599d5..e9dbea09 100644 --- a/app/libs/oauth2/grant_types/IGrantType.php +++ b/app/libs/oauth2/grant_types/IGrantType.php @@ -9,7 +9,8 @@ use oauth2\requests\OAuth2Request; * Defines a common interface for new OAuth2 Grant Types * @package oauth2\grant_types */ -interface IGrantType { +interface IGrantType +{ /** Given an OAuth2Request, returns true if it can handle it, false otherwise * @param OAuth2Request $request @@ -35,8 +36,9 @@ interface IGrantType { */ public function getType(); - /** get grant type response type - * @return mixed + /** + * get grant type response type + * @return array */ public function getResponseType(); diff --git a/app/libs/oauth2/grant_types/ImplicitGrantType.php b/app/libs/oauth2/grant_types/ImplicitGrantType.php index a8fcb8ea..12c0ca05 100644 --- a/app/libs/oauth2/grant_types/ImplicitGrantType.php +++ b/app/libs/oauth2/grant_types/ImplicitGrantType.php @@ -2,31 +2,28 @@ namespace oauth2\grant_types; -use oauth2\exceptions\AccessDeniedException; -use oauth2\exceptions\InvalidClientException; -use oauth2\exceptions\InvalidOAuth2Request; -use oauth2\exceptions\ScopeNotAllowedException; -use oauth2\exceptions\OAuth2GenericException; use oauth2\exceptions\InvalidApplicationType; -use oauth2\exceptions\LockedClientException; - -use oauth2\exceptions\UnsupportedResponseTypeException; -use oauth2\exceptions\UriNotAllowedException; +use oauth2\exceptions\InvalidClientType; +use oauth2\exceptions\InvalidOAuth2Request; +use oauth2\factories\OAuth2AccessTokenFragmentResponseFactory; use oauth2\models\IClient; use oauth2\OAuth2Protocol; +use oauth2\repositories\IServerPrivateKeyRepository; +use oauth2\requests\OAuth2AuthorizationRequest; use oauth2\requests\OAuth2Request; - use oauth2\responses\OAuth2AccessTokenFragmentResponse; use oauth2\services\IApiScopeService; +use oauth2\services\IClientJWKSetReader; use oauth2\services\IClientService; +use oauth2\services\IMementoOAuth2SerializerService; +use oauth2\services\IPrincipalService; +use oauth2\services\ISecurityContextService; use oauth2\services\ITokenService; -use oauth2\services\IMementoOAuth2AuthenticationRequestService; - +use oauth2\services\IUserConsentService; use oauth2\strategies\IOAuth2AuthenticationStrategy; -use ReflectionClass; use utils\services\IAuthService; use utils\services\ILogService; -use oauth2\services\IUserConsentService; + /** * Class ImplicitGrantType * http://tools.ietf.org/html/rfc6749#section-4.2 @@ -50,21 +47,54 @@ use oauth2\services\IUserConsentService; * applications residing on the same device. * @package oauth2\grant_types */ -class ImplicitGrantType extends AbstractGrantType +class ImplicitGrantType extends InteractiveGrantType { - private $auth_service; - private $auth_strategy; - private $scope_service; - - public function __construct(IApiScopeService $scope_service, IClientService $client_service, ITokenService $token_service, IAuthService $auth_service, IMementoOAuth2AuthenticationRequestService $memento_service, IOAuth2AuthenticationStrategy $auth_strategy, ILogService $log_service, IUserConsentService $user_consent_service) + /*** + * @param IApiScopeService $scope_service + * @param IClientService $client_service + * @param ITokenService $token_service + * @param IAuthService $auth_service + * @param IOAuth2AuthenticationStrategy $auth_strategy + * @param ILogService $log_service + * @param IUserConsentService $user_consent_service + * @param IMementoOAuth2SerializerService $memento_service + * @param ISecurityContextService $security_context_service + * @param IPrincipalService $principal_service + * @param IServerPrivateKeyRepository $server_private_key_repository + * @param IClientJWKSetReader $jwk_set_reader_service + */ + public function __construct + ( + IApiScopeService $scope_service, + IClientService $client_service, + ITokenService $token_service, + IAuthService $auth_service, + IOAuth2AuthenticationStrategy $auth_strategy, + ILogService $log_service, + IUserConsentService $user_consent_service, + IMementoOAuth2SerializerService $memento_service, + ISecurityContextService $security_context_service, + IPrincipalService $principal_service, + IServerPrivateKeyRepository $server_private_key_repository, + IClientJWKSetReader $jwk_set_reader_service + ) { - parent::__construct($client_service, $token_service, $log_service); - $this->user_consent_service = $user_consent_service; - $this->scope_service = $scope_service; - $this->auth_service = $auth_service; - $this->memento_service = $memento_service; - $this->auth_strategy = $auth_strategy; + parent::__construct + ( + $client_service, + $token_service, + $log_service, + $security_context_service, + $principal_service, + $auth_service, + $user_consent_service, + $scope_service, + $auth_strategy, + $memento_service, + $server_private_key_repository, + $jwk_set_reader_service + ); } /** Given an OAuth2Request, returns true if it can handle it, false otherwise @@ -73,129 +103,36 @@ class ImplicitGrantType extends AbstractGrantType */ public function canHandle(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); return - ($class_name == 'oauth2\requests\OAuth2AuthorizationRequest' && $request->isValid() && $request->getResponseType() == $this->getResponseType()); - } - - /** get grant type response type - * @return mixed - */ - public function getResponseType() - { - return OAuth2Protocol::OAuth2Protocol_ResponseType_Token; + ( + $request instanceof OAuth2AuthorizationRequest && + $request->isValid() && + OAuth2Protocol::responseTypeBelongsToFlow + ( + $request->getResponseType(false), + OAuth2Protocol::OAuth2Protocol_GrantType_Implicit + ) + ); } /** - * @param OAuth2Request $request - * @return mixed|OAuth2AccessTokenFragmentResponse - * @throws \oauth2\exceptions\UnsupportedResponseTypeException - * @throws \oauth2\exceptions\LockedClientException - * @throws \oauth2\exceptions\InvalidClientException - * @throws \oauth2\exceptions\ScopeNotAllowedException - * @throws \oauth2\exceptions\OAuth2GenericException - * @throws \oauth2\exceptions\InvalidApplicationType - * @throws \oauth2\exceptions\AccessDeniedException - * @throws \oauth2\exceptions\UriNotAllowedException - * @throws \oauth2\exceptions\InvalidOAuth2Request + * get grant type response type + * OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what + * parameters are returned from the endpoints used. When using the Implicit Flow, this value is id_token token or + * id_token. The meanings of both of these values are defined in OAuth 2.0 Multiple Response Type Encoding Practices + * [OAuth.Responses]. No Access Token is returned when the value is id_token. + * NOTE: While OAuth 2.0 also defines the token Response Type value for the Implicit Flow, OpenID Connect does not + * use this Response Type, since no ID Token would be returned. + * @return array */ - public function handle(OAuth2Request $request) + public function getResponseType() { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2AuthorizationRequest') { - - $client_id = $request->getClientId(); - - $response_type = $request->getResponseType(); - - if ($response_type !== $this->getResponseType()) - throw new UnsupportedResponseTypeException(sprintf("response_type %s", $response_type)); - - $client = $this->client_service->getClientById($client_id); - - if (is_null($client)) - throw new InvalidClientException($client_id, sprintf("client_id %s", $client_id)); - - if (!$client->isActive() || $client->isLocked()) { - throw new LockedClientException($client,sprintf('client id %s',$client)); - } - - //check client type - // only public clients could use this grant type - if ($client->getApplicationType() != IClient::ApplicationType_JS_Client) - throw new InvalidApplicationType($client_id,sprintf('client id %s client type must be JS CLIENT',$client_id)); - - //check redirect uri - $redirect_uri = $request->getRedirectUri(); - if (!$client->isUriAllowed($redirect_uri)) - throw new UriNotAllowedException(sprintf("redirect_to %s", $redirect_uri)); - - //check requested scope - $scope = $request->getScope(); - - if (is_null($scope) || empty($scope) || !$client->isScopeAllowed($scope)) - throw new ScopeNotAllowedException(sprintf("scope %s", $scope)); - - $state = $request->getState(); - //check user logged - - $authentication_response = $this->auth_service->getUserAuthenticationResponse(); - - if($authentication_response == IAuthService::AuthenticationResponse_Cancel){ - //clear saved data ... - $this->memento_service->clearCurrentRequest(); - $this->auth_service->clearUserAuthenticationResponse(); - $this->auth_service->clearUserAuthorizationResponse(); - throw new AccessDeniedException; - } - - if (!$this->auth_service->isUserLogged()) { - $this->memento_service->saveCurrentAuthorizationRequest(); - return $this->auth_strategy->doLogin($this->memento_service->getCurrentAuthorizationRequest()); - } - - $approval_prompt = $request->getApprovalPrompt(); - - $user = $this->auth_service->getCurrentUser(); - - if(is_null($user)) - throw new OAuth2GenericException("Invalid Current User"); - //validate authorization - //check for former user consents - $authorization_response = $this->auth_service->getUserAuthorizationResponse(); - $former_user_consent = $this->user_consent_service->get($user->getId(),$client->getId(),$scope); - if( !(!is_null($former_user_consent) && $approval_prompt == OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto)){ - if ($authorization_response == IAuthService::AuthorizationResponse_None) { - $this->memento_service->saveCurrentAuthorizationRequest(); - return $this->auth_strategy->doConsent($this->memento_service->getCurrentAuthorizationRequest()); - } - else if ($authorization_response == IAuthService::AuthorizationResponse_DenyOnce) { - //clear saved data ... - $this->memento_service->clearCurrentRequest(); - $this->auth_service->clearUserAuthorizationResponse(); - throw new AccessDeniedException; - } - //save possitive consent - if(is_null($former_user_consent)) - $this->user_consent_service->add($user->getId(),$client->getId(),$scope); - } - - - // build current audience ... - $audience = $this->scope_service->getStrAudienceByScopeNames(explode(' ',$scope)); - //build access token - $access_token = $this->token_service->createAccessTokenFromParams($client_id,$scope, $audience,$user->getId()); - //clear saved data ... - $this->memento_service->clearCurrentRequest(); - $this->auth_service->clearUserAuthorizationResponse(); - return new OAuth2AccessTokenFragmentResponse($redirect_uri, $access_token->getValue(), $access_token->getLifetime(), $scope, $state); - } - throw new InvalidOAuth2Request; + return OAuth2Protocol::getValidResponseTypes(OAuth2Protocol::OAuth2Protocol_GrantType_Implicit); } - public function completeFlow(OAuth2Request $request){ + + public function completeFlow(OAuth2Request $request) + { throw new InvalidOAuth2Request('not implemented!'); } @@ -217,4 +154,67 @@ class ImplicitGrantType extends AbstractGrantType { throw new InvalidOAuth2Request('not implemented!'); } + + /** + * @param OAuth2AuthorizationRequest $request + * @param bool $has_former_consent + * @return OAuth2AccessTokenFragmentResponse + */ + protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent) + { + // build current audience ... + $audience = $this->scope_service->getStrAudienceByScopeNames + ( + explode + ( + OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, + $request->getScope() + ) + ); + + // http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions + $session_state = self::getSessionState + ( + self::getOrigin + ( + $request->getRedirectUri() + ), + $request->getClientId(), + + $this->principal_service->get()->getOPBrowserState() + ); + + return OAuth2AccessTokenFragmentResponseFactory::build + ( + $request, + $audience, + $session_state, + $this->auth_service->getCurrentUser(), + $this->token_service + ); + } + + /** + * @param IClient $client + * @throws InvalidApplicationType + * @throws InvalidClientType + * @return void + */ + protected function checkClientTypeAccess(IClient $client) + { + //check client type + // only public clients could use this grant type + if( $client->getClientType() != IClient::ClientType_Public ) + { + throw new InvalidClientType + ( + sprintf + ( + 'client id %s client type must be %s', + $client->getClientId(), + IClient::ClientType_Public + ) + ); + } + } } \ No newline at end of file diff --git a/app/libs/oauth2/grant_types/InteractiveGrantType.php b/app/libs/oauth2/grant_types/InteractiveGrantType.php new file mode 100644 index 00000000..7bd0b23d --- /dev/null +++ b/app/libs/oauth2/grant_types/InteractiveGrantType.php @@ -0,0 +1,628 @@ +security_context_service = $security_context_service; + $this->principal_service = $principal_service; + $this->auth_service = $auth_service; + $this->user_consent_service = $user_consent_service; + $this->scope_service = $scope_service; + $this->auth_strategy = $auth_strategy; + $this->memento_service = $memento_service; + $this->server_private_key_repository = $server_private_key_repository; + $this->jwk_set_reader_service = $jwk_set_reader_service; + } + + public function handle(OAuth2Request $request) + { + try + { + + if (!($request instanceof OAuth2AuthorizationRequest)) + { + throw new InvalidOAuth2Request; + } + + $client_id = $request->getClientId(); + $client = $this->client_service->getClientById($client_id); + + if (is_null($client)) { + throw new InvalidClientException + ( + sprintf + ( + "client_id %s does not exists!", + $client_id + ) + ); + } + + if (!$client->isActive() || $client->isLocked()) { + throw new LockedClientException + ( + sprintf + ( + 'client id %s is locked', + $client_id + ) + ); + } + + $this->checkClientTypeAccess($client); + + //check redirect uri + $redirect_uri = $request->getRedirectUri(); + + if (!$client->isUriAllowed($redirect_uri)) { + throw new UriNotAllowedException + ( + $redirect_uri + ); + } + + //check requested scope + $scope = $request->getScope(); + + if (!$client->isScopeAllowed($scope)) { + throw new ScopeNotAllowedException(sprintf("scope %s", $scope)); + } + + $authentication_response = $this->auth_service->getUserAuthenticationResponse(); + + // user has cancelled login action + if ($authentication_response == IAuthService::AuthenticationResponse_Cancel) { + //clear saved data ... + $this->memento_service->forget(); + $this->auth_service->clearUserAuthenticationResponse(); + $this->auth_service->clearUserAuthorizationResponse(); + + if ($this->shouldPromptLogin($request)) { + throw new LoginRequiredException; + } + + throw new AccessDeniedException; + } + + //check user logged + if ($this->mustAuthenticateUser($request, $client)) { + if (!$this->canInteractWithEndUser($request)) { + throw new LoginRequiredException; + } + + $this->memento_service->serialize($request->getMessage()->createMemento()); + + return $this->auth_strategy->doLogin($request); + } + + $approval_prompt = $request->getApprovalPrompt(); + $user = $this->auth_service->getCurrentUser(); + + // check if logged user its the same as login hint + $requested_user_id = $this->security_context_service->get()->getRequestedUserId(); + + if (is_null($user)) { + throw new OAuth2GenericException("Invalid Current User"); + } + + if (!is_null($requested_user_id) && $requested_user_id !== $user->getId()) { + $this->auth_service->logout(); + throw new InvalidLoginHint('invalid login hint'); + } + + $authorization_response = $this->auth_service->getUserAuthorizationResponse(); + //check for former user consents + $former_user_consent = $this->user_consent_service->get + ( + $user->getId(), + $client->getId(), + $scope + ); + + $auto_approval = $approval_prompt == OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto; + $has_former_consent = !is_null($former_user_consent); + $should_prompt_consent = $this->shouldPromptConsent($request); + + if ($should_prompt_consent || !($has_former_consent && $auto_approval)) { + if ($should_prompt_consent || $authorization_response == IAuthService::AuthorizationResponse_None) { + if (!$this->canInteractWithEndUser($request)) { + throw new InteractionRequiredException; + } + + $this->memento_service->serialize($request->getMessage()->createMemento()); + + return $this->auth_strategy->doConsent($request); + } else { + if ($authorization_response == IAuthService::AuthorizationResponse_DenyOnce) { + if ($this->hadPromptConsent($request)) { + throw new ConsentRequiredException('the user denied access to your application'); + } + + throw new AccessDeniedException; + } + } + //save possitive consent + if (is_null($former_user_consent)) { + $this->user_consent_service->add($user->getId(), $client->getId(), $scope); + } + } + $this->auth_service->registerRPLogin($client_id); + + $response = $this->buildResponse($request, $has_former_consent); + + // clear save data ... + $this->auth_service->clearUserAuthorizationResponse(); + $this->memento_service->forget(); + + return $response; + } + catch(\Exception $ex) + { + // clear save data ... + $this->auth_service->clearUserAuthorizationResponse(); + $this->memento_service->forget(); + throw $ex; + } + } + + /** + * @param string $origin + * @param string $client_id + * @param string $session_id + * @return string + */ + static public function getSessionState($origin, $client_id, $session_id) + { + $salt = bin2hex(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)); + $session_state = hash('sha256', "{$client_id}{$origin}{$session_id}{$salt}") . '.' . $salt; + return $session_state; + } + + /** + * @param string $url + * @return string + */ + static public function getOrigin($url) + { + $url_parts = @parse_url($url); + return sprintf("%s://%s%s", $url_parts['scheme'], $url_parts['host'], isset($url_parts['port']) ? ':' . $url_parts['port'] : ''); + } + + /** + * @param OAuth2AuthorizationRequest $request + * @param bool $has_former_consent + * @return OAuth2Response + */ + abstract protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent); + + /** + * @param IClient $client + * @throws InvalidApplicationType + * @throws InvalidClientType + * @return void + */ + abstract protected function checkClientTypeAccess(IClient $client); + + /** + * @param OAuth2AuthorizationRequest $request + * @return bool + */ + protected function canInteractWithEndUser(OAuth2AuthorizationRequest $request) + { + if($request instanceof OAuth2AuthenticationRequest && in_array(OAuth2Protocol::OAuth2Protocol_Prompt_None, $request->getPrompt())) + { + return false; + } + return true; + } + + /** + * @param OAuth2AuthenticationRequest $request + * @return void + */ + protected function processUserHint(OAuth2AuthenticationRequest $request) + { + $login_hint = $request->getLoginHint(); + $token_hint = $request->getIdTokenHint(); + + // process login hint + $user = null; + + if(!empty ($login_hint)) + { + + if (filter_var($login_hint, FILTER_VALIDATE_EMAIL)) + { + $user = $this->auth_service->getUserByUsername($login_hint); + } + else + { + $user_id = $this->auth_service->unwrapUserId($login_hint); + $user = $this->auth_service->getUserByExternaldId($user_id); + } + } + else if(!empty($token_hint)) + { + $client_id = $request->getClientId(); + $client = $this->client_service->getClientById($client_id); + + if (is_null($client)) + { + throw new InvalidClientException + ( + sprintf + ( + "client_id %s does not exists!", + $client_id + ) + ); + } + + $jwt = BasicJWTFactory::build($token_hint); + + if($jwt instanceof IJWE) + { + // decrypt using server key + + $recipient_key = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + TestSeeder::$client_private_key_1, + RSAJWKPEMPrivateKeySpecification::WithoutPassword, + $jwt->getJOSEHeader()->getAlgorithm()->getString() + ) + ); + + $heuristic = new ServerEncryptionKeyFinder($this->server_private_key_repository); + $server_enc_private_key = $heuristic->find + ( + $client, + $client->getIdTokenResponseInfo()->getEncryptionKeyAlgorithm() + ); + + $jwt->setRecipientKey($server_enc_private_key); + + $payload = $jwt->getPlainText(); + $jwt = BasicJWTFactory::build($payload); + } + if($jwt instanceof IJWS) + { + // signed by client ? + try + { + $heuristic = new ClientSigningKeyFinder($this->jwk_set_reader_service); + $client_public_sig_key = $heuristic->find + ( + $client, + $client->getIdTokenResponseInfo()->getSigningAlgorithm() + ); + + $jwt->setKey($client_public_sig_key); + } + catch(RecipientKeyNotFoundException $ex) + { + // try to find the server signing key used ... + + $heuristic = new ServerSigningKeyFinder($this->server_private_key_repository); + $server_private_sig_key = $heuristic->find + ( + $client, + $client->getIdTokenResponseInfo()->getSigningAlgorithm(), + $jwt->getJOSEHeader()->getKeyID()->getValue() + ); + $jwt->setKey($server_private_sig_key); + } + + $verified = $jwt->verify($jwt->getJOSEHeader()->getAlgorithm()->getString()); + + if(!$verified) + throw new InvalidLoginHint('invalid id_token_hint'); + } + + $sub = $jwt->getClaimSet()->getSubject(); + $user_id = $this->auth_service->unwrapUserId($sub->getString()); + $user = $this->auth_service->getUserByExternaldId($user_id); + + $jti = $jwt->getClaimSet()->getJWTID(); + if(is_null($jti)) throw new InvalidLoginHint('invalid jti!'); + + $this->auth_service->reloadSession($jti->getValue()); + + } + + if($user) + { + $principal = $this->principal_service->get(); + + if + ( + !is_null($principal) && + !is_null($principal->getUserId()) && + $principal->getUserId() !== $user->getId() + ) + { + if(!$this->canInteractWithEndUser($request)) + throw new InteractionRequiredException; + + $this->auth_service->logout(); + } + + $this->security_context_service->save + ( + $this->security_context_service->get()->setRequestedUserId + ( + $user->getId() + ) + ); + } + } + + /** + * @param OAuth2AuthorizationRequest $request + * @return bool + */ + protected function shouldPromptLogin(OAuth2AuthorizationRequest $request) + { + if + ( + $request instanceof OAuth2AuthenticationRequest && + !$request->isProcessedParam(OAuth2Protocol::OAuth2Protocol_Prompt_Login) && + in_array(OAuth2Protocol::OAuth2Protocol_Prompt_Login, $request->getPrompt()) + ) + { + $request->markParamAsProcessed(OAuth2Protocol::OAuth2Protocol_Prompt_Login); + return true; + } + return false; + } + + /** + * @param OAuth2AuthorizationRequest $request + * @return bool + */ + protected function shouldPromptConsent(OAuth2AuthorizationRequest $request) + { + if + ( + $request instanceof OAuth2AuthenticationRequest && + !$request->isProcessedParam(OAuth2Protocol::OAuth2Protocol_Prompt_Consent) && + in_array(OAuth2Protocol::OAuth2Protocol_Prompt_Consent, $request->getPrompt()) + ) + { + $request->markParamAsProcessed(OAuth2Protocol::OAuth2Protocol_Prompt_Consent); + return true; + } + return false; + } + + /** + * @param OAuth2AuthorizationRequest $request + * @return bool + */ + protected function hadPromptConsent(OAuth2AuthorizationRequest $request) + { + if + ( + $request instanceof OAuth2AuthenticationRequest && + in_array(OAuth2Protocol::OAuth2Protocol_Prompt_Consent, $request->getPrompt()) + ) + { + return true; + } + return false; + } + + /** + * @param OAuth2AuthorizationRequest $request + * @param IClient $client + * @return bool + */ + protected function shouldForceReLogin(OAuth2AuthorizationRequest $request, IClient $client) + { + $now = time(); + $principal = $this->principal_service->get(); + + if($request instanceof OAuth2AuthenticationRequest) + { + + $max_age = $request->getMaxAge(); + $default_max_age = $client->getDefaultMaxAge(); + + if(is_null($max_age) && $default_max_age > 0) + $max_age = $default_max_age; + + if(!is_null($max_age) && $max_age > 0) + { + // must required teh auth_time claim + $this->security_context_service->save + ( + $this->security_context_service->get()->setAuthTimeRequired(true) + ); + + if + ( + !is_null($principal) && + !is_null($principal->getAuthTime()) && + ($now - $principal->getAuthTime()) > $max_age + ) + { + if (!$this->canInteractWithEndUser($request)) + { + throw new InteractionRequiredException; + } + + return true; + } + } + } + return false; + } + + /** + * @param OAuth2AuthorizationRequest $request + * @param IClient $client + * @return bool + * @throws LoginRequiredException + */ + protected function mustAuthenticateUser(OAuth2AuthorizationRequest $request, IClient $client) + { + + if($request instanceof OAuth2AuthenticationRequest) + { + $this->processUserHint($request); + } + + if($this->shouldPromptLogin($request)) + { + $this->auth_service->logout(); + return true; + } + + if($this->shouldForceReLogin($request, $client)) + { + $this->auth_service->logout(); + return true; + } + + if (!$this->auth_service->isUserLogged()) + return true; + + return false; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/grant_types/RefreshBearerTokenGrantType.php b/app/libs/oauth2/grant_types/RefreshBearerTokenGrantType.php index 6993fe60..8aba2907 100644 --- a/app/libs/oauth2/grant_types/RefreshBearerTokenGrantType.php +++ b/app/libs/oauth2/grant_types/RefreshBearerTokenGrantType.php @@ -3,43 +3,50 @@ namespace oauth2\grant_types; use Exception; +use oauth2\exceptions\InvalidApplicationType; +use oauth2\exceptions\InvalidGrantTypeException; +use oauth2\exceptions\InvalidOAuth2Request; +use oauth2\exceptions\UseRefreshTokenException; use oauth2\models\IClient; -use oauth2\requests\OAuth2Request; use oauth2\OAuth2Protocol; +use oauth2\requests\OAuth2RefreshAccessTokenRequest; +use oauth2\requests\OAuth2Request; +use oauth2\requests\OAuth2TokenRequest; +use oauth2\responses\OAuth2AccessTokenResponse; use oauth2\services\IClientService; use oauth2\services\ITokenService; use utils\services\ILogService; -use oauth2\requests\OAuth2RefreshAccessTokenRequest; - -use ReflectionClass; - -use oauth2\exceptions\InvalidApplicationType; -use oauth2\exceptions\UseRefreshTokenException; -use oauth2\exceptions\InvalidOAuth2Request; -use oauth2\exceptions\InvalidGrantTypeException; - -use oauth2\responses\OAuth2AccessTokenResponse; - /** * Class RefreshBearerTokenGrantType * http://tools.ietf.org/html/rfc6749#section-6 * @package oauth2\grant_types */ +final class RefreshBearerTokenGrantType extends AbstractGrantType +{ -class RefreshBearerTokenGrantType extends AbstractGrantType { - - public function __construct(IClientService $client_service, ITokenService $token_service, ILogService $log_service) + /** + * @param IClientService $client_service + * @param ITokenService $token_service + * @param ILogService $log_service + */ + public function __construct + ( + IClientService $client_service, + ITokenService $token_service, + ILogService $log_service + ) { - parent::__construct($client_service, $token_service,$log_service); + parent::__construct($client_service, $token_service, $log_service); } - + /** + * @param OAuth2Request $request + * @return bool + */ public function canHandle(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - return $class_name == 'oauth2\requests\OAuth2TokenRequest' && $request->isValid() && $request->getGrantType() == $this->getType(); + return $request instanceof OAuth2TokenRequest && $request->isValid() && $request->getGrantType() == $this->getType(); } /** Not implemented , there is no first process phase on this grant type @@ -52,12 +59,9 @@ class RefreshBearerTokenGrantType extends AbstractGrantType { throw new InvalidOAuth2Request('not implemented!'); } - - /** * Access Token issuance using a refresh token * The authorization server MUST: - * * o require client authentication for confidential clients or for any * client that was issued client credentials (or with other * authentication requirements), @@ -65,7 +69,6 @@ class RefreshBearerTokenGrantType extends AbstractGrantType { * ensure that the refresh token was issued to the authenticated * client, and * o validate the refresh token. - * * @param OAuth2Request $request * @return mixed|OAuth2AccessTokenResponse|void * @throws \oauth2\exceptions\UseRefreshTokenException @@ -75,71 +78,130 @@ class RefreshBearerTokenGrantType extends AbstractGrantType { */ public function completeFlow(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2RefreshAccessTokenRequest') { - - parent::completeFlow($request); - - if($this->current_client->getApplicationType()!=IClient::ApplicationType_Web_App) - throw new InvalidApplicationType($this->current_client_id,sprintf('client id %s client type must be WEB_APPLICATION',$this->current_client_id)); - - if(!$this->current_client->use_refresh_token) - throw new UseRefreshTokenException("current client id %s could not use refresh tokens",$this->current_client_id); - - $refresh_token_value = $request->getRefreshToken(); - $scope = $request->getScope(); - $refresh_token = $this->token_service->getRefreshToken($refresh_token_value); - - if(is_null($refresh_token)) - throw new InvalidGrantTypeException(sprintf("refresh token %s does not exists!",$refresh_token_value)); - - if($refresh_token->getClientId() !== $this->current_client->client_id) - throw new InvalidGrantTypeException(sprintf("refresh token %s does not belongs to client %s",$refresh_token_value,$this->current_client->client_id)); - - $access_token = $this->token_service->createAccessTokenFromRefreshToken($refresh_token, $scope); - - $new_refresh_token = null; - /* - * the authorization server could employ refresh token - * rotation in which a new refresh token is issued with every access - * token refresh response. The previous refresh token is invalidated - * but retained by the authorization server. If a refresh token is - * compromised and subsequently used by both the attacker and the - * legitimate client, one of them will present an invalidated refresh - * token, which will inform the authorization server of the breach. - */ - if($this->current_client->rotate_refresh_token){ - $this->token_service->invalidateRefreshToken($refresh_token_value); - $new_refresh_token = $this->token_service->createRefreshToken($access_token); - } - - $response = new OAuth2AccessTokenResponse($access_token->getValue(), $access_token->getLifetime(), !is_null($new_refresh_token) ? $new_refresh_token->getValue() : $scope); - return $response; + if (!($request instanceof OAuth2RefreshAccessTokenRequest)) + { + throw new InvalidOAuth2Request; } - throw new InvalidOAuth2Request; + + parent::completeFlow($request); + + if + ( + $this->current_client->getApplicationType() != IClient::ApplicationType_Web_App && + $this->current_client->getApplicationType() != IClient::ApplicationType_Native + ) + { + throw new InvalidApplicationType + ( + sprintf + ( + 'client id %s client type must be %s or ', + $this->client_auth_context->getId(), + IClient::ApplicationType_Web_App, + IClient::ApplicationType_Native + ) + ); + } + + if (!$this->current_client->use_refresh_token) + { + throw new UseRefreshTokenException + ( + sprintf + ( + "current client id %s could not use refresh tokens", + $this->client_auth_context->getId() + ) + ); + } + + $refresh_token_value = $request->getRefreshToken(); + $scope = $request->getScope(); + $refresh_token = $this->token_service->getRefreshToken($refresh_token_value); + + if (is_null($refresh_token)) + { + throw new InvalidGrantTypeException + ( + sprintf + ( + "refresh token %s does not exists!", + $refresh_token_value + ) + ); + } + + if ($refresh_token->getClientId() !== $this->current_client->client_id) + { + throw new InvalidGrantTypeException + ( + sprintf + ( + "refresh token %s does not belongs to client %s", + $refresh_token_value, $this->current_client->client_id + ) + ); + } + + $access_token = $this->token_service->createAccessTokenFromRefreshToken($refresh_token, $scope); + + $new_refresh_token = null; + /* + * the authorization server could employ refresh token + * rotation in which a new refresh token is issued with every access + * token refresh response. The previous refresh token is invalidated + * but retained by the authorization server. If a refresh token is + * compromised and subsequently used by both the attacker and the + * legitimate client, one of them will present an invalidated refresh + * token, which will inform the authorization server of the breach. + */ + if ($this->current_client->rotate_refresh_token) + { + $this->token_service->invalidateRefreshToken($refresh_token_value); + $new_refresh_token = $this->token_service->createRefreshToken($access_token); + } + + $response = new OAuth2AccessTokenResponse + ( + $access_token->getValue(), + $access_token->getLifetime(), + !is_null($new_refresh_token) ? $new_refresh_token->getValue() : $scope + ); + + return $response; } + /** + * @return string + */ public function getType() { return OAuth2Protocol::OAuth2Protocol_GrantType_RefreshToken; } + /** + * @throws InvalidOAuth2Request + */ public function getResponseType() { throw new InvalidOAuth2Request('not implemented!'); } + /** + * @param OAuth2Request $request + * @return null|OAuth2RefreshAccessTokenRequest + */ public function buildTokenRequest(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2TokenRequest') { - if($request->getGrantType() !== $this->getType()) + if ($request instanceof OAuth2TokenRequest) { + if ($request->getGrantType() !== $this->getType()) { return null; + } + return new OAuth2RefreshAccessTokenRequest($request->getMessage()); } + return null; } } \ No newline at end of file diff --git a/app/libs/oauth2/grant_types/RevokeBearerTokenGrantType.php b/app/libs/oauth2/grant_types/RevokeBearerTokenGrantType.php index 4028f0d9..5e24c8dd 100644 --- a/app/libs/oauth2/grant_types/RevokeBearerTokenGrantType.php +++ b/app/libs/oauth2/grant_types/RevokeBearerTokenGrantType.php @@ -2,23 +2,20 @@ namespace oauth2\grant_types; +use Exception; +use oauth2\exceptions\BearerTokenDisclosureAttemptException; +use oauth2\exceptions\ExpiredAccessTokenException; +use oauth2\exceptions\InvalidGrantTypeException; use oauth2\exceptions\InvalidOAuth2Request; use oauth2\exceptions\UnAuthorizedClientException; -use oauth2\exceptions\BearerTokenDisclosureAttemptException; -use oauth2\exceptions\InvalidGrantTypeException; -use oauth2\exceptions\ExpiredAccessTokenException; - use oauth2\OAuth2Protocol; use oauth2\requests\OAuth2Request; +use oauth2\requests\OAuth2TokenRevocationRequest; use oauth2\responses\OAuth2TokenRevocationResponse; use oauth2\services\IClientService; use oauth2\services\ITokenService; - -use ReflectionClass; use utils\services\ILogService; -use Exception; - /** * Class RevokeTokenGrantType * http://tools.ietf.org/html/rfc7009 @@ -62,10 +59,7 @@ class RevokeBearerTokenGrantType extends AbstractGrantType */ public function canHandle(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - - return $class_name == 'oauth2\requests\OAuth2TokenRevocationRequest' && $request->isValid(); + return $request instanceof Auth2TokenRevocationRequest && $request->isValid(); } /** defines entry point for first request processing @@ -78,89 +72,168 @@ class RevokeBearerTokenGrantType extends AbstractGrantType throw new InvalidOAuth2Request('not implemented!'); } + /** + * @param OAuth2Request $request + * @return OAuth2TokenRevocationResponse + * @throws BearerTokenDisclosureAttemptException + * @throws Exception + * @throws ExpiredAccessTokenException + * @throws InvalidOAuth2Request + * @throws UnAuthorizedClientException + * @throws \oauth2\exceptions\InvalidClientCredentials + * @throws \oauth2\exceptions\InvalidClientException + * @throws \oauth2\exceptions\LockedClientException + * @throws \oauth2\exceptions\MissingClientIdParam + */ public function completeFlow(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2TokenRevocationRequest') { + if (!($request instanceof OAuth2TokenRevocationRequest)) + { + throw new InvalidOAuth2Request; + } - parent::completeFlow($request); - $token_value = $request->getToken(); - $token_hint = $request->getTokenHint(); + parent::completeFlow($request); - try{ - if (!is_null($token_hint) && !empty($token_hint)) { - //we have been provided with a token hint... - switch ($token_hint) { - case OAuth2Protocol::OAuth2Protocol_AccessToken: - { - //check ownership - $access_token = $this->token_service->getAccessToken($token_value); + $token_value = $request->getToken(); + $token_hint = $request->getTokenHint(); - if(is_null($access_token)) - throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $token_value)); - - if ($access_token->getClientId() !== $this->current_client_id) - throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('access token %s does not belongs to client id %s',$token_value, $this->current_client_id)); - - $this->token_service->revokeAccessToken($token_value, false); - } - break; - case OAuth2Protocol::OAuth2Protocol_RefreshToken: - { - //check ownership - $refresh_token = $this->token_service->getRefreshToken($token_value); - - if ($refresh_token->getClientId() !== $this->current_client_id) - throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('refresh token %s does not belongs to client id %s',$token_value, $this->current_client_id)); - - $this->token_service->revokeRefreshToken($token_value, false); - } - break; - } - } else { - /* - * no token hint given :( - * if the server is unable to locate the token using - * the given hint, it MUST extend its search across all of its - * supported token types. - */ - - //check and handle access token first .. - try{ + try + { + if (!is_null($token_hint) && !empty($token_hint)) + { + //we have been provided with a token hint... + switch ($token_hint) + { + case OAuth2Protocol::OAuth2Protocol_AccessToken: + { //check ownership $access_token = $this->token_service->getAccessToken($token_value); - if(is_null($access_token)) + if (is_null($access_token)) + { throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $token_value)); + } - if ($access_token->getClientId() !== $this->current_client_id) - throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('access token %s does not belongs to client id %s',$token_value, $this->current_client_id)); + if ($access_token->getClientId() !== $this->client_auth_context->getId()) + { + throw new BearerTokenDisclosureAttemptException + ( + sprintf + ( + 'access token %s does not belongs to client id %s', + $token_value, + $this->client_auth_context->getId() + ) + ); + } $this->token_service->revokeAccessToken($token_value, false); } - catch(UnAuthorizedClientException $ex1){ - $this->log_service->error($ex1); - throw $ex1; - } - catch(Exception $ex){ - $this->log_service->warning($ex); - //access token was not found, check refresh token + break; + case OAuth2Protocol::OAuth2Protocol_RefreshToken: + { //check ownership $refresh_token = $this->token_service->getRefreshToken($token_value); - if ($refresh_token->getClientId() !== $this->current_client_id) - throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('refresh token %s does not belongs to client id %s',$token_value, $this->current_client_id)); + + if ($refresh_token->getClientId() !== $this->client_auth_context->getId()) + { + throw new BearerTokenDisclosureAttemptException + ( + sprintf + ( + 'refresh token %s does not belongs to client id %s', + $token_value, + $this->client_auth_context->getId() + ) + ); + } + $this->token_service->revokeRefreshToken($token_value, false); } + break; } - return new OAuth2TokenRevocationResponse; } - catch(InvalidGrantTypeException $ex){ - throw new BearerTokenDisclosureAttemptException($this->current_client_id,$ex->getMessage()); + else + { + /* + * no token hint given :( + * if the server is unable to locate the token using + * the given hint, it MUST extend its search across all of its + * supported token types. + */ + + //check and handle access token first .. + try { + //check ownership + $access_token = $this->token_service->getAccessToken($token_value); + + if (is_null($access_token)) + { + throw new ExpiredAccessTokenException + ( + sprintf + ( + 'Access token %s is expired!', + $token_value + ) + ); + } + + if ($access_token->getClientId() !== $this->client_auth_context->getId()) + { + throw new BearerTokenDisclosureAttemptException + ( + sprintf + ( + 'access token %s does not belongs to client id %s', + $token_value, + $this->client_auth_context->getId() + ) + ); + } + + $this->token_service->revokeAccessToken($token_value, false); + + } + catch (UnAuthorizedClientException $ex1) + { + $this->log_service->error($ex1); + throw $ex1; + } + catch (Exception $ex) + { + $this->log_service->warning($ex); + //access token was not found, check refresh token + //check ownership + $refresh_token = $this->token_service->getRefreshToken($token_value); + + if ($refresh_token->getClientId() !== $this->client_auth_context->getId()) + { + throw new BearerTokenDisclosureAttemptException + ( + sprintf + ( + 'refresh token %s does not belongs to client id %s', + $token_value, + $this->client_auth_context->getId() + ) + ); + } + $this->token_service->revokeRefreshToken($token_value, false); + } } + + return new OAuth2TokenRevocationResponse; } - throw new InvalidOAuth2Request; + catch (InvalidGrantTypeException $ex) + { + throw new BearerTokenDisclosureAttemptException + ( + $ex->getMessage() + ); + } + } /** diff --git a/app/libs/oauth2/grant_types/ValidateBearerTokenGrantType.php b/app/libs/oauth2/grant_types/ValidateBearerTokenGrantType.php index f0c57874..6f0cb8e9 100644 --- a/app/libs/oauth2/grant_types/ValidateBearerTokenGrantType.php +++ b/app/libs/oauth2/grant_types/ValidateBearerTokenGrantType.php @@ -2,24 +2,22 @@ namespace oauth2\grant_types; -use oauth2\exceptions\ExpiredAccessTokenException; -use oauth2\exceptions\InvalidApplicationType; -use oauth2\exceptions\InvalidOAuth2Request; -use oauth2\exceptions\InvalidAccessTokenException; use oauth2\exceptions\BearerTokenDisclosureAttemptException; -use oauth2\exceptions\LockedClientException; +use oauth2\exceptions\ExpiredAccessTokenException; +use oauth2\exceptions\InvalidAccessTokenException; +use oauth2\exceptions\InvalidApplicationType; use oauth2\exceptions\InvalidGrantTypeException; - +use oauth2\exceptions\InvalidOAuth2Request; +use oauth2\exceptions\LockedClientException; +use oauth2\models\IClient; +use oauth2\requests\OAuth2AccessTokenValidationRequest; use oauth2\requests\OAuth2Request; use oauth2\responses\OAuth2AccessTokenValidationResponse; use oauth2\services\IClientService; use oauth2\services\ITokenService; use utils\IPHelper; +use utils\services\IAuthService; use utils\services\ILogService; -use oauth2\models\IClient; - -use ReflectionClass; - /** * Class ValidateBearerTokenGrantType @@ -55,18 +53,40 @@ class ValidateBearerTokenGrantType extends AbstractGrantType const OAuth2Protocol_GrantType_Extension_ValidateBearerToken = 'urn:tools.ietf.org:oauth2:grant_type:validate_bearer'; - public function __construct(IClientService $client_service, ITokenService $token_service, ILogService $log_service) + /** + * @var IAuthService + */ + private $auth_service; + /** + * @param IClientService $client_service + * @param ITokenService $token_service + * @param IAuthService $auth_service + * @param ILogService $log_service + */ + public function __construct + ( + IClientService $client_service, + ITokenService $token_service, + IAuthService $auth_service, + ILogService $log_service + ) { - parent::__construct($client_service, $token_service,$log_service); + parent::__construct($client_service, $token_service, $log_service); + $this->auth_service = $auth_service; } + /** + * @param OAuth2Request $request + * @return bool + */ public function canHandle(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - return $class_name == 'oauth2\requests\OAuth2AccessTokenValidationRequest' && $request->isValid(); + return $request instanceof OAuth2AccessTokenValidationRequest && $request->isValid(); } + /** + * @return string + */ public function getType() { return self::OAuth2Protocol_GrantType_Extension_ValidateBearerToken; @@ -92,79 +112,150 @@ class ValidateBearerTokenGrantType extends AbstractGrantType */ public function completeFlow(OAuth2Request $request) { - $reflector = new ReflectionClass($request); - $class_name = $reflector->getName(); - if ($class_name == 'oauth2\requests\OAuth2AccessTokenValidationRequest') { - parent::completeFlow($request); - - $token_value = $request->getToken(); - - try{ - - $access_token = $this->token_service->getAccessToken($token_value); - if(is_null($access_token)) - throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $token_value)); - if(!$this->current_client->isResourceServerClient()){ - // if current client is not a resource server, then we could only access to our own tokens - if($access_token->getClientId()!== $this->current_client_id) - throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('access token %s does not belongs to client id %s',$token_value, $this->current_client_id)); - } - else{ - // current client is a resource server, validate client type (must be confidential) - if($this->current_client->getClientType()!== IClient::ClientType_Confidential) - throw new InvalidApplicationType($this->current_client_id,'resource server client is not of confidential type!'); - //validate resource server IP address - $current_ip = IPHelper::getUserIp(); - $resource_server = $this->current_client->getResourceServer(); - //check if resource server is active - if(!$resource_server->active) - throw new LockedClientException($this->current_client_id,'resource server is disabled!'); - //check resource server ip address - if($current_ip !== $resource_server->ip) - throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('resource server ip (%s) differs from current request ip %s',$resource_server->ip,$current_ip)); - // check if current ip belongs to a registered resource server audience - if(!$this->token_service->checkAccessTokenAudience($access_token,$current_ip)) - throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('access token current audience does not match with current request ip %s', $current_ip)); - } - - $allowed_origins = array(); - $allowed_urls = array(); - $issued_client = $this->client_service->getClientById($access_token->getClientId()); - - if(is_null($issued_client)) - throw new BearerTokenDisclosureAttemptException($this->current_client_id,sprintf('access token %s does not belongs to client id %s',$token_value, $access_token->getClientId())); - - foreach($issued_client->getClientAllowedOrigins() as $origin){ - array_push($allowed_origins, $origin->allowed_origin); - } - - foreach($issued_client->getClientRegisteredUris() as $url){ - array_push($allowed_urls, $url->uri); - } - - return new OAuth2AccessTokenValidationResponse($token_value, $access_token->getScope(), $access_token->getAudience(), $access_token->getClientId(), $access_token->getRemainingLifetime(), $access_token->getUserId(), $issued_client->getApplicationType(), $allowed_urls, $allowed_origins); - } - catch(InvalidAccessTokenException $ex1){ - $this->log_service->error($ex1); - throw new BearerTokenDisclosureAttemptException($this->current_client_id,$ex1->getMessage()); - } - catch(InvalidGrantTypeException $ex2){ - $this->log_service->error($ex2); - throw new BearerTokenDisclosureAttemptException($this->current_client_id,$ex2->getMessage()); - } + if (!($request instanceof OAuth2AccessTokenValidationRequest)) { + throw new InvalidOAuth2Request; + } + + parent::completeFlow($request); + + $token_value = $request->getToken(); + + try { + + $access_token = $this->token_service->getAccessToken($token_value); + + if (is_null($access_token)) + { + throw new ExpiredAccessTokenException + ( + sprintf + ( + 'Access token %s is expired!', + $token_value + ) + ); + } + if (!$this->current_client->isResourceServerClient()) + { + // if current client is not a resource server, then we could only access to our own tokens + if ($access_token->getClientId() !== $this->client_auth_context->getId()) + { + throw new BearerTokenDisclosureAttemptException + ( + sprintf + ( + 'access token %s does not belongs to client id %s', + $token_value, + $this->client_auth_context->getId() + ) + ); + } + } + else + { + // current client is a resource server, validate client type (must be confidential) + if ($this->current_client->getClientType() !== IClient::ClientType_Confidential) + { + throw new InvalidApplicationType + ( + 'resource server client is not of confidential type!' + ); + } + //validate resource server IP address + $current_ip = IPHelper::getUserIp(); + $resource_server = $this->current_client->getResourceServer(); + //check if resource server is active + if (!$resource_server->active) + { + throw new LockedClientException + ( + 'resource server is disabled!' + ); + } + //check resource server ip address + if ($current_ip !== $resource_server->ip) + { + throw new BearerTokenDisclosureAttemptException + ( + sprintf + ( + 'resource server ip (%s) differs from current request ip %s', + $resource_server->ip, + $current_ip + ) + ); + } + // check if current ip belongs to a registered resource server audience + if (!$this->token_service->checkAccessTokenAudience($access_token, $current_ip)) + { + throw new BearerTokenDisclosureAttemptException + ( + sprintf + ( + 'access token current audience does not match with current request ip %s', + $current_ip + ) + ); + } + } + + $issued_client = $this->client_service->getClientById($access_token->getClientId()); + + if (is_null($issued_client)) + { + throw new BearerTokenDisclosureAttemptException + ( + sprintf + ( + 'access token %s does not belongs to client id %s', + $token_value, + $access_token->getClientId() + ) + ); + } + + $user_id = $access_token->getUserId(); + $user = is_null($user_id) ? null: $this->auth_service->getUserById($user_id); + + return new OAuth2AccessTokenValidationResponse + ( + $token_value, + $access_token->getScope(), + $access_token->getAudience(), + $issued_client, + $access_token->getRemainingLifetime(), + $user, + $issued_client->getRedirectUris(), + $issued_client->getClientAllowedOrigins() + ); + } + catch (InvalidAccessTokenException $ex1) + { + $this->log_service->error($ex1); + throw new BearerTokenDisclosureAttemptException($ex1->getMessage()); + } + catch (InvalidGrantTypeException $ex2) + { + $this->log_service->error($ex2); + throw new BearerTokenDisclosureAttemptException($ex2->getMessage()); } - throw new InvalidOAuth2Request; } + /** + * @throws InvalidOAuth2Request + */ public function getResponseType() { throw new InvalidOAuth2Request('Not Implemented!'); } + /** + * @param OAuth2Request $request + * @throws InvalidOAuth2Request + */ public function buildTokenRequest(OAuth2Request $request) { throw new InvalidOAuth2Request('Not Implemented!'); } - } \ No newline at end of file diff --git a/app/libs/oauth2/heuristics/ClientEncryptionKeyFinder.php b/app/libs/oauth2/heuristics/ClientEncryptionKeyFinder.php new file mode 100644 index 00000000..cc9f547a --- /dev/null +++ b/app/libs/oauth2/heuristics/ClientEncryptionKeyFinder.php @@ -0,0 +1,127 @@ +jwk_set_reader_service = $jwk_set_reader_service; + } + + /** + * @param IClient $client + * @param ICryptoAlgorithm $alg + * @param string|null $kid_hint + * @return IJWK + * @throws RecipientKeyNotFoundException + */ + public function find(IClient $client, ICryptoAlgorithm $alg, $kid_hint = null) + { + if($alg instanceof DirectEncryption) + { + // use secret + if($client->getClientType() !== IClient::ClientType_Confidential) + throw new InvalidClientType; + + $jwk = OctetSequenceJWKFactory::build + ( + new OctetSequenceJWKSpecification + ( + $client->getClientSecret(), + $alg->getName() + ) + ); + + $jwk->setId('shared_secret'); + + return $jwk; + } + + + $recipient_key = null; + + if(!is_null($kid_hint)) + { + $recipient_key = $client->getPublicKeyByIdentifier($kid_hint); + if(!$recipient_key->isActive()) $recipient_key = null; + if($recipient_key->getAlg()->getName() !== $alg->getName()) $recipient_key = null; + } + + if(is_null($recipient_key)) + { + $recipient_key = $client->getCurrentPublicKeyByUse + ( + JSONWebKeyPublicKeyUseValues::Encryption, + $alg->getName() + ); + } + + if(!is_null($recipient_key)) + { + $recipient_key->markAsUsed(); + $recipient_key->save(); + $recipient_key = $recipient_key->toJWK(); + } + else + { + // check on jwk uri + $jwk_set = $this->jwk_set_reader_service->read($client); + + if(is_null($jwk_set)) + throw new RecipientKeyNotFoundException; + + foreach($jwk_set->getKeys() as $jwk) + { + if + ( + $jwk->getKeyUse() === JSONWebKeyPublicKeyUseValues::Encryption && + $jwk->getAlgorithm()->getString() === $alg->getName() + ) + { + + $recipient_key = $jwk; + break; + } + } + } + + if(is_null($recipient_key)) + throw new RecipientKeyNotFoundException; + + return $recipient_key; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/heuristics/ClientSigningKeyFinder.php b/app/libs/oauth2/heuristics/ClientSigningKeyFinder.php new file mode 100644 index 00000000..8f5cfd98 --- /dev/null +++ b/app/libs/oauth2/heuristics/ClientSigningKeyFinder.php @@ -0,0 +1,129 @@ +jwk_set_reader_service = $jwk_set_reader_service; + } + + /** + * @param IClient $client + * @param ICryptoAlgorithm $alg + * @param string|null $kid_hint + * @return IJWK + * @throws RecipientKeyNotFoundException + */ + public function find(IClient $client, ICryptoAlgorithm $alg, $kid_hint = null) + { + if($alg instanceof MAC_Algorithm ) + { + // use secret + if($client->getClientType() !== IClient::ClientType_Confidential) + throw new InvalidClientType; + + $jwk = OctetSequenceJWKFactory::build + ( + new OctetSequenceJWKSpecification + ( + $client->getClientSecret(), + $alg->getName() + ) + ); + + $jwk->setId('shared_secret'); + + return $jwk; + } + + $recipient_key = null; + + if(!is_null($kid_hint)) + { + $recipient_key = $client->getPublicKeyByIdentifier($kid_hint); + if(!$recipient_key->isActive()) $recipient_key = null; + if($recipient_key->getAlg()->getName() !== $alg->getName()) $recipient_key = null; + } + + if(is_null($recipient_key)) + { + $recipient_key = $client->getCurrentPublicKeyByUse + ( + JSONWebKeyPublicKeyUseValues::Signature, + $alg->getName() + ); + } + + if(!is_null($recipient_key)) + { + $recipient_key->markAsUsed(); + $recipient_key->save(); + $recipient_key = $recipient_key->toJWK(); + } + else + { + // check on jwk uri + $jwk_set = $this->jwk_set_reader_service->read($client); + + if(is_null($jwk_set)) + throw new RecipientKeyNotFoundException; + + foreach($jwk_set->getKeys() as $jwk) + { + if + ( + $jwk->getKeyUse() === JSONWebKeyPublicKeyUseValues::Signature && + $jwk->getAlgorithm()->getString() === $alg->getName() + ) + { + + $recipient_key = $jwk; + break; + } + } + } + + if(is_null($recipient_key)) + throw new RecipientKeyNotFoundException; + + return $recipient_key; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/heuristics/IKeyFinder.php b/app/libs/oauth2/heuristics/IKeyFinder.php new file mode 100644 index 00000000..9560b06b --- /dev/null +++ b/app/libs/oauth2/heuristics/IKeyFinder.php @@ -0,0 +1,34 @@ +server_private_key_repository = $server_private_key_repository; + } + + /** + * @param IClient $client + * @param ICryptoAlgorithm $alg + * @param string|null $kid_hint + * @return IJWK + * @throws InvalidClientType + * @throws ServerKeyNotFoundException + * @throws InvalidJWKAlgorithm + * @throws JWKInvalidSpecException + */ + public function find(IClient $client, ICryptoAlgorithm $alg, $kid_hint = null) + { + if($alg instanceof DirectEncryption) + { + // use secret + if($client->getClientType() !== IClient::ClientType_Confidential) + throw new InvalidClientType; + + $jwk = OctetSequenceJWKFactory::build + ( + new OctetSequenceJWKSpecification + ( + $client->getClientSecret(), + $alg->getName() + ) + ); + + $jwk->setId('shared_secret'); + + return $jwk; + } + + $key = null; + + if(!is_null($kid_hint)) + { + $key = $this->server_private_key_repository->get($kid_hint); + if(!$key->isActive()) $key = null; + if($key->getAlg()->getName() !== $alg->getName()) $key = null; + } + + if(is_null($key)) + { + $key = $this->server_private_key_repository->getActiveByCriteria + ( + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Encryption, + $alg->getName() + ); + } + + if(is_null($key)) + throw new ServerKeyNotFoundException; + + $jwk = $key->toJWK(); + + $key->markAsUsed(); + $key->save(); + + return $jwk; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/heuristics/ServerSigningKeyFinder.php b/app/libs/oauth2/heuristics/ServerSigningKeyFinder.php new file mode 100644 index 00000000..64688a41 --- /dev/null +++ b/app/libs/oauth2/heuristics/ServerSigningKeyFinder.php @@ -0,0 +1,122 @@ +server_private_key_repository = $server_private_key_repository; + } + + /** + * @param IClient $client + * @param ICryptoAlgorithm $alg + * @param string|null $kid_hint + * @return IJWK + * @throws InvalidClientType + * @throws ServerKeyNotFoundException + * @throws InvalidJWKAlgorithm + * @throws JWKInvalidSpecException + */ + public function find(IClient $client, ICryptoAlgorithm $alg, $kid_hint = null) + { + $jwk = null; + + if ($alg instanceof MAC_Algorithm) { + // use secret + if ($client->getClientType() !== IClient::ClientType_Confidential) { + throw new InvalidClientType; + } + + $jwk = OctetSequenceJWKFactory::build + ( + new OctetSequenceJWKSpecification + ( + $client->getClientSecret(), + $alg->getName() + ) + ); + + $jwk->setId('shared_secret'); + + return $jwk; + } + + $key = null; + + if (!is_null($kid_hint)) + { + $key = $this->server_private_key_repository->get($kid_hint); + if (!$key->isActive()) + { + $key = null; + } + if ($key->getAlg()->getName() !== $alg->getName()) + { + $key = null; + } + } + + if(is_null($key)) + { + $key = $this->server_private_key_repository->getActiveByCriteria + ( + JSONWebKeyTypes::RSA, + JSONWebKeyPublicKeyUseValues::Signature, + $alg->getName() + ); + } + + if (is_null($key)) + { + throw new ServerKeyNotFoundException; + } + + $jwk = $key->toJWK(); + + $key->markAsUsed(); + $key->save(); + + return $jwk; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/models/AuthorizationCode.php b/app/libs/oauth2/models/AuthorizationCode.php index e6c86967..9666e336 100644 --- a/app/libs/oauth2/models/AuthorizationCode.php +++ b/app/libs/oauth2/models/AuthorizationCode.php @@ -10,14 +10,39 @@ use oauth2\OAuth2Protocol; * http://tools.ietf.org/html/rfc6749#section-1.3.1 * @package oauth2\models */ -class AuthorizationCode extends Token { +class AuthorizationCode extends Token +{ private $redirect_uri; private $access_type; private $approval_prompt; private $has_previous_user_consent; + /** + * @var string + */ + private $state; + /** + * @var string + */ + private $nonce; - public function __construct(){ + /** + * @var string + */ + private $response_type; + + /** + * @var bool + */ + private $requested_auth_time; + + /** + * @var int + */ + private $auth_time; + + public function __construct() + { parent::__construct(64); } @@ -31,9 +56,29 @@ class AuthorizationCode extends Token { * @param string $approval_prompt * @param bool $has_previous_user_consent * @param int $lifetime + * @param string|null $state + * @param string|null $nonce + * @param string|null $response_type + * @param $requested_auth_time + * @param $auth_time * @return AuthorizationCode */ - public static function create($user_id, $client_id, $scope, $audience='' ,$redirect_uri = null,$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,$approval_prompt =OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,$has_previous_user_consent=false, $lifetime = 600){ + public static function create( + $user_id, + $client_id, + $scope, + $audience = '', + $redirect_uri = null, + $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, + $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, + $has_previous_user_consent = false, + $lifetime = 600, + $state = null, + $nonce = null, + $response_type = null, + $requested_auth_time = false, + $auth_time = -1 + ) { $instance = new self(); $instance->scope = $scope; $instance->user_id = $user_id; @@ -46,6 +91,12 @@ class AuthorizationCode extends Token { $instance->access_type = $access_type; $instance->approval_prompt = $approval_prompt; $instance->has_previous_user_consent = $has_previous_user_consent; + $instance->state = $state; + $instance->nonce = $nonce; + $instance->response_type = $response_type; + $instance->requested_auth_time = $requested_auth_time; + $instance->auth_time = $auth_time; + return $instance; } @@ -62,10 +113,35 @@ class AuthorizationCode extends Token { * @param string $access_type * @param string $approval_prompt * @param bool $has_previous_user_consent + * @param string|null $state + * @param string|null $nonce + * @param string|null $response_type + * @param $requested_auth_time + * @param $auth_time * @param bool $is_hashed * @return AuthorizationCode */ - public static function load($value, $user_id, $client_id, $scope,$audience='', $redirect_uri = null, $issued = null, $lifetime = 600, $from_ip = '127.0.0.1',$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,$approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,$has_previous_user_consent=false,$is_hashed = false){ + public static function load + ( + $value, + $user_id, + $client_id, + $scope,$audience = '', + $redirect_uri = null, + $issued = null, + $lifetime = 600, + $from_ip = '127.0.0.1', + $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, + $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, + $has_previous_user_consent = false, + $state, + $nonce, + $response_type, + $requested_auth_time = false, + $auth_time = -1, + $is_hashed = false + ) + { $instance = new self(); $instance->value = $value; $instance->user_id = $user_id; @@ -80,27 +156,97 @@ class AuthorizationCode extends Token { $instance->access_type = $access_type; $instance->approval_prompt = $approval_prompt; $instance->has_previous_user_consent = $has_previous_user_consent; + $instance->state = $state; + $instance->nonce = $nonce; + $instance->response_type = $response_type; + $instance->requested_auth_time = $requested_auth_time; + $instance->auth_time = $auth_time; + return $instance; } - - public function getRedirectUri(){ + /** + * @return string + */ + public function getRedirectUri() + { return $this->redirect_uri; } - - public function getAccessType(){ + /** + * @return string + */ + public function getAccessType() + { return $this->access_type; } - public function getApprovalPrompt(){ + /** + * @return string + */ + public function getApprovalPrompt() + { return $this->approval_prompt; } - public function getHasPreviousUserConsent(){ + /** + * @return string + */ + public function getHasPreviousUserConsent() + { return $this->has_previous_user_consent; } + /** + * @return string + */ + public function getState() + { + return $this->state; + } + + /** + * @return string + */ + public function getNonce() + { + return $this->nonce; + } + + /** + * @return string + */ + public function getResponseType() + { + return $this->response_type; + } + + /** + * @return bool + */ + public function isAuthTimeRequested() + { + $res = $this->requested_auth_time; + if (!is_string($res)) return (bool) $res; + switch (strtolower($res)) { + case '1': + case 'true': + case 'on': + case 'yes': + case 'y': + return true; + } + return false; + } + + /** + * @return int + */ + public function getAuthTime() + { + return $this->auth_time; + } + public function toJSON() { return '{}'; diff --git a/app/libs/oauth2/models/ClientAssertionAuthenticationContext.php b/app/libs/oauth2/models/ClientAssertionAuthenticationContext.php new file mode 100644 index 00000000..76f38c46 --- /dev/null +++ b/app/libs/oauth2/models/ClientAssertionAuthenticationContext.php @@ -0,0 +1,100 @@ +type = $type; + $this->jwt_assertion = $jwt_assertion; + + $this->jws = BasicJWTFactory::build($jwt_assertion); + + if(!($this->jws instanceof IJWS)) + throw new InvalidClientAssertionException; + + $alg = $this->jws->getJOSEHeader()->getAlgorithm()->getValue(); + + if( !(DigitalSignatures_MACs_Registry::getInstance()->isSupported($alg) && + in_array($alg, OAuth2Protocol::$supported_signing_algorithms))) + throw new InvalidClientAssertionAlgorithmException($alg); + + $alg = DigitalSignatures_MACs_Registry::getInstance()->get($alg); + + $auth_type = $alg instanceof MAC_Algorithm ? + OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretJwt : + OAuth2Protocol::TokenEndpoint_AuthMethod_PrivateKeyJwt; + + parent::__construct($this->jws->getClaimSet()->getIssuer()->getString(), $auth_type); + } + + /** + * @return IJWS + */ + public function getAssertion() + { + return $this->jws; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/models/ClientAuthenticationContext.php b/app/libs/oauth2/models/ClientAuthenticationContext.php new file mode 100644 index 00000000..25215db2 --- /dev/null +++ b/app/libs/oauth2/models/ClientAuthenticationContext.php @@ -0,0 +1,89 @@ +client_id = $client_id; + $this->auth_type = $auth_type; + } + + /** + * @return string + */ + public function getAuthType() + { + return $this->auth_type; + } + + /** + * @return string + */ + public function getId() + { + return $this->client_id; + } + + /** + * @param IClient $client + * @return $this + */ + public function setClient(IClient $client) + { + $this->client = $client; + return $this; + } + + /** + * @return IClient + */ + public function getClient() + { + return $this->client; + } + +} \ No newline at end of file diff --git a/app/libs/oauth2/models/ClientCredentialsAuthenticationContext.php b/app/libs/oauth2/models/ClientCredentialsAuthenticationContext.php new file mode 100644 index 00000000..14c56d5b --- /dev/null +++ b/app/libs/oauth2/models/ClientCredentialsAuthenticationContext.php @@ -0,0 +1,58 @@ +client_secret = $client_secret; + } + + /** + * @return string + */ + public function getSecret() + { + return $this->client_secret; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/models/IApiScope.php b/app/libs/oauth2/models/IApiScope.php index 01058094..a7997256 100644 --- a/app/libs/oauth2/models/IApiScope.php +++ b/app/libs/oauth2/models/IApiScope.php @@ -7,12 +7,25 @@ namespace oauth2\models; * http://tools.ietf.org/html/rfc6749#section-3.3 * @package oauth2\models */ -interface IApiScope { - public function getShortDescription(); - public function getName(); - public function getDescription(); - public function isActive(); +interface IApiScope extends IScope { + + /** + * @return string + */ public function getApiName(); + + /** + * @return string + */ public function getApiDescription(); + + /** + * @return string + */ public function getApiLogo(); -} \ No newline at end of file + + /** + * @return IApi + */ + public function getApi(); +} \ No newline at end of file diff --git a/app/libs/oauth2/models/IApiScopeGroup.php b/app/libs/oauth2/models/IApiScopeGroup.php new file mode 100644 index 00000000..3f7037ab --- /dev/null +++ b/app/libs/oauth2/models/IApiScopeGroup.php @@ -0,0 +1,55 @@ +sig_alg = $sig_alg; + $this->alg = $alg; + $this->enc = $enc; + } + + /** + * @return IntegrityCheckingAlgorithm + */ + public function getSigningAlgorithm() + { + return $this->sig_alg; + } + + /** + * @return EncryptionAlgorithm + */ + public function getEncryptionKeyAlgorithm() + { + return $this->alg; + } + + /** + * @return ContentEncryptionAlgorithm + */ + public function getEncryptionContentAlgorithm() + { + return $this->enc; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/models/Principal.php b/app/libs/oauth2/models/Principal.php new file mode 100644 index 00000000..8f31a0da --- /dev/null +++ b/app/libs/oauth2/models/Principal.php @@ -0,0 +1,74 @@ +auth_time; + } + + /** + * @return int + */ + public function getUserId() + { + return (int)$this->user_id; + } + + /** + * @return string + */ + public function getOPBrowserState() + { + return $this->opbs; + } + + /** + * @param array $state + * @return $this + */ + public function setState(array $state) + { + $this->user_id = intval($state[0]); + $this->auth_time = intval($state[1]); + $this->opbs = $state[2]; + return $this; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/models/SecurityContext.php b/app/libs/oauth2/models/SecurityContext.php new file mode 100644 index 00000000..ff62ed0b --- /dev/null +++ b/app/libs/oauth2/models/SecurityContext.php @@ -0,0 +1,91 @@ +requested_user_id; + } + + /** + * @param int $requested_user_id + * @return $this + */ + public function setRequestedUserId($requested_user_id) + { + $this->requested_user_id = $requested_user_id; + return $this; + } + + /** + * @return bool + */ + public function isAuthTimeRequired() + { + return $this->requested_auth_time; + } + + /** + * @param bool $requested_auth_time + * @return $this + */ + public function setAuthTimeRequired($requested_auth_time) + { + $this->requested_auth_time = $requested_auth_time; + return $this; + } + + /** + * @return array + */ + public function getState() + { + return array + ( + $this->requested_user_id, + $this->requested_auth_time, + ); + } + + /** + * @param array $state + * @return $this + */ + public function setState(array $state) + { + $this->requested_user_id = $state[0]; + $this->requested_auth_time = $state[1]; + return $this; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/models/Token.php b/app/libs/oauth2/models/Token.php index 0d8f76d3..76da6454 100644 --- a/app/libs/oauth2/models/Token.php +++ b/app/libs/oauth2/models/Token.php @@ -51,19 +51,28 @@ abstract class Token extends Identifier return $this->client_id; } + /** + * @return mixed + */ public function getAudience() { return $this->audience; } + /** + * @return string + */ public function getFromIp() { return $this->from_ip; } + /** + * @return int + */ public function getUserId() { - return $this->user_id; + return intval($this->user_id); } public function getRemainingLifetime() diff --git a/app/libs/oauth2/models/TokenEndpointAuthInfo.php b/app/libs/oauth2/models/TokenEndpointAuthInfo.php new file mode 100644 index 00000000..03023f20 --- /dev/null +++ b/app/libs/oauth2/models/TokenEndpointAuthInfo.php @@ -0,0 +1,59 @@ +auth_method = $auth_method; + $this->auth_signing_alg = $auth_signing_alg; + } + + /** + * @return string + */ + public function getAuthenticationMethod(){ + return $this->auth_method; + } + + /** + * @return IntegrityCheckingAlgorithm + */ + public function getSigningAlgorithm(){ + return $this->auth_signing_alg; + } + +} \ No newline at end of file diff --git a/app/libs/oauth2/repositories/IApiScopeGroupRepository.php b/app/libs/oauth2/repositories/IApiScopeGroupRepository.php new file mode 100644 index 00000000..fffee52d --- /dev/null +++ b/app/libs/oauth2/repositories/IApiScopeGroupRepository.php @@ -0,0 +1,26 @@ +getParam(OAuth2Protocol::OAuth2Protocol_Nonce); + } + + /** + * @return null|string + */ + public function getDisplay() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_Display); + } + + /** + * @return string[] + */ + public function getPrompt() + { + $prompt = $this->getParam(OAuth2Protocol::OAuth2Protocol_Prompt); + if(!empty($prompt)) + return explode(' ', $prompt); + return array(); + } + + /** + * @return null|int + */ + public function getMaxAge() + { + $value = $this->getParam(OAuth2Protocol::OAuth2Protocol_MaxAge); + if(!is_null($value)) + $value = intval($value); + return $value; + } + + /** + * @return null|string + */ + public function getUILocales() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_UILocales); + } + + /** + * @return null|string + */ + public function getIdTokenHint() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_IDTokenHint); + } + + /** + * @return null|string + */ + public function getLoginHint() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_LoginHint); + } + + /** + * @return null|string + */ + public function getACRValues() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_ACRValues); + } + + /** + * @return bool + */ + public function offlineAccessRequested() + { + return str_contains($this->getScope(), OAuth2Protocol::OfflineAccess_Scope); + } + + /** + * @param OAuth2AuthorizationRequest $auth_request + */ + public function __construct(OAuth2AuthorizationRequest $auth_request) + { + parent::__construct($auth_request->getMessage()); + } + + /** + * http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes + * The Response Mode request parameter response_mode informs the Authorization Server of the mechanism to be used + * for returning Authorization Response parameters from the Authorization Endpoint + */ + public function getResponseMode() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseMode); + } + + /** + * Validates current request + * @return bool + */ + public function isValid() + { + $res = parent::isValid(); + if(!$res) throw new InvalidOAuth2Request('invalid parent request!'); + + if($res) + { + // Reject requests without nonce unless using the code flow + $nonce = $this->getNonce(); + if(empty($nonce) && !OAuth2Protocol::responseTypeBelongsToFlow($this->getResponseType(false), OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode)) + throw new InvalidOAuth2Request('missing nonce!'); + + $current_scope = trim($this->getScope()); + + if(!str_contains($current_scope, OAuth2Protocol::OpenIdConnect_Scope)) + throw new InvalidOAuth2Request('missing openid scope!'); + + if($current_scope === OAuth2Protocol::OpenIdConnect_Scope) + { + // add as default scope to current request + $this->setParam(OAuth2Protocol::OAuth2Protocol_Scope, sprintf('%s %s', $current_scope, IUserService::UserProfileScope_Profile)); + } + + $display = $this->getDisplay(); + + if(!empty($display) && !in_array($display, OAuth2Protocol::$valid_display_values)) + throw new InvalidOAuth2Request('not valid display value'); + + $prompt = $this->getPrompt(); + + if(!empty($prompt) && is_array($prompt)) + { + // if this parameter contains none with any other value, an error is returned. + if(in_array(OAuth2Protocol::OAuth2Protocol_Prompt_None, $prompt) && count($prompt) > 1) + throw new InvalidOAuth2Request('not valid prompt!'); + + foreach($prompt as $p) + { + if(!in_array($p, OAuth2Protocol::$valid_prompt_values)) + throw new InvalidOAuth2Request('not valid prompt!'); + } + } + + $response_mode = $this->getResponseMode(); + + if(!empty($response_mode)) + { + if(!in_array($response_mode, OAuth2Protocol::$valid_response_modes)) throw new InvalidOAuth2Request('invalid response_mode!'); + + $default_response_mode = OAuth2Protocol::getDefaultResponseMode($this->getResponseType(false)); + + if($default_response_mode === $response_mode) throw new InvalidOAuth2Request('invalid response_mode!'); + } + + // http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + // MUST ensure that the prompt parameter contains consent unless other conditions for processing the request + // permitting offline access to the requested resources are in place + if($this->offlineAccessRequested() && empty($prompt)) + throw new InvalidOAuth2Request('invalid offline access!'); + + if($this->offlineAccessRequested() && !in_array(OAuth2Protocol::OAuth2Protocol_Prompt_Consent, $prompt)) + throw new InvalidOAuth2Request('invalid offline access!'); + } + + return $res; + } + + /** + * @param string $param_name + * @return bool + */ + public function isProcessedParam($param_name){ + $res = explode(' ', $this->getParam('processed_params')); + return in_array($param_name, $res); + } + + /** + * @param string $param_name + * @return $this + */ + public function markParamAsProcessed($param_name) + { + $res = $this->getParam('processed_params'); + if(!empty($res)) + { + $res = $res .' '; + } + $res = $res .$param_name; + $this->setParam('processed_params', $res); + return $this; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/requests/OAuth2AuthorizationRequest.php b/app/libs/oauth2/requests/OAuth2AuthorizationRequest.php index 9cd79fe2..6408f3e3 100644 --- a/app/libs/oauth2/requests/OAuth2AuthorizationRequest.php +++ b/app/libs/oauth2/requests/OAuth2AuthorizationRequest.php @@ -2,36 +2,49 @@ namespace oauth2\requests; -use oauth2\OAuth2Protocol; use oauth2\OAuth2Message; +use oauth2\OAuth2Protocol; /** * Class OAuth2AuthorizationRequest * http://tools.ietf.org/html/rfc6749#section-4.1.1 * @package oauth2\requests */ -class OAuth2AuthorizationRequest extends OAuth2Request { +class OAuth2AuthorizationRequest extends OAuth2Request +{ public function __construct(OAuth2Message $msg) { parent::__construct($msg); } - public static $params = array( - OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType, - OAuth2Protocol::OAuth2Protocol_ClientId => OAuth2Protocol::OAuth2Protocol_ClientId, - OAuth2Protocol::OAuth2Protocol_RedirectUri => OAuth2Protocol::OAuth2Protocol_RedirectUri, - OAuth2Protocol::OAuth2Protocol_Scope => OAuth2Protocol::OAuth2Protocol_Scope, - OAuth2Protocol::OAuth2Protocol_State => OAuth2Protocol::OAuth2Protocol_State, - OAuth2Protocol::OAuth2Protocol_Approval_Prompt => OAuth2Protocol::OAuth2Protocol_Approval_Prompt, - OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType, + public static $params = array + ( + OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType, + OAuth2Protocol::OAuth2Protocol_ClientId => OAuth2Protocol::OAuth2Protocol_ClientId, + OAuth2Protocol::OAuth2Protocol_RedirectUri => OAuth2Protocol::OAuth2Protocol_RedirectUri, + OAuth2Protocol::OAuth2Protocol_Scope => OAuth2Protocol::OAuth2Protocol_Scope, + OAuth2Protocol::OAuth2Protocol_State => OAuth2Protocol::OAuth2Protocol_State, + OAuth2Protocol::OAuth2Protocol_Approval_Prompt => OAuth2Protocol::OAuth2Protocol_Approval_Prompt, + OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType, ); /** - * @return null|string + * The Response Type request parameter response_type informs the Authorization Server of the desired authorization + * processing flow + * @param bool|true $raw + * @return array|string|null */ - public function getResponseType(){ - return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseType); + public function getResponseType($raw = true) + { + if($raw) + return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseType); + + return explode + ( + OAuth2Protocol::OAuth2Protocol_ResponseType_Delimiter, + $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseType) + ); } /** @@ -39,7 +52,8 @@ class OAuth2AuthorizationRequest extends OAuth2Request { * The value passed in this parameter must exactly match the value shown in the Admin Console. * @return null|string */ - public function getClientId(){ + public function getClientId() + { return $this->getParam(OAuth2Protocol::OAuth2Protocol_ClientId); } @@ -47,7 +61,8 @@ class OAuth2AuthorizationRequest extends OAuth2Request { * One of the redirect_uri values registered * @return null|string */ - public function getRedirectUri(){ + public function getRedirectUri() + { return $this->getParam(OAuth2Protocol::OAuth2Protocol_RedirectUri); } @@ -55,7 +70,8 @@ class OAuth2AuthorizationRequest extends OAuth2Request { * Space-delimited set of permissions that the application requests. * @return null|string */ - public function getScope(){ + public function getScope() + { return $this->getParam(OAuth2Protocol::OAuth2Protocol_Scope); } @@ -66,7 +82,8 @@ class OAuth2AuthorizationRequest extends OAuth2Request { * cross-site-request-forgery mitigations. * @return null|string */ - public function getState(){ + public function getState() + { return $this->getParam(OAuth2Protocol::OAuth2Protocol_State); } @@ -77,10 +94,14 @@ class OAuth2AuthorizationRequest extends OAuth2Request { * previously gave consent to your application for a given set of scopes. * @return null|string */ - public function getApprovalPrompt(){ + public function getApprovalPrompt() + { $approval = $this->getParam(OAuth2Protocol::OAuth2Protocol_Approval_Prompt); - if(is_null($approval)) + if (is_null($approval)) + { $approval = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto; + } + return $approval; } @@ -91,10 +112,14 @@ class OAuth2AuthorizationRequest extends OAuth2Request { * token the first time your application exchanges an authorization code for a user. * @return null|string */ - public function getAccessType(){ + public function getAccessType() + { $access_type = $this->getParam(OAuth2Protocol::OAuth2Protocol_AccessType); - if(is_null($access_type)) + if (is_null($access_type)) + { $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online; + } + return $access_type; } @@ -104,19 +129,36 @@ class OAuth2AuthorizationRequest extends OAuth2Request { */ public function isValid() { - if(is_null($this->getResponseType())) - return false; - - if(is_null($this->getClientId())) - return false; - - if(is_null($this->getRedirectUri())) - return false; - //approval_prompt - $valid_approvals = array(OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force); - if(!in_array($this->getApprovalPrompt(),$valid_approvals)){ + if (is_null($this->getResponseType())) + { return false; } + + if(!OAuth2Protocol::responseTypeBelongsToFlow($this->getResponseType(false), 'all')) + return false; + + if (is_null($this->getClientId())) + { + return false; + } + + if (is_null($this->getRedirectUri())) + { + return false; + } + + //approval_prompt + $valid_approvals = array + ( + OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, + OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force + ); + + if (!in_array($this->getApprovalPrompt(), $valid_approvals)) + { + return false; + } + return true; } } diff --git a/app/libs/oauth2/requests/OAuth2LogoutRequest.php b/app/libs/oauth2/requests/OAuth2LogoutRequest.php new file mode 100644 index 00000000..cb6641cd --- /dev/null +++ b/app/libs/oauth2/requests/OAuth2LogoutRequest.php @@ -0,0 +1,74 @@ +getIdTokenHint(); + if(empty($id_token_hint)) return false; + $log_out_uri = $this->getPostLogoutRedirectUri(); + $state = $this->getState(); + if(!empty($log_out_uri)) + { + return !empty($state); + } + return true; + } + + /** + * @return string|null + */ + public function getIdTokenHint() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_IDTokenHint); + } + + /** + * @return string|null + */ + public function getPostLogoutRedirectUri() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_PostLogoutRedirectUri); + } + + /** + * @return string|null + */ + public function getState() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_State); + } +} \ No newline at end of file diff --git a/app/libs/oauth2/requests/OAuth2Request.php b/app/libs/oauth2/requests/OAuth2Request.php index c36896f7..6ed6dbe6 100644 --- a/app/libs/oauth2/requests/OAuth2Request.php +++ b/app/libs/oauth2/requests/OAuth2Request.php @@ -3,29 +3,66 @@ namespace oauth2\requests; use oauth2\OAuth2Message; -abstract class OAuth2Request extends OAuth2Message { +/** + * Class OAuth2Request + * @package oauth2\requests + */ +abstract class OAuth2Request { + /** + * @var OAuth2Message + */ protected $message; + /** + * @param OAuth2Message $msg + */ public function __construct(OAuth2Message $msg) { $this->message = $msg; } + /** + * @return OAuth2Message + */ public function getMessage(){ return $this->message; } + /** + * @param string $param + * @return null + */ public function getParam($param) { - return $this->message->getParam($param); + $value = $this->message->getParam($param); + if(!empty($value)) $value = urldecode($value); + return $value; } + /** + * @param string $param + * @param mixed $value + * @return $this + */ + public function setParam($param, $value) + { + $this->message->setParam($param, $value); + return $this; + } + + /** + * @return string + */ public function toString() { $string = $this->message->toString(); return $string; } + /** + * @return bool + */ public abstract function isValid(); + } \ No newline at end of file diff --git a/app/libs/oauth2/requests/OAuth2RequestMemento.php b/app/libs/oauth2/requests/OAuth2RequestMemento.php new file mode 100644 index 00000000..b0f6ac9a --- /dev/null +++ b/app/libs/oauth2/requests/OAuth2RequestMemento.php @@ -0,0 +1,65 @@ +state = $state; + } + + /** + * @return array + */ + public function getState() + { + return $this->state; + } + + /** + * @param OAuth2Message $request + * @return OAuth2RequestMemento + */ + static public function buildFromRequest(OAuth2Message $request) + { + $r = new ReflectionObject($request); + $p = $r->getProperty('container'); + $p->setAccessible(true); + return new self($p->getValue($request)); + } + + /** + * @param array $state + * @return OAuth2RequestMemento + */ + static public function buildFromState(array $state){ + return new self($state); + } +} \ No newline at end of file diff --git a/app/libs/oauth2/requests/OAuth2TokenRequest.php b/app/libs/oauth2/requests/OAuth2TokenRequest.php index 015b59a7..ecb36b55 100644 --- a/app/libs/oauth2/requests/OAuth2TokenRequest.php +++ b/app/libs/oauth2/requests/OAuth2TokenRequest.php @@ -10,7 +10,8 @@ use oauth2\OAuth2Protocol; * Base Token Request * @package oauth2\requests */ -class OAuth2TokenRequest extends OAuth2Request { +class OAuth2TokenRequest extends OAuth2Request +{ public function __construct(OAuth2Message $msg) { @@ -27,8 +28,8 @@ class OAuth2TokenRequest extends OAuth2Request { return true; } - public function getGrantType(){ - + public function getGrantType() + { return $this->getParam(OAuth2Protocol::OAuth2Protocol_GrantType); } } \ No newline at end of file diff --git a/app/libs/oauth2/resource_server/IUserService.php b/app/libs/oauth2/resource_server/IUserService.php index 640c80e0..09977d04 100644 --- a/app/libs/oauth2/resource_server/IUserService.php +++ b/app/libs/oauth2/resource_server/IUserService.php @@ -2,11 +2,14 @@ namespace oauth2\resource_server; +use jwt\impl\JWTClaimSet; + /** * Interface IUserService * @package oauth2\resource_server */ -interface IUserService { +interface IUserService +{ /** * This scope value requests access to the End-User's default profile Claims, which are: * name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, @@ -23,4 +26,9 @@ interface IUserService { const UserProfileScope_Address = 'address'; public function getCurrentUserInfo(); + + /** + * @return JWTClaimSet + */ + public function getCurrentUserInfoClaims(); } \ No newline at end of file diff --git a/app/libs/oauth2/resource_server/OAuth2ProtectedService.php b/app/libs/oauth2/resource_server/OAuth2ProtectedService.php index 41c852e0..2aa51300 100644 --- a/app/libs/oauth2/resource_server/OAuth2ProtectedService.php +++ b/app/libs/oauth2/resource_server/OAuth2ProtectedService.php @@ -10,9 +10,16 @@ use utils\services\ILogService; * Base Class for OAUTH2 protected endpoints * @package oauth2\resource_server */ -abstract class OAuth2ProtectedService { +abstract class OAuth2ProtectedService +{ + /** + * @var IResourceServerContext + */ protected $resource_server_context; + /** + * @var ILogService + */ protected $log_service; /** @@ -21,7 +28,7 @@ abstract class OAuth2ProtectedService { */ public function __construct(IResourceServerContext $resource_server_context, ILogService $log_service) { - $this->log_service = $log_service; + $this->log_service = $log_service; $this->resource_server_context = $resource_server_context; } } \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2AccessTokenFragmentResponse.php b/app/libs/oauth2/responses/OAuth2AccessTokenFragmentResponse.php index 85e588dd..d8c694aa 100644 --- a/app/libs/oauth2/responses/OAuth2AccessTokenFragmentResponse.php +++ b/app/libs/oauth2/responses/OAuth2AccessTokenFragmentResponse.php @@ -4,18 +4,33 @@ namespace oauth2\responses; use oauth2\OAuth2Protocol; -class OAuth2AccessTokenFragmentResponse extends OAuth2IndirectFragmentResponse { +/** + * Class OAuth2AccessTokenFragmentResponse + * @package oauth2\responses + */ +class OAuth2AccessTokenFragmentResponse extends OAuth2IndirectFragmentResponse +{ - public function __construct($return_to, $access_token, $expires_in, $scope=null, $state=null) + /** + * @param $return_to + * @param $access_token + * @param $expires_in + * @param null $scope + * @param null $state + */ + public function __construct($return_to, $access_token, $expires_in, $scope = null, $state = null) { parent::__construct(); $this->setReturnTo($return_to); - $this[OAuth2Protocol::OAuth2Protocol_AccessToken] = $access_token; - $this[OAuth2Protocol::OAuth2Protocol_AccessToken_ExpiresIn] = $expires_in; - $this[OAuth2Protocol::OAuth2Protocol_TokenType] = 'Bearer'; + if(!is_null($access_token) && !empty($access_token)) + { + $this[OAuth2Protocol::OAuth2Protocol_AccessToken] = $access_token; + $this[OAuth2Protocol::OAuth2Protocol_AccessToken_ExpiresIn] = $expires_in; + $this[OAuth2Protocol::OAuth2Protocol_TokenType] = 'Bearer'; + } if(!is_null($scope) && !empty($scope)) $this[OAuth2Protocol::OAuth2Protocol_Scope] = $scope; diff --git a/app/libs/oauth2/responses/OAuth2AccessTokenResponse.php b/app/libs/oauth2/responses/OAuth2AccessTokenResponse.php index 64f7707f..03b8d7d6 100644 --- a/app/libs/oauth2/responses/OAuth2AccessTokenResponse.php +++ b/app/libs/oauth2/responses/OAuth2AccessTokenResponse.php @@ -3,6 +3,7 @@ namespace oauth2\responses; use oauth2\OAuth2Protocol; +use utils\http\HttpContentType; /** @@ -10,17 +11,27 @@ use oauth2\OAuth2Protocol; * http://tools.ietf.org/html/rfc6749#section-4.1.4 * @package oauth2\responses */ -class OAuth2AccessTokenResponse extends OAuth2DirectResponse { +class OAuth2AccessTokenResponse extends OAuth2DirectResponse +{ - public function __construct($access_token, $expires_in, $refresh_token=null, $scope=null) + /** + * @param string $access_token + * @param string $expires_in + * @param null|string $refresh_token + * @param null|string $scope + */ + public function __construct($access_token = null, $expires_in = null, $refresh_token = null, $scope = null) { // Successful Responses: A server receiving a valid request MUST send a // response with an HTTP status code of 200. - parent::__construct(self::HttpOkResponse, self::DirectResponseContentType); + parent::__construct(self::HttpOkResponse, HttpContentType::Json); - $this[OAuth2Protocol::OAuth2Protocol_AccessToken] = $access_token; - $this[OAuth2Protocol::OAuth2Protocol_AccessToken_ExpiresIn] = $expires_in; - $this[OAuth2Protocol::OAuth2Protocol_TokenType] = 'Bearer'; + if(!empty($access_token)) + { + $this[OAuth2Protocol::OAuth2Protocol_AccessToken] = $access_token; + $this[OAuth2Protocol::OAuth2Protocol_AccessToken_ExpiresIn] = $expires_in; + $this[OAuth2Protocol::OAuth2Protocol_TokenType] = 'Bearer'; + } if(!is_null($refresh_token) && !empty($refresh_token)) $this[OAuth2Protocol::OAuth2Protocol_RefreshToken] = $refresh_token; diff --git a/app/libs/oauth2/responses/OAuth2AccessTokenValidationResponse.php b/app/libs/oauth2/responses/OAuth2AccessTokenValidationResponse.php index 1a542ff2..2382dbeb 100644 --- a/app/libs/oauth2/responses/OAuth2AccessTokenValidationResponse.php +++ b/app/libs/oauth2/responses/OAuth2AccessTokenValidationResponse.php @@ -2,39 +2,54 @@ namespace oauth2\responses; +use oauth2\models\IClient; use oauth2\OAuth2Protocol; +use openid\model\IOpenIdUser; +use utils\http\HttpContentType; +/** + * Class OAuth2AccessTokenValidationResponse + * @package oauth2\responses + */ class OAuth2AccessTokenValidationResponse extends OAuth2DirectResponse { /** * @param array|int $access_token * @param string $scope * @param $audience - * @param $client_id + * @param IClient $client * @param $expires_in - * @param null $user_id - * @param null $application_type + * @param IOpenIdUser|null $user * @param array $allowed_urls * @param array $allowed_origins */ - public function __construct($access_token,$scope, $audience, $client_id, $expires_in, $user_id = null, $application_type = null, $allowed_urls = array(), $allowed_origins = array()) + public function __construct + ( + $access_token, + $scope, + $audience, + IClient $client, + $expires_in, + IOpenIdUser $user = null, + $allowed_urls = array(), + $allowed_origins = array() + ) { // Successful Responses: A server receiving a valid request MUST send a // response with an HTTP status code of 200. - parent::__construct(self::HttpOkResponse, self::DirectResponseContentType); + parent::__construct(self::HttpOkResponse, HttpContentType::Json); $this[OAuth2Protocol::OAuth2Protocol_AccessToken] = $access_token; - $this[OAuth2Protocol::OAuth2Protocol_ClientId] = $client_id; + $this[OAuth2Protocol::OAuth2Protocol_ClientId] = $client->getClientId(); + $this['application_type'] = $client->getApplicationType(); $this[OAuth2Protocol::OAuth2Protocol_TokenType] = 'Bearer'; $this[OAuth2Protocol::OAuth2Protocol_Scope] = $scope; $this[OAuth2Protocol::OAuth2Protocol_Audience] = $audience; $this[OAuth2Protocol::OAuth2Protocol_AccessToken_ExpiresIn] = $expires_in; - if(!is_null($user_id)){ - $this[OAuth2Protocol::OAuth2Protocol_UserId] = $user_id; - } - - if(!is_null($application_type)){ - $this['application_type'] = $application_type; + if(!is_null($user)) + { + $this[OAuth2Protocol::OAuth2Protocol_UserId] = $user->getId(); + $this['user_external_id'] = $user->getExternalIdentifier(); } if(count($allowed_urls)){ diff --git a/app/libs/oauth2/responses/OAuth2AuthorizationResponse.php b/app/libs/oauth2/responses/OAuth2AuthorizationResponse.php index f693c07a..ee1150ab 100644 --- a/app/libs/oauth2/responses/OAuth2AuthorizationResponse.php +++ b/app/libs/oauth2/responses/OAuth2AuthorizationResponse.php @@ -9,37 +9,51 @@ use oauth2\OAuth2Protocol; * http://tools.ietf.org/html/rfc6749#section-4.1.2 * @package oauth2\responses */ -class OAuth2AuthorizationResponse extends OAuth2IndirectResponse { +class OAuth2AuthorizationResponse extends OAuth2IndirectResponse +{ /** * @param $return_url * @param $code * @param null $scope * @param null $state + * @param null $session_state */ - public function __construct($return_url, $code, $scope=null, $state=null) + public function __construct($return_url, $code, $scope = null, $state = null, $session_state = null) { parent::__construct(); $this[OAuth2Protocol::OAuth2Protocol_ResponseType_Code] = $code; $this->setReturnTo($return_url); - if(!is_null($scope)) + if(!empty($scope)) $this[OAuth2Protocol::OAuth2Protocol_Scope] = $scope; - if(!is_null($state)) + if(!empty($state)) $this[OAuth2Protocol::OAuth2Protocol_State] = $state; + + if(!empty($session_state)) + $this[OAuth2Protocol::OAuth2Protocol_Session_State] = $session_state; } - public function getAuthCode(){ + public function getAuthCode() + { return isset($this[OAuth2Protocol::OAuth2Protocol_ResponseType_Code])?$this[OAuth2Protocol::OAuth2Protocol_ResponseType_Code] :null; } - public function getState(){ + public function getState() + { return isset($this[OAuth2Protocol::OAuth2Protocol_State])?$this[OAuth2Protocol::OAuth2Protocol_State] :null; } - public function getScope(){ + public function getScope() + { return isset($this[OAuth2Protocol::OAuth2Protocol_Scope])?$this[OAuth2Protocol::OAuth2Protocol_Scope] :null; } + + public function getSessionState() + { + return isset($this[OAuth2Protocol::OAuth2Protocol_Session_State])?$this[OAuth2Protocol::OAuth2Protocol_Session_State] :null; + } + } \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2DirectErrorResponse.php b/app/libs/oauth2/responses/OAuth2DirectErrorResponse.php index 79cffda5..b7a0a45d 100644 --- a/app/libs/oauth2/responses/OAuth2DirectErrorResponse.php +++ b/app/libs/oauth2/responses/OAuth2DirectErrorResponse.php @@ -3,13 +3,58 @@ namespace oauth2\responses; use oauth2\OAuth2Protocol; +use utils\http\HttpContentType; -class OAuth2DirectErrorResponse extends OAuth2DirectResponse { - public function __construct($error) +/** + * Class OAuth2DirectErrorResponse + * @package oauth2\responses + */ +class OAuth2DirectErrorResponse extends OAuth2DirectResponse +{ + + /** + * @param string $error + * @param null|string $error_description + * @param null|string $state + */ + public function __construct($error, $error_description = null, $state = null) { // Error Response: A server receiving an invalid request MUST send a // response with an HTTP status code of 400. - parent::__construct(self::HttpErrorResponse, self::DirectResponseContentType); + parent::__construct(self::HttpErrorResponse, HttpContentType::Json); + $this->setError($error); + + if(!empty ($error_description)) + $this->setErrorDescription($error_description); + + if(!empty($state)) + $this->setState($state); + } + + /** + * @param string $error + */ + public function setError($error) + { $this[OAuth2Protocol::OAuth2Protocol_Error] = $error; + return $this; + } + + /** + * @param string $state + */ + public function setState($state) + { + $this[OAuth2Protocol::OAuth2Protocol_State] = $state; + return $this; + } + + /** + * @param string $error_description + */ + public function setErrorDescription($error_description) + { + $this[OAuth2Protocol::OAuth2Protocol_ErrorDescription] = $error_description; + return $this; } } \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2DirectResponse.php b/app/libs/oauth2/responses/OAuth2DirectResponse.php index 86521f07..52db994f 100644 --- a/app/libs/oauth2/responses/OAuth2DirectResponse.php +++ b/app/libs/oauth2/responses/OAuth2DirectResponse.php @@ -2,12 +2,14 @@ namespace oauth2\responses; -class OAuth2DirectResponse extends OAuth2Response { +use utils\http\HttpContentType; + +class OAuth2DirectResponse extends OAuth2Response +{ - const DirectResponseContentType = "application/json;charset=UTF-8"; const OAuth2DirectResponse = 'OAuth2DirectResponse'; - public function __construct($http_code=self::HttpOkResponse, $content_type=self::DirectResponseContentType) + public function __construct($http_code = self::HttpOkResponse, $content_type = HttpContentType::Json) { // Successful Responses: A server receiving a valid request MUST send a // response with an HTTP status code of 200. diff --git a/app/libs/oauth2/responses/OAuth2HybridTokenFragmentResponse.php b/app/libs/oauth2/responses/OAuth2HybridTokenFragmentResponse.php new file mode 100644 index 00000000..163de476 --- /dev/null +++ b/app/libs/oauth2/responses/OAuth2HybridTokenFragmentResponse.php @@ -0,0 +1,55 @@ +return_to = $return_to; + /** + * @param string $error + * @param string $error_description + * @param null|string $return_to + * @param null|string $state + */ + public function __construct($error, $error_description, $return_to = null, $state = null) + { + parent::__construct(); + + if(!empty($state)) + $this[OAuth2Protocol::OAuth2Protocol_State] = $state; + + if(!empty($error_description)) + $this->setErrorDescription($error_description); + + $this->setHttpCode(HttpResponse::HttpErrorResponse); + + $this->setError($error); + + $this->setReturnTo($return_to); } - public function setError($error){ + /** + * @param string $error + */ + public function setError($error) + { $this[OAuth2Protocol::OAuth2Protocol_Error] = $error; } + + /** + * @param string $state + */ + public function setState($state) + { + $this[OAuth2Protocol::OAuth2Protocol_State] = $state; + } + + /** + * @param string $error_description + */ + public function setErrorDescription($error_description) + { + $this[OAuth2Protocol::OAuth2Protocol_ErrorDescription] = $error_description; + } } \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2IndirectFragmentErrorResponse.php b/app/libs/oauth2/responses/OAuth2IndirectFragmentErrorResponse.php index aef2df14..00a7d487 100644 --- a/app/libs/oauth2/responses/OAuth2IndirectFragmentErrorResponse.php +++ b/app/libs/oauth2/responses/OAuth2IndirectFragmentErrorResponse.php @@ -4,16 +4,42 @@ namespace oauth2\responses; use oauth2\OAuth2Protocol; -class OAuth2IndirectFragmentErrorResponse extends OAuth2IndirectFragmentResponse { +/** + * Class OAuth2IndirectFragmentErrorResponse + * @package oauth2\responses + */ +class OAuth2IndirectFragmentErrorResponse extends OAuth2IndirectFragmentResponse +{ - public function __construct($error, $return_to=null){ + /** + * @param string $error + * @param string $error_description + * @param null|string $return_to + * @param null|string $state + */ + public function __construct($error, $error_description, $return_to = null, $state = null) + { parent::__construct(); + + if(!empty($state)) + $this[OAuth2Protocol::OAuth2Protocol_State] = $state; + + if(!empty($error_description)) + $this->setErrorDescription($error_description); + $this->setError($error); + $this->setReturnTo($return_to); } - public function setError($error){ + public function setError($error) + { $this[OAuth2Protocol::OAuth2Protocol_Error] = $error; } + public function setErrorDescription($error_description) + { + $this[OAuth2Protocol::OAuth2Protocol_ErrorDescription] = $error_description; + } + } \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2IndirectFragmentResponse.php b/app/libs/oauth2/responses/OAuth2IndirectFragmentResponse.php index 88148ea7..00938965 100644 --- a/app/libs/oauth2/responses/OAuth2IndirectFragmentResponse.php +++ b/app/libs/oauth2/responses/OAuth2IndirectFragmentResponse.php @@ -2,11 +2,13 @@ namespace oauth2\responses; -class OAuth2IndirectFragmentResponse extends OAuth2IndirectResponse { +class OAuth2IndirectFragmentResponse extends OAuth2IndirectResponse +{ const OAuth2IndirectFragmentResponse ='OAuth2IndirectFragmentResponse'; - public function __construct(){ + public function __construct() + { parent::__construct(); } diff --git a/app/libs/oauth2/responses/OAuth2IndirectResponse.php b/app/libs/oauth2/responses/OAuth2IndirectResponse.php index 5c7141a6..7448cc52 100644 --- a/app/libs/oauth2/responses/OAuth2IndirectResponse.php +++ b/app/libs/oauth2/responses/OAuth2IndirectResponse.php @@ -2,19 +2,27 @@ namespace oauth2\responses; -abstract class OAuth2IndirectResponse extends OAuth2Response { +use utils\http\HttpContentType; +/** + * Class OAuth2IndirectResponse + * @package oauth2\responses + */ +abstract class OAuth2IndirectResponse extends OAuth2Response +{ + + /** + * @var string + */ protected $return_to; - const IndirectResponseContentType = "application/x-www-form-urlencoded"; - const OAuth2IndirectResponse ='OAuth2IndirectResponse'; + const OAuth2IndirectResponse = "OAuth2IndirectResponse"; public function __construct() { // Successful Responses: A server receiving a valid request MUST send a // response with an HTTP status code of 200. - parent::__construct(self::HttpOkResponse, self::IndirectResponseContentType); - + parent::__construct(self::HttpOkResponse, HttpContentType::Form); } public function getType() @@ -22,21 +30,26 @@ abstract class OAuth2IndirectResponse extends OAuth2Response { return self::OAuth2IndirectResponse; } - public function setReturnTo($return_to){ + public function setReturnTo($return_to) + { $this->return_to = $return_to; } - public function getReturnTo(){ + public function getReturnTo() + { return $this->return_to; } public function getContent() { $url_encoded_format = ""; - if ($this->container !== null) { + if ($this->container !== null) + { ksort($this->container); - foreach ($this->container as $key => $value) { - if (is_array($value)) { + foreach ($this->container as $key => $value) + { + if (is_array($value)) + { list($key, $value) = array($value[0], $value[1]); } $value = urlencode($value); @@ -49,6 +62,6 @@ abstract class OAuth2IndirectResponse extends OAuth2Response { public function getContentType() { - return self::IndirectResponseContentType; + return HttpContentType::Form; } } \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2LogoutResponse.php b/app/libs/oauth2/responses/OAuth2LogoutResponse.php new file mode 100644 index 00000000..364ec707 --- /dev/null +++ b/app/libs/oauth2/responses/OAuth2LogoutResponse.php @@ -0,0 +1,39 @@ +setReturnTo($return_to); + + if(!is_null($state) && !empty($state)) + $this[OAuth2Protocol::OAuth2Protocol_State] = $state; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2PostResponse.php b/app/libs/oauth2/responses/OAuth2PostResponse.php new file mode 100644 index 00000000..d8bc0579 --- /dev/null +++ b/app/libs/oauth2/responses/OAuth2PostResponse.php @@ -0,0 +1,103 @@ +return_to; + $fields = ''; + if ($this->container !== null) + { + ksort($this->container); + foreach ($this->container as $key => $value) + { + if (is_array($value)) + { + list($key, $value) = array($value[0], $value[1]); + } + + $fields .= ''; + } + } + $content = << + Submit This Form + +
+ $fields +
+ + +HTML; + return $content; + + } + + public function getType() + { + return self::OAuth2PostResponse; + } + + /** + * @param string $return_to + */ + public function setReturnTo($return_to) + { + $this->return_to = $return_to; + } + + /** + * @return string + */ + public function getReturnTo() + { + return $this->return_to; + } + + public function getContentType() + { + return HttpContentType::Html; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2TokenRevocationResponse.php b/app/libs/oauth2/responses/OAuth2TokenRevocationResponse.php index f9b6f2e3..9fbb9437 100644 --- a/app/libs/oauth2/responses/OAuth2TokenRevocationResponse.php +++ b/app/libs/oauth2/responses/OAuth2TokenRevocationResponse.php @@ -2,6 +2,8 @@ namespace oauth2\responses; +use utils\http\HttpContentType; + /** * Class OAuth2TokenRevocationResponse * http://tools.ietf.org/html/rfc7009#section-2.2 @@ -24,6 +26,6 @@ class OAuth2TokenRevocationResponse extends OAuth2DirectResponse { { // Successful Responses: A server receiving a valid request MUST send a // response with an HTTP status code of 200. - parent::__construct(self::HttpOkResponse, self::DirectResponseContentType); + parent::__construct(self::HttpOkResponse, HttpContentType::Json); } } \ No newline at end of file diff --git a/app/libs/oauth2/responses/OAuth2WWWAuthenticateErrorResponse.php b/app/libs/oauth2/responses/OAuth2WWWAuthenticateErrorResponse.php index 48d8d5aa..628e1848 100644 --- a/app/libs/oauth2/responses/OAuth2WWWAuthenticateErrorResponse.php +++ b/app/libs/oauth2/responses/OAuth2WWWAuthenticateErrorResponse.php @@ -2,6 +2,8 @@ namespace oauth2\responses; +use utils\http\HttpContentType; + /** * Class OAuth2WWWAuthenticateErrorResponse * http://tools.ietf.org/html/rfc6750#section-3 @@ -16,7 +18,7 @@ class OAuth2WWWAuthenticateErrorResponse extends OAuth2DirectResponse { private $http_error; public function __construct($realm, $error, $error_description, $scope, $http_error){ - parent::__construct($http_error, self::DirectResponseContentType); + parent::__construct($http_error, HttpContentType::Json); $this->realm = $realm; $this->error = $error; $this->error_description = $error_description; diff --git a/app/libs/oauth2/services/IAllowedOriginService.php b/app/libs/oauth2/services/IAllowedOriginService.php deleted file mode 100644 index 6c6094bb..00000000 --- a/app/libs/oauth2/services/IAllowedOriginService.php +++ /dev/null @@ -1,17 +0,0 @@ -token_endpoint_url = $token_endpoint_url; + return $this; + } + + /** + * @param ClientAuthenticationContext $context + * @param JsonValue $kid + * @throws InvalidClientAuthenticationContextException + * @return IJWK + */ + abstract protected function getKey(ClientAuthenticationContext $context, JsonValue $kid = null); + + /** + * @param ClientAuthenticationContext $context + * @return bool + */ + public function validate(ClientAuthenticationContext $context) + { + if(!($context instanceof ClientAssertionAuthenticationContext)) + throw new InvalidClientAuthenticationContextException; + + if(empty($this->token_endpoint_url)) + throw new InvalidClientAuthenticationContextException('token_endpoint_url not set!'); + + if( $context->getClient()->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType()) + return false; + + $jws = $context->getAssertion(); + + $client = $context->getClient(); + + $original_alg = $client->getTokenEndpointAuthInfo()->getSigningAlgorithm()->getName(); + + if($original_alg !== $jws->getJOSEHeader()->getAlgorithm()->getString()) + return false; + + $key = $this->getKey + ( + $context, + $jws->getJOSEHeader()->getKeyID() + ); + + if(is_null($key)) + return false; + + $verified = $jws->setKey($key)->verify($original_alg); + + if(!$verified) return false; + + $iss = $jws->getClaimSet()->getIssuer()->getString(); + $aud = $jws->getClaimSet()->getAudience()->getString(); + $sub = $jws->getClaimSet()->getSubject()->getString(); + $jti = $jws->getClaimSet()->getJWTID(); + + //todo: prevent reuse of the token + if(empty($jti)) return false; + + if($iss !== $sub) + throw new InvalidClientAuthenticationContextException('iss not match with sub!'); + + if($aud !== $this->token_endpoint_url) + return false; + + if($jws->isExpired(180)) return false; + + return true; + + } +} \ No newline at end of file diff --git a/app/libs/oauth2/strategies/ClientAuthContextValidatorFactory.php b/app/libs/oauth2/strategies/ClientAuthContextValidatorFactory.php new file mode 100644 index 00000000..fd9c7692 --- /dev/null +++ b/app/libs/oauth2/strategies/ClientAuthContextValidatorFactory.php @@ -0,0 +1,86 @@ +getAuthType()) + { + case OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic: + case OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost: + { + return new ClientPlainCredentialsAuthContextValidator; + } + break; + case OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretJwt: + { + $validator = new ClientSharedSecretAssertionAuthContextValidator; + $validator->setTokenEndpointUrl(self::$token_endpoint_url); + return $validator; + } + break; + case OAuth2Protocol::TokenEndpoint_AuthMethod_PrivateKeyJwt: + { + $validator = new ClientPrivateKeyAssertionAuthContextValidator; + $validator->setTokenEndpointUrl(self::$token_endpoint_url); + $validator->setJWKSetReader(self::$jwks_reader ); + return $validator; + } + break; + } + throw new InvalidTokenEndpointAuthMethodException($context->getAuthType()); + } +} \ No newline at end of file diff --git a/app/libs/oauth2/strategies/ClientPlainCredentialsAuthContextValidator.php b/app/libs/oauth2/strategies/ClientPlainCredentialsAuthContextValidator.php new file mode 100644 index 00000000..79a5b0f0 --- /dev/null +++ b/app/libs/oauth2/strategies/ClientPlainCredentialsAuthContextValidator.php @@ -0,0 +1,53 @@ +getClient())) + throw new InvalidClientAuthenticationContextException('client not set!'); + + if($context->getClient()->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType()) + throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s', $context->getAuthType())); + + if($context->getClient()->getClientType() !== IClient::ClientType_Confidential) + throw new InvalidClientCredentials(sprintf('invalid client type %s', $context->getClient()->getClientType())); + + + return $context->getClient()->getClientId() === $context->getId() && + $context->getClient()->getClientSecret() === $context->getSecret(); + } +} \ No newline at end of file diff --git a/app/libs/oauth2/strategies/ClientPrivateKeyAssertionAuthContextValidator.php b/app/libs/oauth2/strategies/ClientPrivateKeyAssertionAuthContextValidator.php new file mode 100644 index 00000000..1756383a --- /dev/null +++ b/app/libs/oauth2/strategies/ClientPrivateKeyAssertionAuthContextValidator.php @@ -0,0 +1,108 @@ +jwks_reader = $jwks_reader; + + return $this; + } + + /** + * @param ClientAuthenticationContext $context + * @param JsonValue $kid + * @throws InvalidClientAuthenticationContextException + * @return IJWK + */ + protected function getKey(ClientAuthenticationContext $context, JsonValue $kid = null) + { + + if(!($context instanceof ClientAssertionAuthenticationContext)) + throw new InvalidClientAuthenticationContextException; + + $client = $context->getClient(); + + $jws = $context->getAssertion(); + + + if(!is_null($kid)) + { + $key = $client->getPublicKeyByIdentifier($kid->getValue()); + if($key->isActive() && !$key->isExpired()) return $key->toJWK(); + } + $alg = $jws->getJOSEHeader()->getAlgorithm()->getString(); + + $key = $client->getCurrentPublicKeyByUse + ( + JSONWebKeyPublicKeyUseValues::Signature, + $alg + ); + + if(!is_null($key)) return $key->toJWK(); + + // no public keys set, try with jwks_url ... + if (is_null($this->jwks_reader)) + { + throw new InvalidClientAuthenticationContextException('jwks_reader not set!'); + } + + $jwk_set = $this->jwks_reader->read($client); + + if(!is_null($kid)) + { + $key = $jwk_set->getKeyById($kid->getValue()); + if(!is_null($key)) return $key; + } + + foreach ($jwk_set->getKeys() as $key) + { + if + ( + $key->getKeyUse() === JSONWebKeyPublicKeyUseValues::Signature && + $key->getAlgorithm()->getString() === $alg + ) + { + return $key; + } + } + + return null; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/strategies/ClientSharedSecretAssertionAuthContextValidator.php b/app/libs/oauth2/strategies/ClientSharedSecretAssertionAuthContextValidator.php new file mode 100644 index 00000000..bc685fa9 --- /dev/null +++ b/app/libs/oauth2/strategies/ClientSharedSecretAssertionAuthContextValidator.php @@ -0,0 +1,61 @@ +getClient(); + + $jws = $context->getAssertion(); + + $key = OctetSequenceJWKFactory::build + ( + new OctetSequenceJWKSpecification + ( + $client->getClientSecret(), + $jws->getJOSEHeader()->getAlgorithm()->getString() + ) + ); + + return $key; + } +} \ No newline at end of file diff --git a/app/libs/oauth2/strategies/IClientAuthContextValidator.php b/app/libs/oauth2/strategies/IClientAuthContextValidator.php new file mode 100644 index 00000000..d71c2516 --- /dev/null +++ b/app/libs/oauth2/strategies/IClientAuthContextValidator.php @@ -0,0 +1,30 @@ +getName(); - if($class_name =='oauth2\requests\OAuth2AuthorizationRequest'){ - $response_type = $request->getResponseType(); - switch($response_type){ - case OAuth2Protocol::OAuth2Protocol_ResponseType_Token: - return new OAuth2IndirectFragmentErrorResponse($error,$return_url); - break; - case OAuth2Protocol::OAuth2Protocol_ResponseType_Code: - return new OAuth2IndirectErrorResponse($error,$return_url); - break; - default: - throw new Exception(sprintf("invalid response type %s",$response_type)); - break; + + if($request instanceof OAuth2AuthorizationRequest) + { + $response_type = $request->getResponseType(false); + + if (OAuth2Protocol::responseTypeBelongsToFlow($response_type, OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode)) + { + return new OAuth2IndirectErrorResponse + ( + $error, + $error_description, + $return_url, + $request->getState() + ); } + if + ( + OAuth2Protocol::responseTypeBelongsToFlow($response_type, OAuth2Protocol::OAuth2Protocol_GrantType_Implicit) || + OAuth2Protocol::responseTypeBelongsToFlow($response_type, OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid) + ) + { + return new OAuth2IndirectFragmentErrorResponse + ( + $error, + $error_description, + $return_url, + $request->getState() + ); + } + + throw new Exception + ( + sprintf + ( + "invalid response type %s", + $request->getResponseType() + ) + ); } - return $response; } } \ No newline at end of file diff --git a/app/libs/oauth2/strategies/OAuth2ResponseStrategyFactoryMethod.php b/app/libs/oauth2/strategies/OAuth2ResponseStrategyFactoryMethod.php index 19a419c1..fc5e9178 100644 --- a/app/libs/oauth2/strategies/OAuth2ResponseStrategyFactoryMethod.php +++ b/app/libs/oauth2/strategies/OAuth2ResponseStrategyFactoryMethod.php @@ -2,22 +2,67 @@ namespace oauth2\strategies; +use oauth2\requests\OAuth2AuthenticationRequest; +use oauth2\requests\OAuth2Request; use oauth2\responses\OAuth2DirectResponse; use oauth2\responses\OAuth2IndirectFragmentResponse; use oauth2\responses\OAuth2IndirectResponse; +use oauth2\responses\OAuth2PostResponse; use oauth2\responses\OAuth2Response; +use utils\IHttpResponseStrategy; use utils\services\ServiceLocator; +use oauth2\OAuth2Protocol; /** * Class OAuth2ResponseStrategyFactoryMethod * @package oauth2\strategies */ -final class OAuth2ResponseStrategyFactoryMethod { +final class OAuth2ResponseStrategyFactoryMethod +{ - public static function buildStrategy(OAuth2Response $response) + /** + * @param OAuth2Request $request + * @param OAuth2Response $response + * @return IHttpResponseStrategy + * @throws \Exception + */ + public static function buildStrategy(OAuth2Request $request, OAuth2Response $response) { $type = $response->getType(); - switch ($type) { + + if($request instanceof OAuth2AuthenticationRequest) + { + $response_mode = $request->getResponseMode(); + + if(is_null($response_mode)) + { + $response_mode = OAuth2Protocol::getDefaultResponseMode($request->getResponseType(false)); + } + + switch($response_mode) + { + case OAuth2Protocol::OAuth2Protocol_ResponseMode_Fragment: + $type = OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse; + break; + case OAuth2Protocol::OAuth2Protocol_ResponseMode_Query: + $type = OAuth2IndirectResponse::OAuth2IndirectResponse; + break; + case OAuth2Protocol::OAuth2Protocol_ResponseMode_FormPost: + $type = OAuth2PostResponse::OAuth2PostResponse; + break; + case OAuth2Protocol::OAuth2Protocol_ResponseMode_Direct: + $type = OAuth2DirectResponse::OAuth2DirectResponse; + break; + } + } + + switch ($type) + { + case OAuth2PostResponse::OAuth2PostResponse: + { + return ServiceLocator::getInstance()->getService(OAuth2PostResponse::OAuth2PostResponse); + } + break; case OAuth2IndirectResponse::OAuth2IndirectResponse: { return ServiceLocator::getInstance()->getService(OAuth2IndirectResponse::OAuth2IndirectResponse); @@ -29,15 +74,14 @@ final class OAuth2ResponseStrategyFactoryMethod { return ServiceLocator::getInstance()->getService(OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse); } break; - case OAuth2DirectResponse::OAuth2DirectResponse: { return ServiceLocator::getInstance()->getService(OAuth2DirectResponse::OAuth2DirectResponse); } break; default: - throw new \Exception("Invalid OAuth2 response Type"); - break; + throw new \Exception(sprintf("Invalid OAuth2 response Type %s", $type)); + break; } } } \ No newline at end of file diff --git a/app/libs/openid/OpenIdMessage.php b/app/libs/openid/OpenIdMessage.php index bd9e582e..e00f8a09 100644 --- a/app/libs/openid/OpenIdMessage.php +++ b/app/libs/openid/OpenIdMessage.php @@ -5,6 +5,7 @@ namespace openid; use openid\exceptions\InvalidOpenIdMessageMode; use openid\helpers\OpenIdErrorMessages; use utils\http\HttpMessage; +use openid\requests\OpenIdMessageMemento; /** * Class OpenIdMessage @@ -14,12 +15,14 @@ use utils\http\HttpMessage; class OpenIdMessage extends HttpMessage { - public function __construct(array $values) + /** + * @param array $values + */ + public function __construct(array $values = array()) { parent::__construct($values); } - public function getMode() { return $this->getParam(OpenIdProtocol::OpenIDProtocol_Mode); @@ -32,8 +35,12 @@ class OpenIdMessage extends HttpMessage public function getParam($param) { if (isset($this->container[OpenIdProtocol::param($param, "_")])) + { return $this->container[OpenIdProtocol::param($param, "_")]; - if (isset($this->container[OpenIdProtocol::param($param, ".")])) { + } + + if (isset($this->container[OpenIdProtocol::param($param, ".")])) + { return $this->container[OpenIdProtocol::param($param, ".")]; } return null; @@ -64,4 +71,33 @@ class OpenIdMessage extends HttpMessage throw new InvalidOpenIdMessageMode(sprintf(OpenIdErrorMessages::InvalidOpenIdMessageModeMessage, $mode)); $this->container[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Mode)] = $mode;; } + + /** + * @return OpenIdMessageMemento + */ + public function createMemento() + { + return OpenIdMessageMemento::buildFromRequest($this); + } + + /** + * @param OpenIdMessageMemento $memento + * @return $this + */ + public function setMemento(OpenIdMessageMemento $memento) + { + $this->container = $memento->getState(); + return $this; + } + + /** + * @param OpenIdMessageMemento $memento + * @return OpenIdMessage + */ + static public function buildFromMemento(OpenIdMessageMemento $memento) + { + $msg = new self; + $msg->setMemento($memento); + return $msg; + } } \ No newline at end of file diff --git a/app/libs/openid/OpenIdProtocol.php b/app/libs/openid/OpenIdProtocol.php index 21e4268e..51bfeef4 100644 --- a/app/libs/openid/OpenIdProtocol.php +++ b/app/libs/openid/OpenIdProtocol.php @@ -2,23 +2,21 @@ namespace openid; +use openid\handlers\IOpenIdAuthenticationStrategy; use openid\handlers\OpenIdAuthenticationRequestHandler; use openid\handlers\OpenIdCheckAuthenticationRequestHandler; use openid\handlers\OpenIdSessionAssociationRequestHandler; +use openid\services\IAssociationService; +use openid\services\IMementoOpenIdSerializerService; +use openid\services\INonceService; +use openid\services\IServerConfigurationService; +use openid\services\IServerExtensionsService; +use openid\services\ITrustedSitesService; use openid\XRDS\XRDSDocumentBuilder; use openid\XRDS\XRDSService; - -//services -use utils\services\ILogService; -use openid\services\IMementoOpenIdRequestService; -use openid\handlers\IOpenIdAuthenticationStrategy; -use openid\services\IServerExtensionsService; -use openid\services\IAssociationService; -use openid\services\ITrustedSitesService; -use openid\services\IServerConfigurationService; -use openid\services\INonceService; use utils\services\IAuthService; use utils\services\ICheckPointService; +use utils\services\ILogService; use utils\services\IServerConfigurationService as IUtilsServerConfigurationService; /** @@ -124,13 +122,35 @@ class OpenIdProtocol implements IOpenIdProtocol self::OpenIdProtocol_MacKey => self::OpenIdProtocol_MacKey, ); + /** + * @var OpenIdAuthenticationRequestHandler + */ private $request_handlers; + /** + * @var IServerExtensionsService + */ private $server_extension_service; + /** + * @var IServerConfigurationService + */ private $server_config_service; + /** + * @param IAuthService $auth_service + * @param IMementoOpenIdSerializerService $memento_request_service + * @param IOpenIdAuthenticationStrategy $auth_strategy + * @param IServerExtensionsService $server_extension_service + * @param IAssociationService $association_service + * @param ITrustedSitesService $trusted_sites_service + * @param IServerConfigurationService $server_config_service + * @param INonceService $nonce_service + * @param ILogService $log_service + * @param ICheckPointService $checkpoint_service + * @param IUtilsServerConfigurationService $utils_configuration_service + */ public function __construct( IAuthService $auth_service, - IMementoOpenIdRequestService $memento_request_service, + IMementoOpenIdSerializerService $memento_request_service, IOpenIdAuthenticationStrategy $auth_strategy, IServerExtensionsService $server_extension_service, IAssociationService $association_service, diff --git a/app/libs/openid/OpenIdServiceProvider.php b/app/libs/openid/OpenIdServiceProvider.php index c9a4e612..7b6229ab 100644 --- a/app/libs/openid/OpenIdServiceProvider.php +++ b/app/libs/openid/OpenIdServiceProvider.php @@ -30,9 +30,15 @@ class OpenIdServiceProvider extends ServiceProvider { $auth_extension_service = App::make('auth\\IAuthenticationExtensionService'); if(!is_null($auth_extension_service)){ - $memento_service = App::make(OpenIdServiceCatalog::MementoService); + $memento_service = App::make(OpenIdServiceCatalog::MementoSerializerService); $server_configuration_service = App::make(UtilsServiceCatalog::ServerConfigurationService); - $auth_extension_service->addExtension(new OpenIdAuthenticationExtension($memento_service,$server_configuration_service)); + + $auth_extension_service->addExtension( + new OpenIdAuthenticationExtension( + $memento_service, + $server_configuration_service + ) + ); } } diff --git a/app/libs/openid/extensions/OpenIdAuthenticationExtension.php b/app/libs/openid/extensions/OpenIdAuthenticationExtension.php index 7320f9f6..b47dacfd 100644 --- a/app/libs/openid/extensions/OpenIdAuthenticationExtension.php +++ b/app/libs/openid/extensions/OpenIdAuthenticationExtension.php @@ -6,9 +6,10 @@ use auth\exceptions\AuthenticationException; use auth\IAuthenticationExtension; use auth\User; use openid\helpers\OpenIdErrorMessages; +use openid\OpenIdMessage; use openid\requests\OpenIdAuthenticationRequest; +use openid\services\IMementoOpenIdSerializerService; use openid\services\IServerConfigurationService; -use openid\services\IMementoOpenIdRequestService; use Log; /** @@ -17,26 +18,41 @@ use Log; */ class OpenIdAuthenticationExtension implements IAuthenticationExtension { + /** + * @var IMementoOpenIdSerializerService + */ private $memento_service; + /** + * @var IServerConfigurationService + */ private $server_configuration; /** - * @param IMementoOpenIdRequestService $memento_service + * @param IMementoOpenIdSerializerService $memento_service * @param IServerConfigurationService $server_configuration */ - public function __construct(IMementoOpenIdRequestService $memento_service, IServerConfigurationService $server_configuration){ + public function __construct( + IMementoOpenIdSerializerService $memento_service, + IServerConfigurationService $server_configuration + ) + { $this->server_configuration = $server_configuration; $this->memento_service = $memento_service; } public function process(User $user) { + if(!$this->memento_service->exists()) return; + //check if we have a current openid message - $msg = $this->memento_service->getCurrentRequest(); - if (!is_null($msg) && $msg->isValid() && OpenIdAuthenticationRequest::IsOpenIdAuthenticationRequest($msg)) { + $msg = OpenIdMessage::buildFromMemento($this->memento_service->load()); + + if (!is_null($msg) && $msg->isValid() && OpenIdAuthenticationRequest::IsOpenIdAuthenticationRequest($msg)) + { //check if current user is has the same identity that the one claimed on openid message $auth_request = new OpenIdAuthenticationRequest($msg); - if (!$auth_request->isIdentitySelectByOP()) { + if (!$auth_request->isIdentitySelectByOP()) + { $claimed_id = $auth_request->getClaimedId(); $identity = $auth_request->getIdentity(); $current_identity = $this->server_configuration->getUserIdentityEndpointURL($user->getIdentifier()); @@ -44,7 +60,15 @@ class OpenIdAuthenticationExtension implements IAuthenticationExtension //if not return fail ( we cant log in with a different user that the one stated on the authentication message! if ($claimed_id !== $current_identity && $identity !== $current_identity) { Log::warning(sprintf(OpenIdErrorMessages::AlreadyExistSessionMessage, $current_identity, $identity)); - throw new AuthenticationException(sprintf(OpenIdErrorMessages::AlreadyExistSessionMessage, $current_identity, $identity)); + throw new AuthenticationException + ( + sprintf + ( + OpenIdErrorMessages::AlreadyExistSessionMessage, + $current_identity, + $identity + ) + ); } } diff --git a/app/libs/openid/handlers/OpenIdAuthenticationRequestHandler.php b/app/libs/openid/handlers/OpenIdAuthenticationRequestHandler.php index be55cca1..8b7ab4a1 100644 --- a/app/libs/openid/handlers/OpenIdAuthenticationRequestHandler.php +++ b/app/libs/openid/handlers/OpenIdAuthenticationRequestHandler.php @@ -8,6 +8,7 @@ use openid\exceptions\InvalidOpenIdAuthenticationRequestMode; use openid\exceptions\InvalidOpenIdMessageException; use openid\exceptions\OpenIdInvalidRealmException; use openid\exceptions\ReplayAttackException; +use openid\helpers\AssociationFactory; use openid\helpers\OpenIdErrorMessages; use openid\helpers\OpenIdSignatureBuilder; use openid\model\IAssociation; @@ -21,15 +22,14 @@ use openid\responses\OpenIdIndirectGenericErrorResponse; use openid\responses\OpenIdNonImmediateNegativeAssertion; use openid\responses\OpenIdPositiveAssertionResponse; use openid\services\IAssociationService; -use openid\services\IMementoOpenIdRequestService; +use openid\services\IMementoOpenIdSerializerService; use openid\services\INonceService; use openid\services\IServerConfigurationService; use openid\services\IServerExtensionsService; use openid\services\ITrustedSitesService; -use openid\helpers\AssociationFactory; use utils\services\IAuthService; -use utils\services\ILogService; use utils\services\ICheckPointService; +use utils\services\ILogService; /** * Class OpenIdAuthenticationRequestHandler @@ -40,39 +40,84 @@ use utils\services\ICheckPointService; */ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler { + /** + * @var IAuthService + */ private $auth_service; + /** + * @var IMementoOpenIdSerializerService + */ private $memento_service; + /** + * @var IOpenIdAuthenticationStrategy + */ private $auth_strategy; + /** + * @var IServerExtensionsService + */ private $server_extensions_service; + /** + * @var IAssociationService + */ private $association_service; + /** + * @var ITrustedSitesService + */ private $trusted_sites_service; + /** + * @var IServerConfigurationService + */ private $server_configuration_service; + /** + * @var + */ private $extensions; + /** + * @var + */ private $current_request_context; + /** + * @var INonceService + */ private $nonce_service; - public function __construct(IAuthService $authService, - IMementoOpenIdRequestService $memento_service, - IOpenIdAuthenticationStrategy $auth_strategy, - IServerExtensionsService $server_extensions_service, - IAssociationService $association_service, - ITrustedSitesService $trusted_sites_service, - IServerConfigurationService $server_configuration_service, - INonceService $nonce_service, - ILogService $log, - ICheckPointService $checkpoint_service, - $successor) - { - parent::__construct($successor, $log,$checkpoint_service); - $this->auth_service = $authService; - $this->memento_service = $memento_service; - $this->auth_strategy = $auth_strategy; - $this->server_extensions_service = $server_extensions_service; - $this->association_service = $association_service; - $this->trusted_sites_service = $trusted_sites_service; + /** + * @param IAuthService $authService + * @param IMementoOpenIdSerializerService $memento_service + * @param IOpenIdAuthenticationStrategy $auth_strategy + * @param IServerExtensionsService $server_extensions_service + * @param IAssociationService $association_service + * @param ITrustedSitesService $trusted_sites_service + * @param IServerConfigurationService $server_configuration_service + * @param INonceService $nonce_service + * @param ILogService $log + * @param ICheckPointService $checkpoint_service + * @param $successor + */ + public function __construct( + IAuthService $authService, + IMementoOpenIdSerializerService $memento_service, + IOpenIdAuthenticationStrategy $auth_strategy, + IServerExtensionsService $server_extensions_service, + IAssociationService $association_service, + ITrustedSitesService $trusted_sites_service, + IServerConfigurationService $server_configuration_service, + INonceService $nonce_service, + ILogService $log, + ICheckPointService $checkpoint_service, + $successor + ) { + parent::__construct($successor, $log, $checkpoint_service); + + $this->auth_service = $authService; + $this->memento_service = $memento_service; + $this->auth_strategy = $auth_strategy; + $this->server_extensions_service = $server_extensions_service; + $this->association_service = $association_service; + $this->trusted_sites_service = $trusted_sites_service; $this->server_configuration_service = $server_configuration_service; - $this->extensions = $this->server_extensions_service->getAllActiveExtensions(); - $this->nonce_service = $nonce_service; + $this->extensions = $this->server_extensions_service->getAllActiveExtensions(); + $this->nonce_service = $nonce_service; } /** @@ -83,11 +128,15 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler protected function internalHandle(OpenIdMessage $message) { $this->current_request = null; + try { - $this->current_request = new OpenIdAuthenticationRequest($message,$this->server_configuration_service->getUserIdentityEndpointURL('@identifier')); + $this->current_request = new OpenIdAuthenticationRequest( + $message, + $this->server_configuration_service->getUserIdentityEndpointURL('@identifier') + ); - if (!$this->current_request->isValid()){ + if (!$this->current_request->isValid()) { throw new InvalidOpenIdMessageException(OpenIdErrorMessages::InvalidOpenIdAuthenticationRequestMessage); } @@ -95,49 +144,61 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler $mode = $this->current_request->getMode(); switch ($mode) { - case OpenIdProtocol::SetupMode: - { + case OpenIdProtocol::SetupMode: { return $this->doSetupMode(); } break; - case OpenIdProtocol::ImmediateMode: - { + case OpenIdProtocol::ImmediateMode: { return $this->doImmediateMode(); } break; default: - throw new InvalidOpenIdAuthenticationRequestMode(sprintf(OpenIdErrorMessages::InvalidAuthenticationRequestModeMessage, $mode)); + throw new InvalidOpenIdAuthenticationRequestMode(sprintf(OpenIdErrorMessages::InvalidAuthenticationRequestModeMessage, + $mode)); break; } } catch (InvalidAssociationTypeException $inv_assoc_type) { $this->checkpoint_service->trackException($inv_assoc_type); $this->log_service->warning($inv_assoc_type); - if(!is_null($this->current_request)) - $this->log_service->error_msg("current request: ".$this->current_request->toString()); - return new OpenIdIndirectGenericErrorResponse($inv_assoc_type->getMessage(), null, null, $this->current_request); + if (!is_null($this->current_request)) { + $this->log_service->error_msg("current request: " . $this->current_request->toString()); + } + + return new OpenIdIndirectGenericErrorResponse($inv_assoc_type->getMessage(), null, null, + $this->current_request); } catch (OpenIdInvalidRealmException $inv_realm_ex) { $this->checkpoint_service->trackException($inv_realm_ex); $this->log_service->error($inv_realm_ex); - if(!is_null($this->current_request)) - $this->log_service->error_msg("current request: ".$this->current_request->toString()); - return new OpenIdIndirectGenericErrorResponse($inv_realm_ex->getMessage(), null, null, $this->current_request); + if (!is_null($this->current_request)) { + $this->log_service->error_msg("current request: " . $this->current_request->toString()); + } + + return new OpenIdIndirectGenericErrorResponse($inv_realm_ex->getMessage(), null, null, + $this->current_request); } catch (ReplayAttackException $replay_ex) { $this->checkpoint_service->trackException($replay_ex); $this->log_service->error($replay_ex); - if(!is_null($this->current_request)) - $this->log_service->error_msg("current request: ".$this->current_request->toString()); + if (!is_null($this->current_request)) { + $this->log_service->error_msg("current request: " . $this->current_request->toString()); + } + return new OpenIdIndirectGenericErrorResponse($replay_ex->getMessage(), null, null, $this->current_request); } catch (InvalidOpenIdMessageException $inv_msg_ex) { $this->checkpoint_service->trackException($inv_msg_ex); $this->log_service->error($inv_msg_ex); - if(!is_null($this->current_request)) - $this->log_service->error_msg("current request: ".$this->current_request->toString()); - return new OpenIdIndirectGenericErrorResponse($inv_msg_ex->getMessage(), null, null, $this->current_request); + if (!is_null($this->current_request)) { + $this->log_service->error_msg("current request: " . $this->current_request->toString()); + } + + return new OpenIdIndirectGenericErrorResponse($inv_msg_ex->getMessage(), null, null, + $this->current_request); } catch (Exception $ex) { $this->checkpoint_service->trackException($ex); $this->log_service->error($ex); - if(!is_null($this->current_request)) - $this->log_service->error_msg("current request: ".$this->current_request->toString()); + if (!is_null($this->current_request)) { + $this->log_service->error_msg("current request: " . $this->current_request->toString()); + } + return new OpenIdIndirectGenericErrorResponse("Server Error", null, null, $this->current_request); } } @@ -149,43 +210,49 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler private function doSetupMode() { - $authentication_response = $this->auth_service->getUserAuthenticationResponse(); - if($authentication_response == IAuthService::AuthenticationResponse_Cancel){ - //clear saved data ... - $this->memento_service->clearCurrentRequest(); - $this->auth_service->clearUserAuthenticationResponse(); - $this->auth_service->clearUserAuthorizationResponse(); - return new OpenIdNonImmediateNegativeAssertion($this->current_request->getReturnTo()); - } + $authentication_response = $this->auth_service->getUserAuthenticationResponse(); + if ($authentication_response == IAuthService::AuthenticationResponse_Cancel) { + //clear saved data ... + $this->memento_service->forget(); + $this->auth_service->clearUserAuthenticationResponse(); + $this->auth_service->clearUserAuthorizationResponse(); - if (!$this->auth_service->isUserLogged()) + return new OpenIdNonImmediateNegativeAssertion($this->current_request->getReturnTo()); + } + + if (!$this->auth_service->isUserLogged()) { return $this->doLogin(); + } //user already logged $currentUser = $this->auth_service->getCurrentUser(); if (!$this->current_request->isIdentitySelectByOP()) { - $current_claimed_id = $this->current_request->getClaimedId(); - $current_identity = $this->current_request->getIdentity(); - // check is claimed identity match with current one - // if not logs out and do re login - $current_user = $this->auth_service->getCurrentUser(); - if (is_null($current_user)) - throw new Exception("User not set!"); + $current_claimed_id = $this->current_request->getClaimedId(); + $current_identity = $this->current_request->getIdentity(); + // check is claimed identity match with current one + // if not logs out and do re login + $current_user = $this->auth_service->getCurrentUser(); + if (is_null($current_user)) { + throw new Exception("User not set!"); + } $current_owned_identity = $this->server_configuration_service->getUserIdentityEndpointURL($current_user->getIdentifier()); if ($current_claimed_id != $current_owned_identity && $current_identity != $current_owned_identity) { - $this->log_service->warning_msg(sprintf(OpenIdErrorMessages::AlreadyExistSessionMessage, $current_owned_identity, $current_identity)); + $this->log_service->warning_msg(sprintf(OpenIdErrorMessages::AlreadyExistSessionMessage, + $current_owned_identity, $current_identity)); $this->auth_service->logout(); + return $this->doLogin(); } } $authorization_response = $this->auth_service->getUserAuthorizationResponse(); - if ($authorization_response !== IAuthService::AuthorizationResponse_None) + if ($authorization_response !== IAuthService::AuthorizationResponse_None) { return $this->checkAuthorizationResponse($authorization_response); + } // $authorization_response is none ... $this->current_request_context->cleanTrustedData(); @@ -195,27 +262,34 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler } $requested_data = $this->current_request_context->getTrustedData(); - $sites = $this->trusted_sites_service->getTrustedSites($currentUser, $this->current_request->getRealm(), $requested_data); + $sites = $this->trusted_sites_service->getTrustedSites( + $currentUser, + $this->current_request->getRealm(), + $requested_data + ); + //check trusted sites - if (is_null($sites) || count($sites) == 0) + if (is_null($sites) || count($sites) == 0) { return $this->doConsentProcess(); + } //there are trusted sites ... check the former authorization decision - $site = $sites[0]; + $site = $sites[0]; $policy = $site->getAuthorizationPolicy(); switch ($policy) { - case IAuthService::AuthorizationResponse_AllowForever: - { - //save former user choice on session - $this->auth_service->setUserAuthorizationResponse($policy); - return $this->doAssertion(); - } + case IAuthService::AuthorizationResponse_AllowForever: { + //save former user choice on session + $this->auth_service->setUserAuthorizationResponse($policy); + + return $this->doAssertion(); + } break; - case IAuthService::AuthorizationResponse_DenyForever: - // black listed site - return new OpenIdIndirectGenericErrorResponse(sprintf(OpenIdErrorMessages::RealmNotAllowedByUserMessage, $site->getRealm()), null, null, $this->current_request); + case IAuthService::AuthorizationResponse_DenyForever: + // black listed site + return new OpenIdIndirectGenericErrorResponse(sprintf(OpenIdErrorMessages::RealmNotAllowedByUserMessage, + $site->getRealm()), null, null, $this->current_request); break; - default: - throw new Exception("Invalid Realm Policy"); + default: + throw new Exception("Invalid Realm Policy"); break; } } @@ -229,7 +303,9 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler foreach ($this->extensions as $ext) { $ext->parseRequest($this->current_request, $this->current_request_context); } - $this->memento_service->saveCurrentRequest(); + + $this->memento_service->serialize($this->current_request->getMessage()->createMemento()); + return $this->auth_strategy->doLogin($this->current_request, $this->current_request_context); } @@ -256,11 +332,12 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler $context->addSignParam(OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Identity)); $op_endpoint = $this->server_configuration_service->getOPEndpointURL(); - $identity = $this->server_configuration_service->getUserIdentityEndpointURL($currentUser->getIdentifier()); - $nonce = $this->nonce_service->generateNonce(); - $realm = $this->current_request->getRealm(); + $identity = $this->server_configuration_service->getUserIdentityEndpointURL($currentUser->getIdentifier()); + $nonce = $this->nonce_service->generateNonce(); + $realm = $this->current_request->getRealm(); - $response = new OpenIdPositiveAssertionResponse($op_endpoint, $identity, $identity, $this->current_request->getReturnTo(), $nonce->getRawFormat(), $realm); + $response = new OpenIdPositiveAssertionResponse($op_endpoint, $identity, $identity, + $this->current_request->getReturnTo(), $nonce->getRawFormat(), $realm); foreach ($this->extensions as $ext) { $ext->prepareResponse($this->current_request, $response, $context); @@ -270,14 +347,16 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler if (is_null($assoc_handle = $this->current_request->getAssocHandle()) || is_null($association = $this->association_service->getAssociation($assoc_handle))) { //create private association ... - $association = $this->association_service->addAssociation(AssociationFactory::getInstance()->buildPrivateAssociation($realm,$this->server_configuration_service->getConfigValue("Private.Association.Lifetime"))); + $association = $this->association_service->addAssociation(AssociationFactory::getInstance()->buildPrivateAssociation($realm, + $this->server_configuration_service->getConfigValue("Private.Association.Lifetime"))); $response->setAssocHandle($association->getHandle()); if (!empty($assoc_handle)) { $response->setInvalidateHandle($assoc_handle); } } else { - if ($association->getType() != IAssociation::TypeSession) + if ($association->getType() != IAssociation::TypeSession) { throw new InvalidAssociationTypeException(OpenIdErrorMessages::InvalidAssociationTypeMessage); + } $response->setAssocHandle($assoc_handle); } @@ -291,8 +370,9 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler */ $this->nonce_service->associateNonce($nonce, $response->getSig(), $realm); //do cleaning ... - $this->memento_service->clearCurrentRequest(); + $this->memento_service->forget(); $this->auth_service->clearUserAuthorizationResponse(); + return $response; } @@ -302,10 +382,12 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler private function doConsentProcess() { //do consent process - $this->memento_service->saveCurrentRequest(); + $this->memento_service->serialize($this->current_request->getMessage()->createMemento()); + foreach ($this->extensions as $ext) { $ext->parseRequest($this->current_request, $this->current_request_context); } + return $this->auth_strategy->doConsent($this->current_request, $this->current_request_context); } @@ -319,8 +401,7 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler // check response $currentUser = $this->auth_service->getCurrentUser(); switch ($authorization_response) { - case IAuthService::AuthorizationResponse_AllowForever: - { + case IAuthService::AuthorizationResponse_AllowForever: { $this->current_request_context->cleanTrustedData(); foreach ($this->extensions as $ext) { @@ -328,22 +409,23 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler $this->current_request_context->setTrustedData($data); } - $this->trusted_sites_service->addTrustedSite($currentUser, $this->current_request->getRealm(), IAuthService::AuthorizationResponse_AllowForever, $this->current_request_context->getTrustedData()); + $this->trusted_sites_service->addTrustedSite($currentUser, $this->current_request->getRealm(), + IAuthService::AuthorizationResponse_AllowForever, $this->current_request_context->getTrustedData()); + return $this->doAssertion(); } break; case IAuthService::AuthorizationResponse_AllowOnce: return $this->doAssertion(); break; - case IAuthService::AuthorizationResponse_DenyOnce: - { - $this->memento_service->clearCurrentRequest(); + case IAuthService::AuthorizationResponse_DenyOnce: { + $this->memento_service->forget(); $this->auth_service->clearUserAuthorizationResponse(); + return new OpenIdNonImmediateNegativeAssertion($this->current_request->getReturnTo()); } - break; - case IAuthService::AuthorizationResponse_DenyForever: - { + break; + case IAuthService::AuthorizationResponse_DenyForever: { $this->current_request_context->cleanTrustedData(); foreach ($this->extensions as $ext) { @@ -351,15 +433,16 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler $this->current_request_context->setTrustedData($data); } - $this->trusted_sites_service->addTrustedSite($currentUser, $this->current_request->getRealm(), IAuthService::AuthorizationResponse_DenyForever,$this->current_request_context->getTrustedData()); - $this->memento_service->clearCurrentRequest(); + $this->trusted_sites_service->addTrustedSite($currentUser, $this->current_request->getRealm(), + IAuthService::AuthorizationResponse_DenyForever, $this->current_request_context->getTrustedData()); + $this->memento_service->forget(); $this->auth_service->clearUserAuthorizationResponse(); return new OpenIdNonImmediateNegativeAssertion($this->current_request->getReturnTo()); } break; default: - $this->memento_service->clearCurrentRequest(); + $this->memento_service->forget(); $this->auth_service->clearUserAuthorizationResponse(); throw new \Exception("Invalid Authorization response!"); break; @@ -385,31 +468,33 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler $requested_data = $this->current_request_context->getTrustedData(); - $sites = $this->trusted_sites_service->getTrustedSites($currentUser, $this->current_request->getRealm(), $requested_data); + $sites = $this->trusted_sites_service->getTrustedSites($currentUser, $this->current_request->getRealm(), + $requested_data); if (is_null($sites) || count($sites) == 0) { //need setup to continue return new OpenIdImmediateNegativeAssertion($this->current_request->getReturnTo()); } - $site = $sites[0]; + $site = $sites[0]; $policy = $site->getAuthorizationPolicy(); switch ($policy) { - case IAuthService::AuthorizationResponse_DenyForever: - { + case IAuthService::AuthorizationResponse_DenyForever: { // black listed site by user - return new OpenIdIndirectGenericErrorResponse(sprintf(OpenIdErrorMessages::RealmNotAllowedByUserMessage, $site->getRealm()), null, null, $this->current_request); + return new OpenIdIndirectGenericErrorResponse(sprintf(OpenIdErrorMessages::RealmNotAllowedByUserMessage, + $site->getRealm()), null, null, $this->current_request); } break; - case IAuthService::AuthorizationResponse_AllowForever: - { + case IAuthService::AuthorizationResponse_AllowForever: { //save former user choice on session $this->auth_service->setUserAuthorizationResponse($policy); + return $this->doAssertion(); } - break; + break; default: - return new OpenIdIndirectGenericErrorResponse(sprintf(OpenIdErrorMessages::RealmNotAllowedByUserMessage, $this->current_request->getRealm()), null, null, $this->current_request); + return new OpenIdIndirectGenericErrorResponse(sprintf(OpenIdErrorMessages::RealmNotAllowedByUserMessage, + $this->current_request->getRealm()), null, null, $this->current_request); break; } } @@ -421,6 +506,7 @@ final class OpenIdAuthenticationRequestHandler extends OpenIdMessageHandler protected function canHandle(OpenIdMessage $message) { $res = OpenIdAuthenticationRequest::IsOpenIdAuthenticationRequest($message); + return $res; } } \ No newline at end of file diff --git a/app/libs/openid/handlers/OpenIdMessageHandler.php b/app/libs/openid/handlers/OpenIdMessageHandler.php index 8a808ccd..6b591af8 100644 --- a/app/libs/openid/handlers/OpenIdMessageHandler.php +++ b/app/libs/openid/handlers/OpenIdMessageHandler.php @@ -7,6 +7,7 @@ use openid\OpenIdMessage; use utils\services\ILogService; use utils\services\ICheckPointService; use openid\exceptions\InvalidOpenIdMessageException; +use openid\requests\OpenIdAuthenticationRequest; /** * Class OpenIdMessageHandler @@ -22,7 +23,7 @@ abstract class OpenIdMessageHandler */ protected $successor; /** - * @var OpenIdMessage + * @var OpenIdAuthenticationRequest */ protected $current_request; /** diff --git a/app/libs/openid/model/IOpenIdUser.php b/app/libs/openid/model/IOpenIdUser.php index cedda1e2..b78920e9 100644 --- a/app/libs/openid/model/IOpenIdUser.php +++ b/app/libs/openid/model/IOpenIdUser.php @@ -6,7 +6,8 @@ namespace openid\model; * Interface IOpenIdUser * @package openid\model */ -interface IOpenIdUser { +interface IOpenIdUser +{ /** * */ @@ -17,28 +18,100 @@ interface IOpenIdUser { */ public function isOpenstackIdAdmin(); + /** + * @return int + */ public function getId(); + + /** + * @return string + */ public function getIdentifier(); + + /** + * @return string + */ public function getEmail(); + + /** + * @return string + */ public function getFirstName(); + + /** + * @return string + */ public function getLastName(); + + /** + * @return string + */ public function getFullName(); + + /** + * @return string + */ public function getNickName(); + + /** + * @return string + */ public function getGender(); + + /** + * @return string + */ public function getCountry(); + + /** + * @return string + */ public function getStreetAddress(); + + /** + * @return string + */ public function getRegion(); + + /** + * @return string + */ public function getLocality(); + + /** + * @return string + */ public function getPostalCode(); + + /** + * @return string + */ + public function getFormattedAddress(); + public function getLanguage(); + public function getTimeZone(); + public function getDateOfBirth(); + public function getShowProfileFullName(); + public function getShowProfilePic(); + public function getShowProfileBio(); + public function getShowProfileEmail(); + public function getBio(); + public function getPic(); + public function getActions(); - public function getTrustedSites(); + + public function getTrustedSites(); + + /** + * @return int + */ + public function getExternalIdentifier(); } \ No newline at end of file diff --git a/app/libs/openid/requests/OpenIdAuthenticationRequest.php b/app/libs/openid/requests/OpenIdAuthenticationRequest.php index 70ecf5b9..92f2c759 100644 --- a/app/libs/openid/requests/OpenIdAuthenticationRequest.php +++ b/app/libs/openid/requests/OpenIdAuthenticationRequest.php @@ -14,81 +14,97 @@ use openid\OpenIdProtocol; class OpenIdAuthenticationRequest extends OpenIdRequest { - private $user_identity_endpoint; + /** + * @var null|string + */ + private $user_identity_endpoint; - /** - * @param OpenIdMessage $message - * @param string|null $user_identity_endpoint - * @throws InvalidOpenIdMessageException - */ - public function __construct(OpenIdMessage $message, $user_identity_endpoint = null) + /** + * @param OpenIdMessage $message + * @param string|null $user_identity_endpoint + * @throws InvalidOpenIdMessageException + */ + public function __construct(OpenIdMessage $message, $user_identity_endpoint = null) { parent::__construct($message); - $this->user_identity_endpoint = $user_identity_endpoint; - if(!empty($this->user_identity_endpoint)){ - if(!str_contains($this->user_identity_endpoint,'@identifier')){ - throw new InvalidOpenIdMessageException("user_identity_endpoint value must contain @identifier placeholder!."); - } - } + $this->user_identity_endpoint = $user_identity_endpoint; + if (!empty($this->user_identity_endpoint)) { + if (!str_contains($this->user_identity_endpoint, '@identifier')) { + throw new InvalidOpenIdMessageException("user_identity_endpoint value must contain @identifier placeholder!."); + } + } } - /** - * @param OpenIdMessage $message - * @return bool - */ - public static function IsOpenIdAuthenticationRequest(OpenIdMessage $message) + /** + * @param OpenIdMessage $message + * @return bool + */ + public static function IsOpenIdAuthenticationRequest(OpenIdMessage $message) { $mode = $message->getMode(); - if ($mode == OpenIdProtocol::ImmediateMode || $mode == OpenIdProtocol::SetupMode) return true; + if ($mode == OpenIdProtocol::ImmediateMode || $mode == OpenIdProtocol::SetupMode) { + return true; + } + return false; } - /** - * @return string - */ - public function getAssocHandle() + /** + * @return string + */ + public function getAssocHandle() { return $this->getParam(OpenIdProtocol::OpenIDProtocol_AssocHandle); } - /** - * @return bool - * @throws InvalidOpenIdMessageException - */ - public function isValid() + /** + * @return bool + * @throws InvalidOpenIdMessageException + */ + public function isValid() { - $return_to = $this->getReturnTo(); - $claimed_id = $this->getClaimedId(); - $identity = $this->getIdentity(); - $mode = $this->getMode(); - $realm = $this->getRealm(); - $valid_id = $this->isValidIdentifier($claimed_id, $identity); - $valid_realm = OpenIdUriHelper::checkRealm($realm, $return_to); + $return_to = $this->getReturnTo(); + $claimed_id = $this->getClaimedId(); + $identity = $this->getIdentity(); + $mode = $this->getMode(); + $realm = $this->getRealm(); + $valid_id = $this->isValidIdentifier($claimed_id, $identity); + $valid_realm = OpenIdUriHelper::checkRealm($realm, $return_to); - if(empty($return_to)) - throw new InvalidOpenIdMessageException('return_to is empty.'); + if (empty($return_to)) { + throw new InvalidOpenIdMessageException('return_to is empty.'); + } - if(empty($realm)) - throw new InvalidOpenIdMessageException('realm is empty.'); + if (empty($realm)) { + throw new InvalidOpenIdMessageException('realm is empty.'); + } - if(!$valid_realm) - throw new InvalidOpenIdMessageException(sprintf('realm check is not valid realm %s - return_to %s.',$realm,$return_to)); + if (!$valid_realm) { + throw new InvalidOpenIdMessageException(sprintf('realm check is not valid realm %s - return_to %s.', $realm, + $return_to)); + } - if(empty($claimed_id)) - throw new InvalidOpenIdMessageException('claimed_id is empty.'); + if (empty($claimed_id)) { + throw new InvalidOpenIdMessageException('claimed_id is empty.'); + } - if(empty($identity)) - throw new InvalidOpenIdMessageException('identity is empty.'); + if (empty($identity)) { + throw new InvalidOpenIdMessageException('identity is empty.'); + } - if(!$valid_id) - throw new InvalidOpenIdMessageException(sprintf('identity check is not valid claimed_id %s - identity %s.',$claimed_id,$identity)); + if (!$valid_id) { + throw new InvalidOpenIdMessageException(sprintf('identity check is not valid claimed_id %s - identity %s.', + $claimed_id, $identity)); + } - if(empty($mode)) - throw new InvalidOpenIdMessageException('mode is empty.'); + if (empty($mode)) { + throw new InvalidOpenIdMessageException('mode is empty.'); + } - if(!($mode == OpenIdProtocol::ImmediateMode || $mode == OpenIdProtocol::SetupMode)) - throw new InvalidOpenIdMessageException(sprintf('mode %s is invalid.',$mode)); + if (!($mode == OpenIdProtocol::ImmediateMode || $mode == OpenIdProtocol::SetupMode)) { + throw new InvalidOpenIdMessageException(sprintf('mode %s is invalid.', $mode)); + } return true; } @@ -96,6 +112,7 @@ class OpenIdAuthenticationRequest extends OpenIdRequest public function getReturnTo() { $return_to = $this->getParam(OpenIdProtocol::OpenIDProtocol_ReturnTo); + return (OpenIdUriHelper::checkReturnTo($return_to)) ? $return_to : ""; } @@ -112,25 +129,29 @@ class OpenIdAuthenticationRequest extends OpenIdRequest public function getRealm() { $realm = $this->getParam(OpenIdProtocol::OpenIDProtocol_Realm); + return $realm; } - public function isIdentitySelectByOP(){ + public function isIdentitySelectByOP() + { $claimed_id = $this->getClaimedId(); - $identity = $this->getIdentity(); + $identity = $this->getIdentity(); //http://specs.openid.net/auth/2.0/identifier_select - if ($claimed_id == $identity && $identity == OpenIdProtocol::IdentifierSelectType) + if ($claimed_id == $identity && $identity == OpenIdProtocol::IdentifierSelectType) { return true; + } + return false; } - /** - * @param $claimed_id - * @param $identity - * @return bool - * @throws \openid\exceptions\InvalidOpenIdMessageException - */ - private function isValidIdentifier($claimed_id, $identity) + /** + * @param $claimed_id + * @param $identity + * @return bool + * @throws \openid\exceptions\InvalidOpenIdMessageException + */ + private function isValidIdentifier($claimed_id, $identity) { /* * openid.claimed_id" and "openid.identity" SHALL be either both present or both absent. @@ -138,24 +159,30 @@ class OpenIdAuthenticationRequest extends OpenIdRequest * other information in its payload, using extensions. */ - if(empty($this->user_identity_endpoint)) - throw new InvalidOpenIdMessageException("user_identity_endpoint is not set."); + if (empty($this->user_identity_endpoint)) { + throw new InvalidOpenIdMessageException("user_identity_endpoint is not set."); + } - if (is_null($claimed_id) && is_null($identity)) + if (is_null($claimed_id) && is_null($identity)) { return false; + } //http://specs.openid.net/auth/2.0/identifier_select - if ($claimed_id == $identity && $identity == OpenIdProtocol::IdentifierSelectType) + if ($claimed_id == $identity && $identity == OpenIdProtocol::IdentifierSelectType) { return true; + } if (OpenIdUriHelper::isValidUrl($claimed_id) && OpenIdUriHelper::isValidUrl($identity)) { $identity_url_pattern = $this->user_identity_endpoint; $url_parts = explode("@", $identity_url_pattern, 2); $base_identity_url = $url_parts[0]; - if (strpos($identity, $base_identity_url) !== false) + if (strpos($identity, $base_identity_url) !== false) { return true; - if (strpos($claimed_id, $base_identity_url) !== false) + } + if (strpos($claimed_id, $base_identity_url) !== false) { return true; + } } + return false; } diff --git a/app/libs/openid/requests/OpenIdCheckAuthenticationRequest.php b/app/libs/openid/requests/OpenIdCheckAuthenticationRequest.php index d65ec81f..2f26cc1b 100644 --- a/app/libs/openid/requests/OpenIdCheckAuthenticationRequest.php +++ b/app/libs/openid/requests/OpenIdCheckAuthenticationRequest.php @@ -13,57 +13,61 @@ use openid\OpenIdProtocol; class OpenIdCheckAuthenticationRequest extends OpenIdAuthenticationRequest { - private $op_endpoint_url; + private $op_endpoint_url; - /** - * @param OpenIdMessage $message - * @param $op_endpoint_url - */ - public function __construct(OpenIdMessage $message, $op_endpoint_url) + /** + * @param OpenIdMessage $message + * @param $op_endpoint_url + */ + public function __construct(OpenIdMessage $message, $op_endpoint_url) { parent::__construct($message); - $this->op_endpoint_url = $op_endpoint_url; + $this->op_endpoint_url = $op_endpoint_url; } public static function IsOpenIdCheckAuthenticationRequest(OpenIdMessage $message) { $mode = $message->getMode(); - if ($mode == OpenIdProtocol::CheckAuthenticationMode) return true; + if ($mode == OpenIdProtocol::CheckAuthenticationMode) { + return true; + } + return false; } public function isValid() { - $mode = $this->getMode(); - $claimed_assoc = $this->getAssocHandle(); - $claimed_nonce = $this->getNonce(); - $claimed_sig = $this->getSig(); + $mode = $this->getMode(); + $claimed_assoc = $this->getAssocHandle(); + $claimed_nonce = $this->getNonce(); + $claimed_sig = $this->getSig(); $claimed_op_endpoint = $this->getOPEndpoint(); - $claimed_identity = $this->getClaimedId(); - $claimed_realm = $this->getRealm(); - $claimed_returnTo = $this->getReturnTo(); - $signed = $this->getSigned(); + $claimed_identity = $this->getClaimedId(); + $claimed_realm = $this->getRealm(); + $claimed_returnTo = $this->getReturnTo(); + $signed = $this->getSigned(); - $valid_realm = OpenIdUriHelper::checkRealm($claimed_realm, $claimed_returnTo); + $valid_realm = OpenIdUriHelper::checkRealm($claimed_realm, $claimed_returnTo); - $res = !is_null($mode) && !empty($mode) && $mode == OpenIdProtocol::CheckAuthenticationMode - && !is_null($claimed_returnTo) && !empty($claimed_returnTo) && OpenIdUriHelper::checkReturnTo($claimed_returnTo) - && !is_null($claimed_realm) && !empty($claimed_realm) && $valid_realm - && !is_null($claimed_assoc) && !empty($claimed_assoc) - && !is_null($claimed_sig) && !empty($claimed_sig) - && !is_null($signed) && !empty($signed) - && !is_null($claimed_nonce) && !empty($claimed_nonce) - && !is_null($claimed_op_endpoint) && !empty($claimed_op_endpoint) && $claimed_op_endpoint == $this->op_endpoint_url - && !is_null($claimed_identity) && !empty($claimed_identity) && OpenIdUriHelper::isValidUrl($claimed_identity); + $res = !is_null($mode) && !empty($mode) && $mode == OpenIdProtocol::CheckAuthenticationMode + && !is_null($claimed_returnTo) && !empty($claimed_returnTo) && OpenIdUriHelper::checkReturnTo($claimed_returnTo) + && !is_null($claimed_realm) && !empty($claimed_realm) && $valid_realm + && !is_null($claimed_assoc) && !empty($claimed_assoc) + && !is_null($claimed_sig) && !empty($claimed_sig) + && !is_null($signed) && !empty($signed) + && !is_null($claimed_nonce) && !empty($claimed_nonce) + && !is_null($claimed_op_endpoint) && !empty($claimed_op_endpoint) && $claimed_op_endpoint == $this->op_endpoint_url + && !is_null($claimed_identity) && !empty($claimed_identity) && OpenIdUriHelper::isValidUrl($claimed_identity); if (!$res) { - $msg = sprintf("return_to is empty? %b.",empty($claimed_returnTo)).PHP_EOL; - $msg = $msg.sprintf("realm is empty? %b.",empty($claimed_realm)).PHP_EOL; - $msg = $msg.sprintf("claimed_id is empty? %b.",empty($claimed_id)).PHP_EOL; - $msg = $msg.sprintf("identity is empty? %b.",empty($claimed_identity)).PHP_EOL; - $msg = $msg.sprintf("mode is empty? %b.",empty($mode)).PHP_EOL; - $msg = $msg.sprintf("is valid realm? %b.",$valid_realm).PHP_EOL; - throw new InvalidOpenIdMessageException($msg); + $msg = sprintf("return_to is empty? %b.", empty($claimed_returnTo)) . PHP_EOL; + $msg = $msg . sprintf("realm is empty? %b.", empty($claimed_realm)) . PHP_EOL; + $msg = $msg . sprintf("claimed_id is empty? %b.", empty($claimed_id)) . PHP_EOL; + $msg = $msg . sprintf("identity is empty? %b.", empty($claimed_identity)) . PHP_EOL; + $msg = $msg . sprintf("mode is empty? %b.", empty($mode)) . PHP_EOL; + $msg = $msg . sprintf("is valid realm? %b.", $valid_realm) . PHP_EOL; + throw new InvalidOpenIdMessageException($msg); } + return $res; } diff --git a/app/libs/openid/requests/OpenIdMessageMemento.php b/app/libs/openid/requests/OpenIdMessageMemento.php new file mode 100644 index 00000000..7d43b004 --- /dev/null +++ b/app/libs/openid/requests/OpenIdMessageMemento.php @@ -0,0 +1,63 @@ +state = $state; + } + + /** + * @return array + */ + public function getState(){ + return $this->state; + } + + /** + * @param OpenIdMessage $request + * @return OpenIdMessageMemento + */ + static public function buildFromRequest(OpenIdMessage $request){ + $r = new ReflectionObject($request); + $p = $r->getProperty('container'); + $p->setAccessible(true); + return new self($p->getValue($request)); + } + + /** + * @param array $state + * @return OpenIdMessageMemento + */ + static public function buildFromState(array $state){ + return new self($state); + } +} \ No newline at end of file diff --git a/app/libs/openid/requests/OpenIdRequest.php b/app/libs/openid/requests/OpenIdRequest.php index c25f65af..12920add 100644 --- a/app/libs/openid/requests/OpenIdRequest.php +++ b/app/libs/openid/requests/OpenIdRequest.php @@ -10,11 +10,14 @@ use openid\OpenIdMessage; */ abstract class OpenIdRequest { - + /** + * @var OpenIdMessage + */ protected $message; + public function __construct(OpenIdMessage $message) { - $this->message = $message; + $this->message = $message; } public function getMessage() diff --git a/app/libs/openid/responses/OpenIdDirectResponse.php b/app/libs/openid/responses/OpenIdDirectResponse.php index 73742641..5023e672 100644 --- a/app/libs/openid/responses/OpenIdDirectResponse.php +++ b/app/libs/openid/responses/OpenIdDirectResponse.php @@ -5,6 +5,7 @@ namespace openid\responses; use openid\exceptions\InvalidKVFormat; use openid\helpers\OpenIdErrorMessages; use openid\OpenIdProtocol; +use utils\http\HttpContentType; /** * Class OpenIdDirectResponse @@ -15,13 +16,12 @@ class OpenIdDirectResponse extends OpenIdResponse { const OpenIdDirectResponse = "OpenIdDirectResponse"; - const DirectResponseContentType = "text/plain"; public function __construct() { // Successful Responses: A server receiving a valid request MUST send a // response with an HTTP status code of 200. - parent::__construct(self::HttpOkResponse, self::DirectResponseContentType); + parent::__construct(self::HttpOkResponse, HttpContentType::Text); /* * This particular value MUST be present for the response to be a valid OpenID 2.0 * response. Future versions of the specification may define different values in order diff --git a/app/libs/openid/services/IMementoOpenIdRequestService.php b/app/libs/openid/services/IMementoOpenIdRequestService.php deleted file mode 100644 index 605acfa9..00000000 --- a/app/libs/openid/services/IMementoOpenIdRequestService.php +++ /dev/null @@ -1,25 +0,0 @@ -container = $values; } diff --git a/app/libs/utils/http/HttpUtils.php b/app/libs/utils/http/HttpUtils.php new file mode 100644 index 00000000..cfb77c40 --- /dev/null +++ b/app/libs/utils/http/HttpUtils.php @@ -0,0 +1,44 @@ +useSti() && get_class(new $this->stiBaseClass) !== get_class($this)) { + if ($this->useSti() && $this->stiBaseClass !== get_class($this)) { $builder->where($this->stiClassField, "=", $this->class->getShortName()); } return $builder; @@ -48,7 +48,7 @@ abstract class BaseModelEloquent extends Eloquent { public function newFromBuilder($attributes = array()) { - if ($this->useSti() && $attributes->{$this->stiClassField}) { + if ($this->useSti() && property_exists($attributes, $this->stiClassField) ) { $class = $this->class->getName(); $instance = new $class; $instance->exists = true; diff --git a/app/models/IEntity.php b/app/libs/utils/model/IEntity.php similarity index 81% rename from app/models/IEntity.php rename to app/libs/utils/model/IEntity.php index 13c222b4..bc98fedf 100644 --- a/app/models/IEntity.php +++ b/app/libs/utils/model/IEntity.php @@ -1,6 +1,6 @@ lifetime = $lifetime; $this->len = $len; } @@ -49,28 +51,32 @@ abstract class Identifier { * @param IdentifierGenerator $generator * @return $this */ - public function generate(IdentifierGenerator $generator){ + public function generate(IdentifierGenerator $generator) + { return $generator->generate($this); } /** * @return int */ - public function getLenght(){ + public function getLenght() + { return $this->len; } /** * @return int */ - public function getLifetime(){ + public function getLifetime() + { return intval($this->lifetime); } /** * @return string */ - public function getValue(){ + public function getValue() + { return $this->value; } @@ -78,7 +84,8 @@ abstract class Identifier { * @param string $value * @return $this */ - public function setValue($value){ + public function setValue($value) + { $this->value = $value; return $this; } diff --git a/app/libs/utils/services/IAuthService.php b/app/libs/utils/services/IAuthService.php index c9f4b711..a676d27f 100644 --- a/app/libs/utils/services/IAuthService.php +++ b/app/libs/utils/services/IAuthService.php @@ -2,26 +2,38 @@ namespace utils\services; +use oauth2\models\IClient; +use openid\model\IOpenIdUser; + /** * Interface IAuthService * @package utils\services */ -interface IAuthService { +interface IAuthService +{ + // authorization responses + const AuthorizationResponse_None = "None"; const AuthorizationResponse_AllowOnce = "AllowOnce"; const AuthorizationResponse_AllowForever = "AllowForever"; const AuthorizationResponse_DenyForever = "DenyForever"; const AuthorizationResponse_DenyOnce = "DenyOnce"; - const AuthenticationResponse_None = "None"; - const AuthenticationResponse_Cancel = "Cancel"; + // authentication responses + + const AuthenticationResponse_None = "None"; + const AuthenticationResponse_Cancel = "Cancel"; /** * @return bool */ public function isUserLogged(); + /** + * @return IOpenIdUser + */ public function getCurrentUser(); + /** * @param $username * @param $password @@ -30,23 +42,80 @@ interface IAuthService { */ public function login($username, $password, $remember_me); + /** + * @param string $username + * @return IOpenIdUser + */ public function getUserByUsername($username); + /** + * @param int $id + * @return IOpenIdUser + */ public function getUserById($id); public function getUserAuthorizationResponse(); public function setUserAuthorizationResponse($auth_response); - public function clearUserAuthorizationResponse(); + public function clearUserAuthorizationResponse(); - public function getUserAuthenticationResponse(); + public function getUserAuthenticationResponse(); - public function setUserAuthenticationResponse($auth_response); + public function setUserAuthenticationResponse($auth_response); - public function clearUserAuthenticationResponse(); + public function clearUserAuthenticationResponse(); + /** + * @return void + */ public function logout(); + /** + * @param string $openid + * @return IOpenIdUser + */ public function getUserByOpenId($openid); + + /** + * @param int $user_id + * @return string + */ + public function unwrapUserId($user_id); + + /** + * @param int $user_id + * @param IClient $client + * @return string + */ + public function wrapUserId($user_id, IClient $client); + + /** + * @param int $external_id + * @return IOpenIdUser + */ + public function getUserByExternaldId($external_id); + + /** + * @return string + */ + public function getSessionId(); + + /** + * @param $client_id + * @return void + */ + public function registerRPLogin($client_id); + + /** + * @return string[] + */ + public function getLoggedRPs(); + + /** + * @param string $jti + * @return void + */ + public function reloadSession($jti); + } \ No newline at end of file diff --git a/app/libs/utils/services/IServerConfigurationService.php b/app/libs/utils/services/IServerConfigurationService.php index 9fd4cb97..90de6745 100644 --- a/app/libs/utils/services/IServerConfigurationService.php +++ b/app/libs/utils/services/IServerConfigurationService.php @@ -17,4 +17,9 @@ interface IServerConfigurationService { public function getAllConfigValues(); public function saveConfigValue($key,$value); + + /** + * @return string + */ + public function getSiteUrl(); } \ No newline at end of file diff --git a/app/libs/utils/services/ServiceLocator.php b/app/libs/utils/services/ServiceLocator.php index 913fd3e8..7009bab8 100644 --- a/app/libs/utils/services/ServiceLocator.php +++ b/app/libs/utils/services/ServiceLocator.php @@ -8,16 +8,19 @@ use App; * Class ServiceLocator * @package utils\services */ -final class ServiceLocator { +final class ServiceLocator +{ private static $instance = null; - private function __construct(){ + private function __construct() + { } public static function getInstance() { - if (self::$instance == null) { + if (self::$instance == null) + { self::$instance = new ServiceLocator(); } diff --git a/app/libs/utils/services/UniqueIdentifierGenerator.php b/app/libs/utils/services/UniqueIdentifierGenerator.php index 1f9afde2..aaca298e 100644 --- a/app/libs/utils/services/UniqueIdentifierGenerator.php +++ b/app/libs/utils/services/UniqueIdentifierGenerator.php @@ -20,8 +20,8 @@ use utils\model\Identifier; * Class UniqueIdentifierGenerator * @package utils\services */ -abstract class UniqueIdentifierGenerator implements IdentifierGenerator { - +abstract class UniqueIdentifierGenerator implements IdentifierGenerator +{ /** * @var ICacheService @@ -45,9 +45,11 @@ abstract class UniqueIdentifierGenerator implements IdentifierGenerator { $reflect = new \ReflectionClass($identifier); $class_name = strtolower($reflect->getShortName()); - do { + do + { $value = $this->_generate($identifier)->getValue(); - } while(!$this->cache_service->addSingleValue($class_name.'.value.'.$value, $class_name.'.value.'.$value)); + } + while(!$this->cache_service->addSingleValue($class_name.'.value.'.$value, $class_name.'.value.'.$value)); return $identifier; } diff --git a/app/models/Member.php b/app/models/Member.php index 2c31ceff..49c208b8 100644 --- a/app/models/Member.php +++ b/app/models/Member.php @@ -14,6 +14,9 @@ class Member extends BaseModelEloquent //external os members db (SS) protected $connection = 'os_members'; + //no timestamps + public $timestamps = false; + public function checkPassword($password) { $digest = AuthHelper::encrypt_password($password, $this->Salt, $this->PasswordEncryption); @@ -21,8 +24,22 @@ class Member extends BaseModelEloquent return $res; } - public function groups(){ + public function groups() + { return $this->belongsToMany('Group', 'Group_Members', 'MemberID', 'GroupID'); } + + /** + * @return bool + */ + public function canLogin() + { + $attr = $this->getAttributes(); + if(isset($attr['Active'])) + { + return (bool)$attr['Active']; + } + return true; + } } \ No newline at end of file diff --git a/app/models/oauth2/Api.php b/app/models/oauth2/Api.php index 731b33f9..1f1db51c 100644 --- a/app/models/oauth2/Api.php +++ b/app/models/oauth2/Api.php @@ -3,27 +3,36 @@ use oauth2\models\IApi; use utils\model\BaseModelEloquent; -class Api extends BaseModelEloquent implements IApi { +class Api extends BaseModelEloquent implements IApi +{ - protected $fillable = array('name','description','active','resource_server_id','logo'); + protected $fillable = array('name', 'description', 'active', 'resource_server_id', 'logo'); protected $table = 'oauth2_api'; - public function getActiveAttribute(){ - return (bool) $this->attributes['active']; - } + public function getActiveAttribute() + { + return (bool)$this->attributes['active']; + } - public function getIdAttribute(){ - return (int) $this->attributes['id']; - } + public function getIdAttribute() + { + return (int)$this->attributes['id']; + } - public function getResourceServerIdAttribute(){ - return (int) $this->attributes['resource_server_id']; - } + public function getLogoAttribute() + { + return $this->getLogo(); + } + + public function getResourceServerIdAttribute() + { + return (int)$this->attributes['resource_server_id']; + } public function scopes() { - return $this->hasMany('ApiScope','api_id'); + return $this->hasMany('ApiScope', 'api_id'); } public function resource_server() @@ -33,7 +42,7 @@ class Api extends BaseModelEloquent implements IApi { public function endpoints() { - return $this->hasMany('ApiEndpoint','api_id'); + return $this->hasMany('ApiEndpoint', 'api_id'); } /** @@ -51,8 +60,8 @@ class Api extends BaseModelEloquent implements IApi { public function getLogo() { - $url = asset('img/apis/server.png'); - return !empty($this->logo)?$this->logo:$url; + $url = asset('/assets/img/apis/server.png'); + return $url; } @@ -64,11 +73,14 @@ class Api extends BaseModelEloquent implements IApi { public function getScope() { $scope = ''; - foreach($this->scopes()->get() as $s){ - if(!$s->active) continue; - $scope = $scope .$s->name.' '; + foreach ($this->scopes()->get() as $s) { + if (!$s->active) { + continue; + } + $scope = $scope . $s->name . ' '; } $scope = trim($scope); + return $scope; } @@ -93,15 +105,15 @@ class Api extends BaseModelEloquent implements IApi { $this->active = $active; } - public function delete () + public function delete() { - $endpoints = ApiEndpoint::where('api_id','=', $this->id)->get(); - foreach($endpoints as $endpoint){ + $endpoints = ApiEndpoint::where('api_id', '=', $this->id)->get(); + foreach ($endpoints as $endpoint) { $endpoint->delete(); } - $scopes = ApiScope::where('api_id','=', $this->id)->get(); - foreach($scopes as $scope){ + $scopes = ApiScope::where('api_id', '=', $this->id)->get(); + foreach ($scopes as $scope) { $scope->delete(); } diff --git a/app/models/oauth2/ApiScope.php b/app/models/oauth2/ApiScope.php index 74a9d8e6..5a0290f9 100644 --- a/app/models/oauth2/ApiScope.php +++ b/app/models/oauth2/ApiScope.php @@ -3,39 +3,38 @@ use oauth2\models\IApiScope; use utils\model\BaseModelEloquent; -class ApiScope extends BaseModelEloquent implements IApiScope { - - protected $fillable = array('name' ,'short_description', 'description','active','default','system', 'api_id'); - - public function getActiveAttribute(){ - return (bool) $this->attributes['active']; - } - - public function getDefaultAttribute(){ - return (bool) $this->attributes['default']; - } - - public function getSystemAttribute(){ - return (bool) $this->attributes['system']; - } - - public function getIdAttribute(){ - return (int) $this->attributes['id']; - } - - public function getApiIdAttribute(){ - return (int) $this->attributes['api_id']; - } +/** + * Class ApiScope + */ +class ApiScope extends BaseModelEloquent implements IApiScope +{ protected $table = 'oauth2_api_scope'; - protected $hidden = array('pivot'); + protected $fillable = array('name' ,'short_description', 'description','active','default','system', 'api_id', 'assigned_by_groups'); - public function api() - { - return $this->belongsTo('Api'); + public function getActiveAttribute(){ + return (bool) $this->attributes['active']; } + public function getDefaultAttribute(){ + return (bool) $this->attributes['default']; + } + + public function getSystemAttribute(){ + return (bool) $this->attributes['system']; + } + + public function getIdAttribute(){ + return (int) $this->attributes['id']; + } + + public function getApiIdAttribute(){ + return (int) $this->attributes['api_id']; + } + + protected $hidden = array('pivot'); + public function getShortDescription() { return $this->short_description; @@ -56,6 +55,27 @@ class ApiScope extends BaseModelEloquent implements IApiScope { return $this->active; } + /** + * @return boolean + */ + public function isSystem() + { + return $this->system; + } + + /** + * @return boolean + */ + public function isDefault() + { + return $this->default; + } + + public function api() + { + return $this->belongsTo('Api'); + } + public function getApiName() { $api = $this->api()->first(); @@ -69,6 +89,22 @@ class ApiScope extends BaseModelEloquent implements IApiScope { public function getApiLogo(){ $api = $this->api()->first(); - return !is_null($api)? $api->getLogo():asset('img/apis/server.png'); + return !is_null($api) ? $api->getLogo():asset('/assets/apis/server.png'); + } + + /** + * @return \oauth2\models\IApi + */ + public function getApi() + { + return $this->api(); + } + + /** + * @return bool + */ + public function isAssignableByGroups() + { + return $this->assigned_by_groups; } } \ No newline at end of file diff --git a/app/models/oauth2/ApiScopeGroup.php b/app/models/oauth2/ApiScopeGroup.php new file mode 100644 index 00000000..e1bcb2f5 --- /dev/null +++ b/app/models/oauth2/ApiScopeGroup.php @@ -0,0 +1,75 @@ +belongsToMany('ApiScope','oauth2_api_scope_group_scope','group_id','scope_id'); + } + + public function users() + { + return $this->belongsToMany('auth\User','oauth2_api_scope_group_users','group_id','user_id'); + } + + /** + * @param IApiScope $scope + */ + public function addScope(IApiScope $scope) + { + $this->scopes()->attach($scope->id); + } + + /** + * @param IOAuth2User $user + */ + public function addUser(IOAuth2User $user) + { + $this->users()->attach($user->id); + } + + /** + * @param IOAuth2User $scope + */ + public function removeScope(IOAuth2User $scope) + { + $this->scopes()->detach($scope->id); + } + + /** + * @param IOAuth2User $user + */ + public function removeUser(IOAuth2User $user) + { + $this->users()->detach($user->id); + } + + /** + * @return int + */ + public function getId() + { + return (int)$this->id; + } +} \ No newline at end of file diff --git a/app/models/oauth2/AssymetricKey.php b/app/models/oauth2/AssymetricKey.php new file mode 100644 index 00000000..b2bc7364 --- /dev/null +++ b/app/models/oauth2/AssymetricKey.php @@ -0,0 +1,158 @@ +id; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @return string + */ + public function getUse() + { + return $this->usage; + } + + /** + * @return bool + */ + public function isActive() + { + return (bool)$this->active; + } + + /** + * @return \DateTime + */ + public function getLastUse() + { + return $this->last_use; + } + + /** + * @return $this + */ + public function markAsUsed() + { + $this->last_use = new DateTime(); + return $this; + } + + /** + * @return string + */ + public function getKeyId() + { + return $this->kid; + } + + private function calculateThumbprint($alg){ + $pem = str_replace( array("\n","\r"), '', trim($this->getPublicKeyPEM())); + return strtoupper(hash($alg, base64_decode($pem))); + } + + /** + * @return string + */ + public function getSHA_1_Thumbprint() + { + return $this->calculateThumbprint('sha1'); + } + + /** + * @return string + */ + public function getSHA_256_Thumbprint() + { + return $this->calculateThumbprint('sha256'); + } + + abstract public function getPublicKeyPEM(); + + /** + * @return string + */ + public function getPEM() + { + return $this->pem_content; + } + + /** + * checks validatiry range with now + * @return bool + */ + public function isExpired() + { + $now = new \DateTime(); + return ( $this->valid_from <= $now && $this->valid_to >= $now); + } + + /** + * algorithm intended for use with the key + * @return ICryptoAlgorithm + */ + public function getAlg() + { + $algorithm = DigitalSignatures_MACs_Registry::getInstance()->get($this->alg); + + if(is_null($algorithm)) + { + $algorithm = KeyManagementAlgorithms_Registry::getInstance()->get($this->alg); + } + return $algorithm; + } + +} \ No newline at end of file diff --git a/app/models/oauth2/Client.php b/app/models/oauth2/Client.php index 864af077..9968c3fc 100644 --- a/app/models/oauth2/Client.php +++ b/app/models/oauth2/Client.php @@ -1,24 +1,135 @@ attributes['active']; } - public function getIdAttribute(){ + public function getIdAttribute() + { return (int) $this->attributes['id']; } - public function getLockedAttribute(){ + public function getLockedAttribute() + { return (int) $this->attributes['locked']; } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function public_keys() + { + return $this->hasMany('ClientPublicKey','oauth2_client_id','id'); + } + + /** + * @param string $value + */ + public function setApplicationTypeAttribute($value) + { + $this->attributes['application_type'] = strtolower($value); + $this->attributes['client_type'] = $this->infereClientTypeFromAppType($value); + } + + /** + * @param string $app_type + * @return string + */ + private function infereClientTypeFromAppType($app_type) + { + switch($app_type) + { + case IClient::ApplicationType_JS_Client: + return IClient::ClientType_Public; + break; + default: + return IClient::ClientType_Confidential; + break; + } + } + + public function access_tokens() { return $this->hasMany('AccessToken'); @@ -44,17 +155,6 @@ class Client extends BaseModelEloquent implements IClient { return $this->belongsToMany('ApiScope','oauth2_client_api_scope','client_id','scope_id'); } - public function authorized_uris() - { - return $this->hasMany('ClientAuthorizedUri','client_id'); - } - - public function allowed_origins() - { - return $this->hasMany('ClientAllowedOrigin','client_id'); - } - - public function getClientId() { return $this->client_id; @@ -80,7 +180,8 @@ class Client extends BaseModelEloquent implements IClient { $res = array(); - foreach($scopes as $db_scope){ + foreach($scopes as $db_scope) + { $api = !is_null($db_scope)?$db_scope->api()->first():null; $resource_server = !is_null($api) ? $api->resource_server()->first():null; if(!is_null($resource_server) && $resource_server->active && !is_null($api) && $api->active) @@ -89,9 +190,9 @@ class Client extends BaseModelEloquent implements IClient { return $res; } - public function getClientRegisteredUris() + public function getRedirectUris() { - return $this->authorized_uris()->get(); + return explode(',',$this->redirect_uris); } public function isScopeAllowed($scope) @@ -111,25 +212,28 @@ class Client extends BaseModelEloquent implements IClient { return $res; } - public function isUriAllowed($uri) { if(!filter_var($uri, FILTER_VALIDATE_URL)) return false; $parts = @parse_url($uri); - if ($parts == false) { + if ($parts == false) + { return false; } - if(($parts['scheme']!=='https') && (ServerConfigurationService::getConfigValue("SSL.Enable"))) + if(($this->application_type !== IClient::ApplicationType_Native && $parts['scheme']!=='https') && (ServerConfigurationService::getConfigValue("SSL.Enable"))) return false; //normalize uri $normalized_uri = $parts['scheme'].'://'.strtolower($parts['host']); + if(isset($parts['port'])) { + $normalized_uri .= ':'.strtolower($parts['port']); + } if(isset($parts['path'])) { $normalized_uri .= strtolower($parts['path']); } // normalize url and remove trailing / $normalized_uri = rtrim($normalized_uri, '/'); - $client_authorized_uri = ClientAuthorizedUri::where('client_id', '=', $this->id)->where('uri','=',$normalized_uri)->first(); - return !is_null($client_authorized_uri); + + return str_contains($this->redirect_uris, $normalized_uri); } public function getApplicationName() @@ -141,7 +245,10 @@ class Client extends BaseModelEloquent implements IClient { { $app_logo = $this->app_logo; if(is_null($app_logo) || empty($app_logo)) - $app_logo = asset('img/oauth2.default.logo.png'); + $app_logo = asset('assets/img/oauth2.default.logo.png'); + $app_logo_url = $this->logo_uri; + if(!empty($app_logo_url)) + $app_logo = $app_logo_url; return $app_logo; } @@ -197,7 +304,8 @@ class Client extends BaseModelEloquent implements IClient { * @return string * @throws Exception */ - public function getFriendlyApplicationType(){ + public function getFriendlyApplicationType() + { switch($this->application_type){ case IClient::ApplicationType_JS_Client: return 'Client Side (JS)'; @@ -208,13 +316,16 @@ class Client extends BaseModelEloquent implements IClient { case IClient::ApplicationType_Web_App: return 'Web Server Application'; break; + case IClient::ApplicationType_Native: + return 'Native Application'; + break; } throw new Exception('Invalid Application Type'); } public function getClientAllowedOrigins() { - return $this->allowed_origins()->get(); + return explode(',', $this->allowed_origins); } /** @@ -232,18 +343,247 @@ class Client extends BaseModelEloquent implements IClient { if($parts['scheme']!=='https') return false; $origin_without_port = $parts['scheme'].'://'.$parts['host']; - $client_allowed_origin = $this->allowed_origins()->where('allowed_origin','=',$origin_without_port)->first(); - if(!is_null($client_allowed_origin)) return true; + + if(str_contains($this->allowed_origins,$origin_without_port )) return true; if(isset($parts['port'])){ $origin_with_port = $parts['scheme'].'://'.$parts['host'].':'.$parts['port']; - $client_authorized_uri = $this->allowed_origins()->where('allowed_origin','=',$origin_with_port)->first();; - return !is_null($client_authorized_uri); + return str_contains($this->allowed_origins, $origin_with_port ); } return false; } public function getWebsite() { - return $this->website; + $res = $this->website; + if(empty($res)) $res = '#'; + return $res; } -} + + /** + * @return \DateTime + */ + public function getClientSecretExpiration() + { + $exp_date = $this->client_secret_expires_at; + if(is_null($exp_date)) return null; + + if($exp_date instanceof \DateTime) + return $exp_date; + return new \DateTime($exp_date); + } + + /** + * @return bool + */ + public function isClientSecretExpired() + { + $now = new \DateTime(); + $exp_date = $this->getClientSecretExpiration(); + + if(is_null($exp_date)) return false; + return $exp_date < $now; + } + + /** + * @return string[] + */ + public function getContacts() + { + return explode(',',$this->contacts); + } + + /** + * @return int + */ + public function getDefaultMaxAge() + { + return (int)$this->default_max_age; + } + + /** + * @return bool + */ + public function requireAuthTimeClaim() + { + return $this->require_auth_time; + } + + /** + * @return string + */ + public function getLogoUri() + { + return $this->logo_uri; + } + + /** + * @return string + */ + public function getPolicyUri() + { + $res = $this->policy_uri; + if(empty($res)) $res = '#'; + return $res; + } + + /** + * @return string + */ + public function getTermOfServiceUri() + { + $res = $this->tos_uri; + if(empty($res)) $res = '#'; + return $res; + } + + /** + * @return string[] + */ + public function getPostLogoutUris() + { + return explode(',', $this->post_logout_redirect_uris); + } + + /** + * @return string + */ + public function getLogoutUri() + { + return $this->logout_uri; + } + + /** + * @return JWTResponseInfo + */ + public function getIdTokenResponseInfo() + { + return new JWTResponseInfo + ( + DigitalSignatures_MACs_Registry::getInstance()->get($this->id_token_signed_response_alg), + KeyManagementAlgorithms_Registry::getInstance()->get($this->id_token_encrypted_response_alg), + ContentEncryptionAlgorithms_Registry::getInstance()->get($this->id_token_encrypted_response_enc) + ); + } + + /** + * @return JWTResponseInfo + */ + public function getUserInfoResponseInfo() + { + return new JWTResponseInfo + ( + DigitalSignatures_MACs_Registry::getInstance()->get($this->userinfo_signed_response_alg), + KeyManagementAlgorithms_Registry::getInstance()->get($this->userinfo_encrypted_response_alg), + ContentEncryptionAlgorithms_Registry::getInstance()->get($this->userinfo_encrypted_response_enc) + ); + } + + /** + * @return TokenEndpointAuthInfo + */ + public function getTokenEndpointAuthInfo() + { + return new TokenEndpointAuthInfo( + $this->token_endpoint_auth_method, + DigitalSignatures_MACs_Registry::getInstance()->isSupported($this->token_endpoint_auth_signing_alg) ? + DigitalSignatures_MACs_Registry::getInstance()->get($this->token_endpoint_auth_signing_alg) : + null + ); + } + + /** + * @return string + */ + public function getSubjectType() + { + return $this->subject_type; + } + + /** + * @return IClientPublicKey[] + */ + public function getPublicKeys() + { + return $this->public_keys()->get(); + } + + /** + * @return IClientPublicKey[] + */ + public function getPublicKeysByUse($use) + { + return $this->public_keys()->where('usage','=',$use)->all(); + } + + /** + * @param string $kid + * @return IClientPublicKey + */ + public function getPublicKeyByIdentifier($kid) + { + return $this->public_keys()->where('kid','=',$kid)->first(); + } + + /** + * @param IClientPublicKey $public_key + * @return $this + */ + public function addPublicKey(IClientPublicKey $public_key) + { + $this->public_keys()->save($public_key); + } + + /** + * @return string + */ + public function getJWKSUri() + { + return $this->jwks_uri; + } + + /** + * @param string $use + * @param string $alg + * @return IClientPublicKey + */ + public function getCurrentPublicKeyByUse($use, $alg) + { + $now = new \DateTime(); + + return $this->public_keys() + ->where('usage','=',$use) + ->where('alg','=',$alg) + ->where('active','=', true) + ->where('valid_from','<=',$now) + ->where('valid_to','>=',$now) + ->first(); + } + + /** + * @param string $post_logout_uri + * @return bool + */ + public function isPostLogoutUriAllowed($post_logout_uri) + { + if(!filter_var($post_logout_uri, FILTER_VALIDATE_URL)) return false; + + $parts = @parse_url($post_logout_uri); + + if ($parts == false) { + return false; + } + if($parts['scheme']!=='https') + return false; + + $logout_without_port = $parts['scheme'].'://'.$parts['host']; + + if(str_contains($this->post_logout_redirect_uris, $logout_without_port )) return true; + + if(isset($parts['port'])) + { + $logout_with_port = $parts['scheme'].'://'.$parts['host'].':'.$parts['port']; + return str_contains($this->post_logout_redirect_uris, $logout_with_port ); + } + return false; + } +} \ No newline at end of file diff --git a/app/models/oauth2/ClientAllowedOrigin.php b/app/models/oauth2/ClientAllowedOrigin.php deleted file mode 100644 index 0252278f..00000000 --- a/app/models/oauth2/ClientAllowedOrigin.php +++ /dev/null @@ -1,12 +0,0 @@ -belongsTo('Client'); - } -} \ No newline at end of file diff --git a/app/models/oauth2/ClientAuthorizedUri.php b/app/models/oauth2/ClientAuthorizedUri.php deleted file mode 100644 index 942d873d..00000000 --- a/app/models/oauth2/ClientAuthorizedUri.php +++ /dev/null @@ -1,13 +0,0 @@ -belongsTo('Client'); - } -} \ No newline at end of file diff --git a/app/models/oauth2/ClientPublicKey.php b/app/models/oauth2/ClientPublicKey.php new file mode 100644 index 00000000..219b90db --- /dev/null +++ b/app/models/oauth2/ClientPublicKey.php @@ -0,0 +1,84 @@ +belongsTo('Client'); + } + + /** + * @param string $kid + * @param string $type + * @param string $use + * @param string $pem + * @param string $alg + * @param bool $active + * @param \DateTime $valid_from + * @param \DateTime $valid_to + * @return IClientPublicKey + */ + static public function buildFromPEM($kid, $type, $use, $pem, $alg, $active, \DateTime $valid_from, \DateTime $valid_to) + { + $pk = new self; + $pk->kid = $kid; + $pk->pem_content = $pem; + $pk->type = $type; + $pk->usage = $use; + $pk->alg = $alg; + $pk->active = $active; + $pk->valid_from = $valid_from; + $pk->valid_to = $valid_to; + return $pk; + } + + public function getPublicKeyPEM() + { + return $this->pem_content; + } + + /** + * @return IJWK + */ + public function toJWK() + { + $jwk = RSAJWKFactory::build + ( + new RSAJWKPEMPublicKeySpecification + ( + $this->getPublicKeyPEM(), + $this->alg + ) + ); + + $jwk->setId($this->kid); + $jwk->setType($this->type); + $jwk->setKeyUse($this->usage); + return $jwk; + } +} \ No newline at end of file diff --git a/app/models/oauth2/ResourceServer.php b/app/models/oauth2/ResourceServer.php index c62a0882..343238c4 100644 --- a/app/models/oauth2/ResourceServer.php +++ b/app/models/oauth2/ResourceServer.php @@ -3,27 +3,31 @@ use oauth2\models\IResourceServer; use utils\model\BaseModelEloquent; -class ResourceServer extends BaseModelEloquent implements IResourceServer { +class ResourceServer extends BaseModelEloquent implements IResourceServer +{ - protected $fillable = array('host','ip','active','friendly_name'); + protected $fillable = array('host', 'ip', 'active', 'friendly_name'); protected $table = 'oauth2_resource_server'; - public function getActiveAttribute(){ - return (bool) $this->attributes['active']; - } + public function getActiveAttribute() + { + return (bool)$this->attributes['active']; + } - public function getIdAttribute(){ - return (int) $this->attributes['id']; - } + public function getIdAttribute() + { + return (int)$this->attributes['id']; + } public function apis() { - return $this->hasMany('Api','resource_server_id'); + return $this->hasMany('Api', 'resource_server_id'); } - public function client(){ + public function client() + { return $this->hasOne('Client'); } diff --git a/app/models/oauth2/ServerPrivateKey.php b/app/models/oauth2/ServerPrivateKey.php new file mode 100644 index 00000000..65cde246 --- /dev/null +++ b/app/models/oauth2/ServerPrivateKey.php @@ -0,0 +1,145 @@ +decrypt($this->attributes['pem_content']); + } + + public function setPemContentAttribute($value) + { + + $this->attributes['pem_content'] = $this->encrypt($value); + } + + public function getPasswordAttribute() + { + return $this->decrypt($this->attributes['password']); + } + + public function setPasswordAttribute($value) + { + $this->attributes['password'] = $this->encrypt($value); + } + + /** + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * @param string $kid + * @param \DateTime $valid_from + * @param \DateTime $valid_to + * @param string $type + * @param string $use + * @param bool $active + * @param string $pem_content + * @param null|string $password + * @return IServerPrivateKey + */ + static function build($kid, \DateTime $valid_from, \DateTime $valid_to, $type, $use, $alg, $active, $pem_content, $password = null) + { + $key = new self; + $key->kid = $kid; + $key->valid_from = $valid_from; + $key->valid_to = $valid_to; + $key->type = $type; + $key->usage = $use; + $key->active = $active; + $key->alg = $alg; + $key->pem_content = $pem_content; + $key->password = $password; + + return $key; + } + + public function getPublicKeyPEM() + { + $private_key_pem = $this->pem_content; + $rsa = new Crypt_RSA(); + + if(!empty($this->password)){ + $rsa->setPassword($this->password); + } + + $rsa->loadKey($private_key_pem); + return $rsa->getPublicKey(); + } + + /** + * @return IJWK + */ + public function toJWK() + { + //load server private key. + $jwk = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + $this->pem_content, + $this->password, + $this->alg + ) + ); + + $jwk->setId($this->kid); + $jwk->setType($this->type); + $jwk->setKeyUse($this->usage); + + return $jwk; + } +} \ No newline at end of file diff --git a/app/providers/BehatSessionServiceProvider.php b/app/providers/BehatSessionServiceProvider.php new file mode 100644 index 00000000..9b00886c --- /dev/null +++ b/app/providers/BehatSessionServiceProvider.php @@ -0,0 +1,26 @@ +setCurrentPage($page_nbr); + + return $this->entity->Filter($filters)->paginate($page_size, $fields); + } + + /** + * @param IEntity $entity + * @return bool + */ + public function update(IEntity $entity) + { + return $entity->Save(); + } + + /** + * @param IEntity $entity + * @return bool + */ + public function add(IEntity $entity) + { + return $entity->save(); + } + + /** + * @param IEntity $entity + * @return bool + */ + public function delete(IEntity $entity) + { + return $entity->delete(); + } + + /** + * @param int $id + * @return IEntity + */ + public function get($id) + { + return $this->entity->find($id); + } +} \ No newline at end of file diff --git a/app/repositories/EloquentApiScopeGroupRepository.php b/app/repositories/EloquentApiScopeGroupRepository.php new file mode 100644 index 00000000..4ed8e5e8 --- /dev/null +++ b/app/repositories/EloquentApiScopeGroupRepository.php @@ -0,0 +1,43 @@ +entity = $group; + $this->log_service = $log_service; + } + +} \ No newline at end of file diff --git a/app/repositories/EloquentAssymetricKeyRepository.php b/app/repositories/EloquentAssymetricKeyRepository.php new file mode 100644 index 00000000..40068deb --- /dev/null +++ b/app/repositories/EloquentAssymetricKeyRepository.php @@ -0,0 +1,160 @@ +key->where('kid','=',$kid)->first(); + } + + /** + * @param IAssymetricKey $key + * @return void + */ + public function add(IAssymetricKey $key) + { + $key->save(); + } + + /** + * @param IAssymetricKey $key + * @return void + */ + public function delete(IAssymetricKey $key) + { + $key->delete(); + } + + /** + * @param int $id + * @return IAssymetricKey + */ + public function getById($id) + { + return $this->key->find($id); + } + + /** + * @param string $pem + * @return IAssymetricKey + */ + public function getByPEM($pem) + { + return $this->key->where('pem_content','=',$pem)->first(); + } + + /** + * @param string $type + * @param string $usage + * @params string $alg + * @param \DateTime $valid_from + * @param \DateTime $valid_to + * @param int|null $owner_id + * @return IAssymetricKey + */ + public function getByValidityRange($type, $usage, $alg, \DateTime $valid_from, \DateTime $valid_to, $owner_id = null) + { + // (StartA <= EndB) and (EndA >= StartB) + $query = $this->key + ->where('type','=',$type) + ->where('usage','=',$usage) + ->where('alg','=',$alg) + ->where('valid_from','<=',$valid_to) + ->where('valid_to','>=',$valid_from) + ->where('active','=', true); + + if($owner_id) + { + $query = $query->where('oauth2_client_id','=', $owner_id); + } + return $query->get(); + } + + + /** + * @param int $page_nbr + * @param int $page_size + * @param array $filters + * @param array $fields + * @return IAssymetricKey[] + */ + public function getAll($page_nbr = 1, $page_size = 10, array $filters = array(), array $fields = array('*')) + { + DB::getPaginator()->setCurrentPage($page_nbr); + return $this->key->Filter($filters)->paginate($page_size, $fields); + } + + /** + * @return IAssymetricKey[] + */ + public function getActives() + { + $now = new \DateTime(); + return $this->key + ->where('active','=', true) + ->where('valid_from','<=',$now) + ->where('valid_to','>=',$now) + ->get(); + } + + /** + * @param string $type + * @param string $usage + * @param string $alg + * @param int|null $owner_id + * @return IAssymetricKey + */ + public function getActiveByCriteria($type, $usage, $alg, $owner_id = null) + { + $now = new \DateTime(); + $query = $this->key + ->where('active','=', true) + ->where('valid_from','<=',$now) + ->where('valid_to','>=',$now) + ->where('type','=',$type) + ->where('usage','=',$usage) + ->where('alg','=',$alg); + if($owner_id) + { + $query = $query->where('oauth2_client_id','=', $owner_id); + } + return $query->first(); + } +} \ No newline at end of file diff --git a/app/repositories/EloquentClientPublicKeyRepository.php b/app/repositories/EloquentClientPublicKeyRepository.php new file mode 100644 index 00000000..ee629968 --- /dev/null +++ b/app/repositories/EloquentClientPublicKeyRepository.php @@ -0,0 +1,35 @@ +key = $public_key; + $this->log_service = $log_service; + } +} \ No newline at end of file diff --git a/app/repositories/EloquentClientRepository.php b/app/repositories/EloquentClientRepository.php new file mode 100644 index 00000000..1d8006ca --- /dev/null +++ b/app/repositories/EloquentClientRepository.php @@ -0,0 +1,73 @@ +client = $client; + $this->log_service = $log_service; + } + + /** + * @param int id + * @return IClient + */ + public function get($id) + { + return $this->client->find($id); + } + + /** + * @param IClient $client + * @return void + */ + public function add(IClient $client) + { + $client->save(); + } + + /** + * @param IClient $client + * @return void + */ + public function delete(IClient $client) + { + $client->delete(); + } +} \ No newline at end of file diff --git a/app/repositories/EloquentMemberRepository.php b/app/repositories/EloquentMemberRepository.php index 82a40c04..3df79e9d 100644 --- a/app/repositories/EloquentMemberRepository.php +++ b/app/repositories/EloquentMemberRepository.php @@ -10,19 +10,21 @@ use utils\services\ILogService; * Class EloquentMemberRepository * @package repositories */ -class EloquentMemberRepository implements IMemberRepository{ +class EloquentMemberRepository implements IMemberRepository +{ private $member; private $log_service; - /** - * @param Member $member - * @param ILogService $log_service - */ - public function __construct(Member $member, ILogService $log_service){ - $this->member = $member; - $this->log_service = $log_service; - } + /** + * @param Member $member + * @param ILogService $log_service + */ + public function __construct(Member $member, ILogService $log_service) + { + $this->member = $member; + $this->log_service = $log_service; + } /** * @param $id * @return Member diff --git a/app/repositories/EloquentOpenIdAssociationRepository.php b/app/repositories/EloquentOpenIdAssociationRepository.php index 4c00ab9c..1ebd331a 100644 --- a/app/repositories/EloquentOpenIdAssociationRepository.php +++ b/app/repositories/EloquentOpenIdAssociationRepository.php @@ -9,39 +9,42 @@ use OpenIdAssociation; * Class EloquentOpenIdAssociationRepository * @package repositories */ +class EloquentOpenIdAssociationRepository implements IOpenIdAssociationRepository +{ -class EloquentOpenIdAssociationRepository implements IOpenIdAssociationRepository { + private $association; - private $association; + public function __construct(OpenIdAssociation $association) + { + $this->association = $association; + } - public function __construct(OpenIdAssociation $association){ - $this->association = $association; - } + public function add(OpenIdAssociation $a) + { + return $a->Save(); + } - public function add(OpenIdAssociation $a) - { - return $a->Save(); - } + public function deleteById($id) + { + return $this->delete($this->get($id)); + } - public function deleteById($id) - { - return $this->delete($this->get($id)); - } + public function getByHandle($handle) + { + return $this->association->where('identifier', '=', $handle)->first(); + } - public function getByHandle($handle) - { - return $this->association->where('identifier', '=', $handle)->first(); - } + public function delete(OpenIdAssociation $a) + { + if (!is_null($a)) { + return $a->delete(); + } - public function delete(OpenIdAssociation $a) - { - if(!is_null($a)) - return $a->delete(); - return false; - } + return false; + } - public function get($id) - { - return $this->association->find($id); - } + public function get($id) + { + return $this->association->find($id); + } } \ No newline at end of file diff --git a/app/repositories/EloquentServerPrivateKeyRepository.php b/app/repositories/EloquentServerPrivateKeyRepository.php new file mode 100644 index 00000000..e6eaa437 --- /dev/null +++ b/app/repositories/EloquentServerPrivateKeyRepository.php @@ -0,0 +1,33 @@ +key = $private_key; + $this->log_service = $log_service; + } +} \ No newline at end of file diff --git a/app/repositories/EloquentUserRepository.php b/app/repositories/EloquentUserRepository.php index 8e87e01f..cbbbd583 100644 --- a/app/repositories/EloquentUserRepository.php +++ b/app/repositories/EloquentUserRepository.php @@ -4,91 +4,97 @@ namespace repositories; use auth\IUserRepository; use auth\User; -use utils\services\ILogService; use DB; +use utils\services\ILogService; +use \Member; -class EloquentUserRepository implements IUserRepository { +/** + * Class EloquentUserRepository + * @package repositories + */ +final class EloquentUserRepository extends AbstractEloquentEntityRepository implements IUserRepository +{ - private $user; - private $log_service; - public function __construct(User $user,ILogService $log_service){ - $this->user = $user; - $this->log_service = $log_service; - } - /** - * @param $id - * @return User - */ - public function get($id) - { - return $this->user->find($id); - } + /** + * @var ILogService + */ + private $log_service; - public function getByCriteria($filters){ - return $this->user->Filter($filters)->get(); - } + /** + * EloquentUserRepository constructor. + * @param User $user + * @param ILogService $log_service + */ + public function __construct(User $user, ILogService $log_service) + { + $this->entity = $user; + $this->log_service = $log_service; + } - public function getOneByCriteria($filters){ - return $this->user->Filter($filters)->first(); - } + /** + * @param $id + * @return User + */ + public function get($id) + { + return $this->entity->find($id); + } - /** - * @param User $u - * @return bool - */ - public function update(User $u) - { - return $u->Save(); - } + public function getByCriteria($filters) + { + return $this->entity->Filter($filters)->get(); + } - /** - * @param User $u - * @return bool - */ - public function add(User $u) - { - return $u->Save(); - } + public function getOneByCriteria($filters) + { + return $this->entity->Filter($filters)->first(); + } - /** - * @param int $page_nbr - * @param int $page_size - * @param array $filters - * @param array $fields - * @return array - */ - public function getByPage($page_nbr = 1, $page_size = 10, array $filters = array(), array $fields = array('*')) - { - DB::getPaginator()->setCurrentPage($page_nbr); - return $this->user->Filter($filters)->paginate($page_size, $fields); - } + /** + * @param array $filters + * @return int + */ + public function getCount(array $filters = array()) + { + return $this->entity->Filter($filters)->count(); + } - /** - * @param array $filters - * @return int - */ - public function getCount(array $filters = array()) - { - return $this->user->Filter($filters)->count(); - } + /** + * @param $external_id + * @return User + */ + public function getByExternalId($external_id) + { + return $this->entity->where('external_identifier', '=', $external_id)->first(); + } - /** - * @param $external_id - * @return User - */ - public function getByExternalId($external_id) - { - return $this->user->where('external_identifier', '=', $external_id)->first(); - } + /** + * @param mixed $identifier + * @param string $token + * @return User + */ + public function getByToken($identifier, $token) + { + return $this->entity + ->where('external_identifier', '=', $identifier) + ->where('remember_token', '=',$token)->first(); + } - /** - * @param mixed $identifier - * @param string $token - * @return User - */ - public function getByToken($identifier, $token) - { - return $this->user->where('external_identifier', '=', $identifier)->where('remember_token', '=', $token)->first(); - } + /** + * @param string $term + * @return array + */ + public function getByEmailOrName($term) + { + $list = array(); + $members = Member::where('Email', 'like', '%'.$term.'%')->paginate(10); + foreach($members->getItems() as $m) + { + $user = $this->getByExternalId(intval($m->ID)); + if(!is_null($user)) + array_push($list, $user); + } + return $list; + } } \ No newline at end of file diff --git a/app/repositories/RepositoriesProvider.php b/app/repositories/RepositoriesProvider.php index 5a33c7bb..f4047349 100644 --- a/app/repositories/RepositoriesProvider.php +++ b/app/repositories/RepositoriesProvider.php @@ -2,8 +2,8 @@ namespace repositories; -use Illuminate\Support\ServiceProvider; use App; +use Illuminate\Support\ServiceProvider; /** * Class RepositoriesProvider @@ -11,15 +11,21 @@ use App; */ class RepositoriesProvider extends ServiceProvider { - protected $defer = false; + protected $defer = false; - public function boot(){ - } + public function boot() + { + } - public function register(){ - App::singleton('openid\repositories\IOpenIdAssociationRepository', 'repositories\EloquentOpenIdAssociationRepository'); - App::singleton('openid\repositories\IOpenIdTrustedSiteRepository', 'repositories\EloquentOpenIdTrustedSiteRepository'); - App::singleton('auth\IUserRepository', 'repositories\EloquentUserRepository'); - App::singleton('auth\IMemberRepository', 'repositories\EloquentMemberRepository'); - } + public function register() + { + App::singleton('openid\repositories\IOpenIdAssociationRepository', 'repositories\EloquentOpenIdAssociationRepository'); + App::singleton('openid\repositories\IOpenIdTrustedSiteRepository', 'repositories\EloquentOpenIdTrustedSiteRepository'); + App::singleton('auth\IUserRepository', 'repositories\EloquentUserRepository'); + App::singleton('auth\IMemberRepository', 'repositories\EloquentMemberRepository'); + App::singleton('oauth2\repositories\IClientPublicKeyRepository', 'repositories\EloquentClientPublicKeyRepository'); + App::singleton('oauth2\repositories\IServerPrivateKeyRepository', 'repositories\EloquentServerPrivateKeyRepository'); + App::singleton('oauth2\repositories\IClientRepository', 'repositories\EloquentClientRepository'); + App::singleton('oauth2\repositories\IApiScopeGroupRepository', 'repositories\EloquentApiScopeGroupRepository'); + } } \ No newline at end of file diff --git a/app/routes.php b/app/routes.php index 9c7f6b19..e8bb587e 100644 --- a/app/routes.php +++ b/app/routes.php @@ -29,7 +29,8 @@ Route::group(array("before" => "ssl"), function () { * the response to make sure that the OP is authorized to make assertions about the Claimed Identifier. */ Route::get("/{identifier}", "UserController@getIdentity"); - Route::get("/accounts/user/ud/{identifier}", "DiscoveryController@user")->where(array('identifier' => '[\d\w\.\#]+')); + Route::get("/accounts/user/ud/{identifier}", + "DiscoveryController@user")->where(array('identifier' => '[\d\w\.\#]+')); //op endpoint url Route::post('/accounts/openid2', 'OpenIdProviderController@endpoint'); @@ -42,16 +43,29 @@ Route::group(array("before" => "ssl"), function () { }); //oauth2 endpoints -Route::group(array('prefix' => 'oauth2', 'before' => 'ssl|oauth2.enabled'), function() -{ - //authorization endpoint - Route::any('/auth',"OAuth2ProviderController@authorize"); +Route::group( array( 'before' => 'ssl|oauth2.enabled' ), function () { + Route::get('/.well-known/openid-configuration',"OAuth2ProviderController@discovery"); +}); + +Route::group(array('prefix' => 'oauth2', 'before' => 'ssl|oauth2.enabled'), function () { + Route::get('/check-session',"OAuth2ProviderController@checkSessionIFrame"); + Route::get('/end-session',"OAuth2ProviderController@endSession"); + Route::get('/end-session/cancel',"OAuth2ProviderController@cancelLogout"); + Route::post('/end-session',"OAuth2ProviderController@endSession"); + + //authorization endpoint + Route::any('/auth', "OAuth2ProviderController@authorize"); + // OIDC + // certificates + Route::get('/certs',"OAuth2ProviderController@certs"); + // discovery document + Route::get('/.well-known/openid-configuration',"OAuth2ProviderController@discovery"); //token endpoint - Route::group(array('prefix' => 'token'), function(){ - Route::post('/',"OAuth2ProviderController@token"); - Route::post('/revoke',"OAuth2ProviderController@revoke"); - Route::post('/introspection',"OAuth2ProviderController@introspection"); + Route::group(array('prefix' => 'token'), function () { + Route::post('/', "OAuth2ProviderController@token"); + Route::post('/revoke', "OAuth2ProviderController@revoke"); + Route::post('/introspection', "OAuth2ProviderController@introspection"); }); }); @@ -62,129 +76,169 @@ Route::group(array("before" => array("ssl", "auth")), function () { Route::any("/accounts/user/profile", "UserController@getProfile"); Route::any("/accounts/user/profile/trusted_site/delete/{id}", "UserController@deleteTrustedSite"); Route::post('/accounts/user/profile/update', 'UserController@postUserProfileOptions'); - }); +}); -Route::group(array('prefix' => 'admin','before' => 'ssl|auth'), function(){ +Route::group(array('prefix' => 'admin', 'before' => 'ssl|auth'), function () { //client admin UI - Route::get('clients/edit/{id}',array('before' => 'oauth2.enabled|user.owns.client.policy', 'uses' => 'AdminController@editRegisteredClient')); - Route::get('clients',array('before' => 'oauth2.enabled', 'uses' => 'AdminController@listOAuth2Clients')); + Route::get('clients/edit/{id}', + array('before' => 'oauth2.enabled|user.owns.client.policy', 'uses' => 'AdminController@editRegisteredClient')); + Route::get('clients', array('before' => 'oauth2.enabled', 'uses' => 'AdminController@listOAuth2Clients')); - Route::get('/grants',array('before' => 'oauth2.enabled', 'uses' => 'AdminController@editIssuedGrants')); - //oauth2 server admin UI - Route::group(array('before' => 'oauth2.enabled|oauth2.server.admin'), function(){ - Route::get('/resource-servers','AdminController@listResourceServers'); - Route::get('/resource-server/{id}','AdminController@editResourceServer'); - Route::get('/api/{id}','AdminController@editApi'); - Route::get('/scope/{id}','AdminController@editScope'); - Route::get('/endpoint/{id}','AdminController@editEndpoint'); - Route::get('/locked-clients','AdminController@listLockedClients'); + Route::get('/grants', array('before' => 'oauth2.enabled', 'uses' => 'AdminController@editIssuedGrants')); + //oauth2 server admin UI + Route::group(array('before' => 'oauth2.enabled|oauth2.server.admin'), function () { + + Route::get('/api-scope-groups', 'AdminController@listApiScopeGroups'); + Route::get('/api-scope-groups/{id}', 'AdminController@editApiScopeGroup'); + Route::get('/resource-servers', 'AdminController@listResourceServers'); + Route::get('/resource-server/{id}', 'AdminController@editResourceServer'); + Route::get('/api/{id}', 'AdminController@editApi'); + Route::get('/scope/{id}', 'AdminController@editScope'); + Route::get('/endpoint/{id}', 'AdminController@editEndpoint'); + Route::get('/locked-clients', 'AdminController@listLockedClients'); + // server private keys + Route::get('/private-keys', 'AdminController@listServerPrivateKeys'); }); - Route::group(array('before' => 'openstackid.server.admin'), function(){ - Route::get('/locked-users','AdminController@listLockedUsers'); - Route::get('/server-config','AdminController@listServerConfig'); - Route::post('/server-config','AdminController@saveServerConfig'); - Route::get('/banned-ips','AdminController@listBannedIPs'); + Route::group(array('before' => 'openstackid.server.admin'), function () { + Route::get('/locked-users', 'AdminController@listLockedUsers'); + Route::get('/server-config', 'AdminController@listServerConfig'); + Route::post('/server-config', 'AdminController@saveServerConfig'); + Route::get('/banned-ips', 'AdminController@listBannedIPs'); }); }); //Admin Backend API -Route::group(array('prefix' => 'admin/api/v1', 'before' => 'ssl|auth'), function() -{ - - Route::group(array('prefix' => 'users'), function(){ - Route::delete('/{id}/locked',array('before' => 'openstackid.server.admin.json', 'uses' => 'UserApiController@unlock')); - Route::delete('/{id}/token/{value}',array('before' => 'is.current.user', 'uses' => 'UserApiController@revokeToken')); +Route::group(array('prefix' => 'admin/api/v1', 'before' => 'ssl|auth'), function () { + Route::group(array('prefix' => 'users'), function () { + Route::delete('/{id}/locked', + array('before' => 'openstackid.server.admin.json', 'uses' => 'UserApiController@unlock')); + Route::delete('/{id}/token/{value}', + array('before' => 'is.current.user', 'uses' => 'UserApiController@revokeToken')); }); - Route::group(array('prefix' => 'banned-ips', 'before' => 'openstackid.server.admin.json'), function(){ - Route::get('/{id}',"ApiBannedIPController@get"); - Route::get('/',"ApiBannedIPController@getByPage"); - Route::delete('/{id?}',"ApiBannedIPController@delete"); + Route::group(array('prefix' => 'banned-ips', 'before' => 'openstackid.server.admin.json'), function () { + Route::get('/{id}', "ApiBannedIPController@get"); + Route::get('/', "ApiBannedIPController@getByPage"); + Route::delete('/{id?}', "ApiBannedIPController@delete"); }); //client api - Route::group(array('prefix' => 'clients'), function(){ + Route::group(array('prefix' => 'clients'), function () { + + // public keys + Route::post('/{id}/public_keys', array('before' => 'user.owns.client.policy', 'uses' => 'ClientPublicKeyApiController@create')); + Route::get('/{id}/public_keys', array('before' => 'user.owns.client.policy', 'uses' => 'ClientPublicKeyApiController@getByPage')); + Route::delete('/{id}/public_keys/{public_key_id}', array('before' => 'user.owns.client.policy', 'uses' => 'ClientPublicKeyApiController@delete')); + Route::put('/{id}/public_keys/{public_key_id}', array('before' => 'user.owns.client.policy', 'uses' => 'ClientPublicKeyApiController@update')); Route::post('/', array('before' => 'is.current.user', 'uses' => 'ClientApiController@create')); - Route::get('/',array('before' => 'is.current.user', 'uses' => 'ClientApiController@getByPage')); - Route::delete('/{id}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@delete')); - + Route::put('/', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@update')); + Route::get('/{id}', "ClientApiController@get"); + Route::get('/', array('before' => 'is.current.user', 'uses' => 'ClientApiController@getByPage')); + Route::delete('/{id}', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@delete')); //allowed redirect uris endpoints - Route::get('/{id}/uris',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@getRegisteredUris')); - Route::post('/{id}/uris',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@addAllowedRedirectUri')); - Route::delete('/{id}/uris/{uri_id}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@deleteClientAllowedUri')); + Route::get('/{id}/uris', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@getRegisteredUris')); + Route::post('/{id}/uris', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@addAllowedRedirectUri')); + Route::delete('/{id}/uris/{uri_id}', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@deleteClientAllowedUri')); //allowed origin endpoints endpoints - Route::get('/{id}/origins',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@geAllowedOrigins')); - Route::post('/{id}/origins',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@addAllowedOrigin')); - Route::delete('/{id}/origins/{origin_id}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@deleteClientAllowedOrigin')); - - Route::delete('/{id}/lock',array('before' => 'openstackid.server.admin.json', 'uses' => 'ClientApiController@unlock')); - Route::put('/{id}/secret',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@regenerateClientSecret')); - Route::put('/{id}/use-refresh-token',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@setRefreshTokenClient')); - Route::put('/{id}/rotate-refresh-token',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@setRotateRefreshTokenPolicy')); - Route::get('/{id}/access-token',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@getAccessTokens')); - Route::get('/{id}/refresh-token',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@getRefreshTokens')); - Route::delete('/{id}/token/{value}/{hint}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@revokeToken')); - Route::put('/{id}/scopes/{scope_id}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@addAllowedScope')); - Route::delete('/{id}/scopes/{scope_id}',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@removeAllowedScope')); - - Route::put('/{id}/active',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@activate')); - Route::delete('/{id}/active',array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@deactivate')); + Route::get('/{id}/origins', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@geAllowedOrigins')); + Route::post('/{id}/origins', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@addAllowedOrigin')); + Route::delete('/{id}/origins/{origin_id}', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@deleteClientAllowedOrigin')); + Route::delete('/{id}/lock', array('before' => 'openstackid.server.admin.json', 'uses' => 'ClientApiController@unlock')); + Route::put('/{id}/secret', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@regenerateClientSecret')); + Route::put('/{id}/use-refresh-token', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@setRefreshTokenClient')); + Route::put('/{id}/rotate-refresh-token', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@setRotateRefreshTokenPolicy')); + Route::get('/{id}/access-token', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@getAccessTokens')); + Route::get('/{id}/refresh-token', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@getRefreshTokens')); + Route::delete('/{id}/token/{value}/{hint}', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@revokeToken')); + Route::put('/{id}/scopes/{scope_id}', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@addAllowedScope')); + Route::delete('/{id}/scopes/{scope_id}', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@removeAllowedScope')); + Route::put('/{id}/active', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@activate')); + Route::delete('/{id}/active', array('before' => 'user.owns.client.policy', 'uses' => 'ClientApiController@deactivate')); }); - Route::group(array('prefix' => 'resource-servers', 'before' => 'oauth2.server.admin.json'), function(){ - Route::get('/{id}',"ApiResourceServerController@get"); - Route::get('/',"ApiResourceServerController@getByPage"); - Route::post('/',"ApiResourceServerController@create"); - Route::delete('/{id}',"ApiResourceServerController@delete"); - Route::put('/',"ApiResourceServerController@update"); - Route::put('/{id}/client-secret',"ApiResourceServerController@regenerateClientSecret"); - Route::put('/{id}/active',"ApiResourceServerController@activate"); - Route::delete('/{id}/active',"ApiResourceServerController@deactivate"); + // resouce servers + Route::group(array('prefix' => 'resource-servers', 'before' => 'oauth2.server.admin.json'), function () { + Route::get('/{id}', "ApiResourceServerController@get"); + Route::get('/', "ApiResourceServerController@getByPage"); + Route::post('/', "ApiResourceServerController@create"); + Route::delete('/{id}', "ApiResourceServerController@delete"); + Route::put('/', "ApiResourceServerController@update"); + Route::put('/{id}/client-secret', "ApiResourceServerController@regenerateClientSecret"); + Route::put('/{id}/active', "ApiResourceServerController@activate"); + Route::delete('/{id}/active', "ApiResourceServerController@deactivate"); }); - Route::group(array('prefix' => 'apis', 'before' => 'oauth2.server.admin.json'), function(){ - Route::get('/{id}',"ApiController@get"); - Route::get('/',"ApiController@getByPage"); - Route::post('/',"ApiController@create"); - Route::delete('/{id}',"ApiController@delete"); - Route::put('/',"ApiController@update"); - Route::put('/{id}/active',"ApiController@activate"); - Route::delete('/{id}/active',"ApiController@deactivate"); + // api scope groups + Route::group(array('prefix' => 'api-scope-groups', 'before' => 'oauth2.server.admin.json'), function () { + Route::get('/{id}', "ApiScopeGroupController@get"); + Route::get('/', "ApiScopeGroupController@getByPage"); + Route::put('/', "ApiScopeGroupController@update"); + Route::post('/', "ApiScopeGroupController@create"); + Route::delete('/{id}', "ApiScopeGroupController@delete"); + Route::put('/{id}/active', "ApiScopeGroupController@activate"); + Route::delete('/{id}/active', "ApiScopeGroupController@deactivate"); + Route::get('/users', "ApiScopeGroupController@fetchUsers"); }); - Route::group(array('prefix' => 'scopes', 'before' => 'oauth2.server.admin.json'), function(){ - Route::get('/{id}',"ApiScopeController@get"); - Route::get('/',"ApiScopeController@getByPage"); - Route::post('/',"ApiScopeController@create"); - Route::delete('/{id}',"ApiScopeController@delete"); - Route::put('/',"ApiScopeController@update"); - Route::put('/{id}/active',"ApiScopeController@activate"); - Route::delete('/{id}/active',"ApiScopeController@deactivate"); + // apis + Route::group(array('prefix' => 'apis', 'before' => 'oauth2.server.admin.json'), function () { + Route::get('/{id}', "ApiController@get"); + Route::get('/', "ApiController@getByPage"); + Route::post('/', "ApiController@create"); + Route::delete('/{id}', "ApiController@delete"); + Route::put('/', "ApiController@update"); + Route::put('/{id}/active', "ApiController@activate"); + Route::delete('/{id}/active', "ApiController@deactivate"); }); - Route::group(array('prefix' => 'endpoints', 'before' => 'oauth2.server.admin.json'), function(){ - Route::get('/{id}',"ApiEndpointController@get"); - Route::get('/',"ApiEndpointController@getByPage"); - Route::post('/',"ApiEndpointController@create"); - Route::delete('/{id}',"ApiEndpointController@delete"); - Route::put('/',"ApiEndpointController@update"); - Route::put('/{id}/scope/{scope_id}',"ApiEndpointController@addRequiredScope"); - Route::delete('/{id}/scope/{scope_id}',"ApiEndpointController@removeRequiredScope"); - Route::put('/{id}/active',"ApiEndpointController@activate"); - Route::delete('/{id}/active',"ApiEndpointController@deactivate"); + // scopes + Route::group(array('prefix' => 'scopes', 'before' => 'oauth2.server.admin.json'), function () { + Route::get('/{id}', "ApiScopeController@get"); + Route::get('/', "ApiScopeController@getByPage"); + Route::post('/', "ApiScopeController@create"); + Route::delete('/{id}', "ApiScopeController@delete"); + Route::put('/', "ApiScopeController@update"); + Route::put('/{id}/active', "ApiScopeController@activate"); + Route::delete('/{id}/active', "ApiScopeController@deactivate"); }); + + // endpoints + Route::group(array('prefix' => 'endpoints', 'before' => 'oauth2.server.admin.json'), function () { + Route::get('/{id}', "ApiEndpointController@get"); + Route::get('/', "ApiEndpointController@getByPage"); + Route::post('/', "ApiEndpointController@create"); + Route::delete('/{id}', "ApiEndpointController@delete"); + Route::put('/', "ApiEndpointController@update"); + Route::put('/{id}/scope/{scope_id}', "ApiEndpointController@addRequiredScope"); + Route::delete('/{id}/scope/{scope_id}', "ApiEndpointController@removeRequiredScope"); + Route::put('/{id}/active', "ApiEndpointController@activate"); + Route::delete('/{id}/active', "ApiEndpointController@deactivate"); + }); + + // private keys + Route::group(array('prefix' => 'private-keys', 'before' => 'oauth2.server.admin.json'), function () { + Route::get('/', "ServerPrivateKeyApiController@getByPage"); + Route::post('/', "ServerPrivateKeyApiController@create"); + Route::delete('/{id}', "ServerPrivateKeyApiController@delete"); + Route::put('/{id}', "ServerPrivateKeyApiController@update"); + }); + }); //OAuth2 Protected API -Route::group(array('prefix' => 'api/v1', +Route::group(array( + 'prefix' => 'api/v1', 'before' => 'ssl|oauth2.enabled|oauth2.protected.endpoint', - 'after' => ''), function() -{ - Route::group(array('prefix' => 'users'), function(){ - Route::get('/me','OAuth2UserApiController@me'); + 'after' => '' +), function () { + + Route::group(array('prefix' => 'users'), function () { + Route::get('/me', 'OAuth2UserApiController@me'); + Route::get('/info', 'OAuth2UserApiController@userInfo'); + Route::post('/info', 'OAuth2UserApiController@userInfo'); }); }); \ No newline at end of file diff --git a/app/services/exceptions/ValidationException.php b/app/services/exceptions/ValidationException.php new file mode 100644 index 00000000..6841e3a6 --- /dev/null +++ b/app/services/exceptions/ValidationException.php @@ -0,0 +1,17 @@ +first(); - } - - /** - * @param $uri - * @param $client_id - * @return bool|int - */ - public function create($uri, $client_id) - { - $origin = new ClientAllowedOrigin(); - $origin->allowed_origin = $uri; - $client = Client::find($client_id); - if(!is_null($client)){ - $client->allowed_origins()->save($origin); - $origin->Save(); - return $origin->id; - } - return false; - } - - /** - * @param $id - * @return bool - */ - public function delete($id) - { - $origin = $this->get($id); - if(!is_null($origin)){ - return $origin->delete(); - } - return false; - } - - /** - * @param $uri - * @return bool - */ - public function deleteByUri($uri) - { - $origin = $this->getByUri($uri); - if(!is_null($origin)){ - return $origin->delete(); - } - return false; - } -} \ No newline at end of file diff --git a/app/services/oauth2/ApiScopeGroupService.php b/app/services/oauth2/ApiScopeGroupService.php new file mode 100644 index 00000000..68d873f9 --- /dev/null +++ b/app/services/oauth2/ApiScopeGroupService.php @@ -0,0 +1,203 @@ +log_service = $log_service; + $this->repository = $repository; + $this->user_repository = $user_repository; + $this->scope_service = $scope_service; + $this->tx_service = $tx_service; + } + + public function update($id, array $params) + { + $repository = $this->repository; + $scope_service = $this->scope_service; + $user_repository = $this->user_repository; + + return $this->tx_service->transaction(function () use ($id, $params, $repository, $scope_service, $user_repository) { + + $group = ApiScopeGroup::find($id); + + if (is_null($group)) + { + throw new InvalidApiScopeGroup(sprintf('api scope group id %s does not exists!', $id)); + } + + $allowed_update_params = array('name', 'active', 'description', 'users', 'scopes'); + + foreach ($allowed_update_params as $param) + { + if (array_key_exists($param, $params)) + { + + if ($param == 'name') + { + if (ApiScopeGroup::where('name', '=', $params[$param])->where('id', '<>', $id)->count() > 0) + { + throw new InvalidApiScopeGroup(sprintf('there is already another api scope group name (%s).', $params[$param])); + } + } + if($param === 'scopes') + { + $ids = $group->scopes()->getRelatedIds(); + $group->scopes()->detach($ids); + $scopes = explode(',', $params['scopes']); + foreach($scopes as $scope_id) + { + $scope = $scope_service->get(intval($scope_id)); + if(is_null($scope)) throw new EntityNotFoundException(sprintf('scope %s not found.',$scope_id)); + $group->addScope($scope); + } + } + else if($param === 'users'){ + $ids = $group->users()->getRelatedIds(); + $group->users()->detach($ids); + $users = explode(',', $params['users']); + foreach($users as $user_id) + { + $user = $user_repository->get(intval($user_id)); + if(is_null($user)) throw new EntityNotFoundException(sprintf('user %s not found.',$user_id)); + $group->addUser($user); + } + } + else + $group->{$param} = $params[$param]; + } + } + $repository->add($group); + return true; + }); + } + + /** + * @param int $id + * @param bool $status status (active/non active) + * @return void + */ + public function setStatus($id, $status) + { + return ApiScopeGroup::find($id)->update(array('active' => $status)); + } + + /** + * @param string $name + * @param bool $active + * @param string $scopes + * @param string $users + * @return IApiScopeGroup + */ + public function register($name, $active, $scopes, $users) + { + $repository = $this->repository; + $scope_service = $this->scope_service; + $user_repository = $this->user_repository; + + return $this->tx_service->transaction(function () use ( + $name, + $active, + $scopes, + $users, + $repository, + $scope_service, + $user_repository + ) { + + if (ApiScopeGroup::where('name', '=', trim($name))->count() > 0) + { + throw new InvalidApiScopeGroup(sprintf('there is already another group with that name (%s).', $name)); + } + + $repository->add($instance = new ApiScopeGroup + ( + array + ( + 'name' => trim($name), + 'active' => $active, + 'description' => '' + ) + )); + + $scopes = explode(',', $scopes); + $users = explode(',', $users); + foreach($scopes as $scope_id) + { + $scope = $scope_service->get(intval($scope_id)); + if(is_null($scope)) throw new EntityNotFoundException(sprintf('scope %s not found.',$scope_id)); + $instance->addScope($scope); + } + foreach($users as $user_id) + { + $user = $user_repository->get(intval($user_id)); + if(is_null($user)) throw new EntityNotFoundException(sprintf('user %s not found.',$user_id)); + $instance->addUser($user); + } + return $instance; + }); + } +} \ No newline at end of file diff --git a/app/services/oauth2/ApiScopeService.php b/app/services/oauth2/ApiScopeService.php index dd979417..29a9c59a 100644 --- a/app/services/oauth2/ApiScopeService.php +++ b/app/services/oauth2/ApiScopeService.php @@ -2,28 +2,31 @@ namespace services\oauth2; +use Api; +use ApiScope; +use DB; use oauth2\exceptions\InvalidApi; use oauth2\exceptions\InvalidApiScope; use oauth2\models\IApiScope; use oauth2\services\IApiScopeService; -use ApiScope; -use Api; -use DB; use utils\db\ITransactionService; + /** * Class ApiScopeService * @package services\oauth2 */ -class ApiScopeService implements IApiScopeService { +final class ApiScopeService implements IApiScopeService +{ - private $tx_service; + private $tx_service; - /** - * @param ITransactionService $tx_service - */ - public function __construct(ITransactionService $tx_service){ - $this->tx_service = $tx_service; - } + /** + * @param ITransactionService $tx_service + */ + public function __construct(ITransactionService $tx_service) + { + $this->tx_service = $tx_service; + } /** * @param array $scopes_names @@ -31,35 +34,45 @@ class ApiScopeService implements IApiScopeService { */ public function getScopesByName(array $scopes_names) { - return ApiScope::where('active','=',true)->whereIn('name',$scopes_names)->get(); + return ApiScope::where('active', '=', true)->whereIn('name', $scopes_names)->get(); } /** * @param array $scopes_names * @return mixed */ - public function getFriendlyScopesByName(array $scopes_names){ - return DB::table('oauth2_api_scope')->where('active','=',true)->whereIn('name',$scopes_names)->lists('short_description'); + public function getFriendlyScopesByName(array $scopes_names) + { + return DB::table('oauth2_api_scope')->where('active', '=', true)->whereIn('name', + $scopes_names)->lists('short_description'); } /** * @param bool $system + * @param bool $assigned_by_groups * @return array|mixed */ - public function getAvailableScopes($system=false){ - $scopes = ApiScope - ::with('api') + public function getAvailableScopes($system = false, $assigned_by_groups = false) + { + $scopes = ApiScope + ::with('api') ->with('api.resource_server') - ->where('active','=',true) + ->where('active', '=', true) ->orderBy('api_id')->get(); $res = array(); - foreach($scopes as $scope){ + foreach ($scopes as $scope) + { $api = $scope->api()->first(); - if(!is_null($api) && $api->resource_server()->first()->active && $api->active){ - if($scope->system && !$system) continue; - array_push($res,$scope); + if (!is_null($api) && $api->resource_server()->first()->active && $api->active) { + if ($scope->system && !$system) { + continue; + } + if ($scope->assigned_by_groups && !$assigned_by_groups) { + continue; + } + array_push($res, $scope); } } @@ -70,16 +83,18 @@ class ApiScopeService implements IApiScopeService { * @param array $scopes_names * @return array|mixed */ - public function getAudienceByScopeNames(array $scopes_names){ + public function getAudienceByScopeNames(array $scopes_names) + { $scopes = $this->getScopesByName($scopes_names); $audience = array(); - foreach($scopes as $scope){ + foreach ($scopes as $scope) { $api = $scope->api()->first(); - $resource_server = !is_null($api)? $api->resource_server()->first():null; - if(!is_null($resource_server) && !array_key_exists($resource_server->host, $audience)){ + $resource_server = !is_null($api) ? $api->resource_server()->first() : null; + if (!is_null($resource_server) && !array_key_exists($resource_server->host, $audience)) { $audience[$resource_server->host] = $resource_server->ip; } } + return $audience; } @@ -87,13 +102,15 @@ class ApiScopeService implements IApiScopeService { * @param array $scopes_names * @return string */ - public function getStrAudienceByScopeNames(array $scopes_names){ + public function getStrAudienceByScopeNames(array $scopes_names) + { $audiences = $this->getAudienceByScopeNames($scopes_names); - $audience = ''; - foreach($audiences as $resource_server_host => $ip){ - $audience = $audience . $resource_server_host .' '; + $audience = ''; + foreach ($audiences as $resource_server_host => $ip) { + $audience = $audience . $resource_server_host . ' '; } - $audience = trim($audience); + $audience = trim($audience); + return $audience; } @@ -114,10 +131,11 @@ class ApiScopeService implements IApiScopeService { * @param array $fields * @return mixed */ - public function getAll($page_nbr=1,$page_size=10, array $filters=array(), array $fields=array('*')) + public function getAll($page_nbr = 1, $page_size = 10, array $filters = array(), array $fields = array('*')) { DB::getPaginator()->setCurrentPage($page_nbr); - return ApiScope::Filter($filters)->paginate($page_size,$fields); + + return ApiScope::Filter($filters)->paginate($page_size, $fields); } /** @@ -126,9 +144,10 @@ class ApiScopeService implements IApiScopeService { */ public function save(IApiScope $scope) { - if(!$scope->exists() || count($scope->getDirty())>0){ + if (!$scope->exists() || count($scope->getDirty()) > 0) { return $scope->Save(); } + return true; } @@ -140,25 +159,27 @@ class ApiScopeService implements IApiScopeService { */ public function update($id, array $params) { - $res = false; - $this_var = $this; + $res = false; + $this_var = $this; - $this->tx_service->transaction(function () use ($id,$params,&$res,&$this_var) { + $this->tx_service->transaction(function () use ($id, $params, &$res, &$this_var) { //check that scope exists... $scope = ApiScope::find($id); - if(is_null($scope)) - throw new InvalidApiScope(sprintf('scope id %s does not exists!',$id)); + if (is_null($scope)) { + throw new InvalidApiScope(sprintf('scope id %s does not exists!', $id)); + } - $allowed_update_params = array('name','description','short_description','active','system','default'); + $allowed_update_params = array('name', 'description', 'short_description', 'active', 'system', 'default', 'assigned_by_groups'); - foreach($allowed_update_params as $param){ - if(array_key_exists($param,$params)){ + foreach ($allowed_update_params as $param) { + if (array_key_exists($param, $params)) { - if($param=='name'){ + if ($param == 'name') { //check if we have a former scope with selected name - if(ApiScope::where('name','=',$params[$param])->where('id','<>',$id)->count()>0) - throw new InvalidApiScope(sprintf('scope name %s already exists!',$params[$param])); + if (ApiScope::where('name', '=', $params[$param])->where('id', '<>', $id)->count() > 0) { + throw new InvalidApiScope(sprintf('scope name %s already exists!', $params[$param])); + } } $scope->{$param} = $params[$param]; @@ -166,6 +187,7 @@ class ApiScopeService implements IApiScopeService { } $res = $this_var->save($scope); }); + return $res; } @@ -178,11 +200,12 @@ class ApiScopeService implements IApiScopeService { */ public function setStatus($id, $active) { - $scope = ApiScope::find($id); - if(is_null($scope)) - throw new InvalidApiScope(sprintf('scope id %s does not exists!',$id)); + $scope = ApiScope::find($id); + if (is_null($scope)) { + throw new InvalidApiScope(sprintf('scope id %s does not exists!', $id)); + } - return $scope->update(array('active'=>$active)); + return $scope->update(array('active' => $active)); } /** @@ -193,14 +216,16 @@ class ApiScopeService implements IApiScopeService { public function delete($id) { $res = false; - $this->tx_service->transaction(function () use ($id,&$res) { + $this->tx_service->transaction(function () use ($id, &$res) { $scope = ApiScope::find($id); - if(is_null($scope)) - throw new InvalidApiScope(sprintf('scope id %s does not exists!',$id)); + if (is_null($scope)) { + throw new InvalidApiScope(sprintf('scope id %s does not exists!', $id)); + } $res = $scope->delete(); }); + return $res; } @@ -213,43 +238,85 @@ class ApiScopeService implements IApiScopeService { * @param $default * @param $system * @param $api_id + * @param $assigned_by_groups * @throws \oauth2\exceptions\InvalidApi * @return IApiScope */ - public function add($name, $short_description, $description, $active, $default, $system, $api_id) + public function add($name, $short_description, $description, $active, $default, $system, $api_id, $assigned_by_groups) { $instance = null; - $this->tx_service->transaction(function () use ($name, $short_description, $description, $active, $default, $system, $api_id, &$instance) { + $this->tx_service->transaction(function () use ( + $name, + $short_description, + $description, + $active, + $default, + $system, + $api_id, + &$instance + ) { // check if api exists... - if(is_null(Api::find($api_id))) - throw new InvalidApi(sprintf('api id %s does not exists!.',$api_id)); + if (is_null(Api::find($api_id))) { + throw new InvalidApi(sprintf('api id %s does not exists!.', $api_id)); + } //check if we have a former scope with selected name - if(ApiScope::where('name','=',$name)->count()>0) - throw new InvalidApiScope(sprintf('scope name %s not allowed.',$name)); + if (ApiScope::where('name', '=', $name)->count() > 0) { + throw new InvalidApiScope(sprintf('scope name %s not allowed.', $name)); + } - $instance = new ApiScope( - array( - 'name' => $name, - 'description' => $description, - 'short_description' => $short_description, - 'active' => $active, - 'default' => $default, - 'system' => $system, - 'api_id' => $api_id + $instance = new ApiScope + ( + array + ( + 'name' => $name, + 'description' => $description, + 'short_description' => $short_description, + 'active' => $active, + 'default' => $default, + 'system' => $system, + 'api_id' => $api_id, + 'assigned_by_groups' => $assigned_by_groups ) ); $instance->Save(); }); + return $instance; } /** * @return mixed */ - public function getDefaultScopes(){ - return $scopes = ApiScope::where('default','=',true)->where('active','=',true)->get(array('id')); + public function getDefaultScopes() + { + return $scopes = ApiScope::where('default', '=', true)->where('active', '=', true)->get(array('id')); + } + + /** + * @return mixed + */ + public function getAssignedByGroups() + { + $scopes = ApiScope + ::with('api') + ->with('api.resource_server') + ->where('active', '=', true) + ->where('assigned_by_groups', '=', true) + ->orderBy('api_id')->get(); + + $res = array(); + + foreach ($scopes as $scope) + { + $api = $scope->api()->first(); + if (!is_null($api) && $api->resource_server()->first()->active && $api->active) { + array_push($res, $scope); + } + } + + return $res; } } \ No newline at end of file diff --git a/app/services/oauth2/AssymetricKeyService.php b/app/services/oauth2/AssymetricKeyService.php new file mode 100644 index 00000000..28b9c28b --- /dev/null +++ b/app/services/oauth2/AssymetricKeyService.php @@ -0,0 +1,112 @@ +tx_service = $tx_service; + $this->repository = $repository; + } + + /** + * @param array $params + * @return IAssymetricKey + */ + abstract public function register(array $params); + + + /** + * @param int $key_id + * @return bool + */ + public function delete($key_id) + { + $repository = $this->repository; + + return $this->tx_service->transaction(function() use($key_id, $repository) + { + + $key = $repository->getById($key_id); + if(!$key) return false; + $repository->delete($key); + return true; + }); + } + + /** + * @param int $key_id + * @param array $params + * @return bool + */ + public function update($key_id, array $params) + { + $repository = $this->repository; + + return $this->tx_service->transaction(function () use ($key_id, $params, $repository) { + + $key = $repository->getById($key_id); + + if (is_null($key)) + { + return false; + } + + $owner_id = $key->oauth2_client_id; + + $key_active = $repository->getActiveByCriteria($key->getType(), $key->getUse(), $key->getAlg()->getName()); + + if($key_active && $params['active'] === true) + { + $key_active->active = false; + $repository->add($key_active); + } + + $allowed_update_params = array + ( + 'active', + ); + + foreach ($allowed_update_params as $param) { + if (array_key_exists($param, $params)) { + $key->{$param} = $params[$param]; + } + } + + $repository->add($key); + return true; + }); + } + + +} \ No newline at end of file diff --git a/app/services/oauth2/AuthorizationCodeRedeemPolicy.php b/app/services/oauth2/AuthorizationCodeRedeemPolicy.php index 0162ef59..e02497f1 100644 --- a/app/services/oauth2/AuthorizationCodeRedeemPolicy.php +++ b/app/services/oauth2/AuthorizationCodeRedeemPolicy.php @@ -1,26 +1,32 @@ getAuthCode(); - $this->counter_measure->trigger(array('auth_code'=>$auth_code)); + + if ($ex instanceof ReplayAttackException) { + $token = $ex->getToken(); + $this->counter_measure->trigger + ( + array + ( + 'auth_code' => $token + ) + ); } } catch (Exception $ex) { Log::error($ex); diff --git a/app/services/oauth2/CORS/CORSMiddleware.php b/app/services/oauth2/CORS/CORSMiddleware.php index d7b04376..5b6aac5a 100644 --- a/app/services/oauth2/CORS/CORSMiddleware.php +++ b/app/services/oauth2/CORS/CORSMiddleware.php @@ -2,30 +2,40 @@ namespace services\oauth2\CORS; +use App; +use Config; +use Exception; +use Log; use oauth2\models\IApiEndpoint; -use oauth2\services\IAllowedOriginService; +use oauth2\services\IClientService; use oauth2\services\IApiEndpointService; +use Route; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use utils\services\ICacheService; -use Route; -use App; -use Log; -use Exception; -use Config; -use Illuminate\Support\Facades\Cache; + /** * Class CORSMiddleware * @package services\oauth2 * Implementation of http://www.w3.org/TR/cors/ */ -class CORSMiddleware { +final class CORSMiddleware +{ + /** + * @var IApiEndpointService + */ private $endpoint_service; + /** + * @var ICacheService + */ private $cache_service; - private $origin_service; + /** + * @var IClientService + */ + private $client_service; private $actual_request = false; - private $headers = array(); + private $headers = array(); private $allowed_headers; private $allowed_methods; /** @@ -41,16 +51,24 @@ class CORSMiddleware { const DefaultAllowedHeaders = 'origin, content-type, accept, authorization, x-requested-with'; const DefaultAllowedMethods = 'GET, POST, OPTIONS, PUT, DELETE'; - public function __construct(IApiEndpointService $endpoint_service, - ICacheService $cache_service, - IAllowedOriginService $origin_service) + /** + * @param IApiEndpointService $endpoint_service + * @param ICacheService $cache_service + * @param IClientService $client_service + */ + public function __construct + ( + IApiEndpointService $endpoint_service, + ICacheService $cache_service, + IClientService $client_service + ) { - $this->endpoint_service = $endpoint_service; - $this->cache_service = $cache_service; - $this->origin_service = $origin_service; + $this->endpoint_service = $endpoint_service; + $this->cache_service = $cache_service; + $this->client_service = $client_service; - $this->allowed_headers = Config::get('cors.AllowedHeaders',self::DefaultAllowedHeaders); - $this->allowed_methods = Config::get('cors.AllowedMethods',self::DefaultAllowedMethods); + $this->allowed_headers = Config::get('cors.AllowedHeaders', self::DefaultAllowedHeaders); + $this->allowed_methods = Config::get('cors.AllowedMethods', self::DefaultAllowedMethods); } /** @@ -60,20 +78,21 @@ class CORSMiddleware { * @param IApiEndpoint $endpoint * @return Response */ - private function makePreflightResponse(Request $request, IApiEndpoint $endpoint){ + private function makePreflightResponse(Request $request, IApiEndpoint $endpoint) + { $response = new Response(); $allow_credentials = Config::get('cors.AllowCredentials', ''); - if(!empty($allow_credentials)){ + if (!empty($allow_credentials)) { // The Access-Control-Allow-Credentials header indicates whether the response to request // can be exposed when the omit credentials flag is unset. When part of the response to a preflight request // it indicates that the actual request can include user credentials. - $response->headers->set('Access-Control-Allow-Credentials',$allow_credentials ); + $response->headers->set('Access-Control-Allow-Credentials', $allow_credentials); } - if(Config::get('cors.UsePreflightCaching', false)){ + if (Config::get('cors.UsePreflightCaching', false)) { // The Access-Control-Max-Age header indicates how long the response can be cached, so that for // subsequent requests, within the specified time, no preflight request has to be made. $response->headers->set('Access-Control-Max-Age', Config::get('cors.MaxAge', 32000)); @@ -85,6 +104,7 @@ class CORSMiddleware { if (!$this->checkOrigin($request)) { $response->headers->set('Access-Control-Allow-Origin', 'null'); $response->setStatusCode(403); + return $response; } @@ -96,6 +116,7 @@ class CORSMiddleware { if ($request->headers->get('Access-Control-Request-Method') != $endpoint->getHttpMethod()) { $response->setStatusCode(405); + return $response; } //The Access-Control-Allow-Methods header indicates, as part of the response to a preflight request, @@ -108,8 +129,8 @@ class CORSMiddleware { $headers = $request->headers->get('Access-Control-Request-Headers'); if ($headers) { - $headers = trim(strtolower($headers)); - $allow_headers = explode(', ',$this->allowed_headers); + $headers = trim(strtolower($headers)); + $allow_headers = explode(', ', $this->allowed_headers); foreach (preg_split('{, *}', $headers) as $header) { //if they are simple headers then skip them if (in_array($header, self::$simple_headers, true)) { @@ -118,13 +139,14 @@ class CORSMiddleware { //check is the requested header is on the list of allowed headers if (!in_array($header, $allow_headers, true)) { $response->setStatusCode(400); - $response->setContent('Unauthorized header '.$header); + $response->setContent('Unauthorized header ' . $header); break; } } } //OK - No Content $response->setStatusCode(204); + return $response; } @@ -132,17 +154,22 @@ class CORSMiddleware { { // check origin $origin = $request->headers->get('Origin'); - if($this->cache_service->getSingleValue($origin)) return true; - if($origin = $this->origin_service->getByUri($origin)){ - $this->cache_service->addSingleValue($origin,$origin); + if ($this->cache_service->getSingleValue($origin)) { return true; } - Log::warning(sprintf('CORS: origin %s not allowed!',$origin)); + if ($client = $this->client_service->getByOrigin($origin)) + { + $this->cache_service->addSingleValue($origin, $origin); + return true; + } + Log::warning(sprintf('CORS: origin %s not allowed!', $origin)); + return false; } - public function verifyRequest($request){ - try{ + public function verifyRequest($request) + { + try { /** * The presence of the Origin header does not necessarily mean that the request is a cross-origin request. * While all cross-origin requests will contain an Origin header, @@ -155,40 +182,42 @@ class CORSMiddleware { return; } //https://www.owasp.org/index.php/CORS_OriginHeaderScrutiny - $origin = $request->headers->get('Origin',null,false); - $host = $request->headers->get('Host',null,false); - if(is_array($origin) && count($origin)>1){ + $origin = $request->headers->get('Origin', null, false); + $host = $request->headers->get('Host', null, false); + if (is_array($origin) && count($origin) > 1) { // If we reach this point it means that we have multiple instance of the "Origin" header $response = new Response(); $response->setStatusCode(403); + return $response; } //now get the first one - $origin = $request->headers->get('Origin'); - $server_name = isset($_SERVER['SERVER_NAME'])?$_SERVER['SERVER_NAME']:null; - $origin_host = @parse_url($origin,PHP_URL_HOST); + $origin = $request->headers->get('Origin'); + $server_name = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; + $origin_host = @parse_url($origin, PHP_URL_HOST); //Have only one and non empty instance of the host header, - if(is_array($host) && count($host)>1){ + if (is_array($host) && count($host) > 1) { // If we reach this point it means that we have multiple instance of the "Host" header $response = new Response(); $response->setStatusCode(403); + return; } //now get the first one $host = $request->headers->get('Host'); - if(is_null($host) || $server_name != $host || is_null($origin_host) || $origin_host == $server_name){ + if (is_null($host) || $server_name != $host || is_null($origin_host) || $origin_host == $server_name) { return; } - $method = $request->getMethod(); + $method = $request->getMethod(); $preflight = false; //preflight checks if ($method == 'OPTIONS') { - $request_method = $request->headers->get('Access-Control-Request-Method'); - if(!is_null($request_method)){ + $request_method = $request->headers->get('Access-Control-Request-Method'); + if (!is_null($request_method)) { // sets the original method on request in order to be able to find the // correct route $request->setMethod($request_method); @@ -197,34 +226,37 @@ class CORSMiddleware { } //gets routes from container and try to find the route - $router = App::make('router'); - $routes = $router->getRoutes(); - $route = $routes->match($request); - $url = $route->getPath(); + $router = App::make('router'); + $routes = $router->getRoutes(); + $route = $routes->match($request); + $url = $route->getPath(); - if(strpos($url, '/') != 0){ - $url = '/'.$url; + if (strpos($url, '/') != 0) { + $url = '/' . $url; } $endpoint = $this->endpoint_service->getApiEndpointByUrl($url); //check if api endpoint exists or not, if active and if supports cors - if(is_null($endpoint) || !$endpoint->isActive() || !$endpoint->supportCORS()){ + if (is_null($endpoint) || !$endpoint->isActive() || !$endpoint->supportCORS()) { - if(is_null($endpoint)){ - Log::warning(sprintf("does not exists an endpoint for url %s.",$url)); - } - else if(!$endpoint->isActive()){ - Log::warning(sprintf("endpoint %s is not active.",$url)); - } - else if(!$endpoint->supportCORS()){ - Log::warning(sprintf("endpoint %s does not support CORS.",$url)); + if (is_null($endpoint)) { + Log::warning(sprintf("does not exists an endpoint for url %s.", $url)); + } else { + if (!$endpoint->isActive()) { + Log::warning(sprintf("endpoint %s is not active.", $url)); + } else { + if (!$endpoint->supportCORS()) { + Log::warning(sprintf("endpoint %s does not support CORS.", $url)); + } + } } + return; } //perform preflight checks if ($preflight) { - return $this->makePreflightResponse($request,$endpoint); + return $this->makePreflightResponse($request, $endpoint); } //Actual Request if (!$this->checkOrigin($request)) { @@ -234,13 +266,13 @@ class CORSMiddleware { $this->actual_request = true; // Save response headers - $this->headers['Access-Control-Allow-Origin'] = $request->headers->get('Origin'); + $this->headers['Access-Control-Allow-Origin'] = $request->headers->get('Origin'); $allow_credentials = Config::get('cors.AllowCredentials', ''); - if(!empty($allow_credentials)){ + if (!empty($allow_credentials)) { // The Access-Control-Allow-Credentials header indicates whether the response to request // can be exposed when the omit credentials flag is unset. When part of the response to a preflight request // it indicates that the actual request can include user credentials. - $this->headers['Access-Control-Allow-Credentials'] = $allow_credentials ; + $this->headers['Access-Control-Allow-Credentials'] = $allow_credentials; } /** @@ -258,24 +290,24 @@ class CORSMiddleware { * to the client. */ $exposed_headers = Config::get('cors.ExposedHeaders', ''); - if(!empty($exposed_headers)){ - $this->headers['Access-Control-Expose-Headers'] = $exposed_headers ; + if (!empty($exposed_headers)) { + $this->headers['Access-Control-Expose-Headers'] = $exposed_headers; } - } - catch(Exception $ex){ + } catch (Exception $ex) { Log::error($ex); } } public function modifyResponse($request, $response) { - if(!$this->actual_request){ + if (!$this->actual_request) { return $response; } // add CORS response headers Log::info('CORS: Adding CORS HEADERS.'); $response->headers->add($this->headers); + return $response; } diff --git a/app/services/oauth2/CORS/CORSProvider.php b/app/services/oauth2/CORS/CORSProvider.php index b2a4abbf..848c2575 100644 --- a/app/services/oauth2/CORS/CORSProvider.php +++ b/app/services/oauth2/CORS/CORSProvider.php @@ -2,23 +2,25 @@ namespace services\oauth2\CORS; -use Illuminate\Support\ServiceProvider; use App; +use Illuminate\Support\ServiceProvider; -class CORSProvider extends ServiceProvider { +class CORSProvider extends ServiceProvider +{ protected $defer = false; + /** * Register the service provider. - * * @return void */ public function register() { - App::singleton('CORSMiddleware', 'services\oauth2\CORS\CORSMiddleware'); + App::singleton('CORSMiddleware', 'services\oauth2\CORS\CORSMiddleware'); } - public function boot(){ + public function boot() + { } diff --git a/app/services/oauth2/ClienPublicKeyService.php b/app/services/oauth2/ClienPublicKeyService.php new file mode 100644 index 00000000..deccc0b7 --- /dev/null +++ b/app/services/oauth2/ClienPublicKeyService.php @@ -0,0 +1,102 @@ +client_repository = $client_repository; + parent::__construct($repository, $tx_service); + } + + /** + * @param array $params + * @return IAssymetricKey + */ + public function register(array $params) + { + $client_repository = $this->client_repository; + $repository = $this->repository; + + return $this->tx_service->transaction(function() use($params, $repository, $client_repository) + { + + if ($repository->getByPEM($params['pem_content'])) + { + throw new ValidationException('public key already exists on another client, choose another one!.'); + } + + $client = $client_repository->get(intval($params['client_id'])); + + if(is_null($client)) + throw new ValidationException('client does not exits!'); + + $existent_kid = $client->public_keys()->where('kid','=', $params['kid'])->first(); + + if ($existent_kid) + { + throw new ValidationException('public key identifier (kid) already exists!.'); + } + + $old_key_active = $client->public_keys() + ->where('type','=', $params['type']) + ->where('usage','=', $params['usage']) + ->where('alg','=', $params['alg']) + ->where('valid_from','<=',new \DateTime($params['valid_to'])) + ->where('valid_to','>=', new \DateTime($params['valid_from'])) + ->first(); + + $public_key = ClientPublicKey::buildFromPEM + ( + $params['kid'], + $params['type'], + $params['usage'], + $params['pem_content'], + $params['alg'], + $old_key_active ? false : $params['active'], + new \DateTime($params['valid_from']), + new \DateTime($params['valid_to']) + ); + + $client->addPublicKey($public_key); + return $public_key; + }); + } + +} \ No newline at end of file diff --git a/app/services/oauth2/ClientCrendentialGenerator.php b/app/services/oauth2/ClientCrendentialGenerator.php new file mode 100644 index 00000000..9d2801c6 --- /dev/null +++ b/app/services/oauth2/ClientCrendentialGenerator.php @@ -0,0 +1,49 @@ +client_id = Rand::getString(32, OAuth2Protocol::VsChar, true) . '.openstack.client'; + + if ($client->client_type === IClient::ClientType_Confidential) + { + $now = new \DateTime(); + $client->client_secret = Rand::getString(64, OAuth2Protocol::VsChar, true); + // default 6 months + $client->client_secret_expires_at = $now->add( new \DateInterval('P6M')); + } + return $client; + } +} \ No newline at end of file diff --git a/app/services/oauth2/ClientService.php b/app/services/oauth2/ClientService.php index e0b1f39b..21a7ce8e 100644 --- a/app/services/oauth2/ClientService.php +++ b/app/services/oauth2/ClientService.php @@ -3,28 +3,30 @@ namespace services\oauth2; use Client; -use ClientAuthorizedUri; -use ClientAllowedOrigin; - use DB; +use Event; use Input; -use oauth2\exceptions\AllowedClientUriAlreadyExistsException; +use oauth2\exceptions\AbsentClientException; +use oauth2\exceptions\InvalidApiScope; +use oauth2\exceptions\InvalidClientAuthMethodException; use oauth2\exceptions\InvalidClientType; use oauth2\exceptions\MissingClientAuthorizationInfo; -use oauth2\exceptions\AbsentClientException; -use oauth2\exceptions\InvalidAllowedClientUriException; - +use oauth2\models\ClientAssertionAuthenticationContext; +use oauth2\models\ClientAuthenticationContext; +use oauth2\models\ClientCredentialsAuthenticationContext; use oauth2\models\IClient; use oauth2\OAuth2Protocol; -use oauth2\services\IApiScopeService; use oauth2\services\IApiScope; +use oauth2\services\IApiScopeService; +use oauth2\services\IClientCrendentialGenerator; use oauth2\services\IClientService; use oauth2\services\id; use Request; -use utils\services\IAuthService; -use Zend\Math\Rand; -use Event; +use URL\Normalizer; use utils\db\ITransactionService; +use utils\exceptions\EntityNotFoundException; +use utils\http\HttpUtils; +use utils\services\IAuthService; /** * Class ClientService @@ -32,21 +34,41 @@ use utils\db\ITransactionService; */ class ClientService implements IClientService { + /** + * @var IAuthService + */ private $auth_service; + /** + * @var IApiScopeService + */ private $scope_service; - /** - * @param IAuthService $auth_service - * @param IApiScopeService $scope_service - * @param ITransactionService $tx_service - */ - public function __construct(IAuthService $auth_service, IApiScopeService $scope_service,ITransactionService $tx_service) + /** + * @var IClientCrendentialGenerator + */ + private $client_credential_generator; + + /** + * @param IAuthService $auth_service + * @param IApiScopeService $scope_service + * @param IClientCrendentialGenerator $client_credential_generator + * @param ITransactionService $tx_service + */ + public function __construct + ( + IAuthService $auth_service, + IApiScopeService $scope_service, + IClientCrendentialGenerator $client_credential_generator, + ITransactionService $tx_service + ) { - $this->auth_service = $auth_service; - $this->scope_service = $scope_service; - $this->tx_service = $tx_service; + $this->auth_service = $auth_service; + $this->scope_service = $scope_service; + $this->client_credential_generator = $client_credential_generator; + $this->tx_service = $tx_service; } + /** * Clients in possession of a client password MAY use the HTTP Basic * authentication scheme as defined in [RFC2617] to authenticate with @@ -55,150 +77,185 @@ class ClientService implements IClientService * client credentials in the request-body using the following * parameters: * implementation of http://tools.ietf.org/html/rfc6749#section-2.3.1 - * @throws MissingClientAuthorizationInfo; - * @return list + * implementation of http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + * @throws InvalidClientAuthMethodException + * @throws MissingClientAuthorizationInfo + * @return ClientAuthenticationContext */ public function getCurrentClientAuthInfo() { - //check first http basic auth header + $auth_header = Request::header('Authorization'); - if (!is_null($auth_header) && !empty($auth_header)) { + if + ( + Input::has( OAuth2Protocol::OAuth2Protocol_ClientAssertionType) && + Input::has( OAuth2Protocol::OAuth2Protocol_ClientAssertion) + ) + { + return new ClientAssertionAuthenticationContext + ( + Input::get(OAuth2Protocol::OAuth2Protocol_ClientAssertionType, ''), + Input::get(OAuth2Protocol::OAuth2Protocol_ClientAssertion, '') + ); + } + if + ( + Input::has( OAuth2Protocol::OAuth2Protocol_ClientId) && + Input::has( OAuth2Protocol::OAuth2Protocol_ClientSecret) + ) + { + return new ClientCredentialsAuthenticationContext + ( + Input::get(OAuth2Protocol::OAuth2Protocol_ClientId, ''), + Input::get(OAuth2Protocol::OAuth2Protocol_ClientSecret, ''), + OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost + ); + } + if(!empty($auth_header)) + { $auth_header = trim($auth_header); $auth_header = explode(' ', $auth_header); if (!is_array($auth_header) || count($auth_header) < 2) - throw new MissingClientAuthorizationInfo; + { + throw new MissingClientAuthorizationInfo('bad auth header.'); + } $auth_header_content = $auth_header[1]; $auth_header_content = base64_decode($auth_header_content); $auth_header_content = explode(':', $auth_header_content); if (!is_array($auth_header_content) || count($auth_header_content) !== 2) - throw new MissingClientAuthorizationInfo; + { + throw new MissingClientAuthorizationInfo('bad auth header.'); + } - //client_id:client_secret - return array($auth_header_content[0], $auth_header_content[1]); + return new ClientCredentialsAuthenticationContext( + $auth_header_content[0], + $auth_header_content[1], + OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic + ); } - //if not get from http input - $client_id = Input::get(OAuth2Protocol::OAuth2Protocol_ClientId, ''); - $client_secret = Input::get(OAuth2Protocol::OAuth2Protocol_ClientSecret, ''); - return array($client_id, $client_secret); + + throw new InvalidClientAuthMethodException; } - public function addClient($application_type, $user_id, $app_name, $app_description,$app_url=null, $app_logo = '') + public function addClient($application_type, $user_id, $app_name, $app_description, $app_url = null, $app_logo = '') { - $instance = null; - $this_var = $this; - $scope_service = $this_var->scope_service; + $scope_service = $this->scope_service; + $client_credential_generator = $this->client_credential_generator; - $this->tx_service->transaction(function () use ($application_type, $user_id, $app_name,$app_url, $app_description, $app_logo, &$instance, &$this_var,&$scope_service) { + return $this->tx_service->transaction(function () use ( + $application_type, + $user_id, + $app_name, + $app_url, + $app_description, + $app_logo, + $scope_service, + $client_credential_generator + ) { - //check $application_type vs client_type - $client_type = $application_type == IClient::ApplicationType_JS_Client? IClient::ClientType_Public : IClient::ClientType_Confidential; - $instance = new Client; - $instance->app_name = $app_name; - $instance->app_logo = $app_logo; - $instance->client_id = Rand::getString(32, OAuth2Protocol::VsChar, true) . '.openstack.client'; - //only generates secret for confidential clients - if ($client_type == IClient::ClientType_Confidential) - $instance->client_secret = Rand::getString(24, OAuth2Protocol::VsChar, true); + $client = new Client + ( + array + ( + 'max_auth_codes_issuance_basis' => 0, + 'max_refresh_token_issuance_basis' => 0, + 'max_access_token_issuance_qty' => 0, + 'max_access_token_issuance_basis' => 0, + 'max_refresh_token_issuance_qty' => 0, + 'use_refresh_token' => false, + 'rotate_refresh_token' => false, + ) + ); - $instance->client_type = $client_type; - $instance->application_type = $application_type; + $client->app_name = $app_name; + $client->app_logo = $app_logo; + $client->app_description = $app_description; + $client->application_type = $application_type; + $client = $client_credential_generator->generate($client); - $instance->user_id = $user_id; - $instance->active = true; - $instance->use_refresh_token = false; - $instance->rotate_refresh_token = false; - $instance->website = $app_url; - $instance->Save(); - //default allowed url - $this_var->addClientAllowedUri($instance->getId(), 'https://localhost'); + if ($client->client_type === IClient::ClientType_Confidential) { + $client->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic; + } else { + $client->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_None; + } + + $client->user_id = $user_id; + $client->active = true; + $client->use_refresh_token = false; + $client->rotate_refresh_token = false; + $client->website = $app_url; + $client->Save(); //add default scopes $default_scopes = $scope_service->getDefaultScopes(); - foreach($default_scopes as $default_scope){ - $instance->scopes()->attach($default_scope->id); + foreach ($default_scopes as $default_scope) { + if + ( + $default_scope->name === OAuth2Protocol::OfflineAccess_Scope && + !( + $client->application_type == IClient::ApplicationType_Native || + $client->application_type == IClient::ApplicationType_Web_App + ) + ) { + continue; + } + $client->scopes()->attach($default_scope->id); } + return $client; }); - return $instance; - } - - public function addClientAllowedUri($id, $uri) - { - $res = false; - $this->tx_service->transaction(function () use ($id,$uri,&$res){ - $client = Client::find($id); - - if (is_null($client)) - throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); - - if(!filter_var($uri, FILTER_VALIDATE_URL)) return false; - $parts = @parse_url($uri); - if (!$parts) { - throw new InvalidAllowedClientUriException(sprintf('uri : %s', $uri)); - } - if(($parts['scheme']!=='https') && (ServerConfigurationService::getConfigValue("SSL.Enable"))) - throw new InvalidAllowedClientUriException(sprintf('uri : %s', $uri)); - //normalize uri - $normalized_uri = $parts['scheme'].'://'.strtolower($parts['host']); - if(isset($parts['path'])) { - $normalized_uri .= strtolower($parts['path']); - } - // normalize url and remove trailing / - $normalized_uri = rtrim($normalized_uri, '/'); - - $client_uri = ClientAuthorizedUri::where('uri', '=', $normalized_uri)->where('client_id', '=', $id)->first(); - if (!is_null($client_uri)) { - throw new AllowedClientUriAlreadyExistsException(sprintf('uri : %s', $normalized_uri)); - } - - $client_authorized_uri = new ClientAuthorizedUri; - $client_authorized_uri->client_id = $id; - $client_authorized_uri->uri = $uri; - $res = $client_authorized_uri->Save(); - }); - return $res; } public function addClientScope($id, $scope_id) { $client = Client::find($id); - if (is_null($client)) - throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); + if (is_null($client)) { + throw new EntityNotFoundException(sprintf("client id %s not found!.", $id)); + } + $scope = $this->scope_service->get(intval($scope_id)); + if(is_null($scope)) throw new EntityNotFoundException(sprintf("scope %s not found!.", $scope_id)); + $user = $client->user()->first(); + + if($scope->isAssignableByGroups()) { + + $allowed = false; + foreach($user->getGroupScopes() as $group_scope) + { + if(intval($group_scope->id) === intval($scope_id)) + { + $allowed = true; break; + } + } + if(!$allowed) throw new InvalidApiScope(sprintf('you cant assign to this client api scope %s', $scope_id)); + } + if($scope->isSystem() && !$user->canUseSystemScopes()) + throw new InvalidApiScope(sprintf('you cant assign to this client api scope %s', $scope_id)); return $client->scopes()->attach($scope_id); } public function deleteClientScope($id, $scope_id) { $client = Client::find($id); - if (is_null($client)) - throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); - return $client->scopes()->detach($scope_id); - } + if (is_null($client)) { + throw new AbsentClientException(sprintf("client id %s does not exists!", $id)); + } - /** - * Deletes a former client allowed redirection Uri - * @param $id client identifier - * @param $uri_id uri identifier - */ - public function deleteClientAllowedUri($id, $uri_id) - { - return ClientAuthorizedUri::where('id', '=', $uri_id)->where('client_id', '=', $id)->delete(); + return $client->scopes()->detach($scope_id); } public function deleteClientByIdentifier($id) { $res = false; - $this->tx_service->transaction(function () use ($id,&$res){ + $this->tx_service->transaction(function () use ($id, &$res) { $client = Client::find($id); if (!is_null($client)) { - $client->authorized_uris()->delete(); $client->scopes()->detach(); - Event::fire('oauth2.client.delete', array($client->client_id)); + Event::fire('oauth2.client.delete', array($client->client_id)); $res = $client->delete(); } }); @@ -208,29 +265,39 @@ class ClientService implements IClientService /** * Regenerates Client Secret * @param $id client id - * @return mixed + * @return IClient */ public function regenerateClientSecret($id) { - $new_secret = ''; - $this->tx_service->transaction(function () use ($id, &$new_secret) { + $client_credential_generator = $this->client_credential_generator; + + return $this->tx_service->transaction(function () use ($id, $client_credential_generator) + { $client = Client::find($id); - if(is_null($client)) - throw new AbsentClientException(sprintf("client id %d does not exists!.",$id)); + if (is_null($client)) + { + throw new AbsentClientException(sprintf("client id %d does not exists!.", $id)); + } - if($client->client_type != IClient::ClientType_Confidential) - throw new InvalidClientType($id,sprintf("client id %d is not confidential!.",$id)); + if ($client->client_type != IClient::ClientType_Confidential) + { + throw new InvalidClientType + ( + sprintf + ( + "client id %d is not confidential type!.", + $id + ) + ); + } - $client_secret = Rand::getString(24, OAuth2Protocol::VsChar, true); - $client->client_secret = $client_secret; - $client->Save(); - Event::fire('oauth2.client.regenerate.secret', array($client->client_id)); - $new_secret = $client->client_secret; + $client_credential_generator->generate($client, true)->save(); + Event::fire('oauth2.client.regenerate.secret', array($client->client_id)); + return $client; }); - return $new_secret; } /** @@ -240,17 +307,19 @@ class ClientService implements IClientService */ public function lockClient($client_id) { - $res = false; - $this_var = $this; + $res = false; + $this_var = $this; - $this->tx_service->transaction(function () use ($client_id, &$res, &$this_var) { + $this->tx_service->transaction(function () use ($client_id, &$res, &$this_var) { $client = $this_var->getClientByIdentifier($client_id); - if (is_null($client)) - throw new AbsentClientException($client_id,sprintf("client id %s does not exists!",$client_id)); + if (is_null($client)) { + throw new AbsentClientException($client_id, sprintf("client id %s does not exists!", $client_id)); + } $client->locked = true; - $res = $client->Save(); + $res = $client->Save(); }); + return $res; } @@ -261,21 +330,22 @@ class ClientService implements IClientService */ public function unlockClient($client_id) { - $res = false; - $this_var = $this; + $res = false; + $this_var = $this; - $this->tx_service->transaction(function () use ($client_id, &$res, &$this_var) { + $this->tx_service->transaction(function () use ($client_id, &$res, &$this_var) { $client = $this_var->getClientByIdentifier($client_id); - if (is_null($client)) - throw new AbsentClientException($client_id,sprintf("client id %s does not exists!",$client_id)); + if (is_null($client)) { + throw new AbsentClientException($client_id, sprintf("client id %s does not exists!", $client_id)); + } $client->locked = false; - $res = $client->Save(); + $res = $client->Save(); }); + return $res; } - /** * @param $client_id * @return IClient @@ -283,40 +353,48 @@ class ClientService implements IClientService public function getClientById($client_id) { $client = Client::where('client_id', '=', $client_id)->first(); + return $client; } public function activateClient($id, $active) { $client = $this->getClientByIdentifier($id); - if (is_null($client)) - throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); + if (is_null($client)) { + throw new AbsentClientException(sprintf("client id %s does not exists!", $id)); + } $client->active = $active; + return $client->Save(); } public function getClientByIdentifier($id) { $client = Client::where('id', '=', $id)->first(); + return $client; } public function setRefreshTokenUsage($id, $use_refresh_token) { $client = $this->getClientByIdentifier($id); - if (is_null($client)) - throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); + if (is_null($client)) { + throw new AbsentClientException(sprintf("client id %s does not exists!", $id)); + } $client->use_refresh_token = $use_refresh_token; + return $client->Save(); } public function setRotateRefreshTokenPolicy($id, $rotate_refresh_token) { $client = $this->getClientByIdentifier($id); - if (is_null($client)) - throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); + if (is_null($client)) { + throw new AbsentClientException(sprintf("client id %s does not exists!", $id)); + } $client->rotate_refresh_token = $rotate_refresh_token; + return $client->Save(); } @@ -342,10 +420,11 @@ class ClientService implements IClientService * @param array $fields * @return mixed */ - public function getAll($page_nbr=1,$page_size=10,array $filters=array(), array $fields=array('*')) + public function getAll($page_nbr = 1, $page_size = 10, array $filters = array(), array $fields = array('*')) { DB::getPaginator()->setCurrentPage($page_nbr); - return Client::Filter($filters)->paginate($page_size,$fields); + + return Client::Filter($filters)->paginate($page_size, $fields); } /** @@ -354,12 +433,14 @@ class ClientService implements IClientService */ public function save(IClient $client) { - if(!$client->exists() || count($client->getDirty())>0){ + if (!$client->exists() || count($client->getDirty()) > 0) { return $client->Save(); } + return true; } + /** * @param $id * @param array $params @@ -368,70 +449,164 @@ class ClientService implements IClientService */ public function update($id, array $params) { - $res = false; - $this_var = $this; + $this_var = $this; - $this->tx_service->transaction(function () use ($id,$params, &$res, &$this_var) { + return $this->tx_service->transaction(function () use ($id, $params, &$this_var) { $client = Client::find($id); - if(is_null($client)) - throw new AbsentClientException(sprintf('client id %s does not exists!',$id)); + if (is_null($client)) { + throw new AbsentClientException(sprintf('client id %s does not exists.', $id)); + } + $current_app_type = $client->getApplicationType(); + if($current_app_type !== $params['application_type']) + { + throw new \ValidationException('application type does not match.'); + } + + // validate uris + switch($current_app_type) + { + case IClient::ApplicationType_Native: + { + $redirect_uris = explode(',', $params['redirect_uris']); + if(!isset($params['redirect_uris'])) throw new \ValidationException('redirect_uris param is required.'); + //check that custom schema does not already exists for another registerd app + if(!empty($params['redirect_uris'])) { + foreach ($redirect_uris as $uri) { + $uri = @parse_url($uri); + if (!isset($uri['scheme'])) { + throw new \ValidationException('invalid scheme on redirect uri.'); + } + if (HttpUtils::isCustomSchema($uri['scheme'])) { + $already_has_schema_registered = Client::where('redirect_uris', 'like', + '%' . $uri['scheme'] . '://%')->where('id', '<>', $id)->count(); + if ($already_has_schema_registered > 0) { + throw new \ValidationException(sprintf('schema %s:// already registered for another client.', + $uri['scheme'])); + } + } else { + if (!HttpUtils::isHttpSchema($uri['scheme'])) { + throw new \ValidationException(sprintf('scheme %s:// is invalid.', $uri['scheme'])); + } + } + } + } + } + break; + case IClient::ApplicationType_Web_App: + case IClient::ApplicationType_JS_Client: + { + if(!isset($params['redirect_uris'])) throw new \ValidationException('redirect_uris param is required.'); + if(!empty($params['redirect_uris'])) { + $redirect_uris = explode(',', $params['redirect_uris']); + foreach ($redirect_uris as $uri) { + $uri = @parse_url($uri); + if (!isset($uri['scheme'])) { + throw new \ValidationException('invalid scheme on redirect uri.'); + } + if (!HttpUtils::isHttpsSchema($uri['scheme'])) { + throw new \ValidationException(sprintf('scheme %s:// is invalid.', $uri['scheme'])); + } + } + } + if($current_app_type === IClient::ApplicationType_JS_Client && isset($params['allowed_origins']) &&!empty($params['allowed_origins'])){ + $allowed_origins = explode(',', $params['allowed_origins']); + foreach ($allowed_origins as $uri) { + $uri = @parse_url($uri); + if (!isset($uri['scheme'])) { + throw new \ValidationException('invalid scheme on allowed origin uri.'); + } + if (!HttpUtils::isHttpsSchema($uri['scheme'])) { + throw new \ValidationException(sprintf('scheme %s:// is invalid.', $uri['scheme'])); + } + } + } + } + break; + } $allowed_update_params = array( - 'app_name','website','app_description','app_logo','active','locked','use_refresh_token','rotate_refresh_token'); + 'app_name', + 'website', + 'app_description', + 'app_logo', + 'active', + 'locked', + 'use_refresh_token', + 'rotate_refresh_token', + 'contacts', + 'logo_uri', + 'tos_uri', + 'post_logout_redirect_uris', + 'logout_uri', + 'logout_session_required', + 'logout_use_iframe', + 'policy_uri', + 'jwks_uri', + 'default_max_age', + 'logout_use_iframe', + 'require_auth_time', + 'token_endpoint_auth_method', + 'token_endpoint_auth_signing_alg', + 'subject_type', + 'userinfo_signed_response_alg', + 'userinfo_encrypted_response_alg', + 'userinfo_encrypted_response_enc', + 'id_token_signed_response_alg', + 'id_token_encrypted_response_alg', + 'id_token_encrypted_response_enc', + 'redirect_uris', + 'allowed_origins', + ); - foreach($allowed_update_params as $param){ - if(array_key_exists($param,$params)){ + $fields_to_uri_normalize = array + ( + 'post_logout_redirect_uris', + 'logout_uri', + 'policy_uri', + 'jwks_uri', + 'tos_uri', + 'logo_uri', + 'redirect_uris', + 'allowed_origins' + ); + + foreach ($allowed_update_params as $param) + { + if (array_key_exists($param, $params)) + { + if(in_array($param, $fields_to_uri_normalize)) + { + $urls = $params[$param]; + if(!empty($urls)) + { + $urls = explode(',', $urls); + $normalized_uris = ''; + foreach ($urls as $url) { + $un = new Normalizer($url); + $url = $un->normalize(); + if (!empty($normalized_uris)) { + $normalized_uris .= ','; + } + $normalized_uris .= $url; + } + $params[$param] = $normalized_uris; + } + } $client->{$param} = $params[$param]; } } - $res = $this_var->save($client); + return $this_var->save($client); }); - return $res; + } /** - * @param $id - * @param $origin - * @return mixed - * @throws \oauth2\exceptions\AllowedClientUriAlreadyExistsException - * @throws \oauth2\exceptions\AbsentClientException + * @param string $origin + * @return IClient */ - public function addClientAllowedOrigin($id, $origin) + public function getByOrigin($origin) { - $res = false; - - $this->tx_service->transaction(function () use ($id, $origin, &$res) { - - $client = Client::find($id); - - if (is_null($client)) - throw new AbsentClientException(sprintf("client id %s does not exists!",$id)); - - if($client->getApplicationType()!=IClient::ApplicationType_JS_Client) - throw new InvalidClientType($id,sprintf("client id %s application type must be JS_CLIENT",$id)); - - $client_origin = ClientAllowedOrigin::where('allowed_origin', '=', $origin)->where('client_id', '=', $id)->first(); - if (!is_null($client_origin)) { - throw new AllowedClientUriAlreadyExistsException(sprintf('origin : %s', $origin)); - } - - $client_origin = new ClientAllowedOrigin; - $client_origin->client_id = $id; - $client_origin->allowed_origin = $origin; - - $res = $client_origin->Save(); - }); - return $res; - } - - /** - * @param $id - * @param $origin_id - * @return mixed - */ - public function deleteClientAllowedOrigin($id, $origin_id) - { - return ClientAllowedOrigin::where('id', '=', $origin_id)->where('client_id', '=', $id)->delete(); + return Client::where('allowed_origins', 'like', '%'.$origin.'%')->first(); } } \ No newline at end of file diff --git a/app/services/oauth2/HttpIClientJWKSetReader.php b/app/services/oauth2/HttpIClientJWKSetReader.php new file mode 100644 index 00000000..98e50cea --- /dev/null +++ b/app/services/oauth2/HttpIClientJWKSetReader.php @@ -0,0 +1,51 @@ +getJWKSUri(); + if(empty($jwk_set_uri)) return null; + + $client = new HttpClient; + $client->setSslVerification(false); + $request = $client->get($jwk_set_uri); + $response = $request->send(); + if($response->getStatusCode() !== 200) return null; + $content_type = $response->getHeader('content-type'); + if(is_null($content_type)) return null; + if(!$content_type->hasValue(HttpContentType::Json)) return null; + return JWKSet::fromJson($response->getBody()); + } +} \ No newline at end of file diff --git a/app/services/oauth2/IdTokenBuilderImpl.php b/app/services/oauth2/IdTokenBuilderImpl.php new file mode 100644 index 00000000..f52ef39e --- /dev/null +++ b/app/services/oauth2/IdTokenBuilderImpl.php @@ -0,0 +1,182 @@ +server_private_key_repository = $server_private_key_repository; + $this->jwk_set_reader_service = $jwk_set_reader_service; + } + + /** + * @param JWTClaimSet $claim_set + * @param JWTResponseInfo $info + * @param IClient $client + * @return \jwe\IJWE|\jws\IJWS|\jwt\IJWT + * @throws RecipientKeyNotFoundException + * @throws \oauth2\exceptions\InvalidClientType + * @throws \oauth2\exceptions\ServerKeyNotFoundException + */ + public function buildJWT(JWTClaimSet $claim_set, JWTResponseInfo $info, IClient $client) + { + $sig_alg = $info->getSigningAlgorithm(); + $enc_alg = $info->getEncryptionKeyAlgorithm(); + $enc = $info->getEncryptionContentAlgorithm(); + + $jwt = UnsecuredJWT::fromClaimSet($claim_set); + + if(!is_null($sig_alg)) + { + // must sign + // get server private key to sign + + $heuristic = new ServerSigningKeyFinder($this->server_private_key_repository); + + $jwt = self::buildJWS + ( + $heuristic->find + ( + $client, + $sig_alg + ), + $sig_alg->getName(), + $claim_set + ); + + } + + if(!is_null($enc_alg) && !is_null($enc)) + { + //encrypt , get client public key + + $alg = new StringOrURI($enc_alg->getName()); + $enc = new StringOrURI($enc->getName()); + + //encrypt jwt as payload + + $heuristic = new ClientEncryptionKeyFinder($this->jwk_set_reader_service); + + $jwt = self::buildJWE + ( + $heuristic->find + ( + $client, + $enc_alg + ), + $alg, + $enc, + $jwt + ); + } + + return $jwt; + } + + /** + * @param IJWK $recipient_key + * @param StringOrURI $alg + * @param StringOrURI $enc + * @param IBasicJWT $jwt + * @return \jwe\IJWE + * @throws RecipientKeyNotFoundException + * @throws \jwk\exceptions\InvalidJWKAlgorithm + * @throws \jwk\exceptions\InvalidJWKType + */ + static private function buildJWE(IJWK $recipient_key, StringOrURI $alg, StringOrURI $enc, IBasicJWT $jwt) + { + + if(is_null($recipient_key)) + throw new RecipientKeyNotFoundException; + + $jwe = JWEFactory::build + ( + new JWE_ParamsSpecification + ( + $recipient_key, + $alg, + $enc, + $payload = $jwt->toCompactSerialization() + ) + ); + return $jwe; + } + + /** + * @param IJWK $jwk + * @param $alg + * @param JWTClaimSet $claim_set + * @return \jws\IJWS + * @throws \jwk\exceptions\InvalidJWKAlgorithm + * @throws \jwk\exceptions\InvalidJWKType + */ + static private function buildJWS(IJWK $jwk, $alg, JWTClaimSet $claim_set) + { + return JWSFactory::build + ( + new JWS_ParamsSpecification + ( + $jwk, + new StringOrURI + ( + $alg + ), + $claim_set + ) + ); + } +} \ No newline at end of file diff --git a/app/services/oauth2/MementoOAuth2AuthenticationRequestService.php b/app/services/oauth2/MementoOAuth2AuthenticationRequestService.php deleted file mode 100644 index 1c1e58db..00000000 --- a/app/services/oauth2/MementoOAuth2AuthenticationRequestService.php +++ /dev/null @@ -1,93 +0,0 @@ - $value) { - if (array_key_exists($key, OAuth2AuthorizationRequest::$params) == true) { - array_push($oauth2_params, $key); - } - } - - if (count($oauth2_params) > 0) { - Input::flashOnly($oauth2_params); - return true; - } else { - $old_data = Input::old(); - $oauth2_params = array(); - foreach ($old_data as $key => $value) { - if (array_key_exists($key, OAuth2AuthorizationRequest::$params) == true) { - array_push($oauth2_params, $key); - } - } - if (count($oauth2_params) > 0) { - Session::reflash(); - return true; - } - } - - return false; - - } - - /** Retrieve last OAuth2AuthorizationRequest - * @return OAuth2AuthorizationRequest; - */ - public function getCurrentAuthorizationRequest() - { - $msg = new OAuth2AuthorizationRequest(new OAuth2Message(Input::all())); - if (!$msg->isValid()) { - //if not valid , then check on old input - $msg = null; - $old_data = Input::old(); - $oauth2_params = array(); - foreach ($old_data as $key => $value) { - if (array_key_exists($key, OAuth2AuthorizationRequest::$params) == true) { - $oauth2_params[$key] = $value; - } - } - if (count($oauth2_params) > 0) { - $msg = new OAuth2AuthorizationRequest(new OAuth2Message($oauth2_params)); - } - } - return $msg; - } - - public function clearCurrentRequest() - { - $old_data = Input::old(); - $oauth2_params = array(); - - foreach ($old_data as $key => $value) { - if (array_key_exists($key, OAuth2AuthorizationRequest::$params) == true){ - array_push($oauth2_params, $key); - } - } - - if (count($oauth2_params) > 0) { - foreach ($oauth2_params as $oauth2_param) { - Session::forget($oauth2_param); - Session::remove($oauth2_param); - } - } - } - - -} \ No newline at end of file diff --git a/app/services/oauth2/OAuth2MementoSessionSerializerService.php b/app/services/oauth2/OAuth2MementoSessionSerializerService.php new file mode 100644 index 00000000..ffa9e280 --- /dev/null +++ b/app/services/oauth2/OAuth2MementoSessionSerializerService.php @@ -0,0 +1,68 @@ +getState())); + Session::put('oauth2.request.state', $state); + Session::save(); + } + + /** + * @return OAuth2RequestMemento + */ + public function load() + { + $state = Session::get('oauth2.request.state', null); + if(is_null($state)) return null; + + $state = json_decode( base64_decode($state), true); + + return OAuth2RequestMemento::buildFromState($state); + } + + /** + * @return void + */ + public function forget() + { + Session::remove('oauth2.request.state'); + Session::save(); + } + + /** + * @return bool + */ + public function exists() + { + return Session::has('oauth2.request.state'); + } +} \ No newline at end of file diff --git a/app/services/oauth2/OAuth2ServiceProvider.php b/app/services/oauth2/OAuth2ServiceProvider.php index 7733b238..98d3406d 100644 --- a/app/services/oauth2/OAuth2ServiceProvider.php +++ b/app/services/oauth2/OAuth2ServiceProvider.php @@ -7,9 +7,11 @@ use oauth2\services\AccessTokenGenerator; use oauth2\services\AuthorizationCodeGenerator; use oauth2\services\OAuth2ServiceCatalog; use oauth2\services\RefreshTokenGenerator; +use oauth2\strategies\ClientAuthContextValidatorFactory; use services\oauth2\ResourceServer; use App; use utils\services\UtilsServiceCatalog; +use URL; /** * Class OAuth2ServiceProvider @@ -19,23 +21,41 @@ class OAuth2ServiceProvider extends ServiceProvider { protected $defer = false; - public function boot(){ + public function boot() + { } - public function register(){ - + public function register() + { App::singleton('oauth2\\IResourceServerContext', 'services\\oauth2\\ResourceServerContext'); - App::singleton(OAuth2ServiceCatalog::MementoService, 'services\\oauth2\\MementoOAuth2AuthenticationRequestService'); + App::singleton(OAuth2ServiceCatalog::ClientCredentialGenerator, 'services\\oauth2\\ClientCrendentialGenerator'); App::singleton(OAuth2ServiceCatalog::ClientService, 'services\\oauth2\\ClientService'); + App::singleton(OAuth2ServiceCatalog::ClienPublicKeyService, 'services\\oauth2\\ClienPublicKeyService'); + App::singleton(OAuth2ServiceCatalog::ServerPrivateKeyService, 'services\\oauth2\\ServerPrivateKeyService'); App::singleton(OAuth2ServiceCatalog::ScopeService, 'services\\oauth2\\ApiScopeService'); App::singleton(OAuth2ServiceCatalog::ResourceServerService, 'services\\oauth2\\ResourceServerService'); App::singleton(OAuth2ServiceCatalog::ApiService, 'services\\oauth2\\ApiService'); App::singleton(OAuth2ServiceCatalog::ApiEndpointService, 'services\\oauth2\\ApiEndpointService'); App::singleton(OAuth2ServiceCatalog::UserConsentService, 'services\\oauth2\\UserConsentService'); - App::singleton(OAuth2ServiceCatalog::AllowedOriginService, 'services\\oauth2\\AllowedOriginService'); - App::singleton(OAuth2ServiceCatalog::TokenService, function(){ + App::singleton(OAuth2ServiceCatalog::OpenIDProviderConfigurationService,'services\\oauth2\\OpenIDProviderConfigurationService'); + App::singleton(OAuth2ServiceCatalog::MementoSerializerService,'services\\oauth2\\OAuth2MementoSessionSerializerService'); + App::singleton(OAuth2ServiceCatalog::SecurityContextService,'services\\oauth2\\SecurityContextService'); + App::singleton(OAuth2ServiceCatalog::PrincipalService,'services\\oauth2\\PrincipalService'); + App::singleton('oauth2\services\IClientJWKSetReader','services\oauth2\HttpIClientJWKSetReader'); + App::singleton('oauth2\services\IApiScopeGroupService','services\oauth2\ApiScopeGroupService'); - return new TokenService( + App::singleton('oauth2\\builders\\IdTokenBuilder',function() { + return new IdTokenBuilderImpl + ( + App::make('oauth2\repositories\IServerPrivateKeyRepository'), + new HttpIClientJWKSetReader + ); + }); + + App::singleton(OAuth2ServiceCatalog::TokenService, function() + { + return new TokenService + ( App::make(OAuth2ServiceCatalog::ClientService), App::make(UtilsServiceCatalog::LockManagerService), App::make(UtilsServiceCatalog::ServerConfigurationService), @@ -45,6 +65,11 @@ class OAuth2ServiceProvider extends ServiceProvider new AuthorizationCodeGenerator(App::make(UtilsServiceCatalog::CacheService)), new AccessTokenGenerator(App::make(UtilsServiceCatalog::CacheService)), new RefreshTokenGenerator(App::make(UtilsServiceCatalog::CacheService)), + App::make('oauth2\repositories\IServerPrivateKeyRepository'), + new HttpIClientJWKSetReader, + App::make(OAuth2ServiceCatalog::SecurityContextService), + App::make(OAuth2ServiceCatalog::PrincipalService), + App::make('oauth2\\builders\\IdTokenBuilder'), App::make(UtilsServiceCatalog::TransactionService) ); }); diff --git a/app/services/oauth2/OpenIDProviderConfigurationService.php b/app/services/oauth2/OpenIDProviderConfigurationService.php new file mode 100644 index 00000000..d9e0d059 --- /dev/null +++ b/app/services/oauth2/OpenIDProviderConfigurationService.php @@ -0,0 +1,99 @@ +setState + ( + array + ( + $user_id, + $auth_time, + $ops + ) + ); + + return $principal; + } + + /** + * @param IPrincipal $principal + * @return void + */ + public function save(IPrincipal $principal) + { + $this->register + ( + $principal->getUserId(), + $principal->getAuthTime() + ); + } + + /** + * @param int $user_id + * @param int $auth_time + * @return mixed + */ + public function register($user_id, $auth_time) + { + Session::put(self::UserIdParam, $user_id); + Session::put(self::AuthTimeParam, $auth_time); + $opbs = bin2hex(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)); + Cookie::queue('opbs', $opbs, $minutes = 2628000, $path = '/', $domain = null, $secure = false, $httpOnly = false); + Session::put(self::OPBrowserState, $opbs); + Session::save(); + } + + /** + * @return $this + */ + public function clear() + { + Session::remove(self::UserIdParam); + Session::remove(self::AuthTimeParam); + Session::remove(self::OPBrowserState); + Session::save(); + Cookie::queue('opbs', null, $minutes = -2628000, $path = '/', $domain = null, $secure = false, $httpOnly = false); + } + +} \ No newline at end of file diff --git a/app/services/oauth2/ResourceServerService.php b/app/services/oauth2/ResourceServerService.php index ed7425ff..079fe552 100644 --- a/app/services/oauth2/ResourceServerService.php +++ b/app/services/oauth2/ResourceServerService.php @@ -3,31 +3,33 @@ namespace services\oauth2; -use oauth2\models\IResourceServer; +use DB; +use oauth2\exceptions\InvalidResourceServer; use oauth2\models\IClient; +use oauth2\models\IResourceServer; +use oauth2\services\IClientService; use oauth2\services\id; use oauth2\services\IResourceServerService; -use oauth2\services\IClientService; use ResourceServer; -use DB; -use \oauth2\exceptions\InvalidResourceServer; use utils\db\ITransactionService; /** * Class ResourceServerService * @package services\oauth2 */ -class ResourceServerService implements IResourceServerService { +class ResourceServerService implements IResourceServerService +{ private $client_service; - /** - * @param IClientService $client_service - * @param ITransactionService $tx_service - */ - public function __construct(IClientService $client_service,ITransactionService $tx_service){ + /** + * @param IClientService $client_service + * @param ITransactionService $tx_service + */ + public function __construct(IClientService $client_service, ITransactionService $tx_service) + { $this->client_service = $client_service; - $this->tx_service = $tx_service; + $this->tx_service = $tx_service; } /** @@ -37,10 +39,11 @@ class ResourceServerService implements IResourceServerService { * @param array $fields * @return mixed */ - public function getAll($page_nbr=1,$page_size=10,array $filters=array(), array $fields=array('*')) + public function getAll($page_nbr = 1, $page_size = 10, array $filters = array(), array $fields = array('*')) { DB::getPaginator()->setCurrentPage($page_nbr); - return ResourceServer::Filter($filters)->paginate($page_size,$fields); + + return ResourceServer::Filter($filters)->paginate($page_size, $fields); } /** @@ -49,30 +52,38 @@ class ResourceServerService implements IResourceServerService { * @return bool * @throws \oauth2\exceptions\InvalidResourceServer */ - public function update($id, array $params){ + public function update($id, array $params) + { - $res = false; - $this_var = $this; + $res = false; + $this_var = $this; - $this->tx_service->transaction(function () use ($id,$params,&$res, &$this_var) { + $this->tx_service->transaction(function () use ($id, $params, &$res, &$this_var) { $resource_server = ResourceServer::find($id); - if(is_null($resource_server)) - throw new InvalidResourceServer(sprintf('resource server id %s does not exists!',$id)); - $allowed_update_params = array('host','ip','active','friendly_name'); + if (is_null($resource_server)) { + throw new InvalidResourceServer(sprintf('resource server id %s does not exists!', $id)); + } + $allowed_update_params = array('host', 'ip', 'active', 'friendly_name'); - foreach($allowed_update_params as $param){ - if(array_key_exists($param,$params)){ + foreach ($allowed_update_params as $param) { + if (array_key_exists($param, $params)) { - if($param =='host'){ - if(ResourceServer::where('host','=',$params[$param])->where('id','<>',$id)->count()>0) - throw new InvalidResourceServer(sprintf('there is already another resource server with that hostname (%s).',$params[$param])); + if ($param == 'host') { + if (ResourceServer::where('host', '=', $params[$param])->where('id', '<>', $id)->count() > 0) { + throw new InvalidResourceServer(sprintf('there is already another resource server with that hostname (%s).', + $params[$param])); + } } - if($param =='friendly_name'){ - if(ResourceServer::where('friendly_name','=',$params[$param])->where('id','<>',$id)->count()>0) - throw new InvalidResourceServer(sprintf('there is already another resource server with that friendly name (%s).',$params[$param])); + if ($param == 'friendly_name') { + if (ResourceServer::where('friendly_name', '=', $params[$param])->where('id', '<>', + $id)->count() > 0 + ) { + throw new InvalidResourceServer(sprintf('there is already another resource server with that friendly name (%s).', + $params[$param])); + } } $resource_server->{$param} = $params[$param]; @@ -80,6 +91,7 @@ class ResourceServerService implements IResourceServerService { } $res = $this_var->save($resource_server); }); + return $res; } @@ -89,9 +101,10 @@ class ResourceServerService implements IResourceServerService { */ public function save(IResourceServer $resource_server) { - if(!$resource_server->exists() || count($resource_server->getDirty())>0){ + if (!$resource_server->exists() || count($resource_server->getDirty()) > 0) { return $resource_server->Save(); } + return true; } @@ -103,7 +116,7 @@ class ResourceServerService implements IResourceServerService { */ public function setStatus($id, $active) { - return ResourceServer::find($id)->update(array('active'=>$active)); + return ResourceServer::find($id)->update(array('active' => $active)); } /** @@ -113,22 +126,23 @@ class ResourceServerService implements IResourceServerService { */ public function delete($id) { - $res = false; - $client_service = $this->client_service; + $res = false; + $client_service = $this->client_service; - $this->tx_service->transaction(function () use ($id,&$res,&$client_service) { + $this->tx_service->transaction(function () use ($id, &$res, &$client_service) { $resource_server = ResourceServer::find($id); - if(!is_null($resource_server)){ + if (!is_null($resource_server)) { $client = $resource_server->client()->first(); - if(!is_null($client)){ - $client_service->deleteClientByIdentifier($client->id); + if (!is_null($client)) { + $client_service->deleteClientByIdentifier($client->id); } $resource_server->delete(); $res = true; } }); + return $res; } @@ -151,26 +165,39 @@ class ResourceServerService implements IResourceServerService { */ public function add($host, $ip, $friendly_name, $active) { - $instance = null; - $client_service = $this->client_service; - if(is_string($active)){ - $active = strtoupper($active) =='TRUE' ?true:false; + $client_service = $this->client_service; + + if (is_string($active)) + { + $active = strtoupper($active) == 'TRUE' ? true : false; } - $this->tx_service->transaction(function () use ($host, $ip, $friendly_name, $active, &$instance, &$client_service) { + return $this->tx_service->transaction(function () use ( + $host, + $ip, + $friendly_name, + $active, + $client_service + ) { - if(ResourceServer::where('host','=',$host)->count()>0) - throw new InvalidResourceServer(sprintf('there is already another resource server with that hostname (%s).',$host)); + if (ResourceServer::where('host', '=', $host)->count() > 0) { + throw new InvalidResourceServer(sprintf('there is already another resource server with that hostname (%s).', + $host)); + } - if(ResourceServer::where('friendly_name','=',$friendly_name)->count()>0) - throw new InvalidResourceServer(sprintf('there is already another resource server with that friendly name (%s).',$friendly_name)); + if (ResourceServer::where('friendly_name', '=', $friendly_name)->count() > 0) { + throw new InvalidResourceServer(sprintf('there is already another resource server with that friendly name (%s).', + $friendly_name)); + } - $instance = new ResourceServer( - array( - 'host' => $host, - 'ip' => $ip, - 'active' => $active, + $instance = new ResourceServer + ( + array + ( + 'host' => $host, + 'ip' => $ip, + 'active' => $active, 'friendly_name' => $friendly_name ) ); @@ -178,32 +205,44 @@ class ResourceServerService implements IResourceServerService { $instance->Save(); // creates a new client for this brand new resource server - $new_client = $client_service->addClient(IClient::ApplicationType_Service,null,$host.'.confidential.application',$friendly_name.' confidential oauth2 application',''); + $new_client = $client_service->addClient + ( + IClient::ApplicationType_Service, + null, + $host . '.confidential.application', + $friendly_name . ' confidential oauth2 application', '' + ); + $new_client->resource_server()->associate($instance); + // does not expires ... + $new_client->client_secret_expires_at = null; $new_client->Save(); + return $instance; }); - return $instance; + } /** * @param $id * @return bool */ - public function regenerateClientSecret($id){ - $res = null; - $client_service = $this->client_service; + public function regenerateClientSecret($id) + { + $res = null; + $client_service = $this->client_service; - $this->tx_service->transaction(function () use ($id,&$res,&$client_service) { + $this->tx_service->transaction(function () use ($id, &$res, &$client_service) { $resource_server = ResourceServer::find($id); - if(!is_null($resource_server)){ + if (!is_null($resource_server)) { $client = $resource_server->client()->first(); - if(!is_null($client)){ + if (!is_null($client)) { $res = $client_service->regenerateClientSecret($client->id); } } }); + return $res; } } diff --git a/app/services/oauth2/SecurityContextService.php b/app/services/oauth2/SecurityContextService.php new file mode 100644 index 00000000..362df5b1 --- /dev/null +++ b/app/services/oauth2/SecurityContextService.php @@ -0,0 +1,70 @@ +setState + ( + array + ( + Session::get(self::RequestedUserIdParam), + Session::get(self::RequestedAuthTime), + ) + ); + + return $context; + } + + /** + * @param SecurityContext $security_context + * @return void + */ + public function save(SecurityContext $security_context) + { + Session::put(self::RequestedUserIdParam, $security_context->getRequestedUserId()); + Session::put(self::RequestedAuthTime, $security_context->isAuthTimeRequired()); + Session::save(); + } + + /** + * @return $this + */ + public function clear() + { + Session::remove(self::RequestedUserIdParam); + Session::remove(self::RequestedAuthTime); + Session::save(); + } +} \ No newline at end of file diff --git a/app/services/oauth2/ServerPrivateKeyService.php b/app/services/oauth2/ServerPrivateKeyService.php new file mode 100644 index 00000000..94ca4df4 --- /dev/null +++ b/app/services/oauth2/ServerPrivateKeyService.php @@ -0,0 +1,109 @@ +rsa = new Crypt_RSA(); + } + + /** + * @param array $params + * @return IAssymetricKey + * @throws ValidationException + */ + public function register(array $params) + { + $rsa = $this->rsa; + $repository = $this->repository; + + return $this->tx_service->transaction(function() use($params, $rsa, $repository) + { + $pem = isset($params['pem_content']) ? $params['pem_content'] : ''; + $password = $params['password']; + + $old_active_key = $repository->getByValidityRange + ( + $params['type'], + $params['usage'], + $params['alg'], + new \DateTime($params['valid_from']), + new \DateTime($params['valid_to']) + )->first(); + + + if(empty($pem)) + { + if(!empty($password)) + $rsa->setPassword($password); + /** + * array( + * 'privatekey' => $privatekey, + * 'publickey' => $publickey, + * 'partialkey' => false + * ); + */ + $res = $rsa->createKey(2048); + $pem = $res['privatekey']; + } + + + $key = ServerPrivateKey::build + ( + $params['kid'], + new \DateTime($params['valid_from']), + new \DateTime($params['valid_to']), + $params['type'], + $params['usage'], + $params['alg'], + $old_active_key ? false : $params['active'], + $pem, + $password + ); + + $repository->add($key); + + return $key; + }); + } + +} \ No newline at end of file diff --git a/app/services/oauth2/TokenService.php b/app/services/oauth2/TokenService.php index 06775520..4f19ac8c 100644 --- a/app/services/oauth2/TokenService.php +++ b/app/services/oauth2/TokenService.php @@ -17,39 +17,65 @@ namespace services\oauth2; use AccessToken as DBAccessToken; use DB; use Event; +use oauth2\exceptions\RevokedAccessTokenException; +use oauth2\exceptions\RevokedAccessTokenExceptionxtends; +use oauth2\exceptions\RevokedRefreshTokenException; +use Session; +use Crypt; +use jwa\cryptographic_algorithms\HashFunctionAlgorithm; +use jwt\IBasicJWT; +use jwt\impl\JWTClaimSet; +use jwt\JWTClaim; +use oauth2\builders\IdTokenBuilder; use oauth2\exceptions\AbsentClientException; +use oauth2\exceptions\AbsentCurrentUserException; use oauth2\exceptions\ExpiredAccessTokenException; use oauth2\exceptions\InvalidAccessTokenException; use oauth2\exceptions\InvalidAuthorizationCodeException; +use oauth2\exceptions\InvalidClientCredentials; use oauth2\exceptions\InvalidGrantTypeException; +use oauth2\exceptions\RecipientKeyNotFoundException; use oauth2\exceptions\ReplayAttackException; +use oauth2\heuristics\ClientEncryptionKeyFinder; +use oauth2\heuristics\EncryptionClientPublicKeyFinder; +use oauth2\heuristics\SigningClientPublicKeyFinder; use oauth2\models\AccessToken; use oauth2\models\AuthorizationCode; use oauth2\models\IClient; use oauth2\models\RefreshToken; use oauth2\OAuth2Protocol; +use oauth2\repositories\IServerPrivateKeyRepository; use oauth2\services\Authorization; +use oauth2\services\IClientJWKSetReader; use oauth2\services\IClientService; +use oauth2\services\IPrincipalService; +use oauth2\services\ISecurityContextService; use oauth2\services\ITokenService; use oauth2\services\IUserConsentService; -use RefreshToken as DBRefreshToken; use RefreshToken as RefreshTokenDB; +use RefreshToken as DBRefreshToken; +use utils\Base64UrlRepresentation; +use utils\ByteUtil; use utils\db\ITransactionService; use utils\exceptions\UnacquiredLockException; use utils\IPHelper; +use utils\json_types\JsonValue; +use utils\json_types\NumericDate; +use utils\json_types\StringOrURI; use utils\services\IAuthService; use utils\services\ICacheService; use utils\services\IdentifierGenerator; use utils\services\ILockManagerService; use utils\services\IServerConfigurationService; use Zend\Crypt\Hash; +use utils\exceptions\ConfigurationException; /** * Class TokenService * Provides all Tokens related operations (create, get and revoke) * @package services\oauth2 */ -class TokenService implements ITokenService +final class TokenService implements ITokenService { const ClientAccessTokenPrefixList = '.atokens'; const ClientAuthCodePrefixList = '.acodes'; @@ -103,11 +129,36 @@ class TokenService implements ITokenService * @var IdentifierGenerator */ private $refresh_token_generator; + + /** + * @var IServerPrivateKeyRepository + */ + private $server_private_key_repository; + + /** + * @var IClientJWKSetReader + */ + private $jwk_set_reader_service; /** * @var ITransactionService */ private $tx_service; + /** + * @var ISecurityContextService + */ + private $security_context_service; + + /** + * @var IPrincipalService + */ + private $principal_service; + + /** + * @var IdTokenBuilder + */ + private $id_token_builder; + /** * @param IClientService $client_service * @param ILockManagerService $lock_manager_service @@ -118,9 +169,15 @@ class TokenService implements ITokenService * @param IdentifierGenerator $auth_code_generator * @param IdentifierGenerator $access_token_generator * @param IdentifierGenerator $refresh_token_generator + * @param IServerPrivateKeyRepository $server_private_key_repository + * @param IClientJWKSetReader $jwk_set_reader_service + * @param ISecurityContextService $security_context_service + * @param IPrincipalService $principal_service + * @param IdTokenBuilder $id_token_builder * @param ITransactionService $tx_service */ - public function __construct( + public function __construct + ( IClientService $client_service, ILockManagerService $lock_manager_service, IServerConfigurationService $configuration_service, @@ -130,18 +187,30 @@ class TokenService implements ITokenService IdentifierGenerator $auth_code_generator, IdentifierGenerator $access_token_generator, IdentifierGenerator $refresh_token_generator, + IServerPrivateKeyRepository $server_private_key_repository, + IClientJWKSetReader $jwk_set_reader_service, + ISecurityContextService $security_context_service, + IPrincipalService $principal_service, + IdTokenBuilder $id_token_builder, ITransactionService $tx_service - ) { - $this->client_service = $client_service; - $this->lock_manager_service = $lock_manager_service; - $this->configuration_service = $configuration_service; - $this->cache_service = $cache_service; - $this->auth_service = $auth_service; - $this->user_consent_service = $user_consent_service; - $this->auth_code_generator = $auth_code_generator; - $this->access_token_generator = $access_token_generator; - $this->refresh_token_generator = $refresh_token_generator; - $this->tx_service = $tx_service; + ) + { + $this->client_service = $client_service; + $this->lock_manager_service = $lock_manager_service; + $this->configuration_service = $configuration_service; + $this->cache_service = $cache_service; + $this->auth_service = $auth_service; + $this->user_consent_service = $user_consent_service; + $this->auth_code_generator = $auth_code_generator; + $this->access_token_generator = $access_token_generator; + $this->refresh_token_generator = $refresh_token_generator; + $this->server_private_key_repository = $server_private_key_repository; + $this->jwk_set_reader_service = $jwk_set_reader_service; + $this->security_context_service = $security_context_service; + $this->principal_service = $principal_service; + $this->id_token_builder = $id_token_builder; + $this->tx_service = $tx_service; + $this_var = $this; Event::listen('oauth2.client.delete', function ($client_id) use (&$this_var) { @@ -163,45 +232,69 @@ class TokenService implements ITokenService * @param string $access_type * @param string $approval_prompt * @param bool $has_previous_user_consent + * @param string|null $state + * @param string|null $nonce + * @param string|null $response_type * @return AuthorizationCode */ - public function createAuthorizationCode( + public function createAuthorizationCode + ( $user_id, $client_id, $scope, - $audience = '', - $redirect_uri = null, - $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, - $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, - $has_previous_user_consent = false - ) { + $audience = '' , + $redirect_uri = null, + $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, + $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, + $has_previous_user_consent = false, + $state = null, + $nonce = null, + $response_type = null + ) + { //create model - $code = $this->auth_code_generator->generate(AuthorizationCode::create( - $user_id, - $client_id, - $scope, - $audience, - $redirect_uri, - $access_type, - $approval_prompt, $has_previous_user_consent, - $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'))); + $code = $this->auth_code_generator->generate + ( + AuthorizationCode::create + ( + $user_id, + $client_id, + $scope, + $audience, + $redirect_uri, + $access_type, + $approval_prompt, $has_previous_user_consent, + $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'), + $state, + $nonce, + $response_type, + $this->security_context_service->get()->isAuthTimeRequired(), + $this->principal_service->get()->getAuthTime() + ) + ); $hashed_value = Hash::compute('sha256', $code->getValue()); //stores on cache $this->cache_service->storeHash($hashed_value, - array( - 'client_id' => $code->getClientId(), - 'scope' => $code->getScope(), - 'audience' => $code->getAudience(), - 'redirect_uri' => $code->getRedirectUri(), - 'issued' => $code->getIssued(), - 'lifetime' => $code->getLifetime(), - 'from_ip' => $code->getFromIp(), - 'user_id' => $code->getUserId(), - 'access_type' => $code->getAccessType(), - 'approval_prompt' => $code->getApprovalPrompt(), - 'has_previous_user_consent' => $code->getHasPreviousUserConsent() + array + ( + 'client_id' => $code->getClientId(), + 'scope' => $code->getScope(), + 'audience' => $code->getAudience(), + 'redirect_uri' => $code->getRedirectUri(), + 'issued' => $code->getIssued(), + 'lifetime' => $code->getLifetime(), + 'from_ip' => $code->getFromIp(), + 'user_id' => $code->getUserId(), + 'access_type' => $code->getAccessType(), + 'approval_prompt' => $code->getApprovalPrompt(), + 'has_previous_user_consent' => $code->getHasPreviousUserConsent(), + 'state' => $code->getState(), + 'nonce' => $code->getNonce(), + 'response_type' => $code->getResponseType(), + 'requested_auth_time' => $code->isAuthTimeRequested(), + 'auth_time' => $code->getAuthTime(), ), intval($code->getLifetime())); //stores brand new auth code hash value on a set by client id... @@ -223,14 +316,17 @@ class TokenService implements ITokenService $hashed_value = Hash::compute('sha256', $value); - if (!$this->cache_service->exists($hashed_value)) { + if (!$this->cache_service->exists($hashed_value)) + { throw new InvalidAuthorizationCodeException(sprintf("auth_code %s ", $value)); } - try { + try + { $this->lock_manager_service->acquireLock('lock.get.authcode.' . $hashed_value); - $cache_values = $this->cache_service->getHash($hashed_value, array( + $cache_values = $this->cache_service->getHash($hashed_value, array + ( 'user_id', 'client_id', 'scope', @@ -241,10 +337,17 @@ class TokenService implements ITokenService 'from_ip', 'access_type', 'approval_prompt', - 'has_previous_user_consent' + 'has_previous_user_consent', + 'state', + 'nonce', + 'response_type', + 'requested_auth_time', + 'auth_time' )); - $code = AuthorizationCode::load($value, + $code = AuthorizationCode::load + ( + $value, $cache_values['user_id'], $cache_values['client_id'], $cache_values['scope'], @@ -255,12 +358,27 @@ class TokenService implements ITokenService $cache_values['from_ip'], $cache_values['access_type'], $cache_values['approval_prompt'], - $cache_values['has_previous_user_consent'] + $cache_values['has_previous_user_consent'], + $cache_values['state'], + $cache_values['nonce'], + $cache_values['response_type'], + $cache_values['requested_auth_time'], + $cache_values['auth_time'] ); return $code; - } catch (UnacquiredLockException $ex1) { - throw new ReplayAttackException($value, sprintf("auth_code %s ", $value)); + } + catch (UnacquiredLockException $ex1) + { + throw new ReplayAttackException + ( + $value, + sprintf + ( + "Code was already redeemed %s.", + $value + ) + ); } } @@ -273,8 +391,10 @@ class TokenService implements ITokenService public function createAccessToken(AuthorizationCode $auth_code, $redirect_uri = null) { - $access_token = $this->access_token_generator->generate( - AccessToken::create( + $access_token = $this->access_token_generator->generate + ( + AccessToken::create + ( $auth_code, $this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime') ) @@ -285,30 +405,32 @@ class TokenService implements ITokenService $auth_service = $this->auth_service; $this_var = $this; - $this->tx_service->transaction(function () use ( + return $this->tx_service->transaction(function () use ( $auth_code, $redirect_uri, - &$access_token, - &$cache_service, - &$client_service, - &$auth_service, - &$this_var + $access_token, + $cache_service, + $client_service, + $auth_service, + $this_var ) { - $value = $access_token->getValue(); + $value = $access_token->getValue(); $hashed_value = Hash::compute('sha256', $value); - $client_id = $access_token->getClientId(); - $user_id = $access_token->getUserId(); - $client = $client_service->getClientById($client_id); - $user = $auth_service->getUserById($user_id); + $client_id = $access_token->getClientId(); + $user_id = $access_token->getUserId(); + $client = $client_service->getClientById($client_id); + $user = $auth_service->getUserById($user_id); - $access_token_db = new DBAccessToken ( - array( - 'value' => $hashed_value, - 'from_ip' => IPHelper::getUserIp(), + $access_token_db = new DBAccessToken + ( + array + ( + 'value' => $hashed_value, + 'from_ip' => IPHelper::getUserIp(), 'associated_authorization_code' => Hash::compute('sha256', $auth_code->getValue()), 'lifetime' => $access_token->getLifetime(), - 'scope' => $access_token->getScope(), + 'scope' => $access_token->getScope(), 'audience' => $access_token->getAudience() ) ); @@ -319,9 +441,26 @@ class TokenService implements ITokenService $access_token_db->save(); //check if use refresh tokens... - if ($client->use_refresh_token && $client->getApplicationType() == IClient::ApplicationType_Web_App && $auth_code->getAccessType() == OAuth2Protocol::OAuth2Protocol_AccessType_Offline) { + if + ( + $client->use_refresh_token && + ( + $client->getApplicationType() == IClient::ApplicationType_Web_App || + $client->getApplicationType() == IClient::ApplicationType_Native + ) && + ( + $auth_code->getAccessType() == OAuth2Protocol::OAuth2Protocol_AccessType_Offline || + str_contains($auth_code->getScope(), OAuth2Protocol::OfflineAccess_Scope) + ) + ) + { //but only the first time (approval_prompt == force || not exists previous consent) - if (!$auth_code->getHasPreviousUserConsent() || $auth_code->getApprovalPrompt() == OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force) { + if + ( + !$auth_code->getHasPreviousUserConsent() || + $auth_code->getApprovalPrompt() == OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force + ) + { $this_var->createRefreshToken($access_token); } } @@ -329,11 +468,17 @@ class TokenService implements ITokenService $this_var->storesAccessTokenOnCache($access_token); //stores brand new access token hash value on a set by client id... $cache_service->addMemberSet($client_id . TokenService::ClientAccessTokenPrefixList, $hashed_value); - $cache_service->incCounter($client_id . TokenService::ClientAccessTokensQty, - TokenService::ClientAccessTokensQtyLifetime); + + $cache_service->incCounter + ( + $client_id . TokenService::ClientAccessTokensQty, + TokenService::ClientAccessTokensQtyLifetime + ); + + return $access_token; }); - return $access_token; + } /** @@ -422,13 +567,12 @@ class TokenService implements ITokenService public function createAccessTokenFromRefreshToken(RefreshToken $refresh_token, $scope = null) { - - $cache_service = $this->cache_service; - $client_service = $this->client_service; - $configuration_service = $this->configuration_service; - $auth_service = $this->auth_service; + $cache_service = $this->cache_service; + $client_service = $this->client_service; + $configuration_service = $this->configuration_service; + $auth_service = $this->auth_service; $access_token_generator = $this->access_token_generator; - $this_var = $this; + $this_var = $this; //preserve entire operation on db transaction... @@ -443,30 +587,43 @@ class TokenService implements ITokenService $access_token_generator ) { - $refresh_token_value = $refresh_token->getValue(); + $refresh_token_value = $refresh_token->getValue(); $refresh_token_hashed_value = Hash::compute('sha256', $refresh_token_value); //clear current access tokens as invalid $this_var->clearAccessTokensForRefreshToken($refresh_token->getValue()); //validate scope if present... - if (!is_null($scope) && empty($scope)) { - $original_scope = $refresh_token->getScope(); - $aux_original_scope = explode(' ', $original_scope); - $aux_scope = explode(' ', $scope); + if (!is_null($scope) && empty($scope)) + { + $original_scope = $refresh_token->getScope(); + $aux_original_scope = explode(OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, $original_scope); + $aux_scope = explode(OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, $scope); //compare original scope with given one, and validate if its included on original one //or not - if (count(array_diff($aux_scope, $aux_original_scope)) !== 0) { - throw new InvalidGrantTypeException(sprintf("requested scope %s is not contained on original one %s", - $scope, $original_scope)); + if (count(array_diff($aux_scope, $aux_original_scope)) !== 0) + { + throw new InvalidGrantTypeException + ( + sprintf + ( + "requested scope %s is not contained on original one %s", + $scope, + $original_scope + ) + ); } - } else { + } + else + { //get original scope $scope = $refresh_token->getScope(); } //create new access token - $access_token = $access_token_generator->generate( - AccessToken::createFromRefreshToken( + $access_token = $access_token_generator->generate + ( + AccessToken::createFromRefreshToken + ( $refresh_token, $scope, $configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime') @@ -485,12 +642,14 @@ class TokenService implements ITokenService $client = $client_service->getClientById($client_id); //stores in DB - $access_token_db = new DBAccessToken( - array( - 'value' => $hashed_value, - 'from_ip' => IPHelper::getUserIp(), + $access_token_db = new DBAccessToken + ( + array + ( + 'value' => $hashed_value, + 'from_ip' => IPHelper::getUserIp(), 'lifetime' => $access_token->getLifetime(), - 'scope' => $access_token->getScope(), + 'scope' => $access_token->getScope(), 'audience' => $access_token->getAudience() ) ); @@ -501,7 +660,8 @@ class TokenService implements ITokenService $access_token_db->client()->associate($client); - if (!is_null($user_id)) { + if (!is_null($user_id)) + { $user = $auth_service->getUserById($user_id); $access_token_db->user()->associate($user); } @@ -510,8 +670,11 @@ class TokenService implements ITokenService //stores brand new access token hash value on a set by client id... $cache_service->addMemberSet($client_id . TokenService::ClientAccessTokenPrefixList, $hashed_value); - $cache_service->incCounter($client_id . TokenService::ClientAccessTokensQty, - TokenService::ClientAccessTokensQtyLifetime); + $cache_service->incCounter + ( + $client_id . TokenService::ClientAccessTokensQty, + TokenService::ClientAccessTokensQtyLifetime + ); return $access_token; }); } @@ -594,10 +757,10 @@ class TokenService implements ITokenService */ public function getAccessToken($value, $is_hashed = false) { - $cache_service = $this->cache_service; - $lock_manager_service = $this->lock_manager_service; + $cache_service = $this->cache_service; + $lock_manager_service = $this->lock_manager_service; $configuration_service = $this->configuration_service; - $this_var = $this; + $this_var = $this; return $this->tx_service->transaction(function () use ( $this_var, @@ -609,29 +772,39 @@ class TokenService implements ITokenService ) { //hash the given value, bc tokens values are stored hashed on DB $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; - $lock_name = ''; + $lock_name = ''; $access_token = null; - try { + + try + { // check cache ... - if (!$cache_service->exists($hashed_value)) { + if (!$cache_service->exists($hashed_value)) + { // check on DB... $access_token_db = DBAccessToken::where('value', '=', $hashed_value)->first(); - if (is_null($access_token_db)) { - if ($cache_service->exists('access.token:void:' . $hashed_value)) // check if its marked on cache as expired ... + if (is_null($access_token_db)) + { + if($this_var->isAccessTokenRevoked($hashed_value)) + { + throw new RevokedAccessTokenException(sprintf('Access token %s is revoked!', $value)); + } + else if($this_var->isAccessTokenVoid($hashed_value)) // check if its marked on cache as expired ... { throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $value)); - } else { + } + else + { throw new InvalidGrantTypeException(sprintf("Access token %s is invalid!", $value)); } } // lock ... $lock_name = 'lock.get.accesstoken.' . $hashed_value; $lock_manager_service->acquireLock($lock_name); - if ($access_token_db->isVoid()) { + if ($access_token_db->isVoid()) + { // invalid one ... // add to cache as expired ... - $cache_service->addSingleValue('access.token:void:' . $hashed_value, - 'access.token:void:' . $hashed_value); + $this_var->markAccessTokenAsVoid($hashed_value); // and deleted it from db $access_token_db->delete(); throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $value)); @@ -642,7 +815,8 @@ class TokenService implements ITokenService $lock_manager_service->releaseLock($lock_name); } - $cache_values = $cache_service->getHash($hashed_value, array( + $cache_values = $cache_service->getHash($hashed_value, array + ( 'user_id', 'client_id', 'scope', @@ -655,7 +829,8 @@ class TokenService implements ITokenService )); // reload auth code ... - $auth_code = AuthorizationCode::load( + $auth_code = AuthorizationCode::load + ( $cache_values['auth_code'], intval($cache_values['user_id']) == 0 ? null : intval($cache_values['user_id']), $cache_values['client_id'], @@ -668,11 +843,18 @@ class TokenService implements ITokenService $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, $has_previous_user_consent = false, + null, + null, $is_hashed = true ); // reload access token ... - $access_token = AccessToken::load($value, $auth_code, $cache_values['issued'], - $cache_values['lifetime']); + $access_token = AccessToken::load + ( + $value, + $auth_code, + $cache_values['issued'], + $cache_values['lifetime'] + ); $refresh_token_value = $cache_values['refresh_token']; if (!empty($refresh_token_value)) { @@ -792,35 +974,54 @@ class TokenService implements ITokenService public function getRefreshToken($value, $is_hashed = false) { //hash the given value, bc tokens values are stored hashed on DB - $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; + $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; $refresh_token_db = DBRefreshToken::where('value', '=', $hashed_value)->first(); - if (is_null($refresh_token_db)) { - throw new InvalidGrantTypeException(sprintf("Refresh token %s does not exists!", $value)); + if (is_null($refresh_token_db)) + { + if($this->isRefreshTokenRevoked($hashed_value)) + throw new RevokedRefreshTokenException(sprintf("revoked refresh token %s !", $value)); + + throw new InvalidGrantTypeException(sprintf("refresh token %s does not exists!", $value)); } - if ($refresh_token_db->void) { - throw new ReplayAttackException($value, sprintf("Refresh token %s is void", $value)); + if ($refresh_token_db->void) + { + throw new ReplayAttackException + ( + $value, + sprintf + ( + "refresh token %s is void", + $value + ) + ); } //check is refresh token is stills alive... (ZERO is infinite lifetime) - if ($refresh_token_db->isVoid()) { - throw new InvalidGrantTypeException(sprintf("Refresh token %s is expired!", $value)); + if ($refresh_token_db->isVoid()) + { + throw new InvalidGrantTypeException(sprintf("refresh token %s is expired!", $value)); } $client = $refresh_token_db->client()->first(); - $refresh_token = RefreshToken::load(array( - 'value' => $value, - 'scope' => $refresh_token_db->scope, - 'client_id' => $client->client_id, - 'user_id' => $refresh_token_db->user_id, - 'audience' => $refresh_token_db->audience, - 'from_ip' => $refresh_token_db->from_ip, - 'issued' => $refresh_token_db->created_at, - 'is_hashed' => $is_hashed - ), intval($refresh_token_db->lifetime)); + $refresh_token = RefreshToken::load + ( + array + ( + 'value' => $value, + 'scope' => $refresh_token_db->scope, + 'client_id' => $client->client_id, + 'user_id' => $refresh_token_db->user_id, + 'audience' => $refresh_token_db->audience, + 'from_ip' => $refresh_token_db->from_ip, + 'issued' => $refresh_token_db->created_at, + 'is_hashed' => $is_hashed + ), + intval($refresh_token_db->lifetime) + ); return $refresh_token; } @@ -842,7 +1043,7 @@ class TokenService implements ITokenService foreach ($db_access_tokens as $db_access_token) { - $client = $db_access_tokens->client()->first(); + $client = $db_access_token->client()->first(); $access_token_value = $db_access_token->value; $refresh_token_db = $db_access_token->refresh_token()->first(); @@ -870,24 +1071,32 @@ class TokenService implements ITokenService public function revokeAccessToken($value, $is_hashed = false) { - $res = 0; $cache_service = $this->cache_service; + $this_var = $this; - $this->tx_service->transaction(function () use ($value, $is_hashed, &$res, &$cache_service) { + return $this->tx_service->transaction(function () use ($value, $is_hashed, $cache_service, $this_var) { + $res = 0; //hash the given value, bc tokens values are stored hashed on DB $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; $access_token_db = DBAccessToken::where('value', '=', $hashed_value)->first(); - $client = $access_token_db->client()->first(); + $client = $access_token_db->client()->first(); //delete from cache $res = $cache_service->delete($hashed_value); - $res = $cache_service->deleteMemberSet($client->client_id . TokenService::ClientAccessTokenPrefixList, - $access_token_db->value); + $res = $cache_service->deleteMemberSet + ( + $client->client_id . TokenService::ClientAccessTokenPrefixList, + $access_token_db->value + ); + //check on DB... and delete it $res = $access_token_db->delete(); + + $this_var->markAccessTokenAsRevoked($hashed_value); + + return $res > 0; }); - return $res > 0; } /** @@ -897,29 +1106,42 @@ class TokenService implements ITokenService public function revokeClientRelatedTokens($client_id) { //get client auth codes - $auth_codes = $this->cache_service->getSet($client_id . self::ClientAuthCodePrefixList); + $auth_codes = $this->cache_service->getSet($client_id . self::ClientAuthCodePrefixList); //get client access tokens - $access_tokens = $this->cache_service->getSet($client_id . self::ClientAccessTokenPrefixList); + $access_tokens = $this->cache_service->getSet($client_id . self::ClientAccessTokenPrefixList); $client_service = $this->client_service; - $cache_service = $this->cache_service; - + $cache_service = $this->cache_service; + $this_var = $this; $this->tx_service->transaction(function () use ( $client_id, $auth_codes, $access_tokens, - &$cache_service, - &$client_service + $cache_service, + $client_service, + $this_var ) { $client = $client_service->getClientById($client_id); - if (is_null($client)) { + + if (is_null($client)) + { return; } //revoke on cache $cache_service->deleteArray($auth_codes); $cache_service->deleteArray($access_tokens); //revoke on db + foreach($client->access_tokens()->get() as $at) + { + $this_var->markAccessTokenAsRevoked($at->value); + } + + foreach($client->refresh_tokens()->get() as $rt) + { + $this_var->markRefreshTokenAsRevoked($rt); + } + $client->access_tokens()->delete(); $client->refresh_tokens()->delete(); //delete client list (auth codes and access tokens) @@ -928,6 +1150,71 @@ class TokenService implements ITokenService }); } + /** + * @param string $at_hash + */ + public function markAccessTokenAsRevoked($at_hash) + { + $this->cache_service->addSingleValue + ( + 'access.token:revoked:'.$at_hash, + 'access.token:revoked:'.$at_hash, + $this->configuration_service->getConfigValue('OAuth2.AccessToken.Revoked.Lifetime') + ); + } + + /** + * @param string $at_hash + */ + public function markAccessTokenAsVoid($at_hash) + { + $this->cache_service->addSingleValue + ( + 'access.token:void:'.$at_hash, + 'access.token:void:'.$at_hash, + $this->configuration_service->getConfigValue('OAuth2.AccessToken.Void.Lifetime') + ); + } + + /** + * @param string $rt_hash + */ + public function markRefreshTokenAsRevoked($rt_hash) + { + $this->cache_service->addSingleValue + ( + 'refresh.token:revoked:'.$rt_hash, + 'refresh.token:revoked:'.$rt_hash, + $this->configuration_service->getConfigValue('OAuth2.RefreshToken.Revoked.Lifetime') + ); + } + + /** + * @param string $at_hash + * @return bool + */ + public function isAccessTokenRevoked($at_hash) + { + return $this->cache_service->exists('access.token:revoked:' . $at_hash); + } + + /** + * @param string $at_hash + * @return bool + */ + public function isAccessTokenVoid($at_hash) + { + return $this->cache_service->exists('access.token:void:' . $at_hash); + } + + /** + * @param string $rt_hash + * @return bool + */ + public function isRefreshTokenRevoked($rt_hash) + { + return $this->cache_service->exists('refresh.token:revoked:' . $rt_hash); + } /** * Mark a given refresh token as void @@ -938,12 +1225,10 @@ class TokenService implements ITokenService public function invalidateRefreshToken($value, $is_hashed = false) { $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; - $res = RefreshTokenDB::where('value', '=', $hashed_value)->update(array('void' => true)); - + $res = RefreshTokenDB::where('value', '=', $hashed_value)->update(array('void' => true)); return $res > 0; } - /** * Revokes a give refresh token and all related access tokens * @param $value @@ -972,32 +1257,44 @@ class TokenService implements ITokenService public function clearAccessTokensForRefreshToken($value, $is_hashed = false) { - $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; - $res = false; + $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; $cache_service = $this->cache_service; + $this_var = $this; - $this->tx_service->transaction(function () use ($hashed_value, &$res, &$cache_service) { + return $this->tx_service->transaction(function () use ($hashed_value, $cache_service, $this_var) { + $res = false; $refresh_token_db = DBRefreshToken::where('value', '=', $hashed_value)->first(); - if (!is_null($refresh_token_db)) { + + if (!is_null($refresh_token_db)) + { $access_tokens_db = DBAccessToken::where('refresh_token_id', '=', $refresh_token_db->id)->get(); - if (!count($access_tokens_db)) { + + if (!count($access_tokens_db)) + { $res = true; } - foreach ($access_tokens_db as $access_token_db) { + foreach ($access_tokens_db as $access_token_db) + { - $res = $cache_service->delete($access_token_db->value); + $res = $cache_service->delete($access_token_db->value); $client = $access_token_db->client()->first(); - $res = $cache_service->deleteMemberSet($client->client_id . TokenService::ClientAccessTokenPrefixList, - $access_token_db->value); + $res = $cache_service->deleteMemberSet + ( + $client->client_id . TokenService::ClientAccessTokenPrefixList, + $access_token_db->value + ); + + $this_var->markAccessTokenAsRevoked($access_token_db->value); + $access_token_db->delete(); } } - }); - return $res; + return $res; + }); } public function getAccessTokenByClient($client_id) @@ -1020,7 +1317,8 @@ class TokenService implements ITokenService public function getRefreshTokenByClient($client_id) { $client = $this->client_service->getClientById($client_id); - if (is_null($client)) { + if (is_null($client)) + { throw new AbsentClientException(sprintf("client id %d does not exists!", $client_id)); } $res = array(); @@ -1067,4 +1365,226 @@ class TokenService implements ITokenService return $res; } + + /** + * @param string $nonce + * @param string $client_id + * @param AccessToken|null $access_token + * @param AuthorizationCode $auth_code + * @return IBasicJWT + * @throws AbsentClientException + * @throws InvalidClientCredentials + * @throws RecipientKeyNotFoundException + * @throws \jwt\exceptions\ClaimAlreadyExistsException + * @throws \oauth2\exceptions\InvalidClientType + * @throws \oauth2\exceptions\ServerKeyNotFoundException + */ + public function createIdToken + ( + $nonce, + $client_id, + AccessToken $access_token = null, + AuthorizationCode $auth_code = null + ) + { + $issuer = $this->configuration_service->getSiteUrl(); + if(empty($issuer)) throw new ConfigurationException('missing idp url'); + + $client = $this->client_service->getClientById($client_id); + $id_token_lifetime = $this->configuration_service->getConfigValue('OAuth2.IdToken.Lifetime'); + + if (is_null($client)) + { + throw new AbsentClientException + ( + sprintf + ( + "client id %d does not exists!", + $client_id + ) + ); + } + + $user = $this->auth_service->getUserById + ( + $this->principal_service->get()->getUserId() + ); + + if(!$user) + throw new AbsentCurrentUserException; + + // build claim set + $epoch_now = time(); + $session_id = Crypt::encrypt(Session::getId()); + $encoder = new Base64UrlRepresentation(); + $jti = $encoder->encode(hash('sha512', $session_id.$client_id, true)); + + $this->cache_service->addSingleValue($jti, $session_id, $id_token_lifetime); + + $claim_set = new JWTClaimSet + ( + $iss = new StringOrURI($issuer), + $sub = new StringOrURI + ( + $this->auth_service->wrapUserId + ( + $user->getExternalIdentifier(), + $client + ) + ), + $aud = new StringOrURI($client_id), + $iat = new NumericDate($epoch_now), + $exp = new NumericDate($epoch_now + $id_token_lifetime), + $jti = new JsonValue($jti) + ); + + if(!empty($nonce)) + $claim_set->addClaim(new JWTClaim(OAuth2Protocol::OAuth2Protocol_Nonce, new StringOrURI($nonce))); + + $id_token_response_info = $client->getIdTokenResponseInfo(); + $sig_alg = $id_token_response_info->getSigningAlgorithm(); + $enc_alg = $id_token_response_info->getEncryptionKeyAlgorithm(); + $enc = $id_token_response_info->getEncryptionContentAlgorithm(); + + if(!is_null($sig_alg) && !is_null($access_token)) + $this->buildAccessTokenHashClaim($access_token, $sig_alg , $claim_set); + + if(!is_null($sig_alg) && !is_null($auth_code)) + $this->buildAuthCodeHashClaim($auth_code, $sig_alg , $claim_set); + + $this->buildAuthTimeClaim($claim_set); + + return $this->id_token_builder->buildJWT($claim_set, $id_token_response_info, $client); + } + + /** + * @param AccessToken $access_token + * @param HashFunctionAlgorithm $hashing_alg + * @param JWTClaimSet $claim_set + * @return JWTClaimSet + * @throws InvalidClientCredentials + * @throws \jwt\exceptions\ClaimAlreadyExistsException + */ + private function buildAccessTokenHashClaim + ( + AccessToken $access_token, + HashFunctionAlgorithm $hashing_alg, + JWTClaimSet $claim_set + ) + { + $at = $access_token->getValue(); + $at_len = $hashing_alg->getHashKeyLen() / 2 ; + $encoder = new Base64UrlRepresentation(); + + if($at_len > ByteUtil::bitLength(strlen($at))) + throw new InvalidClientCredentials('invalid access token length!.'); + + $claim_set->addClaim + ( + new JWTClaim + ( + OAuth2Protocol::OAuth2Protocol_AccessToken_Hash, + new JsonValue + ( + $encoder->encode + ( + substr + ( + hash + ( + $hashing_alg->getHashingAlgorithm(), + $at, + true + ), + 0, + $at_len / 8 + ) + ) + ) + ) + ); + + return $claim_set; + } + + /** + * @param AuthorizationCode $auth_code + * @param HashFunctionAlgorithm $hashing_alg + * @param JWTClaimSet $claim_set + * @return JWTClaimSet + * @throws InvalidClientCredentials + * @throws \jwt\exceptions\ClaimAlreadyExistsException + */ + private function buildAuthCodeHashClaim + ( + AuthorizationCode $auth_code, + HashFunctionAlgorithm $hashing_alg, + JWTClaimSet $claim_set + ) + { + + $ac = $auth_code->getValue(); + $ac_len = $hashing_alg->getHashKeyLen() / 2 ; + $encoder = new Base64UrlRepresentation(); + + if($ac_len > ByteUtil::bitLength(strlen($ac))) + throw new InvalidClientCredentials('invalid auth code length!.'); + + $claim_set->addClaim + ( + new JWTClaim + ( + OAuth2Protocol::OAuth2Protocol_AuthCode_Hash, + new JsonValue + ( + $encoder->encode + ( + substr + ( + hash + ( + $hashing_alg->getHashingAlgorithm(), + $ac, + true + ), + 0, + $ac_len / 8 + ) + ) + ) + ) + ); + + return $claim_set; + } + + private function buildAuthTimeClaim(JWTClaimSet $claim_set) + { + if($this->security_context_service->get()->isAuthTimeRequired()) + { + $claim_set->addClaim + ( + new JWTClaim + ( + OAuth2Protocol::OAuth2Protocol_AuthTime, + new JsonValue + ( + $this->principal_service->get()->getAuthTime() + ) + ) + ); + } + } + + /** + * @param AuthorizationCode $auth_code + * @return AccessToken|null + */ + public function getAccessTokenByAuthCode(AuthorizationCode $auth_code) + { + $auth_code_value = Hash::compute('sha256', $auth_code->getValue()); + $db_access_token = DBAccessToken::where('associated_authorization_code', '=', $auth_code_value)->first(); + if(is_null($db_access_token)) return null; + return $this->getAccessToken($db_access_token->value, true); + } } \ No newline at end of file diff --git a/app/services/oauth2/UserConsentService.php b/app/services/oauth2/UserConsentService.php index 07d3229e..3da61ce8 100644 --- a/app/services/oauth2/UserConsentService.php +++ b/app/services/oauth2/UserConsentService.php @@ -2,13 +2,14 @@ namespace services\oauth2; +use Client; use oauth2\exceptions\AbsentClientException; use oauth2\models\IUserConsent; use oauth2\services\IUserConsentService; use UserConsent; -use Client; -class UserConsentService implements IUserConsentService{ +class UserConsentService implements IUserConsentService +{ /** * @param $user_id @@ -18,11 +19,102 @@ class UserConsentService implements IUserConsentService{ */ public function get($user_id, $client_id, $scopes) { - return UserConsent::where('user_id','=',$user_id) - ->where('client_id','=',$client_id) - ->where('scopes','=',$scopes)->first(); + + $set = explode(' ', $scopes); + $size = count($set) - 1; + $perm = range(0, $size); + $j = 0; + $perms = array(); + + do + { + foreach ($perm as $i) + { + $perms[$j][] = $set[$i]; + } + } while ($perm = $this->nextPermutation($perm, $size) and ++$j); + + $scope_conditions = array(); + + $query1 = UserConsent::where('user_id', '=', $user_id)->where('client_id', '=', $client_id); + + $query2 = UserConsent::where('user_id', '=', $user_id)->where('client_id', '=', $client_id); + + + $query1 = $query1->where(function ($query) use($perms) + { + foreach ($perms as $p) + { + $str = join(' ', $p); + $query = $query->orWhere('scopes', '=', $str); + } + + return $query; + }); + + + $query2 = $query2->where(function ($query) use($perms) + { + foreach ($perms as $p) + { + $str = join(' ', $p); + $query = $query->orWhere('scopes', 'like', '%'.$str.'%'); + } + + return $query; + }); + + + $consent = $query1->first(); + + if (is_null($consent)) { + $consent = $query2->first(); + } + + return $consent; } + + /** + * @param $p + * @param $size + * @return bool + * + * http://docstore.mik.ua/orelly/webprog/pcook/ch04_26.htm + * + */ + private function nextPermutation($p, $size) + { + // slide down the array looking for where we're smaller than the next guy + for ($i = $size - 1; $i >= 0 && $p[$i] >= $p[$i + 1]; --$i) {} + + // if this doesn't occur, we've finished our permutations + // the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1) + if ($i == -1) + { + return false; + } + + // slide down the array looking for a bigger number than what we found before + for ($j = $size; $p[$j] <= $p[$i]; --$j) {} + + // swap them + $tmp = $p[$i]; + $p[$i] = $p[$j]; + $p[$j] = $tmp; + + // now reverse the elements in between by swapping the ends + for (++$i, $j = $size; $i < $j; ++$i, --$j) + { + $tmp = $p[$i]; + $p[$i] = $p[$j]; + $p[$j] = $tmp; + } + + return $p; + } + + /** * @param $user_id * @param $client_id @@ -34,12 +126,13 @@ class UserConsentService implements IUserConsentService{ { $consent = new UserConsent(); - if(is_null(Client::find($client_id))) + if (is_null(Client::find($client_id))) { throw new AbsentClientException; + } $consent->client_id = $client_id; - $consent->user_id = $user_id; - $consent->scopes = $scopes; + $consent->user_id = $user_id; + $consent->scopes = $scopes; $consent->Save(); } } \ No newline at end of file diff --git a/app/services/oauth2/resource_server/UserService.php b/app/services/oauth2/resource_server/UserService.php index f4fd9268..9e10a9a4 100644 --- a/app/services/oauth2/resource_server/UserService.php +++ b/app/services/oauth2/resource_server/UserService.php @@ -2,30 +2,65 @@ namespace services\oauth2\resource_server; +use Exception; +use jwt\impl\JWTClaimSet; +use jwt\JWTClaim; +use oauth2\AddressClaim; +use oauth2\IResourceServerContext; use oauth2\resource_server\IUserService; use oauth2\resource_server\OAuth2ProtectedService; -use oauth2\IResourceServerContext; -use utils\services\ILogService; +use oauth2\services\IClientService; +use oauth2\StandardClaims; use openid\services\IUserService as IAPIUserService; -use Exception; +use utils\json_types\JsonArray; +use utils\json_types\JsonValue; +use utils\json_types\StringOrURI; +use utils\services\IAuthService; +use utils\services\ILogService; use utils\services\IServerConfigurationService; + /** * Class UserService * OAUTH2 Protected Endpoint * @package services\oauth2\resource_server */ -class UserService extends OAuth2ProtectedService implements IUserService { - +class UserService extends OAuth2ProtectedService implements IUserService +{ + /** + * @var IAPIUserService + */ private $user_service; - private $configuration_service; + /** + * @var IServerConfigurationService + */ + private $configuration_service; - public function __construct(IAPIUserService $user_service, - IResourceServerContext $resource_server_context, - IServerConfigurationService $configuration_service, - ILogService $log_service){ - parent::__construct($resource_server_context,$log_service); - $this->user_service = $user_service; - $this->configuration_service = $configuration_service; + /** + * @var IClientService + */ + private $client_service; + + /** + * @var IAuthService + */ + private $auth_service; + + public function __construct + ( + IAPIUserService $user_service, + IResourceServerContext $resource_server_context, + IServerConfigurationService $configuration_service, + ILogService $log_service, + IClientService $client_service, + IAuthService $auth_service + ) + { + parent::__construct($resource_server_context, $log_service); + + $this->user_service = $user_service; + $this->configuration_service = $configuration_service; + $this->client_service = $client_service; + $this->auth_service = $auth_service; } /** @@ -35,47 +70,129 @@ class UserService extends OAuth2ProtectedService implements IUserService { */ public function getCurrentUserInfo() { - $data = array(); - try{ + $data = array(); + try + { $me = $this->resource_server_context->getCurrentUserId(); - if(is_null($me)){ + if (is_null($me)) { + throw new Exception('me is no set!.'); + } + + $current_user = $this->user_service->get($me); + $scopes = $this->resource_server_context->getCurrentScope(); + + if (in_array(self::UserProfileScope_Address, $scopes)) { + // Address Claims + $data[AddressClaim::Country] = $current_user->getCountry(); + $data[AddressClaim::StreetAddress] = $current_user->getCountry(); + $data[AddressClaim::PostalCode] = $current_user->getPostalCode(); + $data[AddressClaim::Region] = $current_user->getRegion(); + $data[AddressClaim::Locality] = $current_user->getLocality(); + } + if (in_array(self::UserProfileScope_Profile, $scopes)) { + // Profile Claims + $assets_url = $this->configuration_service->getConfigValue('Assets.Url'); + $pic_url = $current_user->getPic(); + $pic_url = str_contains($pic_url, 'http') ? $pic_url : $assets_url . $pic_url; + + $data[StandardClaims::Name] = $current_user->getFullName(); + $data[StandardClaims::GivenName] = $current_user->getFirstName(); + $data[StandardClaims::FamilyName] = $current_user->getLastName(); + $data[StandardClaims::NickName] = $current_user->getNickName(); + $data[StandardClaims::Picture] = $pic_url; + $data[StandardClaims::Birthdate] = $current_user->getDateOfBirth(); + $data[StandardClaims::Gender] = $current_user->getGender(); + } + if (in_array(self::UserProfileScope_Email, $scopes)) { + // Email Claim + $data[StandardClaims::Email] = $current_user->getEmail(); + $data[StandardClaims::EmailVerified] = true; + } + } catch (Exception $ex) { + $this->log_service->error($ex); + throw $ex; + } + + return $data; + } + + /** + * @return JWTClaimSet + * @throws Exception + */ + public function getCurrentUserInfoClaims() + { + try + { + + $me = $this->resource_server_context->getCurrentUserId(); + $client_id = $this->resource_server_context->getCurrentClientId(); + $client = $this->client_service->getClientById($client_id); + + if (is_null($me)) + { throw new Exception('me is no set!.'); } $current_user = $this->user_service->get($me); $scopes = $this->resource_server_context->getCurrentScope(); - if(in_array(self::UserProfileScope_Address, $scopes)){ - // Address Claim - $data['country'] = $current_user->getCountry(); - $data['street_address'] = $current_user->getCountry(); - $data['postal_code'] = $current_user->getPostalCode(); - $data['region'] = $current_user->getRegion(); - $data['locality'] = $current_user->getLocality(); + $claim_set = new JWTClaimSet + ( + null, + $sub = new StringOrURI + ( + $this->auth_service->wrapUserId + ( + $current_user->getExternalIdentifier(), + $client + ) + ), + $aud = new StringOrURI($client_id) + + ); + + if (in_array(self::UserProfileScope_Address, $scopes)) { + // Address Claims + $address = array(); + $address[AddressClaim::Country] = $current_user->getCountry(); + $address[AddressClaim::StreetAddress] = $current_user->getStreetAddress(); + $address[AddressClaim::PostalCode] = $current_user->getPostalCode(); + $address[AddressClaim::Region] = $current_user->getRegion(); + $address[AddressClaim::Locality] = $current_user->getLocality(); + $address[AddressClaim::Formatted] = $current_user->getFormattedAddress(); + + $claim_set->addClaim(new JWTClaim(StandardClaims::Address, new JsonValue($address))); + } - if(in_array(self::UserProfileScope_Profile, $scopes)){ - // Address Claim - $assets_url = $this->configuration_service->getConfigValue('Assets.Url'); - $pic_url = $current_user->getPic(); - $pic_url = str_contains($pic_url,'http')?$pic_url:$assets_url.$pic_url; - $data['name'] = $current_user->getFirstName(); - $data['family_name'] = $current_user->getLastName(); - $data['nickname'] = $current_user->getNickName(); - $data['picture'] = $pic_url; - $data['birthdate'] = $current_user->getDateOfBirth(); - $data['gender'] = $current_user->getGender(); + if (in_array(self::UserProfileScope_Profile, $scopes)) + { + // Profile Claims + $assets_url = $this->configuration_service->getConfigValue('Assets.Url'); + $pic_url = $current_user->getPic(); + $pic_url = str_contains($pic_url, 'http') ? $pic_url : $assets_url . $pic_url; + + $claim_set->addClaim(new JWTClaim(StandardClaims::Name, new StringOrURI($current_user->getFullName()))); + $claim_set->addClaim(new JWTClaim(StandardClaims::GivenName, new StringOrURI($current_user->getFirstName()))); + $claim_set->addClaim(new JWTClaim(StandardClaims::FamilyName, new StringOrURI($current_user->getLastName()))); + $claim_set->addClaim(new JWTClaim(StandardClaims::NickName, new StringOrURI($current_user->getNickName()))); + $claim_set->addClaim(new JWTClaim(StandardClaims::Picture, new StringOrURI($pic_url))); + $claim_set->addClaim(new JWTClaim(StandardClaims::Birthdate, new StringOrURI($current_user->getDateOfBirth()))); + $claim_set->addClaim(new JWTClaim(StandardClaims::Gender, new StringOrURI($current_user->getGender()))); + $claim_set->addClaim(new JWTClaim(StandardClaims::Locale, new StringOrURI($current_user->getLanguage()))); } - if(in_array(self::UserProfileScope_Email, $scopes)){ + if (in_array(self::UserProfileScope_Email, $scopes)) + { // Address Claim - $data['email'] = $current_user->getEmail(); + $claim_set->addClaim(new JWTClaim(StandardClaims::Email, new StringOrURI($current_user->getEmail()))); + $claim_set->addClaim(new JWTClaim(StandardClaims::EmailVerified, new JsonValue(true))); } - } - catch(Exception $ex){ + } catch (Exception $ex) { $this->log_service->error($ex); throw $ex; } - return $data; + return $claim_set; } } \ No newline at end of file diff --git a/app/services/openid/AuthenticationStrategy.php b/app/services/openid/AuthenticationStrategy.php deleted file mode 100644 index 101a6106..00000000 --- a/app/services/openid/AuthenticationStrategy.php +++ /dev/null @@ -1,26 +0,0 @@ -with('context', $context); - } - - public function doConsent(OpenIdAuthenticationRequest $request, RequestContext $context) - { - return Redirect::action('UserController@getConsent')->with('context', $context); - } -} \ No newline at end of file diff --git a/app/services/openid/MementoRequestService.php b/app/services/openid/MementoRequestService.php deleted file mode 100644 index 3bb45d68..00000000 --- a/app/services/openid/MementoRequestService.php +++ /dev/null @@ -1,83 +0,0 @@ - $value) { - if (stristr($key, "openid") !== false) { - array_push($openid_params, $key); - } - } - if (count($openid_params) > 0) { - Input::flashOnly($openid_params); - return true; - } else { - $old_data = Input::old(); - $openid_params = array(); - foreach ($old_data as $key => $value) { - if (stristr($key, "openid") !== false) { - array_push($openid_params, $key); - } - } - if (count($openid_params) > 0) { - Session::reflash(); - return true; - } - } - - return false; - } - - public function getCurrentRequest() - { - $msg = new OpenIdMessage(Input::all()); - if (!$msg->isValid()) { - $msg = null; - $old_data = Input::old(); - $openid_params = array(); - foreach ($old_data as $key => $value) { - if (stristr($key, "openid") !== false) { - $openid_params[$key] = $value; - } - } - if (count($openid_params) > 0) { - $msg = new OpenIdMessage($openid_params); - } - } - return $msg; - } - - public function clearCurrentRequest() - { - $old_data = Input::old(); - $openid_params = array(); - - foreach ($old_data as $key => $value) { - if (stristr($key, "openid") !== false) { - array_push($openid_params, $key); - } - } - - if (count($openid_params) > 0) { - foreach ($openid_params as $open_id_param) { - Session::forget($open_id_param); - Session::remove($open_id_param); - } - } - } -} \ No newline at end of file diff --git a/app/services/openid/OpenIdMementoSessionSerializerService.php b/app/services/openid/OpenIdMementoSessionSerializerService.php new file mode 100644 index 00000000..00796b30 --- /dev/null +++ b/app/services/openid/OpenIdMementoSessionSerializerService.php @@ -0,0 +1,58 @@ +getState())); + Session::put('openid.request.state', $state); + Session::save(); + } + + /** + * @return OpenIdMessageMemento + */ + public function load() + { + $state = Session::get('openid.request.state', null); + + if(is_null($state)) return null; + + $state = json_decode( base64_decode($state), true); + + return OpenIdMessageMemento::buildFromState($state); + } + + /** + * @return void + */ + public function forget() + { + Session::remove('openid.request.state'); + Session::save(); + } + + /** + * @return bool + */ + public function exists() + { + return Session::has('openid.request.state'); + } +} \ No newline at end of file diff --git a/app/services/openid/OpenIdProvider.php b/app/services/openid/OpenIdProvider.php index 50d047de..771375ee 100644 --- a/app/services/openid/OpenIdProvider.php +++ b/app/services/openid/OpenIdProvider.php @@ -17,8 +17,7 @@ class OpenIdProvider extends ServiceProvider { public function register() { //register on boot bc we rely on Illuminate\Redis\ServiceProvider\RedisServiceProvider - App::singleton(OpenIdServiceCatalog::MementoService, 'services\\openid\\MementoRequestService'); - App::singleton(OpenIdServiceCatalog::AuthenticationStrategy, 'services\\openid\\AuthenticationStrategy'); + App::singleton(OpenIdServiceCatalog::MementoSerializerService, 'services\\openid\\OpenIdMementoSessionSerializerService'); App::singleton(OpenIdServiceCatalog::ServerExtensionsService, 'services\\openid\\ServerExtensionsService'); App::singleton(OpenIdServiceCatalog::AssociationService, 'services\\openid\\AssociationService'); App::singleton(OpenIdServiceCatalog::TrustedSitesService, 'services\\openid\\TrustedSitesService'); diff --git a/app/services/openid/UserService.php b/app/services/openid/UserService.php index 8b9b17a8..50c20bca 100644 --- a/app/services/openid/UserService.php +++ b/app/services/openid/UserService.php @@ -2,225 +2,198 @@ namespace services\openid; use auth\IUserRepository; -use openid\model\IOpenIdUser; +use auth\User; use Exception; +use openid\model\IOpenIdUser; use openid\services\IUserService; -use utils\services\ILogService; use utils\db\ITransactionService; +use utils\services\ILogService; + /** * Class UserService * @package services\openid */ -class UserService implements IUserService +final class UserService implements IUserService { - private $repository; - private $log_service; - private $tx_service; + /** + * @var IUserRepository + */ + private $repository; + /** + * @var ILogService + */ + private $log_service; + /** + * @var ITransactionService + */ + private $tx_service; - /** - * @param IUserRepository $repository - * @param ITransactionService $tx_service - * @param ILogService $log_service - */ - public function __construct(IUserRepository $repository, ITransactionService $tx_service, ILogService $log_service){ - $this->repository = $repository; - $this->log_service = $log_service; - $this->tx_service = $tx_service; - } - - - /** - * Associate openid url with given user - * @param IOpenIdUser $user - * @param $proposed_username - * @return bool|IOpenIdUser - * @throws \Exception - */ - public function associateUser(IOpenIdUser &$user, $proposed_username) + /** + * @param IUserRepository $repository + * @param ITransactionService $tx_service + * @param ILogService $log_service + */ + public function __construct(IUserRepository $repository, ITransactionService $tx_service, ILogService $log_service) { - try { - $repository = $this->repository; - if (!is_null($user) && strval($user->identifier) === strval($user->external_identifier)) { - $this->tx_service->transaction(function () use ($proposed_username,&$user,&$repository) { - - $done = false; - $fragment_nbr = 1; - $aux_proposed_username = $proposed_username; - do { - - $old_user = $repository->getOneByCriteria(array( - array('name' => 'identifier','op' => '=','value' => $aux_proposed_username), - array('name' => 'id','op' => '<>','value' => $user->id) )); - - if (is_null($old_user)) { - - $user->identifier = $aux_proposed_username; - $done = $repository->update($user); - } else { - $aux_proposed_username = $proposed_username . "." . $fragment_nbr; - $fragment_nbr++; - } - - } while (!$done); - return $user; - }); - } - } catch (Exception $ex) { - $this->log_service->error($ex); - throw $ex; - } - return false; + $this->repository = $repository; + $this->log_service = $log_service; + $this->tx_service = $tx_service; } - /** - * @param $identifier - * @return mixed|void - * @throws \Exception - */ - public function updateLastLoginDate($identifier) - { - try { - $user = $this->repository->get($identifier); - if (!is_null($user)) { - $user->last_login_date = gmdate("Y-m-d H:i:s", time()); - $this->repository->update($user); - } - } catch (Exception $ex) { - $this->log_service->error($ex); - throw $ex; - } - } - /** - * @param $identifier - * @return mixed|void - * @throws \Exception - */ - public function updateFailedLoginAttempts($identifier) + /** + * @param $identifier + * @return mixed|void + * @throws \Exception + */ + public function updateLastLoginDate($identifier) { try { - $user = $this->repository->get($identifier); + $user = $this->repository->get($identifier); if (!is_null($user)) { - $user->login_failed_attempt+=1; + $user->last_login_date = gmdate("Y-m-d H:i:s", time()); $this->repository->update($user); } } catch (Exception $ex) { - $this->log_service->error($ex); - throw $ex; + $this->log_service->error($ex); + throw $ex; } } - /** - * @param $identifier - * @return mixed|void - * @throws \Exception - */ - public function lockUser($identifier) + /** + * @param $identifier + * @return mixed|void + * @throws \Exception + */ + public function updateFailedLoginAttempts($identifier) { try { - $user = $this->repository->get($identifier); + $user = $this->repository->get($identifier); + if (!is_null($user)) { + $user->login_failed_attempt += 1; + $this->repository->update($user); + } + } catch (Exception $ex) { + $this->log_service->error($ex); + throw $ex; + } + } + + /** + * @param $identifier + * @return mixed|void + * @throws \Exception + */ + public function lockUser($identifier) + { + try { + $user = $this->repository->get($identifier); if (!is_null($user)) { - $user->lock = true; - $this->repository->update($user); + $user->lock = true; + $this->repository->update($user); Log::warning(sprintf("User %d locked ", $identifier)); } } catch (Exception $ex) { - $this->log_service->error($ex); - throw $ex; + $this->log_service->error($ex); + throw $ex; } } - /** - * @param $identifier - * @return mixed|void - * @throws \Exception - */ - public function unlockUser($identifier) - { - try { - $user = $this->repository->get($identifier); - if (!is_null($user)) { - - $user->lock = false; - $this->repository->update($user); - - Log::warning(sprintf("User %d unlocked ", $identifier)); - } - } catch (Exception $ex) { - $this->log_service->error($ex); - throw $ex; - } - } - - /** - * @param $identifier - * @return mixed|void - * @throws \Exception - */ - public function activateUser($identifier) + /** + * @param $identifier + * @return mixed|void + * @throws \Exception + */ + public function unlockUser($identifier) { try { - $user = $this->repository->get($identifier); + $user = $this->repository->get($identifier); if (!is_null($user)) { - $user->active = true; - $this->repository->update($user); + + $user->lock = false; + $this->repository->update($user); + + Log::warning(sprintf("User %d unlocked ", $identifier)); } } catch (Exception $ex) { - $this->log_service->error($ex); - throw $ex; + $this->log_service->error($ex); + throw $ex; } } - /** - * @param $identifier - * @return mixed|void - * @throws \Exception - */ - public function deActivateUser($identifier) - { - try { - $user = $this->repository->get($identifier); - if (!is_null($user)) { - $user->active = false; - $this->repository->update($user); - } - } catch (Exception $ex) { - $this->log_service->error($ex); - throw $ex; - } - } - - /** - * @param $identifier - * @param $show_pic - * @param $show_full_name - * @param $show_email - * @return bool - * @throws \Exception - */ - public function saveProfileInfo($identifier, $show_pic, $show_full_name, $show_email) + /** + * @param $identifier + * @return mixed|void + * @throws \Exception + */ + public function activateUser($identifier) { try { - $user = $this->repository->get($identifier); + $user = $this->repository->get($identifier); + if (!is_null($user)) { + $user->active = true; + $this->repository->update($user); + } + } catch (Exception $ex) { + $this->log_service->error($ex); + throw $ex; + } + } + + /** + * @param $identifier + * @return mixed|void + * @throws \Exception + */ + public function deActivateUser($identifier) + { + try { + $user = $this->repository->get($identifier); + if (!is_null($user)) { + $user->active = false; + $this->repository->update($user); + } + } catch (Exception $ex) { + $this->log_service->error($ex); + throw $ex; + } + } + + /** + * @param $identifier + * @param $show_pic + * @param $show_full_name + * @param $show_email + * @return bool + * @throws \Exception + */ + public function saveProfileInfo($identifier, $show_pic, $show_full_name, $show_email) + { + try { + $user = $this->repository->get($identifier); if (!is_null($user)) { $user->public_profile_show_photo = $show_pic; $user->public_profile_show_fullname = $show_full_name; $user->public_profile_show_email = $show_email; - return $this->repository->update($user); + + return $this->repository->update($user); } } catch (Exception $ex) { - $this->log_service->error($ex); - throw $ex; + $this->log_service->error($ex); + throw $ex; } - return false; + + return false; } - public function get($id){ + public function get($id) + { return $this->repository->get($id); } + /** * @param int $page_nbr * @param int $page_size @@ -230,6 +203,59 @@ class UserService implements IUserService */ public function getAll($page_nbr = 1, $page_size = 10, array $filters = array(), array $fields = array('*')) { - return $this->repository->getByPage($page_nbr, $page_size, $filters,$fields); + return $this->repository->getAll($page_nbr, $page_size, $filters, $fields); } + + /** + * @param \Member $member + * @return IOpenIdUser + */ + public function buildUser(\Member $member) + { + $repository = $this->repository; + return $this->tx_service->transaction(function () use($member, $repository){ + //create user + $user = new User(); + $user->external_identifier = $member->ID; + $user->identifier = $member->ID; + $user->last_login_date = gmdate("Y-m-d H:i:s", time()); + $user->active = true; + $user->lock = false; + $user->login_failed_attempt = 0; + + $repository->add($user); + $proposed_username = strtolower($member->FirstName . "." . $member->Surname); + $done = false; + $fragment_nbr = 1; + $aux_proposed_username = $proposed_username; + do + { + + $old_user = $repository->getOneByCriteria + ( + array + ( + array('name' => 'identifier', 'op' => '=', 'value' => $aux_proposed_username), + array('name' => 'id', 'op' => '<>', 'value' => $user->id) + ) + ); + + if (is_null($old_user)) + { + + $user->identifier = $aux_proposed_username; + $done = $repository->update($user); + } + else + { + $aux_proposed_username = $proposed_username . "." . $fragment_nbr; + $fragment_nbr++; + } + + } while (!$done); + + return $user; + }); + } + } \ No newline at end of file diff --git a/app/services/security_policies/AbstractBlacklistSecurityPolicy.php b/app/services/security_policies/AbstractBlacklistSecurityPolicy.php index bf67b3b0..50d7c78a 100644 --- a/app/services/security_policies/AbstractBlacklistSecurityPolicy.php +++ b/app/services/security_policies/AbstractBlacklistSecurityPolicy.php @@ -2,17 +2,17 @@ namespace services; +use Auth; use BannedIP; use DB; use Log; -use Auth; +use utils\db\ITransactionService; +use utils\IPHelper; use utils\services\ICacheService; use utils\services\ILockManagerService; use utils\services\ISecurityPolicy; use utils\services\ISecurityPolicyCounterMeasure; use utils\services\IServerConfigurationService; -use utils\IPHelper; -use utils\db\ITransactionService; /** * Class AbstractBlacklistSecurityPolicy @@ -22,22 +22,30 @@ abstract class AbstractBlacklistSecurityPolicy implements ISecurityPolicy { protected $server_configuration_service; + /** + * @var ISecurityPolicyCounterMeasure + */ protected $counter_measure; protected $lock_manager_service; protected $cache_service; - protected $tx_service; + protected $tx_service; - /** - * @param IServerConfigurationService $server_configuration_service - * @param ILockManagerService $lock_manager_service - * @param ICacheService $cache_service - * @param ITransactionService $tx_service - */ - public function __construct(IServerConfigurationService $server_configuration_service, ILockManagerService $lock_manager_service, ICacheService $cache_service,ITransactionService $tx_service) { + /** + * @param IServerConfigurationService $server_configuration_service + * @param ILockManagerService $lock_manager_service + * @param ICacheService $cache_service + * @param ITransactionService $tx_service + */ + public function __construct( + IServerConfigurationService $server_configuration_service, + ILockManagerService $lock_manager_service, + ICacheService $cache_service, + ITransactionService $tx_service + ) { $this->server_configuration_service = $server_configuration_service; - $this->lock_manager_service = $lock_manager_service; - $this->cache_service = $cache_service; - $this->tx_service = $tx_service; + $this->lock_manager_service = $lock_manager_service; + $this->cache_service = $cache_service; + $this->tx_service = $tx_service; } public function setCounterMeasure(ISecurityPolicyCounterMeasure $counter_measure) @@ -55,24 +63,26 @@ abstract class AbstractBlacklistSecurityPolicy implements ISecurityPolicy try { $remote_address = IPHelper::getUserIp(); //try to create on cache - $this->cache_service->addSingleValue($remote_address, $initial_hits, intval($this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"))); + $this->cache_service->addSingleValue($remote_address, $initial_hits, + intval($this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"))); - Log::warning(sprintf("AbstractBlacklistSecurityPolicy: Banning ip %s by Exception %s", $remote_address, $exception_type)); + Log::warning(sprintf("AbstractBlacklistSecurityPolicy: Banning ip %s by Exception %s", $remote_address, + $exception_type)); //try to create on db - $this->tx_service->transaction(function () use ($remote_address, $exception_type, $initial_hits) { + $this->tx_service->transaction(function () use ($remote_address, $exception_type, $initial_hits) { - $banned_ip = BannedIP::where("ip", "=", $remote_address)->first(); + $banned_ip = BannedIP::where("ip", "=", $remote_address)->first(); if (!$banned_ip) { - $banned_ip = new BannedIP(); + $banned_ip = new BannedIP(); $banned_ip->ip = $remote_address; } $banned_ip->exception_type = $exception_type; - $banned_ip->hits = $initial_hits; + $banned_ip->hits = $initial_hits; - if(Auth::check()){ - $banned_ip->user_id = Auth::user()->getId(); + if (Auth::check()) { + $banned_ip->user_id = Auth::user()->getId(); } $banned_ip->Save(); diff --git a/app/services/security_policies/BlacklistSecurityPolicy.php b/app/services/security_policies/BlacklistSecurityPolicy.php index eedd4a8e..00e1aa2b 100644 --- a/app/services/security_policies/BlacklistSecurityPolicy.php +++ b/app/services/security_policies/BlacklistSecurityPolicy.php @@ -8,12 +8,13 @@ use DB; use Exception; use Log; use UserExceptionTrail; +use utils\db\ITransactionService; use utils\exceptions\UnacquiredLockException; +use utils\IPHelper; use utils\services\ICacheService; use utils\services\ILockManagerService; use utils\services\IServerConfigurationService; -use utils\IPHelper; -use utils\db\ITransactionService; + /** * Class BlacklistSecurityPolicy * implements check point security pattern @@ -24,29 +25,66 @@ class BlacklistSecurityPolicy extends AbstractBlacklistSecurityPolicy private $exception_dictionary = array(); - /** - * @param IServerConfigurationService $server_configuration_service - * @param ILockManagerService $lock_manager_service - * @param ICacheService $cache_service - * @param ITransactionService $tx_service - */ - public function __construct(IServerConfigurationService $server_configuration_service, ILockManagerService $lock_manager_service, ICacheService $cache_service,ITransactionService $tx_service) - { - parent::__construct($server_configuration_service, $lock_manager_service, $cache_service,$tx_service); + /** + * @param IServerConfigurationService $server_configuration_service + * @param ILockManagerService $lock_manager_service + * @param ICacheService $cache_service + * @param ITransactionService $tx_service + */ + public function __construct( + IServerConfigurationService $server_configuration_service, + ILockManagerService $lock_manager_service, + ICacheService $cache_service, + ITransactionService $tx_service + ) { + parent::__construct($server_configuration_service, $lock_manager_service, $cache_service, $tx_service); // here we configure on which exceptions are we interested and the max occurrence attempts and initial delay on tar pit for // offending IP address $this->exception_dictionary = array( - 'openid\exceptions\ReplayAttackException' => array(null,'BlacklistSecurityPolicy.ReplayAttackExceptionInitialDelay'), - 'openid\exceptions\InvalidNonce' => array('BlacklistSecurityPolicy.MaxInvalidNonceAttempts','BlacklistSecurityPolicy.InvalidNonceInitialDelay'), - 'openid\exceptions\InvalidOpenIdMessageException' => array('BlacklistSecurityPolicy.MaxInvalidOpenIdMessageExceptionAttempts','BlacklistSecurityPolicy.InvalidOpenIdMessageExceptionInitialDelay'), - 'openid\exceptions\OpenIdInvalidRealmException' => array('BlacklistSecurityPolicy.MaxOpenIdInvalidRealmExceptionAttempts','BlacklistSecurityPolicy.OpenIdInvalidRealmExceptionInitialDelay'), - 'openid\exceptions\InvalidOpenIdMessageMode' => array('BlacklistSecurityPolicy.MaxInvalidOpenIdMessageModeAttempts','BlacklistSecurityPolicy.InvalidOpenIdMessageModeInitialDelay'), - 'openid\exceptions\InvalidOpenIdAuthenticationRequestMode' => array('BlacklistSecurityPolicy.MaxInvalidOpenIdAuthenticationRequestModeAttempts','BlacklistSecurityPolicy.InvalidOpenIdAuthenticationRequestModeInitialDelay'), - 'openid\exceptions\InvalidAssociation' => array('BlacklistSecurityPolicy.MaxInvalidAssociationAttempts','BlacklistSecurityPolicy.InvalidAssociationInitialDelay'), - 'auth\exceptions\AuthenticationException' => array('BlacklistSecurityPolicy.MaxAuthenticationExceptionAttempts','BlacklistSecurityPolicy.AuthenticationExceptionInitialDelay'), - 'oauth2\exceptions\ReplayAttackException' => array(null,'BlacklistSecurityPolicy.OAuth2.AuthCodeReplayAttackInitialDelay'), - 'oauth2\exceptions\InvalidAuthorizationCodeException' => array('BlacklistSecurityPolicy.OAuth2.MaxInvalidAuthorizationCodeAttempts','BlacklistSecurityPolicy.OAuth2.InvalidAuthorizationCodeInitialDelay'), - 'oauth2\exceptions\BearerTokenDisclosureAttemptException' => array('BlacklistSecurityPolicy.OAuth2.MaxInvalidBearerTokenDisclosureAttempt','BlacklistSecurityPolicy.OAuth2.BearerTokenDisclosureAttemptInitialDelay'), + 'openid\exceptions\ReplayAttackException' => array( + 'BlacklistSecurityPolicy.MaxReplayAttackExceptionAttempts', + 'BlacklistSecurityPolicy.ReplayAttackExceptionInitialDelay' + ), + 'openid\exceptions\InvalidNonce' => array( + 'BlacklistSecurityPolicy.MaxInvalidNonceAttempts', + 'BlacklistSecurityPolicy.InvalidNonceInitialDelay' + ), + 'openid\exceptions\InvalidOpenIdMessageException' => array( + 'BlacklistSecurityPolicy.MaxInvalidOpenIdMessageExceptionAttempts', + 'BlacklistSecurityPolicy.InvalidOpenIdMessageExceptionInitialDelay' + ), + 'openid\exceptions\OpenIdInvalidRealmException' => array( + 'BlacklistSecurityPolicy.MaxOpenIdInvalidRealmExceptionAttempts', + 'BlacklistSecurityPolicy.OpenIdInvalidRealmExceptionInitialDelay' + ), + 'openid\exceptions\InvalidOpenIdMessageMode' => array( + 'BlacklistSecurityPolicy.MaxInvalidOpenIdMessageModeAttempts', + 'BlacklistSecurityPolicy.InvalidOpenIdMessageModeInitialDelay' + ), + 'openid\exceptions\InvalidOpenIdAuthenticationRequestMode' => array( + 'BlacklistSecurityPolicy.MaxInvalidOpenIdAuthenticationRequestModeAttempts', + 'BlacklistSecurityPolicy.InvalidOpenIdAuthenticationRequestModeInitialDelay' + ), + 'openid\exceptions\InvalidAssociation' => array( + 'BlacklistSecurityPolicy.MaxInvalidAssociationAttempts', + 'BlacklistSecurityPolicy.InvalidAssociationInitialDelay' + ), + 'auth\exceptions\AuthenticationException' => array( + 'BlacklistSecurityPolicy.MaxAuthenticationExceptionAttempts', + 'BlacklistSecurityPolicy.AuthenticationExceptionInitialDelay' + ), + 'oauth2\exceptions\ReplayAttackException' => array( + 'BlacklistSecurityPolicy.OAuth2.MaxAuthCodeReplayAttackAttempts', + 'BlacklistSecurityPolicy.OAuth2.AuthCodeReplayAttackInitialDelay' + ), + 'oauth2\exceptions\InvalidAuthorizationCodeException' => array( + 'BlacklistSecurityPolicy.OAuth2.MaxInvalidAuthorizationCodeAttempts', + 'BlacklistSecurityPolicy.OAuth2.InvalidAuthorizationCodeInitialDelay' + ), + 'oauth2\exceptions\BearerTokenDisclosureAttemptException' => array( + 'BlacklistSecurityPolicy.OAuth2.MaxInvalidBearerTokenDisclosureAttempt', + 'BlacklistSecurityPolicy.OAuth2.BearerTokenDisclosureAttemptInitialDelay' + ), ); } @@ -60,8 +98,9 @@ class BlacklistSecurityPolicy extends AbstractBlacklistSecurityPolicy $remote_address = IPHelper::getUserIp(); try { //check if banned ip is on cache ... - if ($this->cache_service->incCounterIfExists($remote_address)){ + if ($this->cache_service->incCounterIfExists($remote_address)) { $this->counter_measure->trigger(); + return false; } //check on db @@ -70,7 +109,7 @@ class BlacklistSecurityPolicy extends AbstractBlacklistSecurityPolicy $this->lock_manager_service->acquireLock("lock.ip." . $remote_address); try { //check lifetime - $issued = $banned_ip->created_at; + $issued = $banned_ip->created_at; $utc_now = gmdate("Y-m-d H:i:s", time()); $utc_now = DateTime::createFromFormat("Y-m-d H:i:s", $utc_now); //get time lived on seconds @@ -78,30 +117,32 @@ class BlacklistSecurityPolicy extends AbstractBlacklistSecurityPolicy if ($time_lived_seconds >= intval($this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"))) { //void banned ip $banned_ip->delete(); + return true; } $banned_ip->hits = $banned_ip->hits + 1; $banned_ip->Save(); //save ip on cache - $this->cache_service->addSingleValue($banned_ip->ip, $banned_ip->hits,intval($this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.BannedIpLifeTimeSeconds") - $time_lived_seconds)); - } - catch (Exception $ex) { + $this->cache_service->addSingleValue($banned_ip->ip, $banned_ip->hits, + intval($this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.BannedIpLifeTimeSeconds") - $time_lived_seconds)); + } catch (Exception $ex) { Log::error($ex); } //release lock $this->lock_manager_service->releaseLock("lock.ip." . $remote_address); $this->counter_measure->trigger(); + return false; } - } - catch (UnacquiredLockException $ex1) { + } catch (UnacquiredLockException $ex1) { Log::error($ex1); $res = false; } catch (Exception $ex) { Log::error($ex); $res = false; } + return $res; } @@ -112,23 +153,56 @@ class BlacklistSecurityPolicy extends AbstractBlacklistSecurityPolicy */ public function apply(Exception $ex) { - try { - $remote_ip = IPHelper::getUserIp(); + try + { + $remote_ip = IPHelper::getUserIp(); $exception_class = get_class($ex); //check exception count by type on last "MinutesWithoutExceptions" minutes... $exception_count = intval(UserExceptionTrail::where('from_ip', '=', $remote_ip) ->where('exception_type', '=', $exception_class) - ->where('created_at', '>', DB::raw('( UTC_TIMESTAMP() - INTERVAL ' . $this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.MinutesWithoutExceptions") . ' MINUTE )')) + ->where('created_at', '>', + DB::raw('( UTC_TIMESTAMP() - INTERVAL ' . $this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.MinutesWithoutExceptions") . ' MINUTE )')) ->count()); - if(array_key_exists($exception_class,$this->exception_dictionary)){ - $params = $this->exception_dictionary[$exception_class]; - $max_attempts = !is_null($params[0]) && !empty($params[0])? intval($this->server_configuration_service->getConfigValue($params[0])):0; + if (array_key_exists($exception_class, $this->exception_dictionary)) + { + $params = $this->exception_dictionary[$exception_class]; + $max_attempts = !is_null($params[0]) && !empty($params[0]) ? intval($this->server_configuration_service->getConfigValue($params[0])) : 0; + + Log::info + ( + sprintf + ( + 'IP %s, - exception_class %s - exception_count %s - max allowed attempts %s', + $remote_ip, + $exception_class, + $exception_count, + $max_attempts + ) + ); + $initial_delay_on_tar_pit = intval($this->server_configuration_service->getConfigValue($params[1])); if ($exception_count >= $max_attempts) + { + Log::warning + ( + sprintf + ( + 'banning IP %s, - exception_class %s - exception_count %s - max allowed attempts %s', + $remote_ip, + $exception_class, + $exception_count, + $max_attempts + ) + ); + $this->createBannedIP($initial_delay_on_tar_pit, $exception_class); + } } - } catch (Exception $ex) { + } + catch (Exception $ex) + { Log::error($ex); + throw $ex; } } diff --git a/app/services/security_policies/DelayCounterMeasure.php b/app/services/security_policies/DelayCounterMeasure.php index 2b2a9853..89f5219e 100644 --- a/app/services/security_policies/DelayCounterMeasure.php +++ b/app/services/security_policies/DelayCounterMeasure.php @@ -8,6 +8,10 @@ use utils\services\ICacheService; use utils\services\ISecurityPolicyCounterMeasure; use utils\IPHelper; +/** + * Class DelayCounterMeasure + * @package services + */ class DelayCounterMeasure implements ISecurityPolicyCounterMeasure { private $cache_service; diff --git a/app/services/utils/BannedIPService.php b/app/services/utils/BannedIPService.php index 89ac7f5a..b5f656e0 100644 --- a/app/services/utils/BannedIPService.php +++ b/app/services/utils/BannedIPService.php @@ -1,45 +1,50 @@ cache_service = $cache_service; + /** + * @param ICacheService $cache_service + * @param IServerConfigurationService $server_configuration_service + * @param IAuthService $auth_service + * @param ILogService $log_service + * @param ITransactionService $tx_service + */ + public function __construct( + ICacheService $cache_service, + IServerConfigurationService $server_configuration_service, + IAuthService $auth_service, + ILogService $log_service, + ITransactionService $tx_service + ) { + + $this->cache_service = $cache_service; $this->server_configuration_service = $server_configuration_service; - $this->log_service = $log_service; - $this->auth_service = $auth_service; - $this->tx_service = $tx_service; + $this->log_service = $log_service; + $this->auth_service = $auth_service; + $this->tx_service = $tx_service; } /** @@ -53,9 +58,10 @@ class BannedIPService implements IBannedIPService { try { $remote_address = Request::server('REMOTE_ADDR'); //try to create on cache - $this->cache_service->addSingleValue($remote_address, $initial_hits, intval($this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"))); + $this->cache_service->addSingleValue($remote_address, $initial_hits, + intval($this->server_configuration_service->getConfigValue("BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"))); - $this->tx_service->transaction(function () use ($remote_address, $exception_type, $initial_hits,&$res) { + $this->tx_service->transaction(function () use ($remote_address, $exception_type, $initial_hits, &$res) { $banned_ip = BannedIP::where("ip", "=", $remote_address)->first(); if (!$banned_ip) { @@ -63,34 +69,36 @@ class BannedIPService implements IBannedIPService { $banned_ip->ip = $remote_address; } $banned_ip->exception_type = $exception_type; - $banned_ip->hits = $initial_hits; + $banned_ip->hits = $initial_hits; - if(Auth::check()){ - $banned_ip->user_id = Auth::user()->getId(); - } + if (Auth::check()) { + $banned_ip->user_id = Auth::user()->getId(); + } - $res = $banned_ip->Save(); + $res = $banned_ip->Save(); }); } catch (Exception $ex) { $this->log_service->error($ex); $res = false; } + return $res; } public function delete($ip) { - $res = false; - $cache_service = $this->cache_service; - $this_var = $this; - $this->tx_service->transaction(function () use ($ip,&$res,&$cache_service,&$this_var) { + $res = false; + $cache_service = $this->cache_service; + $this_var = $this; + $this->tx_service->transaction(function () use ($ip, &$res, &$cache_service, &$this_var) { - if($banned_ip = $this_var->getByIP($ip)){ + if ($banned_ip = $this_var->getByIP($ip)) { $res = $banned_ip->delete(); - $cache_service->delete($ip); + $cache_service->delete($ip); } }); + return $res; } @@ -99,13 +107,15 @@ class BannedIPService implements IBannedIPService { return BannedIP::find($id); } - public function getByIP($ip){ - return BannedIP::where('ip','=',$ip)->first(); + public function getByIP($ip) + { + return BannedIP::where('ip', '=', $ip)->first(); } public function getByPage($page_nbr = 1, $page_size = 10, array $filters = array(), array $fields = array('*')) { DB::getPaginator()->setCurrentPage($page_nbr); - return BannedIP::Filter($filters)->paginate($page_size,$fields); + + return BannedIP::Filter($filters)->paginate($page_size, $fields); } } \ No newline at end of file diff --git a/app/services/utils/ServerConfigurationService.php b/app/services/utils/ServerConfigurationService.php index 11249838..2230706a 100644 --- a/app/services/utils/ServerConfigurationService.php +++ b/app/services/utils/ServerConfigurationService.php @@ -7,9 +7,10 @@ use DB; use Exception; use openid\services\IServerConfigurationService as IOpenIdServerConfigurationService; use ServerConfiguration; +use utils\db\ITransactionService; use utils\services\ICacheService; use utils\services\IServerConfigurationService; -use utils\db\ITransactionService; + /** * Class ServerConfigurationService * @package services @@ -25,88 +26,132 @@ class ServerConfigurationService implements IOpenIdServerConfigurationService, I const DefaultNonceLifetime = 360; private $default_config_params; - private $tx_service; + private $tx_service; - /*** - * @param ICacheService $cache_service - * @param ITransactionService $tx_service - */ - public function __construct(ICacheService $cache_service, ITransactionService $tx_service) + /*** + * @param ICacheService $cache_service + * @param ITransactionService $tx_service + */ + public function __construct(ICacheService $cache_service, ITransactionService $tx_service) { $this->cache_service = $cache_service; - $this->tx_service = $tx_service; + $this->tx_service = $tx_service; $this->default_config_params = array(); //default config values //general - $this->default_config_params["MaxFailed.Login.Attempts"] = Config::get('server.MaxFailed_Login_Attempts', 10); - $this->default_config_params["MaxFailed.LoginAttempts.2ShowCaptcha"] = Config::get('server.MaxFailed_LoginAttempts_2ShowCaptcha', 3); - $this->default_config_params["Assets.Url"] = Config::get('server.Assets_Url', 'http://www.openstack.org/'); - // remember me cookie lifetime (minutes) - $this->default_config_params["Remember.ExpirationTime"] = Config::get('Remember.ExpirationTime',120); + $this->default_config_params["MaxFailed.Login.Attempts"] = Config::get('server.MaxFailed_Login_Attempts', 10); + $this->default_config_params["MaxFailed.LoginAttempts.2ShowCaptcha"] = Config::get('server.MaxFailed_LoginAttempts_2ShowCaptcha', + 3); + $this->default_config_params["Assets.Url"] = Config::get('server.Assets_Url', self::DefaultAssetsUrl ); + // remember me cookie lifetime (minutes) + $this->default_config_params["Remember.ExpirationTime"] = Config::get('Remember.ExpirationTime', 120); //openid - $this->default_config_params["OpenId.Private.Association.Lifetime"] = Config::get('server.OpenId_Private_Association_Lifetime', 240); - $this->default_config_params["OpenId.Session.Association.Lifetime"] = Config::get('server.OpenId_Session_Association_Lifetime', 21600); - $this->default_config_params["OpenId.Nonce.Lifetime"] = Config::get('server.OpenId_Nonce_Lifetime', 360); + $this->default_config_params["OpenId.Private.Association.Lifetime"] = Config::get('server.OpenId_Private_Association_Lifetime', + 240); + $this->default_config_params["OpenId.Session.Association.Lifetime"] = Config::get('server.OpenId_Session_Association_Lifetime', + 21600); + $this->default_config_params["OpenId.Nonce.Lifetime"] = Config::get('server.OpenId_Nonce_Lifetime', 360); //policies - $this->default_config_params["BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"] = Config::get('server.BlacklistSecurityPolicy_BannedIpLifeTimeSeconds', 21600); - $this->default_config_params["BlacklistSecurityPolicy.MinutesWithoutExceptions"] = Config::get('server.BlacklistSecurityPolicy_MinutesWithoutExceptions', 5);; - $this->default_config_params["BlacklistSecurityPolicy.ReplayAttackExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_ReplayAttackExceptionInitialDelay', 10); - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidNonceAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidNonceAttempts', 10); - $this->default_config_params["BlacklistSecurityPolicy.InvalidNonceInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidNonceInitialDelay', 10); - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdMessageExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdMessageExceptionAttempts', 10); - $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdMessageExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdMessageExceptionInitialDelay', 10); - $this->default_config_params["BlacklistSecurityPolicy.MaxOpenIdInvalidRealmExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxOpenIdInvalidRealmExceptionAttempts', 10); - $this->default_config_params["BlacklistSecurityPolicy.OpenIdInvalidRealmExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OpenIdInvalidRealmExceptionInitialDelay', 10); - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdMessageModeAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdMessageModeAttempts', 10); - $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdMessageModeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdMessageModeInitialDelay', 10); - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdAuthenticationRequestModeAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdAuthenticationRequestModeAttempts', 10); - $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdAuthenticationRequestModeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdAuthenticationRequestModeInitialDelay', 10); - $this->default_config_params["BlacklistSecurityPolicy.MaxAuthenticationExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxAuthenticationExceptionAttempts', 10); - $this->default_config_params["BlacklistSecurityPolicy.AuthenticationExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_AuthenticationExceptionInitialDelay', 20); - $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidAssociationAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidAssociationAttempts', 10); - $this->default_config_params["BlacklistSecurityPolicy.InvalidAssociationInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidAssociationInitialDelay', 20); + $this->default_config_params["BlacklistSecurityPolicy.BannedIpLifeTimeSeconds"] = Config::get('server.BlacklistSecurityPolicy_BannedIpLifeTimeSeconds', + 21600); + $this->default_config_params["BlacklistSecurityPolicy.MinutesWithoutExceptions"] = Config::get('server.BlacklistSecurityPolicy_MinutesWithoutExceptions', + 5);; + $this->default_config_params["BlacklistSecurityPolicy.ReplayAttackExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_ReplayAttackExceptionInitialDelay', + 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidNonceAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidNonceAttempts', + 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidNonceInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidNonceInitialDelay', + 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdMessageExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdMessageExceptionAttempts', + 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdMessageExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdMessageExceptionInitialDelay', + 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxOpenIdInvalidRealmExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxOpenIdInvalidRealmExceptionAttempts', + 10); + $this->default_config_params["BlacklistSecurityPolicy.OpenIdInvalidRealmExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OpenIdInvalidRealmExceptionInitialDelay', + 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdMessageModeAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdMessageModeAttempts', + 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdMessageModeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdMessageModeInitialDelay', + 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidOpenIdAuthenticationRequestModeAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidOpenIdAuthenticationRequestModeAttempts', + 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidOpenIdAuthenticationRequestModeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidOpenIdAuthenticationRequestModeInitialDelay', + 10); + $this->default_config_params["BlacklistSecurityPolicy.MaxAuthenticationExceptionAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxAuthenticationExceptionAttempts', + 10); + $this->default_config_params["BlacklistSecurityPolicy.AuthenticationExceptionInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_AuthenticationExceptionInitialDelay', + 20); + $this->default_config_params["BlacklistSecurityPolicy.MaxInvalidAssociationAttempts"] = Config::get('server.BlacklistSecurityPolicy_MaxInvalidAssociationAttempts', + 10); + $this->default_config_params["BlacklistSecurityPolicy.InvalidAssociationInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_InvalidAssociationInitialDelay', + 20); //oauth2 - $this->default_config_params["OAuth2.Enable"] = Config::get('server.OAuth2_Enable', false); - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxAuthCodeReplayAttackAttempts"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxAuthCodeReplayAttackAttempts', 3); - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.AuthCodeReplayAttackInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_AuthCodeReplayAttackInitialDelay', 10); + $this->default_config_params["OAuth2.Enable"] = Config::get('server.OAuth2_Enable', false); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxAuthCodeReplayAttackAttempts"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxAuthCodeReplayAttackAttempts', + 3); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.AuthCodeReplayAttackInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_AuthCodeReplayAttackInitialDelay', + 10); - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxInvalidAuthorizationCodeAttempts"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxInvalidAuthorizationCodeAttempts', 3); - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.InvalidAuthorizationCodeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_InvalidAuthorizationCodeInitialDelay', 10); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxInvalidAuthorizationCodeAttempts"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxInvalidAuthorizationCodeAttempts', + 3); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.InvalidAuthorizationCodeInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_InvalidAuthorizationCodeInitialDelay', + 10); - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxInvalidBearerTokenDisclosureAttempt"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxInvalidBearerTokenDisclosureAttempt', 3); - $this->default_config_params["BlacklistSecurityPolicy.OAuth2.BearerTokenDisclosureAttemptInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_BearerTokenDisclosureAttemptInitialDelay', 10); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.MaxInvalidBearerTokenDisclosureAttempt"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_MaxInvalidBearerTokenDisclosureAttempt', + 3); + $this->default_config_params["BlacklistSecurityPolicy.OAuth2.BearerTokenDisclosureAttemptInitialDelay"] = Config::get('server.BlacklistSecurityPolicy_OAuth2_BearerTokenDisclosureAttemptInitialDelay', + 10); - $this->default_config_params["OAuth2.AuthorizationCode.Lifetime"] = Config::get('server.OAuth2_AuthorizationCode_Lifetime', 240); - $this->default_config_params["OAuth2.AccessToken.Lifetime"] = Config::get('server.OAuth2_AccessToken_Lifetime', 3600); + $this->default_config_params["OAuth2.AuthorizationCode.Lifetime"] = Config::get('server.OAuth2_AuthorizationCode_Lifetime', + 240); + $this->default_config_params["OAuth2.AccessToken.Lifetime"] = Config::get('server.OAuth2_AccessToken_Lifetime', + 3600); + + $this->default_config_params["OAuth2.IdToken.Lifetime"] = Config::get('server.OAuth2_IdToken_Lifetime', + 3600); //infinite by default - $this->default_config_params["OAuth2.RefreshToken.Lifetime"] = Config::get('server.OAuth2_RefreshToken_Lifetime', 0); + $this->default_config_params["OAuth2.RefreshToken.Lifetime"] = Config::get('server.OAuth2_RefreshToken_Lifetime', + 0); + + $this->default_config_params["OAuth2.AccessToken.Revoked.Lifetime"] = Config::get('server.OAuth2_AccessToken_Revoked_Lifetime', 600); + $this->default_config_params["OAuth2.AccessToken.Void.Lifetime"] = Config::get('server.OAuth2_AccessToken_Void_Lifetime', 600); + $this->default_config_params["OAuth2.RefreshToken.Revoked.Lifetime"] = Config::get('server.OAuth2_RefreshToken_Revoked_Lifetime', 600); //oauth2 policy defaults - $this->default_config_params["OAuth2SecurityPolicy.MinutesWithoutExceptions"] = Config::get('server.OAuth2SecurityPolicy_MinutesWithoutExceptions', 2); - $this->default_config_params["OAuth2SecurityPolicy.MaxBearerTokenDisclosureAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxBearerTokenDisclosureAttempts', 5); - $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidClientExceptionAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidClientExceptionAttempts', 10); - $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidRedeemAuthCodeAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidRedeemAuthCodeAttempts', 10); - $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidInvalidClientCredentialsAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidInvalidClientCredentialsAttempts', 5); + $this->default_config_params["OAuth2SecurityPolicy.MinutesWithoutExceptions"] = Config::get('server.OAuth2SecurityPolicy_MinutesWithoutExceptions', + 2); + $this->default_config_params["OAuth2SecurityPolicy.MaxBearerTokenDisclosureAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxBearerTokenDisclosureAttempts', + 5); + $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidClientExceptionAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidClientExceptionAttempts', + 10); + $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidRedeemAuthCodeAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidRedeemAuthCodeAttempts', + 10); + $this->default_config_params["OAuth2SecurityPolicy.MaxInvalidInvalidClientCredentialsAttempts"] = Config::get('server.OAuth2SecurityPolicy_MaxInvalidInvalidClientCredentialsAttempts', + 5); //ssl $this->default_config_params["SSL.Enable"] = Config::get('server.SSL_Enable', true); + } public function getUserIdentityEndpointURL($identifier) { $url = action("UserController@getIdentity", array("identifier" => $identifier)); + return $url; } public function getOPEndpointURL() { $url = action("OpenIdProviderController@endpoint"); + return $url; } @@ -117,24 +162,26 @@ class ServerConfigurationService implements IOpenIdServerConfigurationService, I */ public function getConfigValue($key) { - $res = null; - $cache_service = $this->cache_service; - $default_config_params = $this->default_config_params; + $res = null; + $cache_service = $this->cache_service; + $default_config_params = $this->default_config_params; - $this->tx_service->transaction(function () use ($key, &$res,&$cache_service,&$default_config_params) { + $this->tx_service->transaction(function () use ($key, &$res, &$cache_service, &$default_config_params) { try { if (!$cache_service->exists($key)) { - if (!is_null($conf = ServerConfiguration::where('key', '=', $key)->first())) - $cache_service->addSingleValue($key, $conf->value); - else - if (isset($default_config_params[$key])) - $cache_service->addSingleValue($key, $default_config_params[$key]); - else { + if (!is_null($conf = ServerConfiguration::where('key', '=', $key)->first())) { + $cache_service->addSingleValue($key, $conf->value); + } else { + if (isset($default_config_params[$key])) { + $cache_service->addSingleValue($key, $default_config_params[$key]); + } else { $res = null; + return; } + } } $res = $cache_service->getSingleValue($key); @@ -156,25 +203,34 @@ class ServerConfigurationService implements IOpenIdServerConfigurationService, I public function saveConfigValue($key, $value) { - $res = false; - $cache_service = $this->cache_service; + $res = false; + $cache_service = $this->cache_service; - $this->tx_service->transaction(function () use ($key, $value, &$res,&$cache_service) { + $this->tx_service->transaction(function () use ($key, $value, &$res, &$cache_service) { $conf = ServerConfiguration::where('key', '=', $key)->first(); if (is_null($conf)) { - $conf = new ServerConfiguration(); - $conf->key = $key; + $conf = new ServerConfiguration(); + $conf->key = $key; $conf->value = $value; $res = $conf->Save(); } else { $conf->value = $value; - $res = $conf->Save(); + $res = $conf->Save(); } - $cache_service->delete($key); + $cache_service->delete($key); }); + return $res; } + + /** + * @return string + */ + public function getSiteUrl() + { + return Config::get('app.url'); + } } diff --git a/app/start/global.php b/app/start/global.php index 43e677ed..c749aa69 100644 --- a/app/start/global.php +++ b/app/start/global.php @@ -98,7 +98,7 @@ App::error(function (Exception $exception, $code) { if ($checkpoint_service) { $checkpoint_service->trackException($exception); } - return View::make('404'); + return Response::view('404', array(), 404); } }); @@ -110,7 +110,7 @@ App::error(function (InvalidOpenIdMessageException $exception, $code) { if ($checkpoint_service) { $checkpoint_service->trackException($exception); } - return View::make('404'); + return Response::view('404', array(), 404); } }); @@ -121,7 +121,7 @@ App::error(function (InvalidOAuth2Request $exception, $code) { if ($checkpoint_service) { $checkpoint_service->trackException($exception); } - return View::make('404'); + return Response::view('404', array(), 404); } }); diff --git a/app/strategies/DefaultLoginStrategy.php b/app/strategies/DefaultLoginStrategy.php index 30c6598f..30066080 100644 --- a/app/strategies/DefaultLoginStrategy.php +++ b/app/strategies/DefaultLoginStrategy.php @@ -1,4 +1,5 @@ getReturnTo(); if (is_null($return_to) || empty($return_to)) { - return \View::make('404'); + return Response::view('404', array(), 404); } $return_to = (strpos($return_to, "?") == false) ? $return_to . "?" . $query_string : $return_to . "&" . $query_string; return Redirect::to($return_to); diff --git a/app/strategies/IndirectResponseUrlFragmentStrategy.php b/app/strategies/IndirectResponseUrlFragmentStrategy.php index 779c50ea..c1c28d35 100644 --- a/app/strategies/IndirectResponseUrlFragmentStrategy.php +++ b/app/strategies/IndirectResponseUrlFragmentStrategy.php @@ -24,7 +24,7 @@ class IndirectResponseUrlFragmentStrategy implements IHttpResponseStrategy $return_to = $response->getReturnTo(); if (is_null($return_to) || empty($return_to)) { - return \View::make('404'); + return Response::view('404', array(), 404);; } $return_to = (strpos($return_to, "#") == false) ? $return_to . "#" . $fragment : $return_to . "&" . $fragment; diff --git a/app/strategies/OAuth2ConsentStrategy.php b/app/strategies/OAuth2ConsentStrategy.php index 4189db2d..9e2cb178 100644 --- a/app/strategies/OAuth2ConsentStrategy.php +++ b/app/strategies/OAuth2ConsentStrategy.php @@ -1,30 +1,51 @@ auth_service = $auth_service; $this->memento_service = $memento_service; @@ -34,16 +55,26 @@ class OAuth2ConsentStrategy implements IConsentStrategy { public function getConsent() { - $request = $this->memento_service->getCurrentAuthorizationRequest(); - $client_id = $request->getClientId(); + $auth_request = OAuth2AuthorizationRequestFactory::getInstance()->build + ( + OAuth2Message::buildFromMemento + ( + $this->memento_service->load() + ) + ); + + $client_id = $auth_request->getClientId(); $client = $this->client_service->getClientById($client_id); - $scopes = explode(' ',$request->getScope()); + $scopes = explode(' ',$auth_request->getScope()); $requested_scopes = $this->scope_service->getScopesByName($scopes); + $data = array(); $data['requested_scopes'] = $requested_scopes; $data['app_name'] = $client->getApplicationName(); - $data['redirect_to'] = $request->getRedirectUri(); + $data['redirect_to'] = $auth_request->getRedirectUri(); $data['website'] = $client->getWebsite(); + $data['tos_uri'] = $client->getTermOfServiceUri(); + $data['policy_uri'] = $client->getPolicyUri(); $app_logo = $client->getApplicationLogo(); @@ -57,6 +88,7 @@ class OAuth2ConsentStrategy implements IConsentStrategy { public function postConsent($trust_action) { $this->auth_service->setUserAuthorizationResponse($trust_action); + return Redirect::action('OAuth2ProviderController@authorize'); } } \ No newline at end of file diff --git a/app/strategies/OAuth2LoginStrategy.php b/app/strategies/OAuth2LoginStrategy.php index abb97e17..0e075e0f 100644 --- a/app/strategies/OAuth2LoginStrategy.php +++ b/app/strategies/OAuth2LoginStrategy.php @@ -3,32 +3,72 @@ namespace strategies; use Auth; -use oauth2\services\IMementoOAuth2AuthenticationRequestService; +use oauth2\factories\OAuth2AuthorizationRequestFactory; +use oauth2\OAuth2Message; +use oauth2\services\IMementoOAuth2SerializerService; +use oauth2\services\ISecurityContextService; use Redirect; -use View; use services\IUserActionService; -use utils\services\IAuthService; use utils\IPHelper; +use utils\services\IAuthService; +use View; +use Session; -class OAuth2LoginStrategy implements ILoginStrategy{ +/** + * Class OAuth2LoginStrategy + * @package strategies + */ +class OAuth2LoginStrategy implements ILoginStrategy +{ - private $memento_service; - private $user_action_service; - private $auth_service; + /** + * @var IMementoOAuth2SerializerService + */ + private $memento_service; + /** + * @var IUserActionService + */ + private $user_action_service; + /** + * @var IAuthService + */ + private $auth_service; - public function __construct(IAuthService $auth_service, - IMementoOAuth2AuthenticationRequestService $memento_service, - IUserActionService $user_action_service - ) - { - $this->memento_service = $memento_service; - $this->user_action_service = $user_action_service; - $this->auth_service = $auth_service; - } + /** + * @var ISecurityContextService + */ + private $security_context_service; - public function getLogin() + /** + * @param IAuthService $auth_service + * @param IMementoOAuth2SerializerService $memento_service + * @param IUserActionService $user_action_service + * @param ISecurityContextService $security_context_service + */ + public function __construct + ( + IAuthService $auth_service, + IMementoOAuth2SerializerService $memento_service, + IUserActionService $user_action_service, + ISecurityContextService $security_context_service + ) { - if (Auth::guest()) { + $this->memento_service = $memento_service; + $this->user_action_service = $user_action_service; + $this->auth_service = $auth_service; + $this->security_context_service = $security_context_service; + } + + public function getLogin() + { + if (Auth::guest()) + { + $requested_user_id = $this->security_context_service->get()->getRequestedUserId(); + if(!is_null($requested_user_id)) + { + Session::put('username', $this->auth_service->getUserById($requested_user_id)->getEmail()); + Session::save(); + } return View::make("login"); } else { return Redirect::action("UserController@getProfile"); @@ -37,14 +77,22 @@ class OAuth2LoginStrategy implements ILoginStrategy{ public function postLogin() { - $auth_request = $this->memento_service->getCurrentAuthorizationRequest(); - $this->user_action_service->addUserAction($this->auth_service->getCurrentUser(), IPHelper::getUserIp(), IUserActionService::LoginAction, $auth_request->getRedirectUri() ); + $auth_request = OAuth2AuthorizationRequestFactory::getInstance()->build( + OAuth2Message::buildFromMemento( + $this->memento_service->load() + ) + ); + + $this->user_action_service->addUserAction($this->auth_service->getCurrentUser(), IPHelper::getUserIp(), + IUserActionService::LoginAction, $auth_request->getRedirectUri()); + return Redirect::action("OAuth2ProviderController@authorize"); } public function cancelLogin() { - $this->auth_service->setUserAuthenticationResponse(IAuthService::AuthenticationResponse_Cancel); + $this->auth_service->setUserAuthenticationResponse(IAuthService::AuthenticationResponse_Cancel); + return Redirect::action("OAuth2ProviderController@authorize"); } } \ No newline at end of file diff --git a/app/strategies/OpenIdAuthenticationStrategy.php b/app/strategies/OpenIdAuthenticationStrategy.php new file mode 100644 index 00000000..c8092150 --- /dev/null +++ b/app/strategies/OpenIdAuthenticationStrategy.php @@ -0,0 +1,42 @@ +memento_service = $memento_service; $this->auth_service = $auth_service; @@ -38,14 +65,20 @@ class OpenIdConsentStrategy implements IConsentStrategy return View::make("openid.consent", $data); } + /** + * @return array + * @throws InvalidRequestContextException + */ private function getViewData() { - $context = Session::get('context'); + $context = Session::get('openid.auth.context'); + if (is_null($context)) throw new InvalidRequestContextException(); - $partial_views = $context->getPartials(); + + $partial_views = $context->getPartials(); $data = array(); - $request = $this->memento_service->getCurrentRequest(); + $request = OpenIdMessage::buildFromMemento( $this->memento_service->load()); $user = $this->auth_service->getCurrentUser(); $data['realm'] = $request->getParam(OpenIdProtocol::OpenIDProtocol_Realm); $data['openid_url'] = $this->server_configuration_service->getUserIdentityEndpointURL($user->getIdentifier()); @@ -53,14 +86,21 @@ class OpenIdConsentStrategy implements IConsentStrategy return $data; } + /** + * @param $trust_action + * @return mixed + * @throws InvalidOpenIdMessageException + */ public function postConsent($trust_action) { if (is_array($trust_action)) { - $msg = $this->memento_service->getCurrentRequest(); + $msg = OpenIdMessage::buildFromMemento( $this->memento_service->load()); if (is_null($msg) || !$msg->isValid()) throw new InvalidOpenIdMessageException(); $this->user_action_service->addUserAction($this->auth_service->getCurrentUser(), IPHelper::getUserIp(), IUserActionService::ConsentAction, $msg->getParam(OpenIdProtocol::OpenIDProtocol_Realm)); $this->auth_service->setUserAuthorizationResponse($trust_action[0]); + Session::remove('openid.auth.context'); + Session::save(); return Redirect::action('OpenIdProviderController@endpoint'); } return Redirect::action('UserController@getConsent'); diff --git a/app/strategies/OpenIdLoginStrategy.php b/app/strategies/OpenIdLoginStrategy.php index b206cb12..bd008622 100644 --- a/app/strategies/OpenIdLoginStrategy.php +++ b/app/strategies/OpenIdLoginStrategy.php @@ -3,29 +3,46 @@ namespace strategies; use Auth; -use Redirect; -use View; +use openid\OpenIdMessage; use openid\OpenIdProtocol; use openid\requests\OpenIdAuthenticationRequest; -use openid\responses\OpenIdNonImmediateNegativeAssertion; -use openid\services\IMementoOpenIdRequestService; -use openid\strategies\OpenIdResponseStrategyFactoryMethod; -use utils\IPHelper; +use openid\services\IMementoOpenIdSerializerService; +use Redirect; use services\IUserActionService; +use utils\IPHelper; use utils\services\IAuthService; +use View; - -class OpenIdLoginStrategy implements ILoginStrategy +/** + * Class OpenIdLoginStrategy + * @package strategies + */ +final class OpenIdLoginStrategy implements ILoginStrategy { + /** + * @var IMementoOpenIdSerializerService + */ private $memento_service; + /** + * @var IUserActionService + */ private $user_action_service; + /** + * @var IAuthService + */ private $auth_service; - public function __construct(IMementoOpenIdRequestService $memento_service, - IUserActionService $user_action_service, - IAuthService $auth_service) - { + /** + * @param IMementoOpenIdSerializerService $memento_service + * @param IUserActionService $user_action_service + * @param IAuthService $auth_service + */ + public function __construct( + IMementoOpenIdSerializerService $memento_service, + IUserActionService $user_action_service, + IAuthService $auth_service + ) { $this->memento_service = $memento_service; $this->user_action_service = $user_action_service; $this->auth_service = $auth_service; @@ -34,16 +51,18 @@ class OpenIdLoginStrategy implements ILoginStrategy public function getLogin() { if (Auth::guest()) { - $msg = $this->memento_service->getCurrentRequest(); + $msg = OpenIdMessage::buildFromMemento($this->memento_service->load()); $auth_request = new OpenIdAuthenticationRequest($msg); - $params = array('realm' => $auth_request->getRealm()); + $params = array('realm' => $auth_request->getRealm()); + if (!$auth_request->isIdentitySelectByOP()) { - $params['claimed_id'] = $auth_request->getClaimedId(); - $params['identity'] = $auth_request->getIdentity(); + $params['claimed_id'] = $auth_request->getClaimedId(); + $params['identity'] = $auth_request->getIdentity(); $params['identity_select'] = false; } else { $params['identity_select'] = true; } + return View::make("login", $params); } else { return Redirect::action("UserController@getProfile"); @@ -53,14 +72,17 @@ class OpenIdLoginStrategy implements ILoginStrategy public function postLogin() { //go to authentication flow again - $msg = $this->memento_service->getCurrentRequest(); - $this->user_action_service->addUserAction($this->auth_service->getCurrentUser(), IPHelper::getUserIp(), IUserActionService::LoginAction, $msg->getParam(OpenIdProtocol::OpenIDProtocol_Realm)); + $msg = OpenIdMessage::buildFromMemento($this->memento_service->load()); + $this->user_action_service->addUserAction($this->auth_service->getCurrentUser(), IPHelper::getUserIp(), + IUserActionService::LoginAction, $msg->getParam(OpenIdProtocol::OpenIDProtocol_Realm)); + return Redirect::action("OpenIdProviderController@endpoint"); } public function cancelLogin() { - $this->auth_service->setUserAuthenticationResponse(IAuthService::AuthenticationResponse_Cancel); - return Redirect::action("OpenIdProviderController@endpoint"); + $this->auth_service->setUserAuthenticationResponse(IAuthService::AuthenticationResponse_Cancel); + + return Redirect::action("OpenIdProviderController@endpoint"); } } \ No newline at end of file diff --git a/app/strategies/PostResponseStrategy.php b/app/strategies/PostResponseStrategy.php new file mode 100644 index 00000000..80659899 --- /dev/null +++ b/app/strategies/PostResponseStrategy.php @@ -0,0 +1,35 @@ +getContent(), $response->getHttpCode()); + $http_response->header('Content-Type', $response->getContentType()); + $http_response->header('Cache-Control','no-cache, no-store, max-age=0, must-revalidate'); + $http_response->header('Pragma','no-cache'); + return $http_response; + } +} \ No newline at end of file diff --git a/app/strategies/StrategyProvider.php b/app/strategies/StrategyProvider.php index 95d2529e..2dcdc145 100644 --- a/app/strategies/StrategyProvider.php +++ b/app/strategies/StrategyProvider.php @@ -2,15 +2,22 @@ namespace strategies; +use App; use Illuminate\Support\ServiceProvider; use oauth2\responses\OAuth2DirectResponse; use oauth2\responses\OAuth2IndirectResponse; +use oauth2\responses\OAuth2PostResponse; use openid\responses\OpenIdDirectResponse; use openid\responses\OpenIdIndirectResponse; use oauth2\responses\OAuth2IndirectFragmentResponse; -use App; +use openid\services\OpenIdServiceCatalog; +use oauth2\services\OAuth2ServiceCatalog; -class StrategyProvider extends ServiceProvider +/** + * Class StrategyProvider + * @package strategies + */ +final class StrategyProvider extends ServiceProvider { public function boot() @@ -20,13 +27,16 @@ class StrategyProvider extends ServiceProvider public function register() { //direct response strategy + App::singleton(OAuth2PostResponse::OAuth2PostResponse, 'strategies\\PostResponseStrategy'); App::singleton(OAuth2DirectResponse::OAuth2DirectResponse, 'strategies\\DirectResponseStrategy'); App::singleton(OpenIdDirectResponse::OpenIdDirectResponse, 'strategies\\DirectResponseStrategy'); //indirect response strategy App::singleton(OpenIdIndirectResponse::OpenIdIndirectResponse, 'strategies\\IndirectResponseQueryStringStrategy'); App::singleton(OAuth2IndirectResponse::OAuth2IndirectResponse, 'strategies\\IndirectResponseQueryStringStrategy'); App::singleton(OAuth2IndirectFragmentResponse::OAuth2IndirectFragmentResponse,'strategies\\IndirectResponseUrlFragmentStrategy'); - App::singleton('oauth2\\strategies\\IOAuth2AuthenticationStrategy', 'strategies\\OAuth2AuthenticationStrategy'); + // authentication strategies + App::singleton(OAuth2ServiceCatalog::AuthenticationStrategy, 'strategies\\OAuth2AuthenticationStrategy'); + App::singleton(OpenIdServiceCatalog::AuthenticationStrategy, 'strategies\\OpenIdAuthenticationStrategy'); } public function provides() diff --git a/app/tests/ClientApiTest.php b/app/tests/ClientApiTest.php new file mode 100644 index 00000000..0e1a2499 --- /dev/null +++ b/app/tests/ClientApiTest.php @@ -0,0 +1,76 @@ +current_realm = Config::get('app.url'); + $parts = parse_url($this->current_realm); + $this->current_host = $parts['host']; + } + + public function testGetById(){ + + $client = Client::where('app_name','=','oauth2_test_app')->first(); + + $response = $this->action("GET", "ClientApiController@get", + $parameters = array('id' => $client->id), + array(), + array(), + array()); + + $content = $response->getContent(); + $response_client = json_decode($content); + + $this->assertResponseStatus(200); + $this->assertTrue($response_client->id === $client->id); + } + + + public function testCreate(){ + + $user = User::where('identifier','=','sebastian.marcet')->first(); + + $data = array( + 'user_id' => $user->id, + 'app_name' => 'test_app', + 'app_description' => 'test app', + 'website' => 'http://www.test.com', + 'application_type' => IClient::ApplicationType_Native + ); + + $response = $this->action("POST", "ClientApiController@create", + $data, + array(), + array(), + array()); + + $content = $response->getContent(); + $json_response = json_decode($content); + + $this->assertResponseStatus(201); + $this->assertTrue(isset($json_response->client_id) && !empty($json_response->client_id)); + } + +} \ No newline at end of file diff --git a/app/tests/ClientPublicKeyApiTest.php b/app/tests/ClientPublicKeyApiTest.php new file mode 100644 index 00000000..db032369 --- /dev/null +++ b/app/tests/ClientPublicKeyApiTest.php @@ -0,0 +1,63 @@ +current_realm = Config::get('app.url'); + $parts = parse_url($this->current_realm); + $this->current_host = $parts['host']; + } + + public function testCreate(){ + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client = Client::where('client_id','=', $client_id)->first(); + + $data = array( + 'kid' => 'test key', + 'pem_content' => TestKeys::$public_key2_pem, + 'usage' => JSONWebKeyPublicKeyUseValues::Signature, + 'type' => JSONWebKeyTypes::RSA + ); + + $response = $this->action("POST", "ClientPublicKeyApiController@create", + $wildcards = array('id' => $client->id), + $data, + array(), + array()); + + $content = $response->getContent(); + $json_response = json_decode($content); + + $this->assertResponseStatus(201); + $this->assertTrue(isset($json_response->id) && !empty($json_response->id)); + + $public_key = $client->getPublicKeyByIdentifier('test key'); + + $this->assertTrue(!is_null($public_key) && $json_response->id === $public_key->getId()); + } +} \ No newline at end of file diff --git a/app/tests/OAuth2ProtectedApiTest.php b/app/tests/OAuth2ProtectedApiTest.php index ed046561..19251ae6 100644 --- a/app/tests/OAuth2ProtectedApiTest.php +++ b/app/tests/OAuth2ProtectedApiTest.php @@ -43,7 +43,7 @@ abstract class OAuth2ProtectedApiTest extends OpenStackIDBaseTest { $scope = $this->getScopes(); $this->client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; - $this->client_secret = 'ITc/6Y5N7kOtGKhg'; + $this->client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; $params = array( 'client_id' => $this->client_id, diff --git a/app/tests/OAuth2ProtocolTest.php b/app/tests/OAuth2ProtocolTest.php index cea81aaf..b4b955e2 100644 --- a/app/tests/OAuth2ProtocolTest.php +++ b/app/tests/OAuth2ProtocolTest.php @@ -33,7 +33,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest { parent::prepareForTests(); App::singleton(UtilsServiceCatalog::ServerConfigurationService, 'StubServerConfigurationService'); - //Route::enableFilters(); $this->current_realm = Config::get('app.url'); $user = User::where('identifier', '=', 'sebastian.marcet')->first(); $this->be($user); @@ -46,6 +45,8 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest public function testAuthCode() { + Route::enableFilters(); + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; $params = array( @@ -55,8 +56,56 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest 'scope' => sprintf('%s/resource-server/read', $this->current_realm), ); + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); - Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $consent_response = $this->call('POST', $url, array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + $auth_response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $auth_response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + } + + public function testAuthCodeInvalidRedirectUri() + { + + Route::enableFilters(); + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + + $params = array( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/invalid_uri', + 'response_type' => 'code', + 'scope' => sprintf('%s/resource-server/read', $this->current_realm), + ); $response = $this->action("POST", "OAuth2ProviderController@authorize", $params, @@ -64,10 +113,11 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest array(), array()); - $url = $response->getTargetUrl(); - $content = $response->getContent(); + $this->assertResponseStatus(400); - $this->assertResponseStatus(302); + $body = $response->getContent(); + + $this->assertTrue(str_contains($body, 'redirect_uri_mismatch')); } /** Get Token Test @@ -77,9 +127,10 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest { $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; - $client_secret = 'ITc/6Y5N7kOtGKhg'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; - $params = array( + $params = array + ( 'client_id' => $client_id, 'redirect_uri' => 'https://www.test.com/oauth2', 'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_Code, @@ -87,7 +138,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType_Offline, ); - Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); $response = $this->action("POST", "OAuth2ProviderController@authorize", @@ -122,7 +172,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest $status = $response->getStatusCode(); $this->assertResponseStatus(200); - + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); $content = $response->getContent(); $response = json_decode($content); @@ -132,6 +182,78 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest $this->assertTrue(!empty($access_token)); $this->assertTrue(!empty($refresh_token)); + } + + /** Get Token Test + * @throws Exception + */ + public function testAuthCodeReplayAttack() + { + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_Code, + 'scope' => sprintf('%s/resource-server/read', $this->current_realm), + OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType_Offline, + ); + + Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $status = $response->getStatusCode(); + $url = $response->getTargetUrl(); + $content = $response->getContent(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $params = array( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + $status = $response->getStatusCode(); + + $this->assertResponseStatus(200); + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + $content = $response->getContent(); + + $response = json_decode($content); + $access_token = $response->access_token; + $refresh_token = $response->refresh_token; + + $this->assertTrue(!empty($access_token)); + $this->assertTrue(!empty($refresh_token)); + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + $this->assertResponseStatus(400); } @@ -144,7 +266,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest try { $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; - $client_secret = 'ITc/6Y5N7kOtGKhg'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); @@ -191,7 +313,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); $this->assertResponseStatus(200); - + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); $content = $response->getContent(); $response = json_decode($content); @@ -215,7 +337,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); $this->assertResponseStatus(200); - + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); $content = $response->getContent(); $response = json_decode($content); @@ -228,7 +350,6 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest } } - /** test validate token grant * @throws Exception */ @@ -240,7 +361,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest $_ENV['access.token.lifetime'] = 1; $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; - $client_secret = 'ITc/6Y5N7kOtGKhg'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); @@ -342,7 +463,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest try { $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; - $client_secret = 'ITc/6Y5N7kOtGKhg'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); @@ -390,7 +511,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); $this->assertResponseStatus(200); - + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); $content = $response->getContent(); $response = json_decode($content); @@ -415,7 +536,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); $this->assertResponseStatus(200); - + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); $content = $response->getContent(); $response = json_decode($content); @@ -441,7 +562,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest try { $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; - $client_secret = 'ITc/6Y5N7kOtGKhg'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); @@ -513,7 +634,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); $this->assertResponseStatus(200); - + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); $content = $response->getContent(); $response = json_decode($content); @@ -800,7 +921,7 @@ class OAuth2ProtocolTest extends OpenStackIDBaseTest try { $client_id = '11z87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; - $client_secret = '11c/6Y5N7kOtGKhg'; + $client_secret = '11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg'; //do get auth token... $params = array( diff --git a/app/tests/OAuth2UserServiceApiTest.php b/app/tests/OAuth2UserServiceApiTest.php index 1c4b6ddd..72ff21e2 100644 --- a/app/tests/OAuth2UserServiceApiTest.php +++ b/app/tests/OAuth2UserServiceApiTest.php @@ -12,6 +12,7 @@ class OAuth2UserServiceApiTest extends OAuth2ProtectedApiTest { * @covers OAuth2UserApiController::get() */ public function testGetInfo(){ + $response = $this->action("GET", "OAuth2UserApiController@me", array(), array(), diff --git a/app/tests/OIDCProtocolTest.php b/app/tests/OIDCProtocolTest.php new file mode 100644 index 00000000..42c22d22 --- /dev/null +++ b/app/tests/OIDCProtocolTest.php @@ -0,0 +1,2520 @@ +current_realm = Config::get('app.url'); + Route::enableFilters(); + Session::start(); + } + + public function testNonePrompt() + { + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + + $params = array( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_None + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('error', $output)); + $this->assertTrue(!empty($output['error'])); + $this->assertTrue($output['error'] === OAuth2Protocol::OAuth2Protocol_Error_Interaction_Required); + + } + + public function testConsentPrompt() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200, + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => IAuthService::AuthorizationResponse_DenyOnce, + '_token' => Session::token() + )); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('error', $output)); + $this->assertTrue(!empty($output['error'])); + $this->assertTrue($output['error'] === OAuth2Protocol::OAuth2Protocol_Error_Consent_Required); + + } + + public function testConsentLogin() + { + //already logged user + $user = User::where('identifier', '=', 'sebastian.marcet')->first(); + $this->be($user); + + //already given consent + + Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200, + OAuth2Protocol::OAuth2Protocol_Prompt => sprintf('%s %s', OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_Prompt_Login) + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => IAuthService::AuthorizationResponse_DenyOnce, + '_token' => Session::token() + )); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('error', $output)); + $this->assertTrue(!empty($output['error'])); + $this->assertTrue($output['error'] === OAuth2Protocol::OAuth2Protocol_Error_Consent_Required); + + } + + public function testAuthCode() + { + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200 + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + } + + public function testAuthCodeInvalidLoginHint() + { + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@sebastian.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200 + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + + $url = $response->getTargetUrl(); + + $this->assertTrue(str_contains($url, '/login')); + + } + + public function testAuthCodeOpenIdScopeOnly() + { + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'scope' => 'openid', + 'state' => 'KtWzJk5Vmk8CZwC0', + 'redirect_uri' => 'https://op.certification.openid.net:60393/authz_cb', + 'response_type' => 'code', + 'client_id' => $client_id, + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + } + + public function testMaxAge1AndWait2() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 1 + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + sleep(2); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $this->assertTrue($response->getTargetUrl() === URL::action("UserController@postLogin")); + } + + public function testToken + ( + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client', + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg', + $use_enc = true + ) { + + + $params = array( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => sprintf('%s profile email address %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Nonce => 'test_nonce', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200 + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $response = $this->action('GET', 'UserController@getConsent'); + + $this->assertResponseStatus(200); + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + $params = array( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + + $this->assertResponseStatus(200); + + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + + $content = $response->getContent(); + + $response = json_decode($content); + $access_token = $response->access_token; + $refresh_token = $response->refresh_token; + $id_token = $response->id_token; + + $this->assertTrue(!empty($access_token)); + $this->assertTrue(!empty($refresh_token)); + $this->assertTrue(!empty($id_token)); + + $jwt = BasicJWTFactory::build($id_token); + + if ($use_enc) { + $this->assertTrue($jwt instanceof IJWE); + + $recipient_key = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + TestSeeder::$client_private_key_1, + RSAJWKPEMPrivateKeySpecification::WithoutPassword, + $jwt->getJOSEHeader()->getAlgorithm()->getString() + ) + ); + + $recipient_key->setKeyUse(JSONWebKeyPublicKeyUseValues::Encryption)->setId('recipient_public_key'); + + + $jwt->setRecipientKey($recipient_key); + + $payload = $jwt->getPlainText(); + + $jwt = BasicJWTFactory::build($payload); + + $this->assertTrue($jwt instanceof IJWS); + } + + return $access_token; + } + + public function testTokenResponseModePost() + { + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 1, + OAuth2Protocol::OAuth2Protocol_ResponseMode => OAuth2Protocol::OAuth2Protocol_ResponseMode_FormPost + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $response = $this->action('GET', 'UserController@getConsent'); + + $this->assertResponseStatus(200); + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(200); + $content = $response->getContent(); + $this->assertEquals('application/x-www-form-urlencoded', $response->headers->get('Content-Type')); + + $output = array(); + parse_str($content, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + $params = array + ( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + $this->assertResponseStatus(200); + + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + + $content = $response->getContent(); + + $response = json_decode($content); + $access_token = $response->access_token; + $refresh_token = $response->refresh_token; + $id_token = $response->id_token; + + $this->assertTrue(!empty($access_token)); + $this->assertTrue(!empty($refresh_token)); + $this->assertTrue(!empty($id_token)); + + $jwt = BasicJWTFactory::build($id_token); + + $this->assertTrue($jwt instanceof IJWE); + + $recipient_key = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + TestSeeder::$client_private_key_1, + RSAJWKPEMPrivateKeySpecification::WithoutPassword, + $jwt->getJOSEHeader()->getAlgorithm()->getString() + ) + ); + + $recipient_key->setKeyUse(JSONWebKeyPublicKeyUseValues::Encryption)->setId('recipient_public_key'); + + + $jwt->setRecipientKey($recipient_key); + + $payload = $jwt->getPlainText(); + + $jwt = BasicJWTFactory::build($payload); + + $this->assertTrue($jwt instanceof IJWS); + + $payload = $jwt->getPayload(); + + $claims_set = $payload->getClaimSet(); + } + + public function testNativeClientBasicAuth() + { + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.android.openstack.client'; + $client_secret = '11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhgfdfdfdf'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'androipapp://oidc_endpoint_callback', + 'response_type' => 'code', + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 10000, + OAuth2Protocol::OAuth2Protocol_ResponseMode => OAuth2Protocol::OAuth2Protocol_ResponseMode_FormPost + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $response = $this->action('GET', 'UserController@getConsent'); + + $this->assertResponseStatus(200); + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(200); + $content = $response->getContent(); + $this->assertEquals('application/x-www-form-urlencoded', $response->headers->get('Content-Type')); + + $output = array(); + parse_str($content, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + $params = array + ( + 'code' => $output['code'], + 'redirect_uri' => 'androipapp://oidc_endpoint_callback', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + $this->assertResponseStatus(200); + + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + + $content = $response->getContent(); + + $response = json_decode($content); + $access_token = $response->access_token; + $refresh_token = $response->refresh_token; + $id_token = $response->id_token; + + $this->assertTrue(!empty($access_token)); + $this->assertTrue(!empty($refresh_token)); + $this->assertTrue(!empty($id_token)); + + $jwt = BasicJWTFactory::build($id_token); + + $this->assertTrue($jwt instanceof UnsecuredJWT); + + $claims_set = $jwt->getClaimSet(); + + $this->assertTrue(!is_null($claims_set)); + + $params = array( + 'client_id' => $client_id, + 'redirect_uri' => 'androipapp://oidc_endpoint_callback', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_IDTokenHint => $jwt->toCompactSerialization(), + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_None + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + } + + public function testClientAuthenticationClientSecretJwt() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x2.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200 + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $response = $this->action('GET', 'UserController@getConsent'); + + $this->assertResponseStatus(200); + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + $now = time(); + + $claim_set = JWTClaimSetFactory::build + ( + array + ( + RegisteredJWTClaimNames::Issuer => $client_id, + RegisteredJWTClaimNames::Subject => $client_id, + RegisteredJWTClaimNames::Audience => URL::action("OAuth2ProviderController@token"), + RegisteredJWTClaimNames::JWTID => '123456789', + RegisteredJWTClaimNames::ExpirationTime => $now + 3600, + RegisteredJWTClaimNames::IssuedAt => $now + ) + ); + + $key = OctetSequenceJWKFactory::build + ( + new OctetSequenceJWKSpecification + ( + $client_secret, + JSONWebSignatureAndEncryptionAlgorithms::HS512 + ) + ); + + $alg = new StringOrURI(JSONWebSignatureAndEncryptionAlgorithms::HS512); + + $jws = JWSFactory::build + ( + new JWS_ParamsSpecification + ( + $key, + $alg, + $claim_set + ) + ); + + $params = array + ( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + OAuth2Protocol::OAuth2Protocol_ClientAssertionType => ClientAssertionAuthenticationContext::RegisteredAssertionType, + OAuth2Protocol::OAuth2Protocol_ClientAssertion => $jws->toCompactSerialization() + ); + + + $response = $this->action + ( + "POST", + "OAuth2ProviderController@token", + $params, + array(), + array(), + array() + ); + + $this->assertResponseStatus(200); + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + $content = $response->getContent(); + + $response = json_decode($content); + $access_token = $response->access_token; + $refresh_token = $response->refresh_token; + $id_token = $response->id_token; + + $this->assertTrue(!empty($access_token)); + $this->assertTrue(!empty($refresh_token)); + $this->assertTrue(!empty($id_token)); + + $jwt = BasicJWTFactory::build($id_token); + + $this->assertTrue($jwt instanceof IJWE); + + $recipient_key = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + TestSeeder::$client_private_key_1, + RSAJWKPEMPrivateKeySpecification::WithoutPassword, + $jwt->getJOSEHeader()->getAlgorithm()->getString() + ) + ); + + $recipient_key->setKeyUse(JSONWebKeyPublicKeyUseValues::Encryption)->setId('recipient_public_key'); + + $jwt->setRecipientKey($recipient_key); + + $payload = $jwt->getPlainText(); + + $jwt = BasicJWTFactory::build($payload); + + $this->assertTrue($jwt instanceof IJWS); + } + + public function testClientAuthenticationPrivateKeyJwt() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.android2.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'androipapp://oidc_endpoint_callback2', + 'response_type' => 'code', + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200 + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $response = $this->action('GET', 'UserController@getConsent'); + + $this->assertResponseStatus(200); + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + $now = time(); + + $claim_set = JWTClaimSetFactory::build + ( + array + ( + RegisteredJWTClaimNames::Issuer => $client_id, + RegisteredJWTClaimNames::Subject => $client_id, + RegisteredJWTClaimNames::Audience => URL::action("OAuth2ProviderController@token"), + RegisteredJWTClaimNames::JWTID => '123456789', + RegisteredJWTClaimNames::ExpirationTime => $now + 3600, + RegisteredJWTClaimNames::IssuedAt => $now + ) + ); + + $key = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + TestSeeder::$client_private_key_2, + RSAJWKPEMPrivateKeySpecification::WithoutPassword, + JSONWebSignatureAndEncryptionAlgorithms::RS512 + ) + ); + + $key->setId('public_key_44'); + + $alg = new StringOrURI(JSONWebSignatureAndEncryptionAlgorithms::RS512); + + $jws = JWSFactory::build + ( + new JWS_ParamsSpecification + ( + $key, + $alg, + $claim_set + ) + ); + + $params = array + ( + 'code' => $output['code'], + 'redirect_uri' => 'androipapp://oidc_endpoint_callback2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + OAuth2Protocol::OAuth2Protocol_ClientAssertionType => ClientAssertionAuthenticationContext::RegisteredAssertionType, + OAuth2Protocol::OAuth2Protocol_ClientAssertion => $jws->toCompactSerialization() + ); + + + $response = $this->action + ( + "POST", + "OAuth2ProviderController@token", + $params, + array(), + array(), + array() + ); + + $this->assertResponseStatus(200); + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + $content = $response->getContent(); + + $response = json_decode($content); + $access_token = $response->access_token; + $id_token = $response->id_token; + + $this->assertTrue(!empty($access_token)); + $this->assertTrue(!empty($id_token)); + } + + public function testImplicitFlowTokenIdToken() + { + // use a public client + + //already given consent + + Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_IdToken . OAuth2Protocol::OAuth2Protocol_ResponseType_Delimiter . OAuth2Protocol::OAuth2Protocol_ResponseType_Token, + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get response + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(array_key_exists('access_token', $output)); + $this->assertTrue(!empty($output['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output)); + $this->assertTrue(!empty($output['id_token'])); + + } + + public function testImplicitFlowIdToken() + { + // use a public client + + //already given consent + + Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_IdToken, + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get response + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(!array_key_exists('access_token', $output)); + $this->assertTrue(empty($output['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output)); + $this->assertTrue(!empty($output['id_token'])); + } + + public function testImplicitFlowIdTokenMaxAge1000() + { + // use a public client + + //already given consent + + Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_IdToken, + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 1000, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get response + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(!array_key_exists('access_token', $output)); + $this->assertTrue(empty($output['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output)); + $this->assertTrue(!empty($output['id_token'])); + + sleep(10); + + $params[OAuth2Protocol::OAuth2Protocol_Prompt] = OAuth2Protocol::OAuth2Protocol_Prompt_None; + $params['scope'] = sprintf('%s profile email', OAuth2Protocol::OpenIdConnect_Scope); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output2 = array(); + parse_str($fragment, $output2); + + $this->assertTrue(!array_key_exists('access_token', $output2)); + $this->assertTrue(empty($output2['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output2)); + $this->assertTrue(!empty($output2['id_token'])); + + } + + public function testImplicitFlowAccessToken() + { + // use a public client + + //already given consent + + Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_Token, + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get response + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(array_key_exists('access_token', $output)); + $this->assertTrue(!empty($output['access_token'])); + $this->assertTrue(!array_key_exists('id_token', $output)); + $this->assertTrue(empty($output['id_token'])); + + } + + public function testImplicitFlowResponseModePost() + { + // use a public client + + //already given consent + + Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); + + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.openstack.client'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_IdToken . OAuth2Protocol::OAuth2Protocol_ResponseType_Delimiter . OAuth2Protocol::OAuth2Protocol_ResponseType_Token, + 'scope' => sprintf('%s profile email %s', OAuth2Protocol::OpenIdConnect_Scope, + OAuth2Protocol::OfflineAccess_Scope), + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent, + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200, + OAuth2Protocol::OAuth2Protocol_ResponseMode => OAuth2Protocol::OAuth2Protocol_ResponseMode_FormPost, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + // do login + $response = $this->call('POST', $url, + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + + $response = $this->action('POST', 'UserController@getConsent', array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get response + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(200); + + $this->assertEquals('application/x-www-form-urlencoded', $response->headers->get('Content-Type')); + $content = $response->getContent(); + $this->assertTrue(!empty($content)); + $output = array(); + parse_str($content, $output); + + $this->assertTrue(array_key_exists('access_token', $output)); + $this->assertTrue(!empty($output['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output)); + $this->assertTrue(!empty($output['id_token'])); + + } + + public function testUserInfoEndpointGETAndBearerHeader() + { + $access_token = $this->testToken(); + $response = $this->action("GET", "OAuth2UserApiController@userInfo", + array(), + array(), + array(), + array("HTTP_Authorization" => " Bearer " . $access_token)); + + $this->assertResponseStatus(200); + $content = $response->getContent(); + $this->assertTrue(!empty($content)); + $user_info = json_decode($content, true); + + $this->assertTrue(isset($user_info['sub'])); + } + + public function testUserInfoEndpointPOSTAndBearerHeader() + { + $access_token = $this->testToken(); + $response = $this->action("POST", "OAuth2UserApiController@userInfo", + array(), + array(), + array(), + array("HTTP_Authorization" => " Bearer " . $access_token)); + + $this->assertResponseStatus(200); + $content = $response->getContent(); + $this->assertTrue(!empty($content)); + $user_info = json_decode($content, true); + + $this->assertTrue(isset($user_info['sub'])); + } + + public function testUserInfoEndpointPOSTAndBearerBody() + { + $access_token = $this->testToken(); + $response = $this->action("POST", "OAuth2UserApiController@userInfo", + array(), + array + ( + 'access_token' => $access_token + ), + array(), + array()); + + $this->assertResponseStatus(200); + $content = $response->getContent(); + $this->assertTrue(!empty($content)); + $user_info = json_decode($content, true); + + $this->assertTrue(isset($user_info['sub'])); + } + + public function testUserInfoEndpointPOSTAndBearerBodyRS512() + { + $access_token = $this->testToken + ( + 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ33.openstack.client', + 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N585OtGKhg55', + false + ); + + $response = $this->action("POST", "OAuth2UserApiController@userInfo", + array(), + array + ( + 'access_token' => $access_token + ), + array(), + array()); + + $this->assertResponseStatus(200); + $user_info_response = $response->getContent(); + + $this->assertTrue($response->headers->get('content-type') === \utils\http\HttpContentType::JWT); + $this->assertTrue(!empty($user_info_response)); + + $jwt = BasicJWTFactory::build($user_info_response); + $this->assertTrue($jwt instanceof IJWS); + + } + + public function testHybridFlowCodeIdToken() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'id_token code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 1000, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + $this->assertTrue(!array_key_exists('access_token', $output)); + $this->assertTrue(empty($output['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output)); + $this->assertTrue(!empty($output['id_token'])); + + $params = array + ( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + $this->assertResponseStatus(200); + } + + public function testHybridFlowCodeIdTokenIdTokenHint() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'id_token code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 1000, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + $this->assertTrue(!array_key_exists('access_token', $output)); + $this->assertTrue(empty($output['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output)); + $this->assertTrue(!empty($output['id_token'])); + + $id_token = $output['id_token']; + + $jwt = BasicJWTFactory::build($id_token); + + $this->assertTrue($jwt instanceof IJWE); + + $recipient_key = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + TestSeeder::$client_private_key_1, + RSAJWKPEMPrivateKeySpecification::WithoutPassword, + $jwt->getJOSEHeader()->getAlgorithm()->getString() + ) + ); + + $recipient_key->setKeyUse(JSONWebKeyPublicKeyUseValues::Encryption)->setId('recipient_public_key'); + $jwt->setRecipientKey($recipient_key); + $payload = $jwt->getPlainText(); + $jwt = BasicJWTFactory::build($payload); + $this->assertTrue($jwt instanceof IJWS); + + + $jwk = OctetSequenceJWKFactory::build + ( + new OctetSequenceJWKSpecification + ( + $client_secret, + JSONWebSignatureAndEncryptionAlgorithms::HS512 + ) + ); + + $jwk->setId('shared_secret'); + + $jwt->setKey($jwk); + + $verified = $jwt->verify(JSONWebSignatureAndEncryptionAlgorithms::HS512); + + $this->assertTrue($verified); + + // ok send as id token hint again .... + + $id_token_hint = $jwt->toCompactSerialization(); + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'id_token code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_None, + OAuth2Protocol::OAuth2Protocol_IDTokenHint => $id_token_hint, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + $this->assertTrue(!array_key_exists('access_token', $output)); + $this->assertTrue(empty($output['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output)); + $this->assertTrue(!empty($output['id_token'])); + + // try with another key client private key signing (RSA) + $claim_set = $jwt->getPayload()->getClaimSet(); + + $key = RSAJWKFactory::build + ( + new RSAJWKPEMPrivateKeySpecification + ( + TestSeeder::$client_private_key_2, + RSAJWKPEMPrivateKeySpecification::WithoutPassword, + JSONWebSignatureAndEncryptionAlgorithms::RS512 + ) + ); + + $key->setId('public_key_2'); + + $alg = new StringOrURI(JSONWebSignatureAndEncryptionAlgorithms::RS512); + $jws = JWSFactory::build( new JWS_ParamsSpecification($key,$alg, $claim_set) ); + // and sign with server private key + $id_token_hint = $jws->toCompactSerialization(); + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'id_token code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_None, + OAuth2Protocol::OAuth2Protocol_IDTokenHint => $id_token_hint, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(array_key_exists('error', $output)); + $this->assertTrue(!empty($output['error'])); + $this->assertTrue($output['error'] == 'server_error'); + + $this->assertTrue(array_key_exists('error_description', $output)); + $this->assertTrue(!empty($output['error_description'])); + $this->assertTrue($output['error_description'] == 'original kid public_key_2 - current kid shared_secret'); + } + + public function testHybridFlowCodeAccessToken() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code token', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 1000, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + $this->assertTrue(array_key_exists('access_token', $output)); + $this->assertTrue(!empty($output['access_token'])); + $this->assertTrue(!array_key_exists('id_token', $output)); + $this->assertTrue(empty($output['id_token'])); + + $params = array + ( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + $this->assertResponseStatus(200); + + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + + $content = $response->getContent(); + + $response = json_decode($content); + $access_token = $response->access_token; + $this->assertTrue(!empty($access_token)); + + $this->assertTrue($output['access_token'] !== $access_token); + + } + + public function testHybridFlowCodeAccessTokenIdToken() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code token id_token', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 1000, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + $comps = @parse_url($url); + $fragment = $comps['fragment']; + + $this->assertTrue(!empty($fragment)); + $output = array(); + parse_str($fragment, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + $this->assertTrue(array_key_exists('access_token', $output)); + $this->assertTrue(!empty($output['access_token'])); + $this->assertTrue(array_key_exists('id_token', $output)); + $this->assertTrue(!empty($output['id_token'])); + + + $params = array + ( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + $this->assertResponseStatus(200); + } + + public function testTryingAuthCodeTwice() + { + $client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client'; + $client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg'; + + $params = array + ( + 'client_id' => $client_id, + 'redirect_uri' => 'https://www.test.com/oauth2', + 'response_type' => 'code', + 'scope' => 'openid profile email', + OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net', + OAuth2Protocol::OAuth2Protocol_MaxAge => 3200, + OAuth2Protocol::OAuth2Protocol_Nonce => 'ctqg5FeNoYnZ', + ); + + $response = $this->action("POST", "OAuth2ProviderController@authorize", + $params, + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + // verify that login hint (email) is populated + $this->assertTrue(str_contains($response->getContent(), 'sebastian@tipit.net')); + + // do login + $response = $this->action('POST', "UserController@postLogin", + array + ( + 'username' => 'sebastian@tipit.net', + 'password' => '1qaz2wsx', + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + //do consent + $url = $response->getTargetUrl(); + + $response = $this->action('POST', "UserController@postConsent", array( + 'trust' => 'AllowOnce', + '_token' => Session::token() + )); + + $this->assertResponseStatus(302); + + // get auth code + + $response = $this->action("GET", "OAuth2ProviderController@authorize", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $url = $response->getTargetUrl(); + + $comps = @parse_url($url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + + $this->assertTrue(array_key_exists('code', $output)); + $this->assertTrue(!empty($output['code'])); + + + $params = array( + 'code' => $output['code'], + 'redirect_uri' => 'https://www.test.com/oauth2', + 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, + ); + + // 1st + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + + $this->assertResponseStatus(200); + + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + + $content = $response->getContent(); + + $response = json_decode($content); + + // 2nd + + $response = $this->action("POST", "OAuth2ProviderController@token", + $params, + array(), + array(), + // Symfony interally prefixes headers with "HTTP", so + array("HTTP_Authorization" => " Basic " . base64_encode($client_id . ':' . $client_secret))); + + + $this->assertResponseStatus(400); + + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + + $content = $response->getContent(); + + $response = json_decode($content); + + $this->assertTrue($response->error === 'invalid_grant'); + } + + public function testDiscovery() + { + $response = $this->action("GET", "OAuth2ProviderController@discovery"); + + $this->assertResponseStatus(200); + + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + + $content = $response->getContent(); + + $this->assertTrue(!empty($content)); + + $response = json_decode($content, true); + + $this->assertTrue(isset($response['issuer'])); + } + + public function testJWK() + { + $response = $this->action("GET", "OAuth2ProviderController@certs"); + + $this->assertResponseStatus(200); + + $this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type')); + + $content = $response->getContent(); + + $this->assertTrue(!empty($content)); + + $response = json_decode($content, true); + + $this->assertTrue(isset($response['keys'])); + } + +} \ No newline at end of file diff --git a/app/tests/OpenIdProtocolTest.php b/app/tests/OpenIdProtocolTest.php index 0013d333..5851b45f 100644 --- a/app/tests/OpenIdProtocolTest.php +++ b/app/tests/OpenIdProtocolTest.php @@ -213,14 +213,13 @@ class OpenIdProtocolTest extends OpenStackIDBaseTest public function testAuthenticationSetupModePrivateAssociation() { //set login info - Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce); - $params = array( - OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_NS) => OpenIdProtocol::OpenID2MessageType, - OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Mode) => OpenIdProtocol::SetupMode, - OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Realm) => "https://www.test.com/", - OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_ReturnTo) => "https://www.test.com/oauth2", - OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Identity) => "http://specs.openid.net/auth/2.0/identifier_select", + $params = array( + OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_NS) => OpenIdProtocol::OpenID2MessageType, + OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Mode) => OpenIdProtocol::SetupMode, + OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Realm) => "https://www.newsite.com/", + OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_ReturnTo) => "https://www.newsite.com/return_to/", + OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Identity) => "http://specs.openid.net/auth/2.0/identifier_select", OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_ClaimedId) => "http://specs.openid.net/auth/2.0/identifier_select", ); @@ -228,15 +227,35 @@ class OpenIdProtocolTest extends OpenStackIDBaseTest $this->assertResponseStatus(302); - $openid_response = $this->parseOpenIdResponse($response->getTargetUrl()); + $url = $response->getTargetUrl(); + + // post consent response ... + + $consent_response = $this->call('POST', $url, array + ( + 'trust' => array('AllowOnce'), + '_token' => Session::token() + ) + ); + + $this->assertResponseStatus(302); + + $auth_response = $this->action("GET", "OpenIdProviderController@endpoint", + array(), + array(), + array(), + array()); + + $this->assertResponseStatus(302); + + $openid_response = $this->parseOpenIdResponse($auth_response->getTargetUrl()); $this->assertTrue(isset($openid_response[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Mode)])); $this->assertTrue(isset($openid_response[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_NS)])); $this->assertTrue(isset($openid_response[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_ReturnTo)])); //http://openid.net/specs/openid-authentication-2_0.html#check_auth - $response = $this->action("POST", "OpenIdProviderController@endpoint", - $this->prepareCheckAuthenticationParams($openid_response)); + $response = $this->action("POST", "OpenIdProviderController@endpoint", $this->prepareCheckAuthenticationParams($openid_response)); $openid_response = $this->getOpenIdResponseLineBreak($response->getContent()); $this->assertResponseStatus(200); $this->assertTrue($openid_response['is_valid'] === 'true'); @@ -471,7 +490,6 @@ class OpenIdProtocolTest extends OpenStackIDBaseTest $this->assertTrue(isset($openid_response[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_ClaimedId)])); $this->assertTrue(!empty($openid_response[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_ClaimedId)])); - //sreg $this->assertTrue(isset($openid_response[OpenIdSREGExtension::paramNamespace()])); @@ -483,7 +501,7 @@ class OpenIdProtocolTest extends OpenStackIDBaseTest $this->assertTrue(isset($openid_response[OpenIdSREGExtension::param(OpenIdSREGExtension::Email)])); $email = $openid_response[OpenIdSREGExtension::param(OpenIdSREGExtension::Email)]; - $this->assertTrue(!empty($email) && $email === 'smarcet@gmail.com'); + $this->assertTrue(!empty($email) && $email === 'sebastian@tipit.net'); //http://openid.net/specs/openid-authentication-2_0.html#check_auth $response = $this->action("POST", "OpenIdProviderController@endpoint", @@ -501,8 +519,7 @@ class OpenIdProtocolTest extends OpenStackIDBaseTest public function testCheckSetupOAuth2Extension() { - //set login info - Session::set("openid.authorization.response", IAuthService::AuthorizationResponse_AllowForever); + $sreg_required_params = array('email', 'fullname'); $scope = array( sprintf('%s/resource-server/read', $this->current_realm), @@ -526,12 +543,35 @@ class OpenIdProtocolTest extends OpenStackIDBaseTest OpenIdOAuth2Extension::param(OpenIdOAuth2Extension::ClientId) => $this->oauth2_client_id, OpenIdOAuth2Extension::param(OpenIdOAuth2Extension::Scope) => implode(' ', $scope), OpenIdOAuth2Extension::param(OpenIdOAuth2Extension::State) => uniqid(), + //sreg + OpenIdSREGExtension::paramNamespace() => OpenIdSREGExtension::NamespaceUrl, + OpenIdSREGExtension::param(OpenIdSREGExtension::Required) => implode(",", $sreg_required_params), ); $response = $this->action("POST", "OpenIdProviderController@endpoint", $params); $this->assertResponseStatus(302); + $url = $response->getTargetUrl(); + + $response = $this->call('GET', $url); + + $this->assertResponseStatus(200); + + $consent_html_content = $response->getContent(); + + $this->assertTrue(str_contains($consent_html_content, 'Welcome to openstackId - consent')); + $this->assertTrue(str_contains($consent_html_content, 'The site has also requested some personal information')); + $this->assertTrue(str_contains($consent_html_content, 'The site has also requested some permissions for following OAuth2 application')); + + + $response = $this->call('POST', $url, array( + 'trust' => array('AllowOnce'), + '_token' => Session::token() + )); + + $response = $this->call('GET', $response->getTargetUrl()); + $openid_response = $this->parseOpenIdResponse($response->getTargetUrl()); $this->assertTrue(isset($openid_response[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_Mode)])); @@ -561,7 +601,6 @@ class OpenIdProtocolTest extends OpenStackIDBaseTest $this->assertTrue(isset($openid_response[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_ClaimedId)])); $this->assertTrue(!empty($openid_response[OpenIdProtocol::param(OpenIdProtocol::OpenIDProtocol_ClaimedId)])); - //oauth2 $this->assertTrue(isset($openid_response[OpenIdOAuth2Extension::paramNamespace()])); @@ -573,6 +612,18 @@ class OpenIdProtocolTest extends OpenStackIDBaseTest $this->assertTrue(isset($openid_response[OpenIdOAuth2Extension::param(OpenIdOAuth2Extension::Scope)])); $this->assertTrue(!empty($openid_response[OpenIdOAuth2Extension::param(OpenIdOAuth2Extension::Scope)])); + //sreg + + $this->assertTrue(isset($openid_response[OpenIdSREGExtension::paramNamespace()])); + $this->assertTrue($openid_response[OpenIdSREGExtension::paramNamespace()] === OpenIdSREGExtension::NamespaceUrl); + + $this->assertTrue(isset($openid_response[OpenIdSREGExtension::param(OpenIdSREGExtension::FullName)])); + $full_name = $openid_response[OpenIdSREGExtension::param(OpenIdSREGExtension::FullName)]; + $this->assertTrue(!empty($full_name) && $full_name === 'Sebastian Marcet'); + + $this->assertTrue(isset($openid_response[OpenIdSREGExtension::param(OpenIdSREGExtension::Email)])); + $email = $openid_response[OpenIdSREGExtension::param(OpenIdSREGExtension::Email)]; + $this->assertTrue(!empty($email) && $email === 'sebastian@tipit.net'); //http://openid.net/specs/openid-authentication-2_0.html#check_auth $response = $this->action("POST", "OpenIdProviderController@endpoint", diff --git a/app/tests/OpenStackIDBaseTest.php b/app/tests/OpenStackIDBaseTest.php index 5ac37ec5..e5a54ac8 100644 --- a/app/tests/OpenStackIDBaseTest.php +++ b/app/tests/OpenStackIDBaseTest.php @@ -41,6 +41,8 @@ abstract class OpenStackIDBaseTest extends TestCase { DB::table('openid_trusted_sites')->delete(); if (Schema::hasTable('openid_associations')) DB::table('openid_associations')->delete(); + if (Schema::hasTable('user_actions')) + DB::table('user_actions')->delete(); if (Schema::hasTable('openid_users')) DB::table('openid_users')->delete(); if (Schema::hasTable('oauth2_api_endpoint_api_scope')) diff --git a/app/tests/UserServiceTest.php b/app/tests/UserServiceTest.php new file mode 100644 index 00000000..43b81e9c --- /dev/null +++ b/app/tests/UserServiceTest.php @@ -0,0 +1,40 @@ +getByEmail("sebastian@tipit.net"); + $member2 = $member_repository->getByEmail("sebastian+1@tipit.net"); + $member3 = $member_repository->getByEmail("sebastian+2@tipit.net"); + + $user2 = $user_service->buildUser($member2); + $user3 = $user_service->buildUser($member3); + + $this->assertTrue($user2->identifier === 'sebastian.marcet.1'); + $this->assertTrue($user3->identifier === 'sebastian.marcet.2'); + } +} \ No newline at end of file diff --git a/app/tests/UserTest.php b/app/tests/UserTest.php index a69e39f8..228bda42 100644 --- a/app/tests/UserTest.php +++ b/app/tests/UserTest.php @@ -6,14 +6,6 @@ class UserTest extends TestCase public function testMember() { $member = Member::findOrFail(1); - $this->assertTrue($member->FirstName == 'Todd'); - } - - public function testOpenIdUserAssociation() - { - $username = 'sebastian@tipit.net'; - $password = 'Koguryo@1981'; - $member = Member::where('Email', '=', $username)->firstOrFail(); - $this->assertTrue($member->checkPassword($password)); + $this->assertTrue($member->FirstName == 'Sebastian'); } } \ No newline at end of file diff --git a/app/tests/features/bootstrap/FeatureContext.php b/app/tests/features/bootstrap/FeatureContext.php new file mode 100644 index 00000000..9169ae41 --- /dev/null +++ b/app/tests/features/bootstrap/FeatureContext.php @@ -0,0 +1,110 @@ +getHash(); + foreach ($hash as $row) { + $this->params[$row['param']] = $row['value']; + if($row['param'] == 'scope') + $this->params[$row['param']] = sprintf($this->params[$row['param']], $this->current_realm); + } + } + + /** + * @Given exits client Id :arg1 + */ + public function exitsClientId($client_id) + { + $client = Client::where('client_id', '=', $client_id)->first(); + if(is_null($client)) + throw new Exception(sprintf('client id %s does not exist', $client_id)); + } + + /** + * @Given navigate to controller action :arg1 with HTTP method :arg2 + */ + public function navigateToControllerActionWithHttpMethod($action, $method) + { + $this->response = $this->action($method, $action, + $this->params, + array(), + array(), + array()); + } + + /** + * @When i get log as user :arg1 using password :arg2 + */ + public function iGetLogAsUserUsingPassword($user, $password) + { + + Session::save(); + + $login_url = $this->response->getTargetUrl(); + + $this->response = $this->call('POST', $login_url, array( + 'username' => $user, + 'password' => $password, + '_token' => Session::token() + )); + + } + + /** + * @When allow consent :arg1 + */ + public function allowConsent($arg1) + { + Session::save(); + + $auth_url = $this->response->getTargetUrl(); + + $this->response = $this->call('GET', $auth_url); + + $consent_url = $this->response->getTargetUrl(); + + $this->response = $this->call('POST', $consent_url, array( + 'trust' => $arg1, + '_token' => Session::token() + )); + } + + /** + * @Then i get a valid Auth code + */ + public function iGetAValidAuthCode() + { + $response_url = $this->response->getTargetUrl(); + + $this->response = $this->call("GET", $response_url); + + $response_url = $this->response->getTargetUrl(); + $comps = @parse_url($response_url); + $query = $comps['query']; + $output = array(); + parse_str($query, $output); + $this->assertTrue(array_key_exists('code', $output) ); + $this->assertTrue(!empty($output['code']) ); + echo $output['code']; + } + +} \ No newline at end of file diff --git a/app/tests/features/bootstrap/LaravelContext.php b/app/tests/features/bootstrap/LaravelContext.php new file mode 100644 index 00000000..cfa2e6ee --- /dev/null +++ b/app/tests/features/bootstrap/LaravelContext.php @@ -0,0 +1,285 @@ +seed('TestSeeder'); + } + + /** + * Creates the application. + * + * @return Symfony\Component\HttpKernel\HttpKernelInterface + */ + public function createApplication() + { + $unitTesting = true; + + $testEnvironment = 'testing'; + + $app = require_once __DIR__ . '/../../../../bootstrap/start.php'; + return $app; + } + + public function setUp() + { + if ( ! $this->app) + { + $this->refreshApplication(); + } + + $this->redis = \RedisLV4::connection(); + $this->redis->flushall(); + $this->prepareForTests(); + } + + /** + * Refresh the application instance. + * + * @return void + */ + protected function refreshApplication() + { + $this->app = $this->createApplication(); + + $this->client = $this->createClient(); + + $this->app->setRequestForConsoleEnvironment(); + + $this->app->boot(); + } + + /** + * Set the session to the given array. + * + * @param array $data + * @return void + */ + public function session(array $data) + { + $this->startSession(); + + foreach ($data as $key => $value) + { + $this->app['session']->put($key, $value); + } + } + + /** + * Flush all of the current session data. + * + * @return void + */ + public function flushSession() + { + $this->startSession(); + + $this->app['session']->flush(); + } + + /** + * Start the session for the application. + * + * @return void + */ + protected function startSession() + { + if ( ! $this->app['session']->isStarted()) + { + $this->app['session']->start(); + } + } + + /** + * Set the currently logged in user for the application. + * + * @param \Illuminate\Auth\UserInterface $user + * @param string $driver + * @return void + */ + public function be(UserInterface $user, $driver = null) + { + $this->app['auth']->driver($driver)->setUser($user); + } + + /** + * Seed a given database connection. + * + * @param string $class + * @return void + */ + public function seed($class = 'DatabaseSeeder') + { + $this->app['artisan']->call('db:seed', array('--class' => $class)); + } + + /** + * Create a new HttpKernel client instance. + * + * @param array $server + * @return \Symfony\Component\HttpKernel\Client + */ + protected function createClient(array $server = array()) + { + return new Client($this->app, $server); + } + + + /** + * @Given Prepare For Tests is Done + */ + public function prepareForTestsIsDone() + { + $this->setUp(); + + if (Schema::hasTable('banned_ips')) + DB::table('banned_ips')->delete(); + if (Schema::hasTable('user_exceptions_trail')) + DB::table('user_exceptions_trail')->delete(); + if (Schema::hasTable('server_configuration')) + DB::table('server_configuration')->delete(); + if (Schema::hasTable('server_extensions')) + DB::table('server_extensions')->delete(); + if (Schema::hasTable('oauth2_client_api_scope')) + DB::table('oauth2_client_api_scope')->delete(); + if (Schema::hasTable('oauth2_client_authorized_uri')) + DB::table('oauth2_client_authorized_uri')->delete(); + if (Schema::hasTable('oauth2_access_token')) + DB::table('oauth2_access_token')->delete(); + if (Schema::hasTable('oauth2_refresh_token')) + DB::table('oauth2_refresh_token')->delete(); + if (Schema::hasTable('oauth2_client')) + DB::table('oauth2_client')->delete(); + if (Schema::hasTable('openid_trusted_sites')) + DB::table('openid_trusted_sites')->delete(); + if (Schema::hasTable('openid_associations')) + DB::table('openid_associations')->delete(); + if (Schema::hasTable('user_actions')) + DB::table('user_actions')->delete(); + if (Schema::hasTable('openid_users')) + DB::table('openid_users')->delete(); + if (Schema::hasTable('oauth2_api_endpoint_api_scope')) + DB::table('oauth2_api_endpoint_api_scope')->delete(); + if (Schema::hasTable('oauth2_api_endpoint')) + DB::table('oauth2_api_endpoint')->delete(); + if (Schema::hasTable('oauth2_api_scope')) + DB::table('oauth2_api_scope')->delete(); + if (Schema::hasTable('oauth2_api')) + DB::table('oauth2_api')->delete(); + if (Schema::hasTable('oauth2_resource_server')) + DB::table('oauth2_resource_server')->delete(); + + $this->prepareForTests(); + $this->current_realm = Config::get('app.url'); + } + + /** + * Call a controller action and return the Response. + * + * @param string $method + * @param string $action + * @param array $wildcards + * @param array $parameters + * @param array $files + * @param array $server + * @param string $content + * @param bool $changeHistory + * @return \Illuminate\Http\Response + */ + public function action($method, $action, $wildcards = array(), $parameters = array(), $files = array(), $server = array(), $content = null, $changeHistory = true) + { + $uri = $this->app['url']->action($action, $wildcards, true); + + return $this->call($method, $uri, $parameters, $files, $server, $content, $changeHistory); + } + + /** + * Call the given URI and return the Response. + * + * @param string $method + * @param string $uri + * @param array $parameters + * @param array $files + * @param array $server + * @param string $content + * @param bool $changeHistory + * @return \Illuminate\Http\Response + */ + public function call() + { + call_user_func_array(array($this->client, 'request'), func_get_args()); + + return $this->client->getResponse(); + } + + /** + * Asserts that a condition is true. + * + * @param boolean $condition + * @param string $message + * @throws PHPUnit_Framework_AssertionFailedError + */ + public static function assertTrue($condition, $message = '') + { + if(!$condition) + throw new \LogicException($message); + } + + +} \ No newline at end of file diff --git a/app/tests/features/get_auth_code.feature b/app/tests/features/get_auth_code.feature new file mode 100644 index 00000000..8c0d2542 --- /dev/null +++ b/app/tests/features/get_auth_code.feature @@ -0,0 +1,17 @@ +Feature: Get OAuth2 Auth Code + + Background: + Given Prepare For Tests is Done + + Scenario: Get Auth Code With Login + Given these OAuth2 parameters: + | param | value | + | client_id | Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client | + | redirect_uri | https://www.test.com/oauth2 | + | response_type | code | + | scope | profile | + And exits client Id "Jiz87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client" + And navigate to controller action "OAuth2ProviderController@authorize" with HTTP method "POST" + When i get log as user "test@test.com" using password "1qaz2wsx" + And allow consent "AllowOnce" + Then i get a valid Auth code diff --git a/app/validators/CustomValidator.php b/app/validators/CustomValidator.php index 97ed08ce..dfd68518 100644 --- a/app/validators/CustomValidator.php +++ b/app/validators/CustomValidator.php @@ -1,15 +1,27 @@ 'GET', - 'HEAD'=>'HEAD', - 'POST'=>'POST', - 'PUT'=>'PUT', - 'DELETE'=>'DELETE', - 'TRACE'=>'TRACE', - 'CONNECT'=>'CONNECT', - 'OPTIONS'=>'OPTIONS', + 'GET' => 'GET', + 'HEAD' => 'HEAD', + 'POST' => 'POST', + 'PUT' => 'PUT', + 'DELETE' => 'DELETE', + 'TRACE' => 'TRACE', + 'CONNECT' => 'CONNECT', + 'OPTIONS' => 'OPTIONS', ); - return array_key_exists($value,$allowed_http_verbs); + return array_key_exists($value, $allowed_http_verbs); } - public function validateRoute($attribute, $value, $parameters){ + public function validateRoute($attribute, $value, $parameters) + { return true; } - public function validateScopename($attribute, $value, $parameters){ + public function validateScopename($attribute, $value, $parameters) + { $value = trim($value); + return preg_match("/^[a-zA-Z0-9\-\.\,\:\_\/]+$/", $value) == 1; } - public function validateHost($attribute, $value, $parameters){ - return filter_var(gethostbyname($value), FILTER_VALIDATE_IP)?true:false; + public function validateHost($attribute, $value, $parameters) + { + return filter_var(gethostbyname($value), FILTER_VALIDATE_IP) ? true : false; } - public function validateApplicationtype($attribute, $value, $parameters){ - if(!is_string($value)) return false; + public function validateApplicationtype($attribute, $value, $parameters) + { + + if (!is_string($value)) { + return false; + } + $value = trim($value); - return ($value == IClient::ApplicationType_Service || $value==IClient::ApplicationType_Web_App || $value==IClient::ApplicationType_JS_Client); + + return in_array($value, Client::$valid_app_types); } - public function validateSslurl($attribute, $value, $parameters){ - return preg_match(";^https:\/\/([\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,]*$;i",$value)==1; + public function validateSslurl($attribute, $value, $parameters) + { + return preg_match(";^https:\/\/([\w@][\w.:@]+)\/?[\w\.?=%&=\-@/$,]*$;i", $value) == 1; } - public function validateFreeText($attribute, $value, $parameters){ - return preg_match('|^[a-z\-@_.,()\'"\s\:\/]+$|i',$value)==1; + public function validateFreeText($attribute, $value, $parameters) + { + return preg_match('|^[a-z\-@_.,()\'"\s\:\/]+$|i', $value) == 1; } - public function validateSslorigin($attribute, $value, $parameters){ - if(filter_var($value, FILTER_VALIDATE_URL)){ + public function validateSslorigin($attribute, $value, $parameters) + { + if (filter_var($value, FILTER_VALIDATE_URL)) { $parts = @parse_url($value); if ($parts == false) { return false; } - if($parts['scheme']!= 'https') + if ($parts['scheme'] != 'https') { return false; + } - if(isset($parts['query'])) + if (isset($parts['query'])) { return false; + } - if(isset($parts['fragment'])) + if (isset($parts['fragment'])) { return false; + } - if(isset($parts['path'])) + if (isset($parts['path'])) { return false; + } - if(isset($parts['user'])) + if (isset($parts['user'])) { return false; + } - if(isset($parts['pass'])) + if (isset($parts['pass'])) { return false; + } return true; } + return false; } + + public function validateEmailSet($attribute, $value, $parameters) + { + $emails = explode(',', $value); + $res = true; + foreach ($emails as $email) { + $res = $this->validateEmail($attribute, $email); + if (!$res) { + break; + } + } + + return $res; + } + + public function validateUrlSet($attribute, $value, $parameters) + { + $urls = explode(',', $value); + $res = true; + foreach ($urls as $url) { + $res = $this->validateUrl($attribute, $url); + if (!$res) { + break; + } + } + + return $res; + } + + public function validateTokenEndpointAuthMethod($attribute, $value, $parameters) + { + return in_array($value,OAuth2Protocol::$token_endpoint_auth_methods); + } + + public function validateSigningAlg($attribute, $value, $parameters) + { + return in_array($value,OAuth2Protocol::$supported_signing_algorithms); + } + + public function validateSubjectType($attribute, $value, $parameters) + { + return in_array($value, Client::$valid_subject_types); + } + + public function validateEncryptedAlg($attribute, $value, $parameters) + { + return in_array($value,OAuth2Protocol::$supported_key_management_algorithms); + } + + public function validateEncryptedEnc($attribute, $value, $parameters) + { + return in_array($value,OAuth2Protocol::$supported_content_encryption_algorithms); + } + + public function validatePublicKeyPem($attribute, $value, $parameters) + { + $res1 = strpos($value,'-----BEGIN PUBLIC KEY-----'); + $res2 = strpos($value,'-----BEGIN RSA PUBLIC KEY-----'); + $res3 = strpos($value,'-----END PUBLIC KEY-----'); + $res4 = strpos($value,'-----END RSA PUBLIC KEY-----'); + + $PKCS8 = $res1 !== false && $res3 !== false; + $PKCS1 = $res2 !== false && $res4 !== false; + + $rsa = new Crypt_RSA; + $parsed = $rsa->loadKey($value); + + return ($PKCS8 || $PKCS1) && $parsed; + } + + public function validatePublicKeyPemLength($attribute, $value, $parameters) + { + $rsa = new Crypt_RSA; + $parsed = $rsa->loadKey($value); + + return $parsed && $rsa->getSize() > 1024; + } + + public function validatePrivateKeyPem($attribute, $value, $parameters) + { + $res1 = strpos($value,'-----BEGIN PRIVATE KEY-----'); + $res2 = strpos($value,'-----BEGIN RSA PRIVATE KEY-----'); + $res3 = strpos($value,'-----END PRIVATE KEY-----'); + $res4 = strpos($value,'-----END RSA PRIVATE KEY-----'); + + $PKCS8 = $res1 !== false && $res3 !== false; + $PKCS1 = $res2 !== false && $res4 !== false; + + $encrypted = strpos($value,'ENCRYPTED') !== false ; + $password_param = $parameters[0]; + $rsa = new Crypt_RSA; + if(isset($this->data[$password_param]) && $encrypted){ + $rsa->setPassword($this->data[$password_param]); + } + + $parsed = $rsa->loadKey($value); + + return ($PKCS8 || $PKCS1) && $parsed; + } + + public function validatePrivateKeyPemLength($attribute, $value, $parameters) + { + + $encrypted = strpos($value,'ENCRYPTED') !== false ; + $password_param = $parameters[0]; + $rsa = new Crypt_RSA; + if(isset($this->data[$password_param]) && $encrypted){ + $rsa->setPassword($this->data[$password_param]); + } + + $parsed = $rsa->loadKey($value); + + return $parsed && $rsa->getSize() >= 2048; + } + + public function validatePublicKeyUsage($attribute, $value, $parameters) + { + return in_array($value, JSONWebKeyPublicKeyUseValues::$valid_uses); + } + + public function validatePublicKeyType($attribute, $value, $parameters) + { + return in_array($value, JSONWebKeyTypes::$valid_keys_set); + } + + public function validatePrivateKeyPassword($attribute, $value, $parameters){ + $pem_param = $parameters[0]; + if(!isset($this->data[$pem_param])) return true; + $pem_content = $this->data[$pem_param]; + $rsa = new Crypt_RSA; + $rsa->setPassword($value); + $parsed = $rsa->loadKey($pem_content); + return $parsed; + } + + public function validateCustomUrlSet($attribute, $value, $parameters) + { + $app_type_param = $parameters[0]; + if(!isset($this->data[$app_type_param])) return true; + $app_type = $this->data[$app_type_param]; + + $urls = explode(',', $value); + $res = true; + foreach ($urls as $url) { + $res = $app_type === IClient::ApplicationType_Native ? $this->validateUrl($attribute, $url, $parameters) : $this->validateSslurl($attribute, $url, $parameters); + if (!$res) { + break; + } + } + + return $res; + } + + public function validateSslUrlSet($attribute, $value, $parameters) + { + + $urls = explode(',', $value); + $res = true; + foreach ($urls as $url) { + $res = $this->validateSslurl($attribute, $url, $parameters); + if (!$res) { + break; + } + } + + return $res; + } + + public function validateKeyAlg($attribute, $value, $parameters) + { + $key_type_param = $parameters[0]; + $key_type = $this->data[$key_type_param]; + + if($key_type === JSONWebKeyPublicKeyUseValues::Signature) + { + return in_array($value, OAuth2Protocol::$supported_signing_algorithms); + } + else + { + return in_array($value, OAuth2Protocol::$supported_key_management_algorithms); + } + } } \ No newline at end of file diff --git a/app/views/400.blade.php b/app/views/400.blade.php new file mode 100644 index 00000000..d20cb849 --- /dev/null +++ b/app/views/400.blade.php @@ -0,0 +1,15 @@ +@extends('layout') +@section('content') +

OpenstackId Idp - 400

+
+

+ 400. That’s an error. +

+

+ {{ $error_code }} +

+

+ {{ $error_description }} +

+
+@stop \ No newline at end of file diff --git a/app/views/admin/banned-ips.blade.php b/app/views/admin/banned-ips.blade.php index 31cf2cfc..5d0907fe 100644 --- a/app/views/admin/banned-ips.blade.php +++ b/app/views/admin/banned-ips.blade.php @@ -7,8 +7,8 @@ @section('content') @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) Banned Ips -
-
+
+
@@ -36,7 +36,7 @@ @endif @endforeach @@ -48,5 +48,5 @@ @stop @section('scripts') -{{ HTML::script('js/admin/banned-ips.js') }} -@stop +{{ HTML::script('assets/js/admin/banned-ips.js') }} +@append diff --git a/app/views/admin/server-config.blade.php b/app/views/admin/server-config.blade.php index d7910802..fcd50903 100644 --- a/app/views/admin/server-config.blade.php +++ b/app/views/admin/server-config.blade.php @@ -6,38 +6,56 @@ @section('content') @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) -
-
+
+
-
General Configuration - - - - +
+ + +
+
+ + +
OPENID Configuration - - - - - - +
+ + +
+
+ + +
+
+ + +
OAUTH2 Configuration - - - - - - - -
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@foreach($errors->all() as $message) -
- + @endforeach @@ -45,5 +63,5 @@ @stop @section('scripts') -{{ HTML::script('js/admin/server-config.js') }} -@stop \ No newline at end of file +{{ HTML::script('assets/js/admin/server-config.js') }} +@append \ No newline at end of file diff --git a/app/views/admin/users.blade.php b/app/views/admin/users.blade.php index 48021e61..548f89fb 100644 --- a/app/views/admin/users.blade.php +++ b/app/views/admin/users.blade.php @@ -7,8 +7,8 @@ @section('content') @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) Locked Users -
-
+
+
- {{ HTML::link(URL::action("ApiBannedIPController@delete",array("id"=>$ip->id)),'Revoke',array('data-ip-id'=>$ip->id,'class'=>'btn revoke-ip','title'=>'Revoke given banned ip address')) }} + {{ HTML::link(URL::action("ApiBannedIPController@delete",array("id"=>$ip->id)),'Revoke',array('data-ip-id'=>$ip->id,'class'=>'btn btn-default btn-md active revoke-ip','title'=>'Revoke given banned ip address')) }}
@@ -31,7 +31,7 @@ @endforeach @@ -42,5 +42,5 @@ @stop @section('scripts') -{{ HTML::script('js/admin/users.js') }} -@stop \ No newline at end of file +{{ HTML::script('assets/js/admin/users.js') }} +@append \ No newline at end of file diff --git a/app/views/extensions/ax.blade.php b/app/views/extensions/ax.blade.php index 98db2e6c..06b13c61 100644 --- a/app/views/extensions/ax.blade.php +++ b/app/views/extensions/ax.blade.php @@ -4,7 +4,7 @@
    @foreach ($attributes as $attr) -
  • {{$attr}} 
  • +
  • {{$attr}} 
  • @endforeach
@endif \ No newline at end of file diff --git a/app/views/extensions/oauth2.blade.php b/app/views/extensions/oauth2.blade.php index f77f0785..5bbe4fee 100644 --- a/app/views/extensions/oauth2.blade.php +++ b/app/views/extensions/oauth2.blade.php @@ -3,33 +3,31 @@ The site has also requested some permissions for following OAuth2 application
-
-
-
-
+
+
+
+
-
-

{{$app_name}} 

+
+

{{$app_name}} 

This app would like to:
    @foreach ($requested_scopes as $scope) -
  • {{$scope->short_description}} 
  • +
  • {{$scope->short_description}} 
  • @endforeach

** {{$app_name}} Application and Openstack will use this information in accordance with their respective terms of service and privacy policies.

-
@endif @section('scripts') -@parent -@stop \ No newline at end of file +@append \ No newline at end of file diff --git a/app/views/extensions/sreg.blade.php b/app/views/extensions/sreg.blade.php index 54e4da85..13ef06c9 100644 --- a/app/views/extensions/sreg.blade.php +++ b/app/views/extensions/sreg.blade.php @@ -4,7 +4,7 @@
    @foreach ($attributes as $attr) -
  • {{$attr}} 
  • +
  • {{$attr}} 
  • @endforeach
@endif diff --git a/app/views/home.blade.php b/app/views/home.blade.php index cfea31d0..0330e434 100644 --- a/app/views/home.blade.php +++ b/app/views/home.blade.php @@ -1,25 +1,26 @@ @extends('layout') @section('title') -Welcome to openstackId + Welcome to openstackId @stop @section('meta') -@stop +@append @section('content') -
-
-
- -

OpenstackId Identity Provider

-
-
Log in to OpenStack
-
- Sign in to your account - Register for an OpenStack ID +
+
+
+

OpenstackId Identity Provider

+
+
Log in to OpenStack
+ +

Once you're signed in, you can manage your trusted sites, change + your settings and more.

-

Once you're signed in, you can manage your trusted sites, change your settings and more.

-
@stop \ No newline at end of file diff --git a/app/views/identity.blade.php b/app/views/identity.blade.php index f221a067..25e9343a 100644 --- a/app/views/identity.blade.php +++ b/app/views/identity.blade.php @@ -4,7 +4,7 @@ @stop @section('meta') -@stop +@append @section('content')
@if(Auth::guest() || $another_user) @@ -15,16 +15,16 @@ {{ $username }} @endif @if( $show_pic && !empty($pic)) -
-
- +
+
+
@endif @if( $show_email ) -
- {{ HTML::link(URL::action("UserApiController@unlock",array("id"=>$user->id)),'Unlock',array('data-user-id'=>$user->id,'class'=>'btn unlock-user','title'=>'Unlocks given user')) }} + {{ HTML::link(URL::action("UserApiController@unlock", array("id"=>$user->id)),'Unlock',array('data-user-id'=>$user->id,'class'=>'btn btn-default btn-md active unlock-user','title'=>'Unlocks given user')) }}
+ + + + + + + + + @foreach ($groups as $group) + + + + + + @endforeach + +
NameActive 
{{$group->name}} + active) + checked + @endif + value="{{$group->id}}"/> + +   + {{ HTML::link(URL::action("AdminController@editApiScopeGroup",array("id"=>$group->id)),'Edit',array('class'=>'btn btn-default active edit-api-scope-group','title'=>'Edit a Registered Api Scope Group')) }} + {{ HTML::link(URL::action("ApiScopeGroupController@delete",array("id"=>$group->id)),'Delete',array('class'=>'btn btn-default btn-delete active delete-api-scope-group','title'=>'Deletes a Registered Api Scope Group')) }} +
+ + +
+ +@include('modal', array ('modal_id' => 'dialog-form-api-scope-group', 'modal_title' => 'Register New Api Scope Group', 'modal_save_css_class' => 'save-api-scope-group', 'modal_save_text' => 'Save', 'modal_form' => 'oauth2.profile.admin.api-scope-group-add-form', 'modal_form_data' => array())) + +@stop + +@section('scripts') + + {{ HTML::script('bower_assets/typeahead.js/dist/typeahead.bundle.js')}} + {{ HTML::script('bower_assets/bootstrap-tagsinput/dist/bootstrap-tagsinput.js')}} + {{ HTML::script('assets/js/oauth2/profile/admin/api-scope-groups.js') }} +@append + +@section('css') + {{ HTML::style('bower_assets/bootstrap-tagsinput/dist/bootstrap-tagsinput.css') }} + {{ HTML::style('bower_assets/bootstrap-tagsinput/dist/bootstrap-tagsinput-typeahead.css') }} +@append \ No newline at end of file diff --git a/app/views/oauth2/profile/admin/clients.blade.php b/app/views/oauth2/profile/admin/clients.blade.php index ce7cca60..236ba074 100644 --- a/app/views/oauth2/profile/admin/clients.blade.php +++ b/app/views/oauth2/profile/admin/clients.blade.php @@ -39,5 +39,5 @@
@stop @section('scripts') -{{ HTML::script('js/oauth2/profile/admin/clients.js') }} +{{ HTML::script('assets/js/oauth2/profile/admin/clients.js') }} @stop \ No newline at end of file diff --git a/app/views/oauth2/profile/admin/edit-api-scope-group.blade.php b/app/views/oauth2/profile/admin/edit-api-scope-group.blade.php new file mode 100644 index 00000000..32dbd754 --- /dev/null +++ b/app/views/oauth2/profile/admin/edit-api-scope-group.blade.php @@ -0,0 +1,94 @@ +@extends('layout') + +@section('title') + Welcome to openstackId - Server Admin - Edit Api Scope Group +@stop + +@section('content') + @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) + Go Back + Edit Api Scope Group - Id {{ $group->id }} +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+ +
+
+
+ +@stop + +@section('scripts') + + {{ HTML::script('bower_assets/typeahead.js/dist/typeahead.bundle.js')}} + {{ HTML::script('bower_assets/bootstrap-tagsinput/dist/bootstrap-tagsinput.js')}} + {{ HTML::script('assets/js/oauth2/profile/admin/edit-api-scope-group.js') }} + + +@append + +@section('css') + {{ HTML::style('bower_assets/bootstrap-tagsinput/dist/bootstrap-tagsinput.css') }} + {{ HTML::style('bower_assets/bootstrap-tagsinput/dist/bootstrap-tagsinput-typeahead.css') }} +@append \ No newline at end of file diff --git a/app/views/oauth2/profile/admin/edit-api.blade.php b/app/views/oauth2/profile/admin/edit-api.blade.php index c9e68ce4..7ea02435 100644 --- a/app/views/oauth2/profile/admin/edit-api.blade.php +++ b/app/views/oauth2/profile/admin/edit-api.blade.php @@ -8,66 +8,57 @@ @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) {{ Lang::get("messages.edit_api_go_back") }} {{ Lang::get("messages.edit_api_title", array("id" => $api->id)) }} -
-
+ +
+
-
-
- -
- -
+
+ +
-
- -
- -
+
+ +
-
-
- -
-
-
-
- -
+
+
+ -
-
-
-
-

 Available Scopes

+
+
+
+

 Available Scopes

-
+
+ +
-
+
-
-
- {{ HTML::link(URL::action("ApiScopeController@create",null),'Register Scope',array('class'=>'btn add-scope','title'=>'Adds a New API Scope')) }} +
+
+ {{ HTML::link(URL::action("ApiScopeController@create",null),'Register Scope',array('class'=>'btn active btn-primary add-scope','title'=>'Adds a New API Scope')) }}
-
-
+
+
@@ -105,8 +96,8 @@ @endforeach @@ -117,65 +108,32 @@ - - +@include('modal', array ('modal_id' => 'dialog-form-scope', 'modal_title' => 'Register New API Scope', 'modal_save_css_class' => 'save-scope', 'modal_save_text' => 'Save', 'modal_form' => 'oauth2.profile.admin.scope-add-form', 'modal_form_data' => array())) -
-
-
-

 Available Endpoints

+
+
+
+

 Available Endpoints

-
+
+ +
-
+
-
-
- {{ HTML::link(URL::action("ApiEndpointController@create",null),'Register Endpoint',array('class'=>'btn add-endpoint','title'=>'Adds a New API Endpoint')) }} +
+
+ {{ HTML::link(URL::action("ApiEndpointController@create",null),'Register Endpoint',array('class'=>'btn active btn-primary add-endpoint','title'=>'Adds a New API Endpoint')) }}
-
-
+
+
  - {{ HTML::link(URL::action("AdminController@editScope",array("id"=>$scope->id)),'Edit',array('class'=>'btn edit-scope','title'=>'Edits a Registered API Scope')) }} - {{ HTML::link(URL::action("ApiScopeController@delete",array("id"=>$scope->id)),'Delete',array('class'=>'btn delete-scope','title'=>'Deletes a Registered API Scope'))}} + {{ HTML::link(URL::action("AdminController@editScope",array("id"=>$scope->id)),'Edit',array('class'=>'btn btn-default active edit-scope','title'=>'Edits a Registered API Scope')) }} + {{ HTML::link(URL::action("ApiScopeController@delete",array("id"=>$scope->id)),'Delete',array('class'=>'btn btn-default btn-delete active delete-scope','title'=>'Deletes a Registered API Scope'))}}
@@ -201,8 +159,8 @@ @endforeach @@ -213,54 +171,11 @@ - - - +@include('modal', array ('modal_id' => 'dialog-form-endpoint', 'modal_title' => 'Register New API Endpoint', 'modal_save_css_class' => 'save-endpoint', 'modal_save_text' => 'Save', 'modal_form' => 'oauth2.profile.admin.endpoint-add-form', 'modal_form_data' => array())) @stop + @section('scripts') + -{{ HTML::script('js/oauth2/profile/admin/edit-api.js') }} -@stop \ No newline at end of file +{{ HTML::script('assets/js/oauth2/profile/admin/edit-api.js') }} + +@append \ No newline at end of file diff --git a/app/views/oauth2/profile/admin/edit-endpoint.blade.php b/app/views/oauth2/profile/admin/edit-endpoint.blade.php index 4a848627..9f17c32b 100644 --- a/app/views/oauth2/profile/admin/edit-endpoint.blade.php +++ b/app/views/oauth2/profile/admin/edit-endpoint.blade.php @@ -1,112 +1,108 @@ @extends('layout') @section('title') -Welcome to openstackId - Server Admin - Edit API Endpoint + Welcome to openstackId - Server Admin - Edit API Endpoint @stop @section('content') -@include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) -Go Back -{{ Lang::get("messages.edit_endpoint_title", array("id" => $endpoint->id)) }} -
-
-
-
-
- -
- -
+ @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) + Go Back + {{ Lang::get("messages.edit_endpoint_title", array("id" => $endpoint->id)) }} +
+
+ +
+ +
-
- -
- -
+
+ +
-
- -
- -
+
+ +
-
- -
- -
+
+ + +
+
+ + {{ Form::select('http_method', array('GET' => 'GET', 'POST' => 'POST', 'PUT' => 'PUT', 'DELETE' => 'DELETE'), $endpoint->http_method,array('class' => 'form-control', 'id' => 'http_method')); }} +
+
+ +
+
+
-
- -
- {{ Form::select('http_method', array('GET' => 'GET', 'POST' => 'POST', 'PUT' => 'PUT', 'DELETE' => 'DELETE'), $endpoint->http_method); }} -
-
-
-
- -
-
-
-
- -
-
-
-
- -
-
+ -
- + +
- -
-
- {{Lang::get("messages.edit_endpoint_scope_title")}}  -
    - @foreach($endpoint->api()->first()->scopes()->get() as $scope) - {{-- scope header --}} -
  • - -
  • - @endforeach -
+
+
+ {{Lang::get("messages.edit_endpoint_scope_title")}}  + +
    + @foreach($endpoint->api()->first()->scopes()->get() as $scope) + {{-- scope header --}} +
  • +
    + +
    +
  • + @endforeach +
+
-
@stop @section('scripts') - -{{ HTML::script('js/oauth2/profile/admin/edit-endpoint.js') }} -@stop \ No newline at end of file + + {{ HTML::script('assets/js/oauth2/profile/admin/edit-endpoint.js') }} +@append \ No newline at end of file diff --git a/app/views/oauth2/profile/admin/edit-resource-server.blade.php b/app/views/oauth2/profile/admin/edit-resource-server.blade.php index b2e0f8f0..980eb25c 100644 --- a/app/views/oauth2/profile/admin/edit-resource-server.blade.php +++ b/app/views/oauth2/profile/admin/edit-resource-server.blade.php @@ -8,140 +8,125 @@ @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) Go Back Edit Resource Server - Id {{ $resource_server->id }} -
-
-
-
-
- -
- -
+
+
+ +
+ +
-
- -
- -
+ +
+ +
-
- -
- -
+ +
+ +
-
-
- -
+ +
+
+ @if(!is_null($resource_server->client()->first())) -
-
- - {{ $resource_server->client()->first()->client_id }} - - {{ $resource_server->client()->first()->client_secret }} - {{ HTML::link(URL::action("ApiResourceServerController@regenerateClientSecret",array("id"=> $resource_server->id)),'Regenerate',array('class'=>'btn regenerate-client-secret','title'=>'Regenerates Client Secret')) }} +
+
+
+
+
+ +
+
+ {{ $resource_server->client()->first()->client_id }} +
+
+
+
+ +
+
+ {{ $resource_server->client()->first()->client_secret }} +
+
+ {{ HTML::link(URL::action("ApiResourceServerController@regenerateClientSecret",array("id"=> $resource_server->id)),'Regenerate',array('class'=>'btn regenerate-client-secret btn-xs btn-default active btn-delete','title'=>'Regenerates Client Secret')) }} +
+ +
+
@endif
- +
-
+
- -
-
-
-

 Available Apis

-
-
+
+Available Apis  +
+
+
+
+ {{ HTML::link(URL::action("ApiController@create"),'Register API',array('class'=>'btn btn-primary active btn-sm add-api','title'=>'Adds a New API')) }} +
-
-
- -
-
- {{ HTML::link(URL::action("ApiController@create"),'Register API',array('class'=>'btn add-api','title'=>'Adds a New API')) }} +
+
+
{{$endpoint->http_method}}   - {{ HTML::link(URL::action("AdminController@editEndpoint",array("id"=>$endpoint->id)),'Edit',array('class'=>'btn edit-endpoint','title'=>'Edits a Registered API Endpoint')) }} - {{ HTML::link(URL::action("ApiEndpointController@delete",array("id"=>$endpoint->id)),'Delete',array('class'=>'btn delete-endpoint','title'=>'Deletes a Registered API Endpoint'))}} + {{ HTML::link(URL::action("AdminController@editEndpoint",array("id"=>$endpoint->id)),'Edit',array('class'=>'btn btn-default active edit-endpoint','title'=>'Edits a Registered API Endpoint')) }} + {{ HTML::link(URL::action("ApiEndpointController@delete",array("id"=>$endpoint->id)),'Delete',array('class'=>'btn btn-default btn-delete active delete-endpoint','title'=>'Deletes a Registered API Endpoint'))}}
+ + + + + + + + + + @foreach($resource_server->apis()->get() as $api) + + + + + + + @endforeach + +
 NameActive 
{{ $api->name}} logo{{ $api->name}} + active) + checked + @endif + value="{{$api->id}}"/> + +   + {{ HTML::link(URL::action("AdminController@editApi",array("id"=>$api->id)),'Edit',array('class'=>'btn btn-default active edit-api','title'=>'Edits a Registered Resource Server API')) }} + {{ HTML::link(URL::action("ApiController@delete",array("id"=>$api->id)),'Delete',array('class'=>'btn btn-default btn-delete active delete-api','title'=>'Deletes a Registered Resource Server API'))}} +
+
-
-
- - - - - - - - - - - @foreach($resource_server->apis()->get() as $api) - - - - - - - @endforeach - -
 NameActive 
{{ $api->name}} logo{{ $api->name}} - active) - checked - @endif - value="{{$api->id}}"/> - -   - {{ HTML::link(URL::action("AdminController@editApi",array("id"=>$api->id)),'Edit',array('class'=>'btn edit-api','title'=>'Edits a Registered Resource Server API')) }} - {{ HTML::link(URL::action("ApiController@delete",array("id"=>$api->id)),'Delete',array('class'=>'btn delete-api','title'=>'Deletes a Registered Resource Server API'))}} -
-
-
-
- - - +@include('modal', array ('modal_id' => 'dialog-form-api', 'modal_title' => 'Register New Resource Server API', 'modal_save_css_class' => 'save-api', 'modal_save_text' => 'Save', 'modal_form' => 'oauth2.profile.admin.resource-server-api-add-form', 'modal_form_data' => array())) @stop @section('scripts') @@ -162,5 +147,5 @@ success : '{{ Lang::get("messages.global_successfully_save_entity", array("entity" => "Resource Server")) }}' }; -{{ HTML::script('js/oauth2/profile/admin/edit-resource-server.js') }} -@stop \ No newline at end of file +{{ HTML::script('assets/js/oauth2/profile/admin/edit-resource-server.js') }} +@append \ No newline at end of file diff --git a/app/views/oauth2/profile/admin/edit-scope.blade.php b/app/views/oauth2/profile/admin/edit-scope.blade.php index c26471b4..46ec94db 100644 --- a/app/views/oauth2/profile/admin/edit-scope.blade.php +++ b/app/views/oauth2/profile/admin/edit-scope.blade.php @@ -1,84 +1,77 @@ @extends('layout') @section('title') -Welcome to openstackId - Server Admin - Edit API Scope + Welcome to openstackId - Server Admin - Edit API Scope @stop @section('content') -@include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) -Go Back -Edit API Scope - Id {{ $scope->id }} -
-
-
-
-
- -
- -
+ @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) + Go Back + Edit API Scope - Id {{ $scope->id }} +
+
+ +
+ +
-
- -
- -
+
+ +
-
- -
- -
+
+ +
-
-
-
-
-
-
-
-
- -
-
-
-
- -
+
+
+ -
-
+ +
-
@stop @section('scripts') - -{{ HTML::script('js/oauth2/profile/admin/edit-scope.js') }} -@stop \ No newline at end of file + + {{ HTML::script('assets/js/oauth2/profile/admin/edit-scope.js') }} +@append \ No newline at end of file diff --git a/app/views/oauth2/profile/admin/endpoint-add-form.blade.php b/app/views/oauth2/profile/admin/endpoint-add-form.blade.php new file mode 100644 index 00000000..3cbb0682 --- /dev/null +++ b/app/views/oauth2/profile/admin/endpoint-add-form.blade.php @@ -0,0 +1,45 @@ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/app/views/oauth2/profile/admin/resource-server-add-form.blade.php b/app/views/oauth2/profile/admin/resource-server-add-form.blade.php new file mode 100644 index 00000000..fe8fb226 --- /dev/null +++ b/app/views/oauth2/profile/admin/resource-server-add-form.blade.php @@ -0,0 +1,20 @@ +
+
+ + +
+
+ + +
+ +
+ + +
+
+ +
+
\ No newline at end of file diff --git a/app/views/oauth2/profile/admin/resource-server-api-add-form.blade.php b/app/views/oauth2/profile/admin/resource-server-api-add-form.blade.php new file mode 100644 index 00000000..0de0f0b1 --- /dev/null +++ b/app/views/oauth2/profile/admin/resource-server-api-add-form.blade.php @@ -0,0 +1,15 @@ +
+
+ + +
+
+ + +
+
+ +
+
\ No newline at end of file diff --git a/app/views/oauth2/profile/admin/resource-servers.blade.php b/app/views/oauth2/profile/admin/resource-servers.blade.php index aa750ae7..ed14dd7d 100644 --- a/app/views/oauth2/profile/admin/resource-servers.blade.php +++ b/app/views/oauth2/profile/admin/resource-servers.blade.php @@ -6,22 +6,21 @@ @section('content') @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) -
- -
-

 Resource Servers

+
+
+

 Resource Servers

-
+
-
+
-
-
- {{ HTML::link(URL::action("ApiResourceServerController@create"),'Add Resource Server',array('class'=>'btn add-resource-server','title'=>'Adds a New Resource Server')) }} +
+
+ {{ HTML::link(URL::action("ApiResourceServerController@create"),'Add Resource Server',array('class'=>'btn active btn-primary add-resource-server','title'=>'Adds a New Resource Server')) }}
@@ -50,8 +49,8 @@ @endforeach @@ -59,31 +58,7 @@
  - {{ HTML::link(URL::action("AdminController@editResourceServer",array("id"=>$resource_server->id)),'Edit',array('class'=>'btn edit-resource-server','title'=>'Edits a Registered Resource Server')) }} - {{ HTML::link(URL::action("ApiResourceServerController@delete",array("id"=>$resource_server->id)),'Delete',array('class'=>'btn delete-resource-server','title'=>'Deletes a Registered Resource Server')) }} + {{ HTML::link(URL::action("AdminController@editResourceServer",array("id"=>$resource_server->id)),'Edit',array('class'=>'btn btn-default active edit-resource-server','title'=>'Edits a Registered Resource Server')) }} + {{ HTML::link(URL::action("ApiResourceServerController@delete",array("id"=>$resource_server->id)),'Delete',array('class'=>'btn btn-default btn-delete active delete-resource-server','title'=>'Deletes a Registered Resource Server')) }}
- +@include('modal', array ('modal_id' => 'dialog-form-resource-server', 'modal_title' => 'Register New Resource Server', 'modal_save_css_class' => 'save-resource-server', 'modal_save_text' => 'Save', 'modal_form' => 'oauth2.profile.admin.resource-server-add-form', 'modal_form_data' => array())) @stop @@ -98,5 +73,5 @@ add : '{{URL::action("ApiResourceServerController@create",null)}}' }; -{{ HTML::script('js/oauth2/profile/admin/resource-servers.js') }} -@stop \ No newline at end of file +{{ HTML::script('assets/js/oauth2/profile/admin/resource-servers.js') }} +@append \ No newline at end of file diff --git a/app/views/oauth2/profile/admin/scope-add-form.blade.php b/app/views/oauth2/profile/admin/scope-add-form.blade.php new file mode 100644 index 00000000..baa39c82 --- /dev/null +++ b/app/views/oauth2/profile/admin/scope-add-form.blade.php @@ -0,0 +1,30 @@ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/app/views/oauth2/profile/admin/server-private-key-add-form.blade.php b/app/views/oauth2/profile/admin/server-private-key-add-form.blade.php new file mode 100644 index 00000000..7efd7f98 --- /dev/null +++ b/app/views/oauth2/profile/admin/server-private-key-add-form.blade.php @@ -0,0 +1,45 @@ +
+
+ + +
+
+ +
+ + to + +
+
+
+ +
+
+ + {{ Form::select('usage', utils\ArrayUtils::convert2Assoc(\jwk\JSONWebKeyPublicKeyUseValues::$valid_uses) ,null , array('class' => 'form-control', 'id' => 'usage')) }} +
+
+ + {{ Form::select('alg', array() ,null , array('class' => 'form-control', 'id' => 'alg')) }} +
+
+ + +
+
+ + +
+
+ +
+ +
\ No newline at end of file diff --git a/app/views/oauth2/profile/admin/server-private-keys.blade.php b/app/views/oauth2/profile/admin/server-private-keys.blade.php new file mode 100644 index 00000000..fc5f5a67 --- /dev/null +++ b/app/views/oauth2/profile/admin/server-private-keys.blade.php @@ -0,0 +1,104 @@ +@extends('layout') +@section('title') + Welcome to openstackId - Server Admin - Server Private Keys +@stop +@section('css') + {{ HTML::style('bower_assets/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css') }} + {{ HTML::style('assets/css/private-keys.css') }} +@append +@section('scripts') + {{ HTML::script('bower_assets/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js')}} + {{ HTML::script('bower_assets/pwstrength-bootstrap/dist/pwstrength-bootstrap-1.2.7.min.js')}} + {{ HTML::script('assets/js/oauth2/profile/admin/server-private-keys.js') }} + + +@append +@section('content') + @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) + + + + + + + + + + + + + + @foreach ($private_keys as $private_key) + + + + + + @endforeach + +
+
+
Private keys 
+
+ Add Private Key +
+

This is a list of Private Keys keys associated with the server. Remove any keys that you do not recognize.

+
+
+
+   +
+
+ +
+
+
+
+
+
+
+ {{$private_key->kid}} {{$private_key->usage}} {{$private_key->type}} {{$private_key->alg}} +
+
+
+
+ {{$private_key->getSHA_256_Thumbprint()}} +
+
+
+
+ valid from {{$private_key->valid_from}} to {{$private_key->valid_to}} +
+
+
+
+
Delete
+ + + @include('modal', array ('modal_id' => 'ModalAddPrivateKey', 'modal_title' => 'Add Private Key', 'modal_save_css_class' => 'save-private-key', 'modal_save_text' => 'Save', 'modal_form' => 'oauth2.profile.admin.server-private-key-add-form', 'modal_form_data' => array())) +@stop \ No newline at end of file diff --git a/app/views/oauth2/profile/clients.blade.php b/app/views/oauth2/profile/clients.blade.php index ab981a97..0381d693 100644 --- a/app/views/oauth2/profile/clients.blade.php +++ b/app/views/oauth2/profile/clients.blade.php @@ -1,132 +1,94 @@ @extends('layout') @section('title') -Welcome to openstackId - OAUTH2 Console - Clients + Welcome to openstackId - OAUTH2 Console - Clients @stop @section('content') -@include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) -
-
-  Registered Applications - {{ HTML::link(URL::action("ClientApiController@create",null),'Register Application',array('class'=>'btn add-client','title'=>'Adds a Registered Application')) }} - @if (count($clients)>0) - - - - - - - - - - - - - @foreach ($clients as $client) - - - - - - - - - @endforeach - -
Application NameApplication TypeIs ActiveIs LockedModified 
{{ $client->app_name }}{{ $client->getFriendlyApplicationType()}} - active) - checked - @endif - value="{{$client->id}}"/> - - locked) - checked - @endif - value="{{$client->id}}" disabled="disabled" /> - {{ $client->updated_at }}  - {{ HTML::link(URL::action("AdminController@editRegisteredClient",array("id"=>$client->id)),'Edit',array('class'=>'btn edit-client','title'=>'Edits a Registered Application')) }} - {{ HTML::link(URL::action("ClientApiController@delete",array("id"=>$client->id)),'Delete',array('class'=>'btn del-client','title'=>'Deletes a Registered Application')) }}
- @endif + @include('menu',array('is_oauth2_admin' => $is_oauth2_admin, 'is_openstackid_admin' => $is_openstackid_admin)) +
+
+  Registered + Applications + + {{ HTML::link(URL::action("ClientApiController@create",null),'Register Application',array('class'=>'btn btn-primary btn-md active add-client','title'=>'Adds a Registered Application')) }} + @if (count($clients)>0) + + + + + + + + + + + + + @foreach ($clients as $client) + + + + + + + + + @endforeach + +
Application NameApplication TypeIs ActiveIs LockedModified 
{{ $client->app_name }}{{ $client->getFriendlyApplicationType()}} + active) + checked + @endif + value="{{$client->id}}"/> + + locked) + checked + @endif + value="{{$client->id}}" disabled="disabled" /> + {{ $client->updated_at }}  + {{ HTML::link(URL::action("AdminController@editRegisteredClient",array("id"=>$client->id)),'Edit',array('class'=>'btn btn-default btn-md active edit-client','title'=>'Edits a Registered Application')) }} + {{ HTML::link(URL::action("ClientApiController@delete",array("id"=>$client->id)),'Delete',array('class'=>'btn btn-default btn-md active del-client','title'=>'Deletes a Registered Application')) }}
+ @endif +
-
-