openstackid/app/Services/OAuth2/ClientService.php

679 lines
25 KiB
PHP

<?php namespace Services\OAuth2;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Auth\Repositories\IUserRepository;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Request;
use Models\OAuth2\Client;
use OAuth2\Exceptions\AbsentClientException;
use OAuth2\Exceptions\InvalidApiScope;
use OAuth2\Exceptions\InvalidClientAuthMethodException;
use OAuth2\Exceptions\InvalidClientType;
use OAuth2\Exceptions\MissingClientAuthorizationInfo;
use OAuth2\Factories\IOAuth2ClientFactory;
use OAuth2\Models\ClientAssertionAuthenticationContext;
use OAuth2\Models\ClientAuthenticationContext;
use OAuth2\Models\ClientCredentialsAuthenticationContext;
use OAuth2\Models\IClient;
use OAuth2\OAuth2Protocol;
use OAuth2\Repositories\IApiScopeRepository;
use OAuth2\Repositories\IClientRepository;
use OAuth2\Services\IApiScopeService;
use OAuth2\Services\IClientCredentialGenerator;
use OAuth2\Services\IClientService;
use Services\Exceptions\ValidationException;
use URL\Normalizer;
use Utils\Db\ITransactionService;
use Utils\Exceptions\EntityNotFoundException;
use Utils\Http\HttpUtils;
use Utils\Services\IAuthService;
/**
* Class ClientService
* @package Services\OAuth2
*/
class ClientService implements IClientService
{
/**
* @var IAuthService
*/
private $auth_service;
/**
* @var IApiScopeService
*/
private $scope_service;
/**
* @var IUserRepository
*/
private $user_repository;
/**
* @var IClientCredentialGenerator
*/
private $client_credential_generator;
/**
* @var IClientRepository
*/
private $client_repository;
/**
* @var IOAuth2ClientFactory
*/
private $client_factory;
/**
* @var IApiScopeRepository
*/
private $scope_repository;
/**
* ClientService constructor.
* @param IUserRepository $user_repository
* @param IClientRepository $client_repository
* @param IAuthService $auth_service
* @param IApiScopeService $scope_service
* @param IClientCredentialGenerator $client_credential_generator
* @param IOAuth2ClientFactory $client_factory
* @param IApiScopeRepository $scope_repository
* @param ITransactionService $tx_service
*/
public function __construct
(
IUserRepository $user_repository,
IClientRepository $client_repository,
IAuthService $auth_service,
IApiScopeService $scope_service,
IClientCredentialGenerator $client_credential_generator,
IOAuth2ClientFactory $client_factory,
IApiScopeRepository $scope_repository,
ITransactionService $tx_service
)
{
$this->auth_service = $auth_service;
$this->user_repository = $user_repository;
$this->scope_service = $scope_service;
$this->client_credential_generator = $client_credential_generator;
$this->client_repository = $client_repository;
$this->scope_repository = $scope_repository;
$this->client_factory = $client_factory;
$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
* the authorization server
* Alternatively, the authorization server MAY support including the
* client credentials in the request-body using the following
* parameters:
* implementation of @see http://tools.ietf.org/html/rfc6749#section-2.3.1
* implementation of @see http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
* @throws InvalidClientAuthMethodException
* @throws MissingClientAuthorizationInfo
* @return ClientAuthenticationContext
*/
public function getCurrentClientAuthInfo()
{
$auth_header = Request::header('Authorization');
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('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('bad auth header.');
}
return new ClientCredentialsAuthenticationContext(
$auth_header_content[0],
$auth_header_content[1],
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic
);
}
throw new InvalidClientAuthMethodException;
}
/**
* @param string $application_type
* @param string $app_name
* @param string $app_description
* @param null|string $app_url
* @param array $admin_users
* @param string $app_logo
* @return IClient
* @throws ValidationException
*/
public function register
(
$application_type,
$app_name,
$app_description,
$app_url = null,
array $admin_users = [],
$app_logo = ''
)
{
$scope_service = $this->scope_service;
$client_credential_generator = $this->client_credential_generator;
$user_repository = $this->user_repository;
$client_repository = $this->client_repository;
$client_factory = $this->client_factory;
$current_user = $this->auth_service->getCurrentUser();
return $this->tx_service->transaction(function () use (
$application_type,
$current_user,
$app_name,
$app_url,
$app_description,
$app_logo,
$admin_users,
$scope_service,
$user_repository,
$client_repository,
$client_factory,
$client_credential_generator
) {
if($this->client_repository->getByApplicationName($app_name) != null){
throw new ValidationException('there is already another application with that name, please choose another one.');
}
$client = $client_factory->build($app_name, $current_user, $application_type);
$client = $client_credential_generator->generate($client);
$client->app_logo = $app_logo;
$client->app_description = $app_description;
$client->website = $app_url;
$client_repository->add($client);
//add default scopes
foreach ($this->scope_repository->getDefaults() 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->addScope($default_scope);
}
//add admin users
foreach($admin_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));
$client->addAdminUser($user);
}
return $client;
});
}
/**
* @param int $id
* @param array $params
* @throws ValidationException
* @throws EntityNotFoundException
* @return IClient
*/
public function update($id, array $params)
{
$client_repository = $this->client_repository;
$user_repository = $this->user_repository;
$editing_user = $this->auth_service->getCurrentUser();
return $this->tx_service->transaction(function () use ($id, $editing_user, $params, $client_repository, $user_repository) {
$client = $client_repository->get($id);
if (is_null($client)) {
throw new EntityNotFoundException(sprintf('client id %s does not exists.', $id));
}
$app_name = isset($params['app_name']) ? trim($params['app_name']) : null;
if(!empty($app_name)) {
$old_client = $client_repository->getByApplicationName($app_name);
if(!is_null($old_client) && $old_client->id !== $client->id)
throw new ValidationException('there is already another application with that name, please choose another one.');
}
$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: {
if (isset($params['redirect_uris'])) {
$redirect_uris = explode(',', $params['redirect_uris']);
//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'])){
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',
'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',
'admin_users',
);
$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($param === 'admin_users'){
$admin_users = trim($params['admin_users']);
$admin_users = empty($admin_users) ? array():explode(',',$admin_users);
$client->removeAllAdminUsers();
foreach($admin_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));
$client->addAdminUser($user);
}
}
else {
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} = trim($params[$param]);
}
}
}
$client_repository->add($client->setEditedBy($editing_user));
return $client;
});
}
/**
* @param int $id
* @param int $scope_id
* @return IClient
* @throws EntityNotFoundException
*/
public function addClientScope($id, $scope_id)
{
return $this->tx_service->transaction(function() use ($id, $scope_id){
$client = $this->client_repository->get($id);
if (is_null($client)) {
throw new EntityNotFoundException(sprintf("client id %s not found!.", $id));
}
$scope = $this->scope_repository->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));
$client->scopes()->attach($scope_id);
$client->setEditedBy($this->auth_service->getCurrentUser());
$this->client_repository->add($client);
return $client;
});
}
/**
* @param $id
* @param $scope_id
* @return IClient
* @throws EntityNotFoundException
*/
public function deleteClientScope($id, $scope_id)
{
return $this->tx_service->transaction(function() use ($id, $scope_id){
$client = $this->client_repository->get($id);
if (is_null($client)) {
throw new EntityNotFoundException(sprintf("client id %s does not exists!", $id));
}
$client->scopes()->detach($scope_id);
$client->setEditedBy($this->auth_service->getCurrentUser());
$this->client_repository->add($client);
return $client;
});
}
/**
* @param int $id
* @return bool
* @throws EntityNotFoundException
*/
public function deleteClientByIdentifier($id)
{
return $this->tx_service->transaction(function () use ($id) {
$client = $this->client_repository->get($id);
if (is_null($client)) {
throw new EntityNotFoundException(sprintf("client id %s does not exists!", $id));
}
$client->scopes()->detach();
Event::fire('oauth2.client.delete', array($client->client_id));
$this->client_repository->delete($client);
true;
});
}
/**
* Regenerates Client Secret
* @param $id client id
* @return IClient
* @throws EntityNotFoundException
*/
public function regenerateClientSecret($id)
{
$client_credential_generator = $this->client_credential_generator;
$current_user = $this->auth_service->getCurrentUser();
return $this->tx_service->transaction(function () use ($id, $current_user, $client_credential_generator)
{
$client = $this->client_repository->get($id);
if (is_null($client))
{
throw new EntityNotFoundException(sprintf("client id %d does not exists!.", $id));
}
if ($client->client_type != IClient::ClientType_Confidential)
{
throw new InvalidClientType
(
sprintf
(
"client id %d is not confidential type!.",
$id
)
);
}
$client = $client_credential_generator->generate($client, true);
$client->setEditedBy($current_user);
$this->client_repository->add($client);
Event::fire('oauth2.client.regenerate.secret', array($client->client_id));
return $client;
});
}
/**
* @param client $client_id
* @return mixed
* @throws EntityNotFoundException
*/
public function lockClient($client_id)
{
return $this->tx_service->transaction(function () use ($client_id) {
$client = $this->client_repository($client_id);
if (is_null($client)) {
throw new EntityNotFoundException($client_id, sprintf("client id %s does not exists!", $client_id));
}
$client->locked = true;
$this->client_repository->update($client);
return true;
});
}
/**
* @param int $id
* @return bool
* @throws EntityNotFoundException
*/
public function unlockClient($id)
{
return $this->tx_service->transaction(function () use ($id) {
$client = $this->client_repository->getClientByIdentifier($id);
if (is_null($client)) {
throw new EntityNotFoundException($id, sprintf("client id %s does not exists!", $id));
}
$client->locked = false;
$this->client_repository->update($client);
return true;
});
}
/**
* @param int $id
* @param bool $active
* @return bool
* @throws EntityNotFoundException
*/
public function activateClient($id, $active)
{
return $this->tx_service->transaction(function () use ($id, $active) {
$client = $this->client_repository->getClientByIdentifier($id);
if (is_null($client)) {
throw new EntityNotFoundException($id, sprintf("client id %s does not exists!", $id));
}
$client->active = $active;
$this->client_repository->update($client);
return true;
});
}
/**
* @param int $id
* @param bool $use_refresh_token
* @return bool
* @throws EntityNotFoundException
*/
public function setRefreshTokenUsage($id, $use_refresh_token)
{
return $this->tx_service->transaction(function () use ($id, $use_refresh_token) {
$client = $this->client_repository->getClientByIdentifier($id);
if (is_null($client)) {
throw new EntityNotFoundException($id, sprintf("client id %s does not exists!", $id));
}
$client->use_refresh_token = $use_refresh_token;
$client->setEditedBy($this->auth_service->getCurrentUser());
$this->client_repository->update($client);
return true;
});
}
/**
* @param int $id
* @param bool $rotate_refresh_token
* @return bool
* @throws EntityNotFoundException
*/
public function setRotateRefreshTokenPolicy($id, $rotate_refresh_token)
{
return $this->tx_service->transaction(function () use ($id, $rotate_refresh_token) {
$client = $this->client_repository->getClientByIdentifier($id);
if (is_null($client)) {
throw new EntityNotFoundException($id, sprintf("client id %s does not exists!", $id));
}
$client->rotate_refresh_token = $rotate_refresh_token;
$client->setEditedBy($this->auth_service->getCurrentUser());
$this->client_repository->update($client);
return true;
});
}
}