Laravel Socialite - Different Providers With Same Email - Security How to Fix - laravel

Ok so I read how to implement Laravel socialite into my application so I can let users log in using Google or Facebook. I read how to do it here. The problem I encounter with this approach is that if a user logs in from say Google with email myemail#example.com then logs in from Facebook using myemail#example.com they are logging into the same account! Security issue right. So I thought that before I let them log in I would check the provider id which you can get, however when I try to compare the provider_id that I store in the database when they create the account with the provider_id stored in the socialite user variable I get this error:
Argument 1 passed to Illuminate\Auth\SessionGuard::login() must implement interface Illuminate\Contracts\Auth\Authenticatable, instance of Illuminate\Http\RedirectResponse given
Here is all the code I am using for Socialite:
<?php
namespace App\Http\Controllers;
use Socialite;
use App\User;
use Auth;
use Illuminate\Support\Facades\Redirect;
use Flash;
use Illuminate\Http\Request;
use App\Http\Requests;
class SocialiteController extends Controller
{
public function redirectToProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
public function handleProviderCallback($provider)
{
try
{
$social_user = Socialite::driver($provider)->user();
}
catch(Exception $e)
{
return Redirect::to('auth/' . $provider);
}
$authUser = $this->findOrCreateUser($social_user);
Auth::login($authUser, true);
flash()->overlay('You have been logged in successfully!', 'Congratulations');
return Redirect::to('/');
}
//create a new user in our database or grab existing user
private function findOrCreateUser($social_user)
{
if ($authUser = User::where('email', $social_user->email)->first()) {
//this creates an error
if($authUser->provider_id == $social_user->id)
return $authUser;
else
{
flash()->overlay('An account for that email already exists!', 'Error');
return Redirect::to('/');
}
}
return User::Create([
'provider_id' => $social_user->id,
'name' => $social_user->name,
'email' => $social_user->email,
'nickname' => $social_user->nickname,
'avatar_img' => $social_user->avatar,
'role_id' => 1, //set role to guest
'social_login' => true //tell the database they are logging in from oauth
]);
}
}

The error message is actually self explained.
When you do User::where('provider_id', $social_user->id) you end up with builder object that implements
Illuminate\Database\Eloquent\Builder.
You can call ->get() on it to get the collection of results
(a collection of objects that implements
Illuminate\Contracts\Auth\Authenticatable
in your case, so you can iterate over them), or as you did, you can get the first match with ->first() (one object that implement
Illuminate\Contracts\Auth\Authenticatable).
You can read more in the Eloquent documentation.
The main point is that until you call ->get() or ->first() you are working with builder object.
There is actually ->find() method also, that is used to retrieve record by pk, you cannot use it to search for records by constraints (where), but it also returns model object.

The problem I encounter with this approach is that if a user logs in
from say Google with email myemail#example.com then logs in from
Facebook using myemail#example.com they are logging into the same
account! Security issue right.
In my opinion, this is an incorrect assumption. It is not a security issue.
OAuth is used as an authentication delegation, if a user controls accounts in different providers with the same email, it simply means he has more 3rd party OAuth services with which to validate control of his identity (email)
I would consider it a bug, if you limited me to only be able to sign in with one OAuth provider, and prohibited me to user another one with the same email.

Related

Create session on consuming login with api on laravel

I have an api that has a method to start and I am calling it from a frontend project.
In the front end project I use Guzzle to make the call via post to the api and login, from which I get back a json with the user data and a jwt token.
But when I receive the token as I manage the session, I must create a session and save the token, since the laravel to authenticate I need a model user and have a database, which of course I do not have in this backend because I call the api to log in, which brings a token and user data, then as I manage it from the backend, I'm a little lost there.
$api = new Api();
$response = $api->loginapi(['user'=>'wings#test.com','password'=>'123']);
Because here I could not do Auth::login($user) to generate the session.
Because I don't have here the database because the login is done from the api.
There I call the api, of which the answer is the token, but how do I manage it from here, creating a session? saving the token?
thanks for your help.
With api, you don't usually manage a session. usually, you'd call something like
Auth::attempt([
'email' => 'me#example.com',
'password' => 'myPassword'
]);
If the credentials are correct, laravel will include a Set-Cookie header in response, and, that is how you authenticate with api. Via an auth cookie. You don't need to do anything else.
Let's show you how:
//AuthController.php
public function login(Request $request) {
$validatedData = $request->validate([
'email' => 'required|email',
'password' => 'required'
]);
if(Auth::attempt($validatedData)){
return ['success' => 'true'];
}
else{
return ['success' => false, 'message' => 'Email or password Invalid'];
}
}
public function currentUser (){
return Auth::user();
}
Now, the APi file
Route::post('/login', ['App\Http\Controllers\AuthController', 'login']);
Route::get('/current_user', ['App\Http\Controllers\AuthController', 'currentUser']);
Now if you make a call to /api/current_user initially, you'll get null response since you're not currently logged in. But once you make request to /api/login and you get a successful response, you are now logged in. Now if you go to /api/current_user, you should see that you're already logged in.
Important ::
If you are using fetch, you need to include credentials if you're using something other than fetch, check out how to use credentials with that library or api
You want to use the API to authenticate and then use the SessionGuard to create session including the remember_me handling.
This is the default login controller endpoint for logging in. You don't want to change this, as it makes sure that user's do not have endless login attempts (protects for brut-force attacks) and redirects to your current location.
public function login(Request $request)
{
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
// the IP address of the client making these requests into this application.
if (method_exists($this, 'hasTooManyLoginAttempts') &&
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
if ($this->attemptLogin($request)) {
if ($request->hasSession()) {
$request->session()->put('auth.password_confirmed_at', time());
}
return $this->sendLoginResponse($request);
}
// If the login attempt was unsuccessful we will increment the number of attempts
// to login and redirect the user back to the login form. Of course, when this
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
The core happens when we try to "attemptLogin" at
protected function attemptLogin(Request $request)
{
return $this->guard()->attempt(
$this->credentials($request), $request->boolean('remember')
);
}
When using the SessioGurad (which is default) the method attemptLogin fires a couple of events, checks if the user has valid credentials (by hashing the password and matching it with db) and then logs the user in, including the remember me functionality.
Now, if you don't care about events, you can just check from your API if the credentials match and then use the login method from the guard. This will also handle the remember me functionality. Something like this:
protected function attemptLogin(Request $request)
{
$username = $request->input($this->username());
$password = $request->input('password');
$result = \Illuminate\Support\Facades\Http::post(env('YOUR_API_DOMAIN') . '/api/v0/login' , [
'username' => $username,
'password' => $password
])->json();
if(empty($result['success'])){
return false;
}
// Maybe you need to create the user here if the login is for the first time?
$user = User::where('username', '=', $username)->first();
$this->guard()->login(
$user, $request->boolean('remember')
);
return true;
}

Laravel Socialite Github provider email is null

I'm trying to setup login in Laravel with GitHub using Socialite.
Here is my provider callback function:
public function handleProviderCallback($provider)
{
$provider_user = Socialite::driver($provider)->user();
dd($provider_user);
if($provider_user->email) {
$user = User::firstOrCreate([
'email' => $provider_user->email
], [
'name' => $provider_user->name ?? $provider_user->nickname,
'password' => Hash::make(Str::random(16)),
]);
Auth::login($user, true);
}
return redirect('/');
}
When I dd the user that provider returns, I can see that values $provider_user->name and $provider_user->nickname are correct, but $provider_user->email is null.
Which, according to the GitHub API documentation shouldn't be the case, as my e-mail is public:
I have tried https://api.github.com/user/emails where I've tried putting user nickname, as well as client ID in place of user, but to no avail. The response is:
{ "message": "Not Found", "documentation_url": "https://docs.github.com/rest" }
So, am I doing something wrong or GitHub API simply no longer returns e-mail addresses? Is there a way to get a public e-mail address?
I was having the same problem earlier.
What I did upon creating the github app is change the User permissions->Email addresses to read only. You can update it on the permission & events:

Laravel Passport custom validation for any /oauth/token request

I need to validate extra fields in my users table before i create the requested tokens, but i can't find a simple way to do it with Passport.
I find similar workarunds which returns a token using $user->createToken() but i can't find someone covering all default /oauth/token options like the refresh_token
Also i see Passport have some simple ways to customize the username column and the password validation but i think this not cover my neededs.
Update
Im not sure if this is the best solution but in Laravel 5.8 the passport package have this validateForPassportPasswordGrant() function which allow you to add some extra conditionals before allowing the authentication process to get completed.
class User extends Authenticatable
{
public function validateForPassportPasswordGrant($password)
{
if ($this->active != true) {
throw OAuthServerException::accessDenied('The account is not active');
}
return Hash::check($password, $this->password);
}
}
In your login method
if (Auth::attempt(['email' => $request->email, 'password' => $request->password, 'is_active' => 1, 'username' => $request->username])) {
// create a token here
}

Laravel api authentication - how do I get User by api_token

I am doing some POSTing to my laravel API.
I have added a unique 'api_token' column to the users table, and I want to retrieve the user from this token.
In a controller I can validate a token - the $apiUserValid is true or false depending on whether or not a row exists with a matching api_token value:
$apiToken = "Y2uibY5MIV";//correct token
$apiCredentials = ['api_token' => $apiToken];
$apiUserValid = \Auth::guard('api')->validate($apiCredentials);
if ($apiUserValid) {
var_dump(\Illuminate\Support\Facades\Auth::user()); // shows "NULL"
var_dump(\Illuminate\Support\Facades\Auth::id()); //shows NULL
var_dump(\Illuminate\Support\Facades\Auth::guard('api')->user()); // shows "NULL"
die();
} else {
die('not valid');// when the token is incorrect this branch is taken
}
However, I want to get the userId of the user with that token.
The SessionGuard has some methods that look relevant - such as login() but these are not present in the TokenGuard.
I am currently simulating this in a controller rather than dealing with a http request object - this could be part of my problem.
How do I get the user id of the person making the POST?
You can use this:
<?php
use Illuminate\Support\Facades\Auth;
$userId = Auth::id();
The relevant documentation is here.
Alternatively, you can use this:
<?php
use App\User;
$user = User::where('api_token', $apiToken)->first();
$userId = $user->id;

Laravel auth loses session after redirect

Due to laravel's default auth system not working for my current project I used their manual authentication system. The authenticating works but after I redirect the authentication gets destroyed or it simply isn't getting stored properly.
My authController:
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
class manualAuthController extends Controller {
public function authenticate(){
if (Auth::attempt(['email' => $_POST['email'], 'password' => $_POST['password']])) {
// Authentication passed...
$user = Auth::user();
//return '$user';
return redirect("test2");
}
return 'Auth failed!';
}
}
My route to test if the authenticated user is still stored:
Route::get('test2', function(){
echo 'test2<br>';
$user = Auth::user();
echo $user;
return '.';
});
If I uncomment "return '$user';" then the authenticated user gets returned properly.
I know that this question has been asked before but I couldn't find anybody with a working/ reliable solution to this problem so can someone here please help me with this problem(or finding the exact problem)?
Side note: I previously used the Socialite system which we had to switch for the laravel auth due to hosting problems. After this switch it worked in development for a while with my current session and db settings. Due to some other bugs I switched to manual authentication.

Resources