fixing up the authenticator to allow identiy chaining

pull/26/head
Ondřej Hruška 7 years ago
parent 598f2f8024
commit d74b76da3e
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 76
      app/SocialLogin/Authenticator.php
  2. 28
      app/SocialLogin/EloquentIdentityStore.php
  3. 11
      app/SocialLogin/Facades/OAuth.php
  4. 9
      app/SocialLogin/IdentityStore.php
  5. 26
      app/SocialLogin/OAuthIdentity.php
  6. 37
      app/SocialLogin/OAuthManager.php
  7. 23
      app/SocialLogin/Session.php
  8. 27
      app/SocialLogin/UserStore.php
  9. 4
      composer.json
  10. 12
      composer.lock
  11. 7
      config/services.php
  12. 2
      database/migrations/2014_10_12_000000_create_users_table.php
  13. 10
      resources/views/auth/login.blade.php
  14. 58
      routes/web.php
  15. 3
      sideload/adamwathan/eloquent-oauth-l5/src/EloquentOAuthServiceProvider.php
  16. 55
      sideload/adamwathan/eloquent-oauth/src/Authenticator.php
  17. 42
      sideload/adamwathan/eloquent-oauth/src/EloquentIdentityStore.php
  18. 12
      sideload/adamwathan/eloquent-oauth/src/IdentityStore.php
  19. 13
      sideload/adamwathan/eloquent-oauth/src/OAuthIdentity.php
  20. 18
      sideload/adamwathan/eloquent-oauth/src/OAuthManager.php
  21. 13
      sideload/adamwathan/eloquent-oauth/src/Session.php
  22. 22
      sideload/adamwathan/eloquent-oauth/src/UserStore.php
  23. 1
      sideload/socialnorm/facebook/src/FacebookProvider.php
  24. 4
      sideload/socialnorm/google/src/GoogleProvider.php
  25. 4
      sideload/socialnorm/socialnorm/src/Provider.php
  26. 7
      sideload/socialnorm/socialnorm/src/ProviderRegistry.php
  27. 8
      sideload/socialnorm/socialnorm/src/ProviderUser.php
  28. 10
      sideload/socialnorm/socialnorm/src/Providers/OAuth2Provider.php
  29. 12
      sideload/socialnorm/socialnorm/src/SocialNorm.php
  30. 2
      sideload/socialnorm/socialnorm/src/StateGenerator.php
  31. 4
      sideload/socialnorm/stackoverflow/.gitignore
  32. 11
      sideload/socialnorm/stackoverflow/.travis.yml
  33. 29
      sideload/socialnorm/stackoverflow/composer.json
  34. 18
      sideload/socialnorm/stackoverflow/phpunit.xml
  35. 3
      sideload/socialnorm/stackoverflow/readme.md
  36. 100
      sideload/socialnorm/stackoverflow/src/StackOverflowProvider.php
  37. 0
      sideload/socialnorm/stackoverflow/tests/.gitkeep
  38. 75
      sideload/socialnorm/stackoverflow/tests/GoogleProviderTest.php
  39. 12
      sideload/socialnorm/stackoverflow/tests/TestCase.php
  40. 24
      sideload/socialnorm/stackoverflow/tests/_fixtures/google_accesstoken.php
  41. 23
      sideload/socialnorm/stackoverflow/tests/_fixtures/google_user.php

@ -1,76 +0,0 @@
<?php namespace AdamWathan\EloquentOAuth;
class Authenticator
{
protected $auth;
protected $users;
protected $identities;
public function __construct($auth, $users, $identities)
{
$this->auth = $auth;
$this->users = $users;
$this->identities = $identities;
}
public function login($providerAlias, $userDetails, $callback = null, $remember = false)
{
$user = $this->getUser($providerAlias, $userDetails);
$user = $this->runCallback($callback, $user, $userDetails);
$this->updateUser($user, $providerAlias, $userDetails);
$this->auth->login($user, $remember);
}
protected function getUser($providerAlias, $details)
{
if ($this->identities->userExists($providerAlias, $details)) {
return $this->getExistingUser($providerAlias, $details);
}
return $this->users->create();
}
protected function runCallback($callback, $user, $userDetails)
{
$callback = $callback ?: function () {};
$callbackUser = $callback($user, $userDetails);
return $callbackUser ?: $user;
}
protected function updateUser($user, $providerAlias, $details)
{
$this->users->store($user);
$this->storeProviderIdentity($user, $providerAlias, $details);
}
protected function getExistingUser($providerAlias, $details)
{
$identity = $this->identities->getByProvider($providerAlias, $details);
return $this->users->findByIdentity($identity);
}
protected function storeProviderIdentity($user, $providerAlias, $details)
{
if ($this->identities->userExists($providerAlias, $details)) {
$this->updateProviderIdentity($providerAlias, $details);
} else {
$this->addProviderIdentity($user, $providerAlias, $details);
}
}
protected function updateProviderIdentity($providerAlias, $details)
{
$identity = $this->identities->getByProvider($providerAlias, $details);
$identity->access_token = $details->access_token;
$this->identities->store($identity);
}
protected function addProviderIdentity($user, $providerAlias, $details)
{
$identity = new OAuthIdentity;
$identity->user_id = $user->getKey();
$identity->provider = $providerAlias;
$identity->provider_user_id = $details->id;
$identity->access_token = $details->access_token;
$this->identities->store($identity);
}
}

@ -1,28 +0,0 @@
<?php namespace AdamWathan\EloquentOAuth;
class EloquentIdentityStore implements IdentityStore
{
public function getByProvider($provider, $providerUser)
{
return OAuthIdentity::where('provider', $provider)
->where('provider_user_id', $providerUser->id)
->first();
}
public function flush($user, $provider)
{
OAuthIdentity::where('user_id', $user->getKey())
->where('provider', $provider)
->delete();
}
public function store($identity)
{
$identity->save();
}
public function userExists($provider, $providerUser)
{
return (bool) $this->getByProvider($provider, $providerUser);
}
}

@ -1,11 +0,0 @@
<?php namespace AdamWathan\EloquentOAuth\Facades;
use Illuminate\Support\Facades\Facade;
class OAuth extends Facade
{
protected static function getFacadeAccessor()
{
return 'adamwathan.oauth';
}
}

@ -1,9 +0,0 @@
<?php namespace AdamWathan\EloquentOAuth;
interface IdentityStore
{
public function getByProvider($provider, $providerUser);
public function flush($user, $provider);
public function store($identity);
public function userExists($provider, $providerUser);
}

@ -1,26 +0,0 @@
<?php namespace AdamWathan\EloquentOAuth;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Config;
/**
* @property $id
* @property $user_id
* @property $provider
* @property $provider_user_id
* @property $access_token
*/
class OAuthIdentity extends Eloquent
{
protected static $configuredTable = 'oauth_identities';
public static function configureTable($table)
{
static::$configuredTable = $table;
}
public function getTable()
{
return static::$configuredTable;
}
}

@ -1,37 +0,0 @@
<?php namespace AdamWathan\EloquentOAuth;
class OAuthManager
{
protected $redirect;
protected $authenticator;
protected $socialnorm;
public function __construct($redirect, $authenticator, $socialnorm)
{
$this->redirect = $redirect;
$this->authenticator = $authenticator;
$this->socialnorm = $socialnorm;
}
public function authorize($providerAlias)
{
return $this->redirect->to($this->socialnorm->authorize($providerAlias));
}
public function login($providerAlias, $callback = null)
{
$details = $this->socialnorm->getUser($providerAlias);
return $this->authenticator->login($providerAlias, $details, $callback, $remember = false);
}
public function loginForever($providerAlias, $callback = null)
{
$details = $this->socialnorm->getUser($providerAlias);
return $this->authenticator->login($providerAlias, $details, $callback, $remember = true);
}
public function registerProvider($alias, $provider)
{
$this->socialnorm->registerProvider($alias, $provider);
}
}

@ -1,23 +0,0 @@
<?php namespace AdamWathan\EloquentOAuth;
use SocialNorm\Session as SocialNormSession;
class Session implements SocialNormSession
{
private $store;
public function __construct($store)
{
$this->store = $store;
}
public function get($key)
{
return $this->store->get($key);
}
public function put($key, $value)
{
return $this->store->put($key, $value);
}
}

@ -1,27 +0,0 @@
<?php namespace AdamWathan\EloquentOAuth;
class UserStore
{
protected $model;
public function __construct($model)
{
$this->model = $model;
}
public function create()
{
$user = new $this->model;
return $user;
}
public function store($user)
{
return $user->save();
}
public function findByIdentity($identity)
{
return $identity->belongsTo($this->model, 'user_id')->firstOrFail();
}
}

@ -15,7 +15,8 @@
"laravel/framework": "5.6.*", "laravel/framework": "5.6.*",
"laravel/socialite": "^3.0", "laravel/socialite": "^3.0",
"laravel/tinker": "^1.0", "laravel/tinker": "^1.0",
"phpoffice/phpspreadsheet": "^1.3" "phpoffice/phpspreadsheet": "^1.3",
"guzzlehttp/guzzle": "^6.0"
}, },
"require-dev": { "require-dev": {
"filp/whoops": "^2.0", "filp/whoops": "^2.0",
@ -37,6 +38,7 @@
"SocialNorm\\Facebook\\": "sideload/socialnorm/facebook/src", "SocialNorm\\Facebook\\": "sideload/socialnorm/facebook/src",
"SocialNorm\\GitHub\\": "sideload/socialnorm/github/src", "SocialNorm\\GitHub\\": "sideload/socialnorm/github/src",
"SocialNorm\\Google\\": "sideload/socialnorm/google/src", "SocialNorm\\Google\\": "sideload/socialnorm/google/src",
"SocialNorm\\StackOverflow\\": "sideload/socialnorm/stackoverflow/src",
"SocialNorm\\": "sideload/socialnorm/socialnorm/src" "SocialNorm\\": "sideload/socialnorm/socialnorm/src"
}, },
"files": [ "files": [

12
composer.lock generated

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "44870bae9f6db81ba961bf49ae2f64ad", "content-hash": "cdb2e12195da014433178600b1a1899f",
"packages": [ "packages": [
{ {
"name": "barryvdh/laravel-ide-helper", "name": "barryvdh/laravel-ide-helper",
@ -1115,16 +1115,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v5.6.26", "version": "v5.6.27",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "7047df295e77cecb6a2f84736a732af66cc6789c" "reference": "2fe661f2444410a576aa40054ad9b7fe0bb5cee5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/7047df295e77cecb6a2f84736a732af66cc6789c", "url": "https://api.github.com/repos/laravel/framework/zipball/2fe661f2444410a576aa40054ad9b7fe0bb5cee5",
"reference": "7047df295e77cecb6a2f84736a732af66cc6789c", "reference": "2fe661f2444410a576aa40054ad9b7fe0bb5cee5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1250,7 +1250,7 @@
"framework", "framework",
"laravel" "laravel"
], ],
"time": "2018-06-20T14:21:11+00:00" "time": "2018-07-10T13:47:01+00:00"
}, },
{ {
"name": "laravel/socialite", "name": "laravel/socialite",

@ -54,5 +54,12 @@ return [
'redirect_uri' => env('OAUTH_GITHUB_REDIRECT'), 'redirect_uri' => env('OAUTH_GITHUB_REDIRECT'),
'scope' => [], 'scope' => [],
], ],
'stack' => [
'client_id' => env('OAUTH_STACK_ID'),
'client_secret' => env('OAUTH_STACK_SECRET'),
'redirect_uri' => env('OAUTH_STACK_REDIRECT'),
'key' => env('OAUTH_STACK_KEY'),
'scope' => [],
],
], ],
]; ];

@ -17,7 +17,7 @@ class CreateUsersTable extends Migration
$table->increments('id'); $table->increments('id');
$table->timestamps(); $table->timestamps();
$table->string('name'); $table->string('name');
$table->string('email')->unique(); $table->string('email')->unique()->nullable();
$table->string('password')->nullable(); $table->string('password')->nullable();
$table->rememberToken(); $table->rememberToken();
}); });

@ -65,15 +65,21 @@
<div class="card-footer bg-white"> <div class="card-footer bg-white">
<div class="form-group row mb-0"> <div class="form-group row mb-0">
<span class="col-md-4 col-form-label text-md-right">{{ __('Log in with') }}</span> <span class="col-md-2 col-form-label text-md-right">{{ __('Log in with') }}</span>
<div class="col-md-6"> <div class="col-md-8">
@set('services.oauth_providers.github.client_id') @set('services.oauth_providers.github.client_id')
<a type="submit" href="{{route('oauth-github-authorize')}}" class="btn btn-dark"> <a type="submit" href="{{route('oauth-github-authorize')}}" class="btn btn-dark">
{{ __('GitHub') }} {{ __('GitHub') }}
</a> </a>
@endset @endset
@set('services.oauth_providers.stack.client_id')
<a type="submit" href="{{route('oauth-stack-authorize')}}" class="btn btn-dark">
{{ __('StackOverflow') }}
</a>
@endset
@set('services.oauth_providers.google.client_id') @set('services.oauth_providers.google.client_id')
<a type="submit" href="{{route('oauth-google-authorize')}}" class="btn btn-dark"> <a type="submit" href="{{route('oauth-google-authorize')}}" class="btn btn-dark">
{{ __('Google') }} {{ __('Google') }}

@ -1,6 +1,7 @@
<?php <?php
use SocialNorm\Exceptions\ApplicationRejectedException; use SocialNorm\Exceptions\ApplicationRejectedException;
use SocialNorm\Exceptions\InvalidAuthorizationCodeException; use SocialNorm\Exceptions\InvalidAuthorizationCodeException;
use SocialNorm\ProviderUser;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -36,21 +37,35 @@ Route::get('/home', 'HomeController@index')->name('home');
// ----------------- SOCIAL LOGIN -------------------- // ----------------- SOCIAL LOGIN --------------------
Route::get('/auth/github/authorize', function() { function _loginVia($method) {
return SocialAuth::authorize('github');
})->name('oauth-github-authorize');
Route::get('/auth/github/callback', function() {
try { try {
SocialAuth::login('github'); SocialAuth::login($method, function (\App\Models\User $user, ProviderUser $details) {
// update user name first time user logs in
if (!$user->exists) {
$user->name = $details->nickname ?: ($details->full_name ?: $details->email);
}
// set e-mail from provider data, only if user e-mail is empty
if ("$user->email" === "") {
$user->email = $details->email;
}
});
} catch (ApplicationRejectedException $e) { } catch (ApplicationRejectedException $e) {
abort(401, $e->getMessage()); abort(401, $e->getMessage());
} catch (InvalidAuthorizationCodeException $e) { } catch (InvalidAuthorizationCodeException $e) {
abort(401, $e->getMessage()); abort(401, $e->getMessage());
} }
return Redirect::intended(); return Redirect::intended();
})->name('oauth-github-callback'); };
Route::get('/auth/github/authorize', function() {
return SocialAuth::authorize('github');
})->name('oauth-github-authorize');
Route::get('/auth/github/callback', function() {
return _loginVia('github');
})->name('oauth-github-callback');
Route::get('/auth/google/authorize', function() { Route::get('/auth/google/authorize', function() {
@ -58,30 +73,23 @@ Route::get('/auth/google/authorize', function() {
})->name('oauth-google-authorize'); })->name('oauth-google-authorize');
Route::get('/auth/google/callback', function() { Route::get('/auth/google/callback', function() {
try { return _loginVia('google');
SocialAuth::login('google');
} catch (ApplicationRejectedException $e) {
abort(401);
} catch (InvalidAuthorizationCodeException $e) {
abort(401);
}
return Redirect::intended();
})->name('oauth-google-callback'); })->name('oauth-google-callback');
Route::get('/auth/facebook/authorize', function() { Route::get('/auth/facebook/authorize', function() {
return SocialAuth::authorize('facebook'); return SocialAuth::authorize('facebook');
})->name('oauth-facebook-authorize'); })->name('oauth-facebook-authorize');
Route::get('/auth/facebook/callback', function() { Route::get('/auth/facebook/callback', function() {
try { return _loginVia('facebook');
SocialAuth::login('facebook');
} catch (ApplicationRejectedException $e) {
abort(401);
} catch (InvalidAuthorizationCodeException $e) {
abort(401);
}
return Redirect::intended();
})->name('oauth-facebook-callback'); })->name('oauth-facebook-callback');
Route::get('/auth/stack/authorize', function() {
return SocialAuth::authorize('stack');
})->name('oauth-stack-authorize');
Route::get('/auth/stack/callback', function() {
return _loginVia('stack');
})->name('oauth-stack-callback');

@ -20,6 +20,7 @@ class EloquentOAuthServiceProvider extends ServiceProvider {
'facebook' => 'SocialNorm\Facebook\FacebookProvider', 'facebook' => 'SocialNorm\Facebook\FacebookProvider',
'github' => 'SocialNorm\GitHub\GitHubProvider', 'github' => 'SocialNorm\GitHub\GitHubProvider',
'google' => 'SocialNorm\Google\GoogleProvider', 'google' => 'SocialNorm\Google\GoogleProvider',
'stack' => 'SocialNorm\StackOverflow\StackOverflowProvider',
]; ];
/** /**
@ -74,7 +75,7 @@ class EloquentOAuthServiceProvider extends ServiceProvider {
}); });
} }
protected function registerProviders($socialnorm, $request) protected function registerProviders(SocialNorm $socialnorm, $request)
{ {
if (! $providerAliases = $this->app['config']['services.oauth_providers']) { if (! $providerAliases = $this->app['config']['services.oauth_providers']) {
return; return;

@ -1,77 +1,92 @@
<?php namespace AdamWathan\EloquentOAuth; <?php namespace AdamWathan\EloquentOAuth;
use SocialNorm\User; use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use SocialNorm\ProviderUser;
class Authenticator class Authenticator
{ {
protected $auth; /** @var \Illuminate\Auth\SessionGuard */
protected $guard;
/** @var UserStore */
protected $users; protected $users;
/** @var IdentityStore */
protected $identities; protected $identities;
public function __construct($auth, $users, $identities) public function __construct(\Illuminate\Auth\SessionGuard $auth, UserStore $users, IdentityStore $identities)
{ {
$this->auth = $auth; $this->guard = $auth;
$this->users = $users; $this->users = $users;
$this->identities = $identities; $this->identities = $identities;
} }
public function login($providerAlias, $userDetails, $callback = null) public function login($providerAlias, ProviderUser $userDetails, $callback = null)
{ {
/** @var Model|Authenticatable $user */
$user = $this->getUser($providerAlias, $userDetails); $user = $this->getUser($providerAlias, $userDetails);
$user = $this->runCallback($callback, $user, $userDetails); $user = $this->runCallback($callback, $user, $userDetails);
$this->updateUser($user, $providerAlias, $userDetails); $this->updateUser($user, $providerAlias, $userDetails);
$this->auth->login($user); $this->guard->login($user);
} }
protected function getUser($providerAlias, \SocialNorm\User $details) protected function getUser($providerAlias, ProviderUser $details) // returns User
{ {
if ($this->identities->userExists($providerAlias, $details)) { if ($this->identities->userExists($providerAlias, $details, false)) {
return $this->getExistingUser($providerAlias, $details); return $this->getExistingUser($providerAlias, $details);
} }
/** @var \App\Models\User $newuser */ // this is potentially unsafe - allows user to attach a social identity to an
$newuser = $this->users->create(); // existing account just my matching the e-mail.
$newuser->name = $details->nickname ?: ($details->full_name ?: $details->email);
$newuser->email = $details->email; // we have to assume the providers are trustworthy.
return $newuser; if ($details->email && $this->identities->userExists($providerAlias, $details, true)) {
return $this->users->findByEmail($details->email);
}
return $this->users->create();
} }
protected function runCallback($callback, $user, $userDetails) protected function runCallback($callback, Model $user, ProviderUser $userDetails)
{ {
$callback = $callback ?: function () {}; $callback = $callback ?: function () {};
$callbackUser = $callback($user, $userDetails); $callbackUser = $callback($user, $userDetails);
return $callbackUser ?: $user; return $callbackUser ?: $user;
} }
protected function updateUser($user, $providerAlias, $details) protected function updateUser(Model $user, $providerAlias, ProviderUser $details)
{ {
$this->users->store($user); $this->users->store($user);
$this->storeProviderIdentity($user, $providerAlias, $details); $this->storeProviderIdentity($user, $providerAlias, $details);
} }
protected function getExistingUser($providerAlias, $details) protected function getExistingUser($providerAlias, ProviderUser $details)
{ {
$identity = $this->identities->getByProvider($providerAlias, $details); $identity = $this->identities->getByProvider($providerAlias, $details);
return $this->users->findByIdentity($identity); return $this->users->findByIdentity($identity);
} }
protected function storeProviderIdentity($user, $providerAlias, $details) protected function storeProviderIdentity(Model $user, string $providerAlias, ProviderUser $details)
{ {
if ($this->identities->userExists($providerAlias, $details)) { if ($this->identities->userExists($providerAlias, $details, false)) {
$this->updateProviderIdentity($providerAlias, $details); $this->updateProviderIdentity($providerAlias, $details);
} else { } else {
$this->addProviderIdentity($user, $providerAlias, $details); $this->addProviderIdentity($user, $providerAlias, $details);
} }
} }
protected function updateProviderIdentity($providerAlias, $details) protected function updateProviderIdentity(string $providerAlias, ProviderUser $details)
{ {
$identity = $this->identities->getByProvider($providerAlias, $details); $identity = $this->identities->getByProvider($providerAlias, $details);
$identity->access_token = $details->access_token; $identity->access_token = $details->access_token;
$this->identities->store($identity); $this->identities->store($identity);
} }
protected function addProviderIdentity($user, $providerAlias, $details) /**
* @param Model $user
* @param $providerAlias
* @param $details
*/
protected function addProviderIdentity(Model $user, string $providerAlias, ProviderUser $details)
{ {
$identity = new OAuthIdentity; $identity = new OAuthIdentity;
$identity->user_id = $user->getKey(); $identity->user_id = $user->getKey();

@ -1,28 +1,58 @@
<?php namespace AdamWathan\EloquentOAuth; <?php
namespace AdamWathan\EloquentOAuth;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use SocialNorm\ProviderUser;
class EloquentIdentityStore implements IdentityStore class EloquentIdentityStore implements IdentityStore
{ {
public function getByProvider($provider, $providerUser) public function getByProvider(string $provider, ProviderUser $providerUser)
{ {
return OAuthIdentity::where('provider', $provider) return OAuthIdentity::where('provider', $provider)
->where('provider_user_id', $providerUser->id) ->where('provider_user_id', $providerUser->id)
->first(); ->first();
} }
public function flush($user, $provider) /**
* Try to retrieve user by their e-mail
* (!!! this is coupled to the application code !!!)
*
* @param ProviderUser $providerUser
* @return User|Model|null|object
*/
public function getByEmail(ProviderUser $providerUser)
{
$first = User::where('email', $providerUser->email)->first();
if($first->email) return $first;
return null;
}
public function flush(Model $user, string $provider)
{ {
OAuthIdentity::where('user_id', $user->getKey()) OAuthIdentity::where('user_id', $user->getKey())
->where('provider', $provider) ->where('provider', $provider)
->delete(); ->delete();
} }
public function store($identity) public function store(OAuthIdentity $identity)
{ {
$identity->save(); $identity->save();
} }
public function userExists($provider, $providerUser) public function userExists(string $provider, ProviderUser $providerUser, bool $allowByEmail)
{ {
return (bool) $this->getByProvider($provider, $providerUser); $byProvider = (bool) $this->getByProvider($provider, $providerUser);
if ($byProvider) return true;
if (!$allowByEmail) return false;
if ($providerUser->email) {
$byEmail = $this->getByEmail($providerUser);
if ($byEmail) return true;
}
return false;
} }
} }

@ -1,9 +1,13 @@
<?php namespace AdamWathan\EloquentOAuth; <?php namespace AdamWathan\EloquentOAuth;
use Illuminate\Database\Eloquent\Model;
use SocialNorm\ProviderUser;
interface IdentityStore interface IdentityStore
{ {
public function getByProvider($provider, $providerUser); public function getByProvider(string $provider, ProviderUser $providerUser);
public function flush($user, $provider); public function flush(Model $user, string $provider);
public function store($identity); public function getByEmail(ProviderUser $providerUser);
public function userExists($provider, $providerUser); public function store(OAuthIdentity $identity);
public function userExists(string $provider, ProviderUser $providerUser, bool $allowByEmail);
} }

@ -1,7 +1,6 @@
<?php namespace AdamWathan\EloquentOAuth; <?php namespace AdamWathan\EloquentOAuth;
use Illuminate\Database\Eloquent\Model as Eloquent; use Illuminate\Database\Eloquent\Model;
use Config;
/** /**
* @property $id * @property $id
@ -10,14 +9,14 @@ use Config;
* @property $provider_user_id * @property $provider_user_id
* @property $access_token * @property $access_token
*/ */
class OAuthIdentity extends Eloquent class OAuthIdentity extends Model
{ {
protected static $configuredTable = 'oauth_identities'; protected static $configuredTable = 'oauth_identities';
// public static function configureTable($table) public static function configureTable($table)
// { {
// static::$configuredTable = $table; static::$configuredTable = $table;
// } }
public function getTable() public function getTable()
{ {

@ -1,10 +1,11 @@
<?php namespace AdamWathan\EloquentOAuth; <?php namespace AdamWathan\EloquentOAuth;
use SocialNorm\Provider;
use SocialNorm\SocialNorm; use SocialNorm\SocialNorm;
class OAuthManager class OAuthManager
{ {
/** @var string */ /** @var \Illuminate\Routing\Redirector */
protected $redirect; protected $redirect;
/** @var Authenticator */ /** @var Authenticator */
@ -13,25 +14,32 @@ class OAuthManager
/** @var SocialNorm */ /** @var SocialNorm */
protected $socialnorm; protected $socialnorm;
public function __construct($redirect, $authenticator, $socialnorm) public function __construct(
\Illuminate\Routing\Redirector $redirect,
Authenticator $authenticator,
SocialNorm $socialnorm)
{ {
$this->redirect = $redirect; $this->redirect = $redirect;
$this->authenticator = $authenticator; $this->authenticator = $authenticator;
$this->socialnorm = $socialnorm; $this->socialnorm = $socialnorm;
} }
public function authorize($providerAlias) public function authorize(string $providerAlias)
{ {
return $this->redirect->to($this->socialnorm->authorize($providerAlias)); return $this->redirect->to($this->socialnorm->authorize($providerAlias));
} }
public function login($providerAlias, $callback = null) /**
* @param $providerAlias
* @param \Closure|null $callback
*/
public function login(string $providerAlias, $callback = null)
{ {
$details = $this->socialnorm->getUser($providerAlias); $details = $this->socialnorm->getUser($providerAlias);
return $this->authenticator->login($providerAlias, $details, $callback); return $this->authenticator->login($providerAlias, $details, $callback);
} }
public function registerProvider($alias, $provider) public function registerProvider(string $alias, Provider $provider)
{ {
$this->socialnorm->registerProvider($alias, $provider); $this->socialnorm->registerProvider($alias, $provider);
} }

@ -4,20 +4,29 @@ use SocialNorm\Session as SocialNormSession;
class Session implements SocialNormSession class Session implements SocialNormSession
{ {
/** @var \Illuminate\Session\SessionManager */
private $store; private $store;
public function __construct($store) public function __construct(\Illuminate\Session\SessionManager $store)
{ {
$this->store = $store; $this->store = $store;
} }
/**
* @param string $key
* @return mixed
*/
public function get($key) public function get($key)
{ {
return $this->store->get($key); return $this->store->get($key);
} }
/**
* @param string $key
* @param mixed|null $value
*/
public function put($key, $value) public function put($key, $value)
{ {
return $this->store->put($key, $value); $this->store->put($key, $value);
} }
} }

@ -1,27 +1,35 @@
<?php namespace AdamWathan\EloquentOAuth; <?php namespace AdamWathan\EloquentOAuth;
use Illuminate\Database\Eloquent\Model;
class UserStore class UserStore
{ {
protected $model; /** @var string */
protected $modelClass;
public function __construct($model) public function __construct(string $modelClass)
{ {
$this->model = $model; $this->modelClass = $modelClass;
} }
public function create() public function create()
{ {
$user = new $this->model; $user = new $this->modelClass;
return $user; return $user;
} }
public function store($user) public function store(Model $user)
{ {
return $user->save(); return $user->save();
} }
public function findByIdentity($identity) public function findByIdentity(OAuthIdentity $identity)
{
return $identity->belongsTo($this->modelClass, 'user_id')->firstOrFail();
}
public function findByEmail(string $email)
{ {
return $identity->belongsTo($this->model, 'user_id')->firstOrFail(); return $this->modelClass::where('email', $email)->firstOrFail();
} }
} }

@ -1,5 +1,6 @@
<?php namespace SocialNorm\Facebook; <?php namespace SocialNorm\Facebook;
use GuzzleHttp\Exception\BadResponseException;
use SocialNorm\Exceptions\InvalidAuthorizationCodeException; use SocialNorm\Exceptions\InvalidAuthorizationCodeException;
use SocialNorm\Providers\OAuth2Provider; use SocialNorm\Providers\OAuth2Provider;

@ -58,12 +58,12 @@ class GoogleProvider extends OAuth2Provider
protected function nickname() protected function nickname()
{ {
return $this->getProviderUserData('email'); return $this->getProviderUserData('given_name');
} }
protected function fullName() protected function fullName()
{ {
return $this->getProviderUserData('given_name') . ' ' . $this->getProviderUserData('family_name'); return $this->getProviderUserData('name'); // this is the full name
} }
protected function avatar() protected function avatar()

@ -2,6 +2,6 @@
interface Provider interface Provider
{ {
public function authorizeUrl($state); public function authorizeUrl(string $state) : string;
public function getUser(); public function getUser() : ProviderUser;
} }

@ -4,14 +4,15 @@ use SocialNorm\Exceptions\ProviderNotRegisteredException;
class ProviderRegistry class ProviderRegistry
{ {
/** @var Provider[] */
private $providers = []; private $providers = [];
public function registerProvider($alias, Provider $provider) public function registerProvider(string $alias, Provider $provider)
{ {
$this->providers[$alias] = $provider; $this->providers[$alias] = $provider;
} }
public function getProvider($providerAlias) public function getProvider(string $providerAlias)
{ {
if (! $this->hasProvider($providerAlias)) { if (! $this->hasProvider($providerAlias)) {
throw new ProviderNotRegisteredException("No provider has been registered under the alias '{$providerAlias}'"); throw new ProviderNotRegisteredException("No provider has been registered under the alias '{$providerAlias}'");
@ -19,7 +20,7 @@ class ProviderRegistry
return $this->providers[$providerAlias]; return $this->providers[$providerAlias];
} }
protected function hasProvider($alias) protected function hasProvider(string $alias)
{ {
return isset($this->providers[$alias]); return isset($this->providers[$alias]);
} }

@ -1,14 +1,16 @@
<?php namespace SocialNorm; <?php namespace SocialNorm;
/** /**
* @property-read string $accessToken * @property-read string $access_token
* @property-read string $id * @property-read string $id
* @property-read string $nickname * @property-read string $nickname
* @property-read string $fullName * @property-read string $fullName
* @property-read string $imageUrl * @property-read string $avatar
* @property-read string $email * @property-read string $email
* @property-read string $full_name
* @property-read array $raw
*/ */
class User class ProviderUser
{ {
protected $access_token; protected $access_token;
protected $id; protected $id;

@ -4,7 +4,7 @@ use SocialNorm\Exceptions\ApplicationRejectedException;
use SocialNorm\Exceptions\InvalidAuthorizationCodeException; use SocialNorm\Exceptions\InvalidAuthorizationCodeException;
use SocialNorm\Provider; use SocialNorm\Provider;
use SocialNorm\Request; use SocialNorm\Request;
use SocialNorm\User; use SocialNorm\ProviderUser;
use GuzzleHttp\Client as HttpClient; use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Exception\BadResponseException; use GuzzleHttp\Exception\BadResponseException;
@ -27,7 +27,7 @@ abstract class OAuth2Provider implements Provider
protected $accessToken; protected $accessToken;
protected $providerUserData; protected $providerUserData;
public function __construct($config, HttpClient $httpClient, Request $request) public function __construct(array $config, HttpClient $httpClient, Request $request)
{ {
$this->httpClient = $httpClient; $this->httpClient = $httpClient;
$this->request = $request; $this->request = $request;
@ -44,7 +44,7 @@ abstract class OAuth2Provider implements Provider
return $this->redirectUri; return $this->redirectUri;
} }
public function authorizeUrl($state) public function authorizeUrl(string $state) : string
{ {
$url = $this->getAuthorizeUrl(); $url = $this->getAuthorizeUrl();
$url .= '?' . $this->buildAuthorizeQueryString($state); $url .= '?' . $this->buildAuthorizeQueryString($state);
@ -66,11 +66,11 @@ abstract class OAuth2Provider implements Provider
return implode(',', $this->scope); return implode(',', $this->scope);
} }
public function getUser() public function getUser() : ProviderUser
{ {
$this->accessToken = $this->requestAccessToken(); $this->accessToken = $this->requestAccessToken();
$this->providerUserData = $this->requestUserData(); $this->providerUserData = $this->requestUserData();
return new User([ return new ProviderUser([
'access_token' => $this->accessToken, 'access_token' => $this->accessToken,
'id' => $this->userId(), 'id' => $this->userId(),
'nickname' => $this->nickname(), 'nickname' => $this->nickname(),

@ -4,9 +4,13 @@ use SocialNorm\Exceptions\InvalidAuthorizationCodeException;
class SocialNorm class SocialNorm
{ {
/** @var ProviderRegistry */
protected $providers; protected $providers;
/** @var Session */
protected $session; protected $session;
/** @var Request */
protected $request; protected $request;
/** @var StateGenerator */
protected $stateGenerator; protected $stateGenerator;
public function __construct( public function __construct(
@ -22,12 +26,12 @@ class SocialNorm
$this->stateGenerator = $stateGenerator; $this->stateGenerator = $stateGenerator;
} }
public function registerProvider($alias, Provider $provider) public function registerProvider(string $alias, Provider $provider)
{ {
$this->providers->registerProvider($alias, $provider); $this->providers->registerProvider($alias, $provider);
} }
public function authorize($providerAlias) public function authorize(string $providerAlias)
{ {
$state = $this->stateGenerator->generate(); $state = $this->stateGenerator->generate();
@ -36,13 +40,13 @@ class SocialNorm
return $this->getProvider($providerAlias)->authorizeUrl($state); return $this->getProvider($providerAlias)->authorizeUrl($state);
} }
public function getUser($providerAlias) public function getUser(string $providerAlias)
{ {
$this->verifyState(); $this->verifyState();
return $this->getProvider($providerAlias)->getUser(); return $this->getProvider($providerAlias)->getUser();
} }
protected function getProvider($providerAlias) protected function getProvider(string $providerAlias)
{ {
return $this->providers->getProvider($providerAlias); return $this->providers->getProvider($providerAlias);
} }

@ -2,7 +2,7 @@
class StateGenerator class StateGenerator
{ {
public function generate($length = 32) public function generate(int $length = 32)
{ {
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return substr(str_shuffle(str_repeat($pool, $length)), 0, $length); return substr(str_shuffle(str_repeat($pool, $length)), 0, $length);

@ -0,0 +1,4 @@
/vendor
composer.phar
composer.lock
.DS_Store

@ -0,0 +1,11 @@
language: php
php:
- 5.5
- 5.6
before_script:
- curl -s http://getcomposer.org/installer | php
- php composer.phar install --dev
script: phpunit

@ -0,0 +1,29 @@
{
"name": "socialnorm/stackoverflow",
"description": "StackOverflow provider for SocialNorm",
"authors": [
{
"name": "MightyPork"
}
],
"license": "MIT",
"require": {
"php": ">=5.5.0",
"guzzlehttp/guzzle": "^6.0",
"socialnorm/socialnorm": "^0.2"
},
"require-dev": {
"mockery/mockery": "~0.8",
"phpunit/phpunit": "^4.8"
},
"autoload": {
"psr-4": {
"SocialNorm\\StackOverflow\\": "src/"
}
},
"autoload-dev": {
"files": [
"tests/TestCase.php"
]
}
}

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix="Test.php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>

@ -0,0 +1,3 @@
## SocialNorm Google Provider
@todo: Add docs :)

@ -0,0 +1,100 @@
<?php namespace SocialNorm\StackOverflow;
use SocialNorm\Providers\OAuth2Provider;
use GuzzleHttp\Client as HttpClient;
use SocialNorm\Request;
// https://stackapps.com/apps/oauth/view/12789
// https://api.stackexchange.com/docs/authentication
// This is entirely untested; it might work, but since we don't get e-mail from it,
// it's use as an auth method is problematic.
class StackOverflowProvider extends OAuth2Provider
{
protected $authorizeUrl = "https://stackoverflow.com/oauth";
protected $accessTokenUrl = "https://stackoverflow.com/oauth/access_token";
protected $userDataUrl = "https://api.stackexchange.com/2.2/me";
protected $scope = [];
private $key;
protected $headers = [
'authorize' => [],
'access_token' => [
'Content-Type' => 'application/x-www-form-urlencoded'
],
'user_details' => [],
];
public function __construct(array $config, HttpClient $httpClient, Request $request)
{
parent::__construct($config, $httpClient, $request);
$this->key = $config['key'];
}
protected function compileScopes()
{
return implode(' ', $this->scope);
}
protected function getAuthorizeUrl()
{
return $this->authorizeUrl;
}
protected function getAccessTokenBaseUrl()
{
return $this->accessTokenUrl;
}
protected function getUserDataUrl()
{
return $this->userDataUrl;
}
protected function buildUserDataUrl()
{
$url = $this->getUserDataUrl();
$url .= "?access_token=".$this->accessToken
. "&site=stackoverflow"
. "&key=".$this->key;
return $url;
}
protected function parseTokenResponse($response)
{
return $this->parseJsonTokenResponse($response);
}
protected function parseUserDataResponse($response)
{
return json_decode($response, true);
}
protected function userId()
{
return array_get($this->providerUserData, 'items.0.account_id');
}
protected function nickname()
{
return array_get($this->providerUserData, 'items.0.display_name');
}
protected function fullName()
{
return array_get($this->providerUserData, 'items.0.display_name');
}
protected function avatar()
{
return array_get($this->providerUserData, 'items.0.profile_image');
}
protected function email()
{
return null;
}
}

@ -0,0 +1,75 @@
<?php
use Mockery as M;
use SocialNorm\Google\GoogleProvider;
use SocialNorm\Request;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Client as HttpClient;
class GoogleProviderTest extends TestCase
{
private function getStubbedHttpClient($fixtures = [])
{
$mock = new MockHandler($this->createResponses($fixtures));
$handler = HandlerStack::create($mock);
return new HttpClient(['handler' => $handler]);
}
private function createResponses($fixtures)
{
$responses = [];
foreach ($fixtures as $fixture) {
$response = require $fixture;
$responses[] = new Response($response['status'], $response['headers'], $response['body']);
}
return $responses;
}
/** @test */
public function it_can_retrieve_a_normalized_user()
{
$client = $this->getStubbedHttpClient([
__DIR__ . '/_fixtures/google_accesstoken.php',
__DIR__ . '/_fixtures/google_user.php',
]);
$provider = new GoogleProvider([
'client_id' => 'abcdefgh',
'client_secret' => '12345678',
'redirect_uri' => 'http://example.com/login',
], $client, new Request(['code' => 'abc123']));
$user = $provider->getUser();
$this->assertEquals('103904294571447333816', $user->id);
$this->assertEquals('adam.wathan@example.com', $user->nickname);
$this->assertEquals('Adam Wathan', $user->full_name);
$this->assertEquals('adam.wathan@example.com', $user->email);
$this->assertEquals('https://lh3.googleusercontent.com/-w0_RpDnsIE4/AAAAAAAAAAI/AAAAAAAAAKM/NEiV3jig1HA/photo.jpg', $user->avatar);
$this->assertEquals('ya29.8xFOTYpQK48RgPH8KjQpSu9SrcANcOQx9JtRnEu52dNsXqai8VD4iY3nFzUBURWnAPeTPtPeIBNjIF', $user->access_token);
}
/**
* @test
* @expectedException SocialNorm\Exceptions\ApplicationRejectedException
*/
public function it_fails_to_retrieve_a_user_when_the_authorization_code_is_omitted()
{
$client = $this->getStubbedHttpClient([
__DIR__ . '/_fixtures/google_accesstoken.php',
__DIR__ . '/_fixtures/google_user.php',
]);
$provider = new GoogleProvider([
'client_id' => 'abcdefgh',
'client_secret' => '12345678',
'redirect_uri' => 'http://example.com/login',
], $client, new Request([]));
$user = $provider->getUser();
}
}

@ -0,0 +1,12 @@
<?php
use Mockery as M;
class TestCase extends PHPUnit_Framework_TestCase
{
public function tearDown()
{
M::close();
parent::tearDown();
}
}

@ -0,0 +1,24 @@
<?php
return [
'status' => 200,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'Cache-Control' => 'no-cache, no-store, max-age=0, must-revalidate',
'Pragma' => 'no-cache',
'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT',
'Date' => 'Thu, 19 Mar 2015 13:14:56 GMT',
'Content-Disposition' => 'attachment; filename="json.txt"; filename*=UTF-8\'\'json.txt',
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'Server' => 'GSE',
'Alternate-Protocol' => '443:quic,p=0.5',
'Accept-Ranges' => 'none',
'Vary' => 'Accept-Encoding',
'Transfer-Encoding' => 'chunked'
],
'body' => '{"access_token" :"ya29.8xFOTYpQK48RgPH8KjQpSu9SrcANcOQx9JtRnEu52dNsXqai8VD4iY3nFzUBURWnAPeTPtPeIBNjIF","token_type" :"Bearer","expires_in" :3600,"id_token" :"6XFIidpthpu8iFNhyo5xjcGa9bUdiTL1zKnZz2dN-35IFzlCJ_IcmHYcA1i03iA5YVMOM1nLGiAbRaLTaMoWM5X_j0MyFBknwGMs3MM2WmdiIhdC7uNiXY2US8mWvCiVNwMnchpkJJUGIMbTNNPQd5_08XQvNi3TINiYOwZLxOIJ6X3pi9hNVY0dNNtUYwYdiNgwIW3ClRNNY1jiZxciXSckj18jb2jbSMMYujDKbw9ipMOIUxlnm5zIzEjNLV1BcMFMbjQWBnhjFUW4li-QTjcSnfj5iMdUIkjlGvkWcoLQ3pNNOwMhwjZFhdv0G3DTyT2nyOIR3eejckDXGWGw4l2Ni3izRwMYctNF1I_VstkDwn1idZoMOdR3b-zEO5C9ItMzl0TyNJjeb73VFrQIwXpHcTTQMgym2o3mijwJTn2ypu9NP0jjM1lcjd0Euh0OLJYxIYTIfbBC5WuCDcDMZxCbcz7oYLMl2yYDmI0akcuiVZyDO2I1aS2DAIUip.oTuI0HnAbtiA5y.Tnywh3VU0JTZ2Iu9OhJnYkvYmhzOm2AYbu5P9Q71UQObK2tLNcYQmBDmZGsQNIMwh1tQtl0sDTXj9WTVETuYAx4WK9hoOzcOVEY8iNHcl"}'
];

@ -0,0 +1,23 @@
<?php
return [
'status' => 200,
'headers' => [
'Cache-Control' => 'no-cache, no-store, max-age=0, must-revalidate',
'Pragma' => 'no-cache',
'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT',
'Date' => 'Thu, 19 Mar 2015 13:15:29 GMT',
'Vary' => 'X-Origin, Origin,Accept-Encoding',
'Content-Type' => 'application/json; charset=UTF-8',
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'Server' => 'GSE',
'Alternate-Protocol' => '443:quic,p=0.5',
'Accept-Ranges' => 'none',
'Transfer-Encoding' => 'chunked'
],
'body' => '{"id":"103904294571447333816","email":"adam.wathan@example.com","verified_email":true,"name":"Adam Wathan","given_name":"Adam","family_name":"Wathan","link":"https://plus.google.com/103904294571447333816","picture":"https://lh3.googleusercontent.com/-w0_RpDnsIE4/AAAAAAAAAAI/AAAAAAAAAKM/NEiV3jig1HA/photo.jpg","gender":"male","locale":"en"}'
];
Loading…
Cancel
Save