diff --git a/README.md b/README.md index cce7fe2..f80107f 100755 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ The goal of this package is to split it up in multiple Framework Branches. Right **v0.5** is the stable Laravel 5.2 compatible release +**v5.1.x** is the tag pattern for the the stable Laravel 5.1 compatible release ##### Dependencies **Oauth2 Server** - The Oauth2 Stack is based on [Brent Shaffer's Oauth2 Server](https://github.com/bshaffer/oauth2-server-php), tweaked for multi-layer usage. @@ -88,6 +89,20 @@ $ nano app/config/app.php $ php artisan dump-autoload ``` +##Laravel 5.1 + +Laravel 5.1 maintenance is kept in the `lvl51` branch and the releases are tagged as `v5.1.x`. + +For the installation follow the same instructions of the Laravel 5.2 installation. Just change the relevant line in `composer.json` to + +``` +"cloudoki/oauth2-stack": "v5.1.*" +``` + +or use a specific version to lock the dependency to it. + +--- + If you go deep into the package you'll find out that the `/oauth2` routes are defined right there. Feel free to override this by copy-pasting the routes to your project `./app/routes.php` file and disabling the include in `OaStackServiceProvider.php`. The same goes for the filters file, which identifies `auth`, a basic token check. @@ -100,7 +115,7 @@ $ php artisan config:publish cloudoki/oauth2-stack ``` *You may also create environment specific configs by placing them like so `app/config/packages/cloudoki/oastack/environment`.* -**Laravel 5.2** +**Laravel 5.1 and 5.2** ``` $ php artisan vendor:publish ``` @@ -119,7 +134,7 @@ The Oauth2 related models, **Oauth2AccessToken**, **Oauth2Authorization**, **Oau $ php artisan migrate --package="cloudoki/oauth2-stack" ``` -**Laravel 5.2** +**Laravel 5.1 and 5.2** ``` $ php artisan vendor:publish --tag="migrations" ``` diff --git a/src/Cloudoki/OaStack/Controllers/BaseController.php b/src/Cloudoki/OaStack/Controllers/BaseController.php index 06c658f..ef1402d 100755 --- a/src/Cloudoki/OaStack/Controllers/BaseController.php +++ b/src/Cloudoki/OaStack/Controllers/BaseController.php @@ -8,7 +8,6 @@ use Illuminate\Validation\ValidationException; use Illuminate\Support\Facades\Redirect; - class BaseController extends Controller { /** @@ -28,7 +27,6 @@ class BaseController extends Controller */ var $request; - /** * BaseController construct * MQ preps @@ -63,11 +61,9 @@ public function validate ($input, $rules = []) // Add path attributes $input = $this->prepInput ($input); - // Perform validation $validator = Validator::make ($input, $rules); - // Check if the validator failed if ($validator->fails ()) @@ -90,15 +86,15 @@ public static function jobdispatch($job, $jobload, $direct = false) # Response $response = app()->frontqueue->request($job, $jobload); - - if (isset ($response->error)) - + + if (isset ($response->error)) + return response ($response->error, $response->code); # Frontqueue call - return $direct? - - $response: + return $direct? + + $response: response()->json ($response); } @@ -111,18 +107,46 @@ public static function jobdispatch($job, $jobload, $direct = false) */ public function restDispatch ($method, $controller, $input = [], $rules = []) { + # Extend rules $rules = array_merge ($this->baseValidationRules, $rules); # Validation $payload = array_intersect_key ($this->validate ($input, $rules), $rules); - # Request Foreground Job - $response = self::jobdispatch ('controllerDispatch', (object) ['action'=> $method, 'controller'=> $controller, 'payload'=> (object) $payload], true); - - return is_string ($response)? - - json_decode ($response): + $externalDispatcher = config ('oastack.job_dispatcher', null); + + if ($externalDispatcher !== null) { + // Instead of using the built-in job dispatching logic, + // we call the user-specified method that handles it + // in the base application. + $dispatchFunc = array($externalDispatcher, 'dispatch'); + + $response = call_user_func($dispatchFunc, + 'controllerDispatch', + (object) [ + 'action'=> $method, + 'controller'=> $controller, + 'payload'=> (object) $payload + ], + true + ); + } else { + # Request Foreground Job + $response = self::jobdispatch ( + 'controllerDispatch', + (object) [ + 'action'=> $method, + 'controller'=> $controller, + 'payload'=> (object) $payload + ], + true + ); + } + + return is_string ($response)? + + json_decode ($response): (object) $response; } diff --git a/src/Cloudoki/OaStack/Controllers/OAuth2Controller.php b/src/Cloudoki/OaStack/Controllers/OAuth2Controller.php index 382dcd8..2665968 100755 --- a/src/Cloudoki/OaStack/Controllers/OAuth2Controller.php +++ b/src/Cloudoki/OaStack/Controllers/OAuth2Controller.php @@ -45,16 +45,22 @@ public static function login ($payload) throw new \Cloudoki\InvalidParameterException ('Invalid client id or redirect uri'); } - # Validate user - if (!empty($payload->email)) { - $user = User::email ($payload->email)->first (); - } else { + + if (empty($payload->email)) { throw new \Cloudoki\InvalidParameterException ('Invalid e-mail.'); } + $userModelClass = config ('oastack.user_model', User::class); + + // We have to use the base app's user model and authentication strategy + $userModel = app()->make($userModelClass); + + $user = call_user_func(array($userModel, 'findByLoginId'), $payload->email); + if (!isset($user) || !$user->checkPassword ($payload->password)) { throw new \Cloudoki\InvalidParameterException ('Invalid password or e-mail.'); } + # Validate Authorization $authorization = $user->oauth2authorizations ()->where ('client_id', $client->getClientId ())->first (); @@ -64,15 +70,17 @@ public static function login ($payload) [ 'access_token'=> Oauth2AccessToken::generateAccessToken(), 'client_id'=> $client->getClientId (), - 'user_id'=> $user->getId (), + 'user_id'=> $user->id, 'expires'=> new Carbon('+ 2 minute', Config::get ('app.timezone')) ]); + + return [ 'view'=> 'approve', 'session_token'=> $sessiontoken->getToken (), - 'user'=> $user->schema ('basic'), + 'user'=> $user->getViewPresenter (), 'client'=> $client->schema ('basic') ]; } @@ -85,10 +93,11 @@ public static function login ($payload) [ 'access_token'=> Oauth2AccessToken::generateAccessToken(), 'client_id'=> $client->getClientId (), - 'user_id'=> $user->getId (), + 'user_id'=> $user->id, 'expires'=> Carbon::now(new DateTimeZone(Config::get ('app.timezone')))->addYear () ]); + return [ 'uri'=> $client->getRedirectUri () . '?access_token=' . $accesstoken->getToken () @@ -111,19 +120,18 @@ public static function authorize ($payload) # Validate session token $sessiontoken = Oauth2AccessToken::whereAccessToken ($payload->session_token)->valid ()->first (); - if (!$sessiontoken || $sessiontoken->user->getId () != (int) $payload->approve) + if (!$sessiontoken || $sessiontoken->user->id != (int) $payload->approve) throw new \Cloudoki\InvalidParameterException ('Session expired or invalid approval.'); - # Token handling - Oauth2Authorization::create (['client_id'=> $sessiontoken->client->getClientId (), 'user_id'=> $sessiontoken->user->getId (), 'authorization_date'=> Carbon::now(new DateTimeZone(Config::get ('app.timezone')))]); + Oauth2Authorization::create (['client_id'=> $sessiontoken->client->getClientId (), 'user_id'=> $sessiontoken->user->id, 'authorization_date'=> Carbon::now(new DateTimeZone(Config::get ('app.timezone')))]); $accesstoken = Oauth2AccessToken::create ( [ 'access_token'=> Oauth2AccessToken::generateAccessToken(), 'client_id'=> $sessiontoken->client->getClientId (), - 'user_id'=> $sessiontoken->user->getId (), + 'user_id'=> $sessiontoken->user->id, 'expires'=> Carbon::now(new DateTimeZone(Config::get ('app.timezone')))->addYear () ]); @@ -235,7 +243,7 @@ public static function resetpassword ($payload) public static function changepassword ($payload) { $token = $payload->reset_token; - + $user = User::email ($payload->email) ->whereHas ('accounts', function ($q) use ($token) { $q->where ('account_user.reset_token', $token); }) ->first (); @@ -250,7 +258,7 @@ public static function changepassword ($payload) throw new \Cloudoki\InvalidParameterException ('The passwords do not match.'); - + # Update user $user->setPassword ($payload->password) ->setResetToken (null) @@ -292,7 +300,7 @@ public function identifyinvite ($payload) else return [ - 'user'=> $user->schema ('full'), + 'user'=> $user->getViewPresenter (), 'account'=> $account->schema ('basic') ]; } @@ -348,7 +356,7 @@ public function registeruser () public function registerclient ($payload = null) { $payload = $payload ?: json_decode (Input::get ('payload')); - + $client = new Oauth2Client(); $client->appendPayload ($payload) ->save(); diff --git a/src/Cloudoki/OaStack/Controllers/OaStackViewController.php b/src/Cloudoki/OaStack/Controllers/OaStackViewController.php index b02602f..430d39e 100755 --- a/src/Cloudoki/OaStack/Controllers/OaStackViewController.php +++ b/src/Cloudoki/OaStack/Controllers/OaStackViewController.php @@ -6,6 +6,7 @@ use Illuminate\Http\Request; use Cloudoki\OaStack\Controllers\BaseController; use Cloudoki\InvalidParameterException; +use Cloudoki\OaStack\Exceptions\Handler as OaStackHandler; class OaStackViewController extends BaseController { @@ -14,7 +15,7 @@ class OaStackViewController extends BaseController { 'email'=> 'required|email', 'password'=> 'required|min:4', 'client_id'=> 'required|min:18', - 'response_type'=> 'required|min:5', + 'response_type'=> 'required|min:4', 'redirect_uri'=> 'required|min:8', 'state'=> '' ); @@ -59,6 +60,18 @@ class OaStackViewController extends BaseController { 'user_id' => 'required|integer', ); + public function __construct (Request $request) + { + parent::__construct($request); + // Override the base app's global exception handler with this + // package's custom exception handler + // As seen here: https://laracasts.com/discuss/channels/requests/custom-exception-handler-based-on-route-group + \App::singleton( + \Illuminate\Contracts\Debug\ExceptionHandler::class, + OaStackHandler::class + ); + } + /** * User Login * Show user login fields @@ -75,12 +88,13 @@ public function login () */ public function loginrequest () { + // Request Foreground Job $login = $this->restDispatch ('login', 'Cloudoki\OaStack\OAuth2Controller', [], self::$loginRules); - + if (isset ($login->error)) - return view('oastack::oauth2.login', ['error'=> isset ($login->message)? $login->message: "something went wrong"]); + return view('oastack::oauth2.login', ['error'=> isset ($login->error)? $login->error: "something went wrong"]); else if (isset ($login->view)) @@ -188,10 +202,10 @@ public function subscribe ($token) { // Request Foreground Job $invite = $this->restDispatch ('identifyinvite', 'Cloudoki\OaStack\OAuth2Controller', ['token'=> $token], self::$invitationRules); - + // Build View - return view ('oastack::oauth2.subscribe', + return view ('oastack::oauth2.subscribe', [ 'user'=> (array) $invite->user, 'account'=> (array) $invite->account diff --git a/src/Cloudoki/OaStack/Exceptions/Handler.php b/src/Cloudoki/OaStack/Exceptions/Handler.php new file mode 100644 index 0000000..d00f2fd --- /dev/null +++ b/src/Cloudoki/OaStack/Exceptions/Handler.php @@ -0,0 +1,56 @@ +getMessage(); + } else { + $message = 'Invalid request.'; + } + return response()->view('oastack::oauth2.login', ['error'=> $message]); + } +} diff --git a/src/Cloudoki/OaStack/Models/Oauth2AccessToken.php b/src/Cloudoki/OaStack/Models/Oauth2AccessToken.php index ff71788..bb62cc0 100755 --- a/src/Cloudoki/OaStack/Models/Oauth2AccessToken.php +++ b/src/Cloudoki/OaStack/Models/Oauth2AccessToken.php @@ -2,7 +2,6 @@ namespace Cloudoki\OaStack\Models; -use Cloudoki\OaStack\Models\User; use Cloudoki\OaStack\Models\Oauth2Client; use \Illuminate\Database\Eloquent\Model as Eloquent; @@ -34,7 +33,8 @@ class Oauth2AccessToken extends Eloquent */ public function user () { - return $this->belongsTo (User::class); + $userModelClass = config ('oastack.user_model', 'Cloudoki\\OaStack\\Models\\User'); + return $this->belongsTo ($userModelClass); } /** @@ -98,6 +98,20 @@ public function getToken () return $this->access_token; } + /** + * Expires all authentication tokens of the provided user id. + * + * @param int $userId + * + * @return null + */ + public static function expireAllUserTokens ($userId) + { + self::where('user_id', '=', $userId) + ->whereRaw('expires > now()') + ->update(['expires' => date('Y-m-d H:i:s')]); + } + /** * Generates an unique access token. @@ -112,7 +126,7 @@ public function getToken () */ protected static function generateAccessToken() { - if (function_exists('mcrypt_create_iv')) + if (function_exists('mcrypt_create_iv')) { $randomData = mcrypt_create_iv(20, MCRYPT_DEV_URANDOM); if ($randomData !== false && strlen($randomData) === 20) @@ -128,7 +142,7 @@ protected static function generateAccessToken() return bin2hex($randomData); } - if (@file_exists('/dev/urandom')) + if (@file_exists('/dev/urandom')) { $randomData = file_get_contents('/dev/urandom', false, null, 0, 20); if ($randomData !== false && strlen($randomData) === 20) diff --git a/src/Cloudoki/OaStack/Models/User.php b/src/Cloudoki/OaStack/Models/User.php index 3e97647..94723ff 100755 --- a/src/Cloudoki/OaStack/Models/User.php +++ b/src/Cloudoki/OaStack/Models/User.php @@ -5,6 +5,8 @@ use Cloudoki\OaStack\Models\Oauth2Authorization; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Hash; +use Cloudoki\OaStack\Traits\User as UserTrait; + /** * User Model * Add the namespace if you want to extend your custom User model with this one. @@ -12,7 +14,8 @@ class User extends BaseModel { use SoftDeletes; - + use UserTrait; + /** * The model type. * @@ -40,36 +43,6 @@ public function accounts () return $this->belongsToMany ('Cloudoki\OaStack\Models\Account')->withPivot ('invitation_token'); } - /** - * Acces Token relationship - * - * @return hasMany - */ - public function oauth2accesstokens () - { - return $this->hasMany('Cloudoki\OaStack\Models\Oauth2AccessToken'); - } - - /** - * Authorisations relationship - * - * @return hasMany - */ - public function oauth2authorizations () - { - return $this->hasMany('Cloudoki\OaStack\Models\Oauth2Authorization'); - } - - /** - * Clients relationship - * - * @return hasMany - */ - public function oauth2clients () - { - return $this->hasMany('Cloudoki\OaStack\Models\Oauth2Client'); - } - /** * Get Accounts * All accounts related to the user. @@ -127,7 +100,7 @@ public function getFirstName () public function setFirstName ($firstname) { $this->firstname = $firstname; - + return $this; } @@ -149,10 +122,10 @@ public function getLastName () public function setLastName ($name) { $this->lastname = $name; - + return $this; } - + /** * Get user name * @@ -181,7 +154,7 @@ public function getEmail () public function setEmail ($email) { $this->email = $email; - + return $this; } @@ -194,19 +167,8 @@ public function setEmail ($email) public function setPassword($value) { $this->password = Hash::make ($value); - - return $this; - } - /** - * Check password - * - * @param string $value - * @return bool - */ - public function checkPassword ($value) - { - return Hash::check ($value, $this->password); + return $this; } /** @@ -243,7 +205,7 @@ public function makeToken () return md5 (uniqid ( $rand[rand (0, 9)] . ' ' . $rand[rand (0, 9)], true)); } - + /** * Set Reset Token * diff --git a/src/Cloudoki/OaStack/Traits/User.php b/src/Cloudoki/OaStack/Traits/User.php new file mode 100644 index 0000000..0d2ae62 --- /dev/null +++ b/src/Cloudoki/OaStack/Traits/User.php @@ -0,0 +1,75 @@ +first(); + } + + /** + * Acces Token relationship + * + * @return hasMany + */ + public function oauth2accesstokens () + { + return $this->hasMany('Cloudoki\OaStack\Models\Oauth2AccessToken'); + } + + /** + * Authorisations relationship + * + * @return hasMany + */ + public function oauth2authorizations () + { + return $this->hasMany('Cloudoki\OaStack\Models\Oauth2Authorization'); + } + + /** + * Clients relationship + * + * @return hasMany + */ + public function oauth2clients () + { + return $this->hasMany('Cloudoki\OaStack\Models\Oauth2Client'); + } + + /** + * Check password + * + * @param string $value + * @return bool + */ + public function checkPassword ($value) + { + return Hash::check ($value, $this->password); + } + + /** + * Return an object with the most important user properties + * which will be used for the view templates. + * The object must contain at least the following properties: + * - id + * - email + * - firstname + * - lastname + * - fullname + */ + public function getViewPresenter () { + $user = $this->schema ('basic'); + $user->fullname = $user->firstname . ' ' . $user->lastname; + + return $user; + } +} \ No newline at end of file diff --git a/src/config/oastack.php b/src/config/oastack.php index c36222d..329f7c6 100755 --- a/src/config/oastack.php +++ b/src/config/oastack.php @@ -1,7 +1,7 @@ 'http://localhost/oauth2/invitation', - 'reset_url' => 'http://localhost/oauth2/reset', - 'privacy_url' => 'http://en.wikipedia.org/wiki/Privacy_policy', - /* - * The URL to which users should be redirected after resetting their password - */ + */ + 'invite_url' => env('OASTACK_INVITE_URL', 'http://localhost/oauth2/invitation'), + 'reset_url' => env('OASTACK_RESET_URL', 'http://localhost/oauth2/reset'), + 'privacy_url' => env('OASTACK_PRIVACY_URL', 'http://en.wikipedia.org/wiki/Privacy_policy'), + // Optional. A job dispatcher class with a static `dispatch` method. + 'job_dispatcher' => env('OASTACK_JOB_DISPATCHER', null), + // Optional. The `user` model of the base application. + // The user model must use the provided Traits\User trait. + 'user_model' => env('OASTACK_USER_MODEL', null), + // The URL to which users should be redirected after resetting their password 'redirect_url' => '' ); diff --git a/src/migrations/2016_03_01_094650_oastack_create_oauth_access_tokens_table.php b/src/migrations/2016_03_01_094650_oastack_create_oauth_access_tokens_table.php index 3437d81..9b423c0 100755 --- a/src/migrations/2016_03_01_094650_oastack_create_oauth_access_tokens_table.php +++ b/src/migrations/2016_03_01_094650_oastack_create_oauth_access_tokens_table.php @@ -13,13 +13,13 @@ class OastackCreateOauthAccessTokensTable extends Migration { public function up() { if (!Schema::hasTable('oauth_access_tokens')) - + Schema::create ('oauth_access_tokens', function (Blueprint $table) { $table->increments ('id'); - $table->string ('access_token', 40); + $table->string ('access_token', 40)->unique(); $table->string ('client_id', 80); - $table->integer ('user_id'); + $table->integer ('user_id')->index(); $table->timestamp ('expires'); $table->string ('scope', 80)->nullable (); });