From dc6a956391aadbbf3398ad6476a0e934c8b2ccf3 Mon Sep 17 00:00:00 2001 From: Devon Weller Date: Sat, 5 Sep 2015 10:19:48 -0500 Subject: [PATCH] initial commit --- .gitignore | 6 + .travis.yml | 5 + LICENSE | 21 ++ README.md | 33 ++++ composer.json | 23 +++ config/tokenlyaccounts.php | 13 ++ examples/controllers/AccountController.php | 183 ++++++++++++++++++ examples/routes.php | 17 ++ examples/views/authorization-failed.blade.php | 20 ++ examples/views/layouts/base.blade.php | 18 ++ examples/views/loggedout.blade.php | 5 + examples/views/login.blade.php | 15 ++ examples/views/sync-failed.blade.php | 20 ++ examples/views/sync.blade.php | 40 ++++ examples/views/welcome.blade.php | 19 ++ phpunit.xml.dist | 7 + src/Facade/TokenlyAccounts.php | 18 ++ .../TokenlyAccountsServiceProvider.php | 41 ++++ .../TokenlyAccountsSocialiteManager.php | 55 ++++++ src/Socialite/Two/TokenlyAccountsProvider.php | 107 ++++++++++ src/TokenlyAccounts.php | 35 ++++ test/base/TestCase.php | 86 ++++++++ test/bootstrap.php | 9 + test/tests/SampleTest.php | 20 ++ 24 files changed, 816 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 config/tokenlyaccounts.php create mode 100644 examples/controllers/AccountController.php create mode 100644 examples/routes.php create mode 100644 examples/views/authorization-failed.blade.php create mode 100644 examples/views/layouts/base.blade.php create mode 100644 examples/views/loggedout.blade.php create mode 100644 examples/views/login.blade.php create mode 100644 examples/views/sync-failed.blade.php create mode 100644 examples/views/sync.blade.php create mode 100644 examples/views/welcome.blade.php create mode 100644 phpunit.xml.dist create mode 100644 src/Facade/TokenlyAccounts.php create mode 100644 src/Provider/TokenlyAccountsServiceProvider.php create mode 100644 src/Socialite/TokenlyAccountsSocialiteManager.php create mode 100644 src/Socialite/Two/TokenlyAccountsProvider.php create mode 100644 src/TokenlyAccounts.php create mode 100644 test/base/TestCase.php create mode 100644 test/bootstrap.php create mode 100644 test/tests/SampleTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a97110f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +var +vendor +composer.phar +phpunit.xml +composer.lock +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b0bffe2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: php +php: + - "5.5" +before_script: + - composer install diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c65bb34 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Devon Weller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea3a8a8 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +A Laravel package for applications that wish to use Tokenly Accounts for user authentication. + +# Installation + + +### Add the Laravel package via composer + +composer require tokenly/accounts-client + + + +### Add Service Provider + +Add the following to the `providers` array in your application config: + +`Tokenly\AccountsClient\Provider\TokenlyAccountsServiceProvider::class` + + + +### Publish and set the config + +`artisan vendor:publish --provider="Tokenly\AccountsClient\Provider\TokenlyAccountsServiceProvider"` + +Then edit the file at `config/tokenlyaccounts.php`. + +You will need a client id and client secret generated by Tokenly Accounts. + + + +### Configure your controller, routes and views + +See the example [controller](examples/controllers/AccountController.php), [views](examples/view) and [routes](examples/routes.php). + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b32110c --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "tokenly/accounts-client", + "description": "A Laravel package for applications that wish to use Tokenly Accounts for user authentication.", + "keywords": ["laravel","tokenly","accounts","oauth"], + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Devon Weller", + "email": "devon@tokenly.com", + "homepage": "http://tokenly.com" + } + ], + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "~4" + }, + "autoload": { + "psr-4": {"Tokenly\\AccountsClient\\": "src/"} + } +} diff --git a/config/tokenlyaccounts.php b/config/tokenlyaccounts.php new file mode 100644 index 0000000..30490fb --- /dev/null +++ b/config/tokenlyaccounts.php @@ -0,0 +1,13 @@ + 'YOUR_TOKENLY_ACCOUNTS_CLIENT_ID_HERE', + 'client_secret' => 'YOUR_TOKENLY_ACCOUNTS_CLIENT_SECRET_HERE', + + // this is the URL that Tokenly Accounts uses to redirect the user back to your application + 'redirect' => 'https://YourSiteHere.com/account/authorize/callback', + + // this is the Tokenly Accounts URL + 'base_url' => 'https://accounts.tokenly.com', +]; diff --git a/examples/controllers/AccountController.php b/examples/controllers/AccountController.php new file mode 100644 index 0000000..1db44f3 --- /dev/null +++ b/examples/controllers/AccountController.php @@ -0,0 +1,183 @@ + $user]); + } + + + + /** + * Login or redirect + */ + public function login() { + // if the user is already signed in, go straight to the welcome page + $user = Auth::user(); + if ($user) { return redirect('/account/welcome'); } + + return view('account.login', ['user' => $user]); + } + + + /** + * Logout + */ + public function logout() { + Auth::logout(); + return view('account.loggedout', []); + } + + + /** + * Redirect the user to Tokenly Accounts to get authorization + */ + public function redirectToProvider() + { + return Socialite::redirect(); + } + + + + /** + * Obtain the user information from Accounts. + * + * This is the route called after Tokenly Accounts has granted (or denied) permission to this application + * This application is now responsible for loading the user information from Tokenly Accounts and storing + * it in the local user database. + * + * @return Response + */ + public function handleProviderCallback(Request $request) + { + + try { + // check for an error returned from Tokenly Accounts + $error_description = TokenlyAccounts::checkForError($request); + if ($error_description) { + return view('authorization-failed', ['error_msg' => $error_description]); + } + + + // retrieve the user from Tokenly Accounts + $oauth_user = Socialite::user(); + + // get all the properties from the oAuth user object + $tokenly_uuid = $oauth_user->id; + $oauth_token = $oauth_user->token; + $username = $oauth_user->user['username']; + $name = $oauth_user->user['name']; + $email = $oauth_user->user['email']; + $email_is_confirmed = $oauth_user->user['email_is_confirmed']; + + // find an existing user based on the credentials provided + $existing_user = User::where('tokenly_uuid', $tokenly_uuid); + + // if an existing user wasn't found, we might need to find a user to merge into + $mergable_user = ($existing_user ? null : User::where('username', $username)->where('tokenly_uuid', null)); + + if ($existing_user) { + // update the user + $existing_user->update(['oauth_token' => $oauth_token, 'name' => $name, 'email' => $email, /* etc */ ]); + + // login + Auth::login($existing_user); + } else if ($mergable_user) { + // an existing user was found with a matching username + // migrate it to the tokenly accounts control + + if ($mergable_user['tokenly_uuid']) { + throw new Exception("Can't merge a user already associated with a different tokenly account", 1); + } + + // update if needed + $mergable_user->update(['name' => $name, 'email' => $email, /* etc */ ]); + + // login + Auth::login($mergable_user); + + } else { + // no user was found - create a new user based on the information we received + $new_user = User::create(['tokenly_uuid' => $tokenly_uuid, 'oauth_token' => $oauth_token, 'name' => $name, 'username' => $username, 'email' => $email, /* etc */ ]); + + // login + Auth::login($mergable_user); + } + + + return redirect('/account/login'); + + } catch (Exception $e) { + // some unexpected error happened + return view('authorization-failed', ['error_msg' => 'Failed to authenticate this user.']); + } + } + + + + /** + * Obtain the user information from Tokenly Accounts. + * + * And sync it with our local database + * + * @return Response + */ + public function sync(Request $request) + { + + try { + $logged_in_user = Auth::user(); + + $oauth_user = null; + if ($logged_in_user['oauth_token']) { + $oauth_user = Socialite::getUserByExistingToken($logged_in_user['oauth_token']); + } + + if ($oauth_user) { + $tokenly_uuid = $oauth_user->id; + $oauth_token = $oauth_user->token; + $username = $oauth_user->user['username']; + $name = $oauth_user->user['name']; + $email = $oauth_user->user['email']; + $email_is_confirmed = $oauth_user->user['email_is_confirmed']; + + // find an existing user based on the credentials provided + $existing_user = User::where('tokenly_uuid', $tokenly_uuid); + if ($existing_user) { + // update + $existing_user->update(['name' => $name, 'email' => $email, /* etc */ ]); + } + + $synced = true; + } else { + // not able to sync this user + $synced = false; + } + + return view('account.sync', ['synced' => $synced, 'user' => $logged_in_user, ]); + + } catch (Exception $e) { + return view('sync-failed', ['error_msg' => 'Failed to sync this user.']); + } + } + +} diff --git a/examples/routes.php b/examples/routes.php new file mode 100644 index 0000000..bda5533 --- /dev/null +++ b/examples/routes.php @@ -0,0 +1,17 @@ +get('/account/welcome', 'Account\AccountController@welcome'); + +// routes for logging in and logging out +$router->get('/account/login', 'Account\AccountController@login'); +$router->get('/account/logout', 'Account\AccountController@logout'); + +// This is a route to sync the user with their Tokenly Accounts information +// Redirect the user here to update their local user information with their Tokenly Accounts information +$router->get('/account/sync', 'Account\AccountController@sync'); + +// oAuth handlers +$router->get('/account/authorize', 'Account\AccountController@redirectToProvider'); +$router->get('/account/authorize/callback', 'Account\AccountController@handleProviderCallback'); + diff --git a/examples/views/authorization-failed.blade.php b/examples/views/authorization-failed.blade.php new file mode 100644 index 0000000..ef7424b --- /dev/null +++ b/examples/views/authorization-failed.blade.php @@ -0,0 +1,20 @@ +@extends('layouts.base') + +@section('content') +
+
+
+
+

This login was not successful.

+

{{$error_msg}}

+
+ +
+

Sorry about that.

+ +
+

Try Again ?

+
+
+
+@stop diff --git a/examples/views/layouts/base.blade.php b/examples/views/layouts/base.blade.php new file mode 100644 index 0000000..71d9c46 --- /dev/null +++ b/examples/views/layouts/base.blade.php @@ -0,0 +1,18 @@ + + + + + My App + + + + + + + +
+ @yield('content') +
+ + + diff --git a/examples/views/loggedout.blade.php b/examples/views/loggedout.blade.php new file mode 100644 index 0000000..f10b75a --- /dev/null +++ b/examples/views/loggedout.blade.php @@ -0,0 +1,5 @@ +@extends('layouts.base') + +@section('content') +

You are logged out.

+@stop diff --git a/examples/views/login.blade.php b/examples/views/login.blade.php new file mode 100644 index 0000000..b57f634 --- /dev/null +++ b/examples/views/login.blade.php @@ -0,0 +1,15 @@ +@extends('layouts.base') + +@section('content') +
+
+
+

Login or Register

+ +

You are not logged in yet.

+ + Login or Register Now +
+
+
+@stop diff --git a/examples/views/sync-failed.blade.php b/examples/views/sync-failed.blade.php new file mode 100644 index 0000000..f18ba8b --- /dev/null +++ b/examples/views/sync-failed.blade.php @@ -0,0 +1,20 @@ +@extends('layouts.base') + +@section('content') +
+
+
+
+

This sync attempt was not successful.

+

{{$error_msg}}

+
+ +
+

Sorry about that.

+ +
+

Try Again ?

+
+
+
+@stop diff --git a/examples/views/sync.blade.php b/examples/views/sync.blade.php new file mode 100644 index 0000000..b9ee4cd --- /dev/null +++ b/examples/views/sync.blade.php @@ -0,0 +1,40 @@ +@extends('layouts.base') + + +@section('content') +
+
+
+

Hello {{$user['name']}}

+ +
+ + @if ($synced) +

Your account settings are now up to date with your Tokenly Account.

+ +

To make changes to your account, please edit your Tokenly Account and then Sync your account again.

+ +
+ +

+ Sync My Account + Return + +

+ @else +

Please authorize Tokenly Accounts to sync your information with this application by clicking the button below.

+ +

+ Sync My Account + Return + +

+ + @endif + +
+
+
+ + +@stop diff --git a/examples/views/welcome.blade.php b/examples/views/welcome.blade.php new file mode 100644 index 0000000..8251185 --- /dev/null +++ b/examples/views/welcome.blade.php @@ -0,0 +1,19 @@ +@extends('layouts.base') + +@section('content') +
+
+
+

Hello {{$user['name']}}

+ +
+ +

You are signed in as user {{$user['username']}}.

+ +
+ + Logout +
+
+
+@stop diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..6f5cc04 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,7 @@ + + + + test/tests + + + diff --git a/src/Facade/TokenlyAccounts.php b/src/Facade/TokenlyAccounts.php new file mode 100644 index 0000000..7890e80 --- /dev/null +++ b/src/Facade/TokenlyAccounts.php @@ -0,0 +1,18 @@ +publishes([$config_source => config_path('tokenlyaccounts.php')], 'config'); + } + + public function register() { + $this->app->bind('tokenly-accounts', function($app) { + return new TokenlyAccounts(); + }); + + $this->app->bindShared('Laravel\Socialite\Contracts\Factory', function ($app) { + return new TokenlyAccountsSocialiteManager($app); + }); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return ['Laravel\Socialite\Contracts\Factory']; + } + +} \ No newline at end of file diff --git a/src/Socialite/TokenlyAccountsSocialiteManager.php b/src/Socialite/TokenlyAccountsSocialiteManager.php new file mode 100644 index 0000000..04d0a79 --- /dev/null +++ b/src/Socialite/TokenlyAccountsSocialiteManager.php @@ -0,0 +1,55 @@ +app['config']['tokenlyaccounts']; + + return $this->buildProvider( + 'Tokenly\AccountsClient\Socialite\Two\TokenlyAccountsProvider', $config + ); + } + + /** + * Get the default driver name. + * + * @throws \InvalidArgumentException + * + * @return string + */ + public function getDefaultDriver() + { + return 'tokenlyAccounts'; + } + + /** + * Build an OAuth 2 provider instance. + * + * @param string $provider + * @param array $config + * @return \Laravel\Socialite\Two\AbstractProvider + */ + public function buildProvider($provider, $config) + { + $provider = new $provider( + $this->app['request'], $config['client_id'], + $config['client_secret'], $config['redirect'] + ); + + $provider->setBaseURL($config['base_url']); + + return $provider; + } + +} diff --git a/src/Socialite/Two/TokenlyAccountsProvider.php b/src/Socialite/Two/TokenlyAccountsProvider.php new file mode 100644 index 0000000..2804630 --- /dev/null +++ b/src/Socialite/Two/TokenlyAccountsProvider.php @@ -0,0 +1,107 @@ +base_url = $base_url; + } + + + /** + * {@inheritdoc} + */ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase($this->base_url.'/oauth/authorize', $state); + } + + /** + * {@inheritdoc} + */ + protected function getTokenUrl() + { + return $this->base_url.'/oauth/access-token'; + } + + /** + * {@inheritdoc} + */ + protected function getUserByToken($token) + { + $userUrl = $this->base_url.'/oauth/user?access_token='.$token; + + $response = $this->getHttpClient()->get( + $userUrl, $this->getRequestOptions() + ); + + $user = json_decode($response->getBody(), true); + + return $user; + } + + /** + * {@inheritdoc} + */ + public function getUserByExistingToken($token) { + $user = $this->mapUserToObject($this->getUserByToken($token)); + return $user->setToken($token); + } + + + + /** + * {@inheritdoc} + */ + protected function mapUserToObject(array $user) + { + return (new User)->setRaw($user)->map([ + 'id' => $user['id'], + 'username' => $user['username'], + 'name' => array_get($user, 'name'), + 'email' => array_get($user, 'email'), + // 'avatar' => $user['avatar_url'], + ]); + } + + + /** + * Get the POST fields for the token request. + * + * @param string $code + * @return array + */ + protected function getTokenFields($code) + { + return array_add( + parent::getTokenFields($code), 'grant_type', 'authorization_code' + ); + } + + + /** + * Get the default options for an HTTP request. + * + * @return array + */ + protected function getRequestOptions() + { + return [ + ]; + } +} diff --git a/src/TokenlyAccounts.php b/src/TokenlyAccounts.php new file mode 100644 index 0000000..c1c0861 --- /dev/null +++ b/src/TokenlyAccounts.php @@ -0,0 +1,35 @@ +get('error'); + if ($error_code) { + if ($error_code == 'access_denied') { + $error_description = 'Access was denied.'; + } else { + $error_description = $request->get('error_description'); + } + return $error_description; + } + + // no error + return null; + } + +} \ No newline at end of file diff --git a/test/base/TestCase.php b/test/base/TestCase.php new file mode 100644 index 0000000..9905e6d --- /dev/null +++ b/test/base/TestCase.php @@ -0,0 +1,86 @@ +useDatabase) + { + $this->setUpDb(); + } + } + + public function setUpDb() + { + // // create an artisan object for calling migrations + // $artisan = $this->app->make('Illuminate\Contracts\Console\Kernel'); + + // // call migrations that will be part of your package, assumes your migrations are in src/migrations + // // not neccessary if your package doesn't require any migrations to be run for + // // proper installation + // $artisan->call('migrate', [ + // '--database' => 'testbench', + // '--path' => 'migrations', + // ]); + + } + + public function teardownDb() + { + // $this->app['Illuminate\Contracts\Console\Kernel']->call('migrate:reset'); + } + + + // protected function resolveApplicationConfiguration($app) { + // parent::resolveApplicationConfiguration($app); + // // $app['config']['app.log'] = 'single'; + // } + + /** + * Get package providers. + * + * @param \Illuminate\Foundation\Application $app + * @return array + */ + protected function getPackageProviders($app) + { + // return ['App\Listener\XChainListenerServiceProvider']; + return []; + } + + + /** + * Define environment setup. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function getEnvironmentSetUp($app) + { + // $app['config']['app.log'] = 'single'; + + // $app['config']->set('database.default', 'testbench'); + // $app['config']->set('database.connections.testbench', array( + // 'driver' => 'sqlite', + // 'database' => ':memory:', + // 'prefix' => '', + // )); + } + + // /** + // * Get base path. + // * + // * @return string + // */ + // protected function getBasePath() + // { + // // reset base path to point to our package's src directory + // return __DIR__.'/../..'; + // } + +} diff --git a/test/bootstrap.php b/test/bootstrap.php new file mode 100644 index 0000000..52da537 --- /dev/null +++ b/test/bootstrap.php @@ -0,0 +1,9 @@ +