Skip to content

Commit d02d2f4

Browse files
authored
Allow to connect an identity after registration (#3)
* Allow to connect an identity after registration * Extract handling of additional attributes, also for connect action * Update readme
1 parent 47169a7 commit d02d2f4

File tree

12 files changed

+326
-63
lines changed

12 files changed

+326
-63
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ as well as the community driven [Socialite Providers](https://socialiteproviders
1616

1717
- Handle user registration via third party providers;
1818
- Handle user log in via third party providers;
19+
- Allow existing user to link a third party identity;
1920
- Customizable controllers, migration and models that will live in your application namespace;
2021
- Save identity and token inside the database, using
2122
[encryption and pseudoanonimization](#how-data-is-stored-in-the-database);
22-
- Provide login/register button as Blade component;
23+
- Provide login/register/connect button as Blade component;
2324
- Support all [Laravel Socialite](https://laravel.com/docs/socialite)
2425
and [Socialite Providers](https://socialiteproviders.com/);
2526
- Add custom providers.
@@ -130,7 +131,7 @@ however we provide a Blade Component to quickly add login and register links/but
130131
class="button button--primary" />
131132
```
132133

133-
The available `action`s are `login` and `register`. The `provider` refers to what
134+
The available `action`s are `login`, `connect` and `register`. The `provider` refers to what
134135
identity provider to use, the name of the provider is the same as the Socialite
135136
providers' name. See [Blade components](https://laravel.com/docs/blade#components) for more.
136137

src/Auth/AuthenticatesUsersWithIdentity.php

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@
22

33
namespace Oneofftech\Identities\Auth;
44

5-
use Illuminate\Database\Eloquent\ModelNotFoundException;
65
use Illuminate\Http\Request;
76
use Illuminate\Http\Response;
87
use Illuminate\Support\Facades\Auth;
98
use Illuminate\Foundation\Auth\RedirectsUsers;
109
use Illuminate\Validation\ValidationException;
1110
use Oneofftech\Identities\Facades\Identity;
12-
use Oneofftech\Identities\Facades\IdentityCrypt;
11+
use Oneofftech\Identities\Support\FindIdentity;
1312
use Oneofftech\Identities\Support\InteractsWithPreviousUrl;
1413

1514
trait AuthenticatesUsersWithIdentity
1615
{
17-
use RedirectsUsers, InteractsWithPreviousUrl;
16+
use RedirectsUsers, InteractsWithPreviousUrl, FindIdentity;
1817

1918
/**
2019
* Redirect the user to the provider authentication page.
@@ -71,18 +70,6 @@ public function login(Request $request, $provider)
7170
return $this->sendLoginResponse($request);
7271
}
7372

74-
/**
75-
* @return \Illuminate\Contracts\Auth\Authenticatable|null
76-
*/
77-
protected function findUserFromIdentity($identity, $provider)
78-
{
79-
try {
80-
return Identity::findUserByIdentity($provider, IdentityCrypt::hash($identity->getId()));
81-
} catch (ModelNotFoundException $mntfex) {
82-
return null;
83-
}
84-
}
85-
8673
/**
8774
* Get the failed login response instance.
8875
*

src/Auth/ConnectUserIdentity.php

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace Oneofftech\Identities\Auth;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Http\Response;
7+
use Illuminate\Support\Facades\Auth;
8+
use Illuminate\Foundation\Auth\RedirectsUsers;
9+
use Illuminate\Support\Facades\DB;
10+
use Oneofftech\Identities\Facades\IdentityCrypt;
11+
use Oneofftech\Identities\Facades\Identity;
12+
use Oneofftech\Identities\Support\FindIdentity;
13+
use Oneofftech\Identities\Support\InteractsWithPreviousUrl;
14+
use Oneofftech\Identities\Support\InteractsWithAdditionalAttributes;
15+
16+
trait ConnectUserIdentity
17+
{
18+
use RedirectsUsers, InteractsWithPreviousUrl, InteractsWithAdditionalAttributes, FindIdentity;
19+
20+
/**
21+
* Redirect the user to the Authentication provider authentication page.
22+
*
23+
* @return \Illuminate\Http\Response
24+
*/
25+
public function redirect(Request $request, $provider)
26+
{
27+
// save the previous url as the callback will
28+
// probably have the referrer header set
29+
// and in case of validation errors the
30+
// referrer has precedence over _previous.url
31+
$this->savePreviousUrl();
32+
33+
// get additional user defined attributes
34+
$this->pushAttributes($request);
35+
36+
return Identity::driver($provider)
37+
->redirectUrl(route('oneofftech::connect.callback', ['provider' => $provider]))
38+
->redirect();
39+
}
40+
41+
/**
42+
* Obtain the user information from Authentication provider.
43+
*
44+
* if the identity exists it will be updated, otherwise a new identity will be created
45+
*
46+
* @return \Illuminate\Http\Response
47+
*/
48+
public function connect(Request $request, $provider)
49+
{
50+
// Load the previous url from the
51+
// session to redirect back in
52+
// case of errors
53+
$previous_url = $this->getPreviousUrl();
54+
55+
$oauthUser = Identity::driver($provider)
56+
->redirectUrl(route('oneofftech::connect.callback', ['provider' => $provider]))
57+
->user();
58+
59+
// if user denies the authorization request we get
60+
// GuzzleHttp\Exception\ClientException
61+
// Client error: `POST https://gitlab.com/oauth/token` resulted in a `401 Unauthorized`
62+
// response: {"error":"invalid_grant","error_description":"The provided authorization grant is invalid, expired, revoked, does not ma (truncated...)
63+
64+
// GuzzleHttp\Exception\ClientException
65+
// Client error: `POST https://gitlab/oauth/token` resulted in a `401 Unauthorized`
66+
// response: {"error":"invalid_grant","error_description":"The provided authorization grant is invalid, expired, revoked, does not ma (truncated...)
67+
68+
$user = $request->user();
69+
70+
// create or update the user's identity
71+
72+
list($user, $identity) = DB::transaction(function () use ($user, $provider, $oauthUser) {
73+
$identity = $this->createIdentity($user, $provider, $oauthUser);
74+
75+
return [$user, $identity];
76+
});
77+
78+
// todo: event(new Connected($user, $identity));
79+
80+
return $this->sendConnectionResponse($request, $identity);
81+
}
82+
83+
protected function createIdentity($user, $provider, $oauthUser)
84+
{
85+
return $user->identities()->updateOrCreate(
86+
[
87+
'provider'=> $provider,
88+
'provider_id'=> IdentityCrypt::hash($oauthUser->getId())
89+
],
90+
[
91+
'token'=> IdentityCrypt::encryptString($oauthUser->token),
92+
'refresh_token'=> IdentityCrypt::encryptString($oauthUser->refreshToken),
93+
'expires_at'=> $oauthUser->expiresIn ? now()->addSeconds($oauthUser->expiresIn) : null,
94+
'registration' => true,
95+
]
96+
);
97+
}
98+
99+
protected function sendConnectionResponse(Request $request, $identity)
100+
{
101+
$request->session()->regenerate();
102+
103+
if ($response = $this->connected($this->guard()->user(), $identity, $this->pullAttributes($request), $request)) {
104+
return $response;
105+
}
106+
107+
return redirect()->intended($this->redirectPath());
108+
}
109+
110+
/**
111+
* The user identity has been connected.
112+
*
113+
* @param mixed $user
114+
* @param mixed $identity
115+
* @param array $attributes
116+
* @param \Illuminate\Http\Request $request
117+
* @return mixed
118+
*/
119+
protected function connected($user, $identity, array $attributes, Request $request)
120+
{
121+
//
122+
}
123+
124+
/**
125+
* Get the guard to retrieve currently authenticated user.
126+
*
127+
* @return \Illuminate\Contracts\Auth\StatefulGuard
128+
*/
129+
protected function guard()
130+
{
131+
return Auth::guard();
132+
}
133+
}

src/Auth/RegistersUsersWithIdentity.php

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
use Laravel\Socialite\AbstractUser as SocialiteUser;
1515
use Oneofftech\Identities\Facades\Identity;
1616
use Oneofftech\Identities\Support\InteractsWithPreviousUrl;
17+
use Oneofftech\Identities\Support\InteractsWithAdditionalAttributes;
1718

1819
trait RegistersUsersWithIdentity
1920
{
20-
use RedirectsUsers, InteractsWithPreviousUrl;
21+
use RedirectsUsers, InteractsWithPreviousUrl, InteractsWithAdditionalAttributes;
2122

2223
/**
2324
* Redirect the user to the Authentication provider authentication page.
@@ -163,47 +164,4 @@ protected function guard()
163164
{
164165
return Auth::guard();
165166
}
166-
167-
/**
168-
* The attributes that should be retrieved from
169-
* the request to append to the redirect
170-
*
171-
* @var array
172-
*/
173-
protected function redirectAttributes()
174-
{
175-
if (method_exists($this, 'attributes')) {
176-
return $this->attributes();
177-
}
178-
179-
return property_exists($this, 'attributes') ? $this->attributes : [];
180-
}
181-
182-
protected function pushAttributes($request)
183-
{
184-
$attributes = $this->redirectAttributes() ?? [];
185-
186-
if (empty($attributes)) {
187-
return;
188-
}
189-
190-
$request->session()->put('_oot.identities.attributes', json_encode($request->only($attributes)));
191-
}
192-
193-
protected function pullAttributes($request)
194-
{
195-
$attributes = $this->redirectAttributes() ?? [];
196-
197-
if (empty($attributes)) {
198-
return [];
199-
}
200-
201-
$savedAttributes = $request->session()->pull('_oot.identities.attributes') ?? null;
202-
203-
if (! $savedAttributes) {
204-
return [];
205-
}
206-
207-
return json_decode($savedAttributes, true);
208-
}
209167
}

src/Facades/Identity.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public static function routes()
6666
->name("oneofftech::register.provider");
6767
$router->get('register-via/{provider}/callback', "$namespace\Http\Controllers\Identities\Auth\RegisterController@register")
6868
->name("oneofftech::register.callback");
69+
70+
$router->get('connect-via/{provider}', "$namespace\Http\Controllers\Identities\Auth\ConnectController@redirect")
71+
->name("oneofftech::connect.provider");
72+
$router->get('connect-via/{provider}/callback', "$namespace\Http\Controllers\Identities\Auth\ConnectController@connect")
73+
->name("oneofftech::connect.callback");
6974
}
7075

7176
/**

src/Support/FindIdentity.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Oneofftech\Identities\Support;
4+
5+
use Oneofftech\Identities\Facades\Identity;
6+
use Oneofftech\Identities\Facades\IdentityCrypt;
7+
use Illuminate\Database\Eloquent\ModelNotFoundException;
8+
9+
trait FindIdentity
10+
{
11+
/**
12+
* @return \Illuminate\Contracts\Auth\Authenticatable|null
13+
*/
14+
protected function findUserFromIdentity($identity, $provider)
15+
{
16+
try {
17+
return Identity::findUserByIdentity($provider, IdentityCrypt::hash($identity->getId()));
18+
} catch (ModelNotFoundException $mntfex) {
19+
return null;
20+
}
21+
}
22+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace Oneofftech\Identities\Support;
4+
5+
trait InteractsWithAdditionalAttributes
6+
{
7+
8+
/**
9+
* The attributes that should be retrieved from
10+
* the request to append to the redirect
11+
*
12+
* @var array
13+
*/
14+
protected function redirectAttributes()
15+
{
16+
if (method_exists($this, 'attributes')) {
17+
return $this->attributes();
18+
}
19+
20+
return property_exists($this, 'attributes') ? $this->attributes : [];
21+
}
22+
23+
protected function pushAttributes($request)
24+
{
25+
$attributes = $this->redirectAttributes() ?? [];
26+
27+
if (empty($attributes)) {
28+
return;
29+
}
30+
31+
$request->session()->put('_oot.identities.attributes', json_encode($request->only($attributes)));
32+
}
33+
34+
protected function pullAttributes($request)
35+
{
36+
$attributes = $this->redirectAttributes() ?? [];
37+
38+
if (empty($attributes)) {
39+
return [];
40+
}
41+
42+
$savedAttributes = $request->session()->pull('_oot.identities.attributes') ?? null;
43+
44+
if (! $savedAttributes) {
45+
return [];
46+
}
47+
48+
return json_decode($savedAttributes, true);
49+
}
50+
}

src/View/Components/IdentityLink.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
class IdentityLink extends Component
1010
{
11-
private static $availableActions = ['register', 'login'];
11+
private static $availableActions = ['register', 'login', 'connect'];
1212

1313
private static $actionLabels = [
1414
'register' => 'Register via :Provider',
15-
'login' => 'Log in via :Provider'
15+
'login' => 'Log in via :Provider',
16+
'connect' => 'Connect :Provider',
1617
];
1718

1819
/**

0 commit comments

Comments
 (0)