I have three types of Authenticatable model and I need to have separate JWT authentication for each. Let me explain more about my issue.
I'm using MongoDB as my database and Laravel MongoDB is the package that I use.
User, Admin, and ServiceProvider are my models.
To having JWT auth in Laravel I use jwt-auth package. It's ok with user model (collection). when I want to use JWT with any of other models It not work and do everything with user again.
I search a lot an I found out that to change the provider user model I can use Config::set(); method like below,
Config::set('jwt.user', Admin::class);
Config::set('auth.providers.users.model', Admin::class);
But no effect on JWT auth. (I checked the value of 'jwt.user' and 'auth.providers.users.model' with Config::get() method and returned it, It has been changed to 'App\Admin').
Need to say, My codes are as simple as possible according to the documentation of the package.
Here is my UserController code:
class UserController extends Controller
{
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|string|email|max:255',
'password' => 'required|min:6'
]);
if ($validator->fails()) {
return response()->json($validator->errors());
}
$credentials = $request->only('email', 'password');
try {
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
return response()->json(['error' => 'could_not_create_token'], 500);
}
$user = User::where('email', $request->email)->first();
return response()->json([
'user' => $user,
'token' => $token
]);
}
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|string|email|max:255|unique:users',
'phone' => 'required|valid_phone|unique:users',
'password' => 'required|min:6',
'first_name' => 'required',
'last_name' => 'required',
]);
if ($validator->fails()) {
return response()->json($validator->errors());
}
User::create([
'phone' => $request->get('phone'),
'first_name' => $request->get('first_name'),
'last_name' => $request->get('last_name'),
'city_abbr' => $request->get('city_abbr'),
'email' => $request->get('email'),
'password' => bcrypt($request->get('password')),
]);
$user = User::first();
$token = JWTAuth::fromUser($user);
return response()->json([
'user' => $user,
'token' => $token
]);
}
}
And my AdminController:
class AdminController extends Controller
{
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|string|email|max:255',
'password' => 'required|min:6'
]);
if ($validator->fails()) {
return response()->json($validator->errors());
}
$credentials = $request->only('email', 'password');
Config::set('jwt.user', Admin::class);
Config::set('auth.providers.users.model', Admin::class);
try {
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
return response()->json(['error' => 'could_not_create_token'], 500);
}
$admin = Admin::where('email', $request->email)->first();
return response()->json([
'admin' => $admin,
'token' => $token
]);
}
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|string|email|max:255|unique:admins',
'phone' => 'required|valid_phone|unique:admins',
'password' => 'required|min:6',
'name' => 'required',
]);
if ($validator->fails()) {
return response()->json($validator->errors());
}
$admin = Admin::create([
'phone' => $request->get('phone'),
'name' => $request->get('name'),
'access' => $request->get('access'),
'email' => $request->get('email'),
'password' => bcrypt($request->get('password')),
]);
Config::set('jwt.user', Admin::class);
Config::set('auth.providers.users.model', Admin::class);
$token = JWTAuth::fromUser($admin);
return response()->json([
'admin' => $admin,
'token' => $token
]);
}
}
Am I wrong in somewhere?
Is there any solution for this?
Update:
To be sure about MongoDB functionality, I test all of above doings with a relational database, actually MySQL. Nothing changed!
JWTAuth generates token but when I run toUser method with any models except User, it returns null!
Any solution will be appreciated.
Here is what you must fo to add multi auth ability with JWT to my project.
In tymon JWT auth package. In JWTAuthServiceProvider, Change Tymon\JWTAuth\JWTAuth and Tymon\JWTAuth\Providers\User\UserInterface definition type from singleton to bind in bootBindings method.
Defined a new middleware and below code is its handle method:
public function handle($request, Closure $next){
if (!$request->header('Auth-Type')) {
return response()->json([
'success' => 0,
'result' => 'auth type could not found!'
]);
}
switch ($request->header('Auth-Type')) {
case 'user':
$auth_class = 'App\User';
break;
case 'admin':
$auth_class = 'App\Admin';
break;
case 'provider':
$auth_class = 'App\ServiceProvider';
break;
default:
$auth_class = 'App\User';
}
if (!Helpers::modifyJWTAuthUser($auth_class))
return response()->json([
'status' => 0,
'error' => 'provider not found!'
]);
return $next($request); }
Defined a function with name modifyJWTAuthUser in Helpers and here is its inner:
public static function modifyJWTAuthUser($user_class){
if (!$user_class ||
(
$user_class != 'App\User' &&
$user_class != 'App\Admin' &&
$user_class != 'App\ServiceProvider'
))
return false;
try {
Config::set('jwt.user', $user_class);
Config::set('auth.providers.users.model', $user_class);
app()->make('tymon.jwt.provider.user');
return true;
} catch (\Exception $e) {
return false;
} }
Introduced another $routeMiddleware like below in Kernel.php:
...
'modify.jwt.auth.user' => ChangeJWTAuthUser::class,
and the last step, Adding 'modify.jwt.auth.user' middleware to the routes that you want.
But even with this steps, You must have encountered a new issue. It was about getting the auth token by credentials in login and getting auth user from the token. (It seems that changing config value not effect on JWTAuth::attempt($credentials) and $this->auth->authenticate($token))
To solve the getting auth user from the token issue:
Create a new middleware CustomGetUserFromTokenwhich extends of Tymon'sjwt.authmiddleware, I meanGetUserFromTokenand in line 35, and **replace**$user = $this->auth->authenticate($token);with$user = JWTAuth::toUser($token);`
And to solve getting the auth token by credentials in login issue:
At first, Find the auth user and after that, check the user existence and valid the password with Hash::check() method, if these conditions return true, Generate a token from the user. Here is login code:
$admin = Admin::where('email', $request->email)->first();
if (!$admin || !Hash::check($request->get('password'), $admin->password)) {
return response()->json([
'success' => '0',
'error' => 'invalid_credentials'
], 401);
}
I'm not sure about this way but I think it's true until finding a correct way to do!
Conclusion:
Having multi JWT auth ability in Laravel perhaps have many other ways to do but I did like this and shared it to be helpful.
I think the only important point of this issue was app()->make('tymon.jwt.provider.user');, the ability to remake user provider after config values change.
Any other solutions will be appreciated.
You should use just one model (actually table) for authentication. When you save user and admin you can handle it. But when a user has request with jwt token, you cann't know which model will return (Admin or User)?
Use only User model for authentication and Admin model extends from User.
Redesign database like this:
users table : id, email, password, is_admin
user_details table : id, user_id, first_name, last_name, city_abbr, phone
admin_details table: id, user_id, name, phone
Put this your Admin Model for overriding all queries:
protected $table = "users";
public function newQuery()
{
return parent::newQuery()
->where("is_admin", true);
}
Related
I have been building a blog project with React, Laravel, Laravel Sanctum, which provides authentication.
I have learned that clients, such as web browsers, must request to api/sanctum/csrf-cookie since I have to retrieve csrf token. Next, by requesting to api/login with appropriate login data(email, password), I can get plainTextToken, which is generated by Laravel Sanctum.
Now I am finding out that how can i use plainTextToken when i want to login. To put Bearer Token in request headers, I need to store plainTextToken somewhere.
Can i store plainTextToken in LocalStorage, or SessionStorage?
Because.. I think that whenever I access pages authentication required, React Components should maintain and provide token data.
I will attach AuthController of Laravel having been written.
class AuthController extends Controller
{
public function register(Request $request)
{
$val = $request->validate([
'name' => 'required|string|max:255',
"email" => "required|email|string|max:255|unique:users",
"password" => "required|confirmed|string|min:12"
]);
$user = User::create([
"name" => $val['name'],
'email' => $val['email'],
'password' => Hash::make($val['password']),
]);
return response()->json([
'user' => $user
], 201);
}
public function login(Request $request)
{
if(!Auth::attempt($request->only('email', 'password'))) {
return response()->json([
'message' => "Invalid Login Details"
], 401);
}
$user = User::where('email', $request['email'])->firstOrFail();
$token = $user->createToken("auth_token")->plainTextToken;
return response()->json([
'access_token' => $token,
"token_type" => "Bearer",
]);
}
public function account(Request $request)
{
return $request->user();
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
"msg" => "See You Later"
]);
}
public function delete(Request $request)
{
$request->user()->softDelete();
return response()->json([
"msg" => "You never register this email again"
]);
}
}
I was using Laravel's built-in api token authentication before but I wanted to provide multiple api tokens for different clients and with Laravel 7.x, I'm trying to migrate to Laravel Sanctum.
API seems authenticates user without any problem but when I try to get user data with Auth::user();, it returns null. Also Auth::guard('api')->user(); returns null too.
What should I use as Auth guard? Or is it correct way to get user data based on provided token?
Thank you very much....
auth('sanctum')->user()->id
auth('sanctum')->check()
without middleware, you could use these.
First, route through the sanctum auth middleware.
Route::get('/somepage', 'SomeController#MyMethod')->middleware('auth:sanctum');
Then, get the user.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class AuthController extends Controller
{
public function MyMethod(Request $request) {
return $request->user();
}
}
auth()->user() is a global helper, Auth::user() is a support facade, and $request->user() uses http. You can use any of them.
For a quick test, try
Route::get('/test', function() {
return auth()->user();
})->middleware('auth:sanctum');
Be sure to send your token in a header like so:
Authorization: Bearer UserTokenHere
Send token in the Authorization header, below code return the auth user.
Route::middleware('auth:sanctum')->group(function () {
Route::get('/profile/me', function (Request $request) {
return $request->user();
});
});
In case of restful api, suggest you to send Accept header also for checking at authenticate middleware for redirection if not authenticated. By default for restful api it redirect to login form (if any) if user not authenticated.
namespace App\Http\Middleware;
protected function redirectTo($request)
{
if (!$request->expectsJson()) {
return route('login');
}
}
When you are logging in the user, in your login function use something like this
public function login(Request $request)
{
if(Auth::attempt($credentials))
{
$userid = auth()->user()->id;
}
}
Then send this user id to the client and let it store in a secured way on client-side. Then with every request, you can use this user-id to serve data for next requests.
private $status_code= 200; // successfully
public function register(Request $request)
{
// $validator = $this->validator($request->all())->validate();
$validator = Validator::make($request->all(),
[
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255'], // , 'unique:users'
'password' => ['required', 'string', 'min:4'],
]
);
if($validator->fails()) {
return response()->json(["status" => "failed", "message" => "Please Input Valid Data", "errors" => $validator->errors()]);
}
$user_status = User::where("email", $request->email)->first();
if(!is_null($user_status)) {
return response()->json(["status" => "failed", "success" => false, "message" => "Whoops! email already registered"]);
}
$user = $this->create($request->all());
if(!is_null($user)) {
$this->guard()->login($user);
return response()->json(["status" => $this->status_code, "success" => true, "message" => "Registration completed successfully", "data" => $user]);
}else {
return response()->json(["status" => "failed", "success" => false, "message" => "Failed to register"]);
}
}
/**
* Get a validator for an incoming registration request.
*
* #param array $data
* #return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:4'],
]);
}
/**
* Create a new user instance after a valid registration.
* #author Mohammad Ali Abdullah ..
* #param array $data
* #return \App\User
*/
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
protected function guard()
{
return Auth::guard();
}
/**
* method public
* #author Mohammad Ali Abdullah
* #date 01-01-2021.
*/
public function login(Request $request)
{
$validator = Validator::make($request->all(),
[
"email" => "required|email",
"password" => "required"
]
);
// check validation email and password ..
if($validator->fails()) {
return response()->json(["status" => "failed", "validation_error" => $validator->errors()]);
}
// check user email validation ..
$email_status = User::where("email", $request->email)->first();
if(!is_null($email_status)) {
// check user password validation ..
// ---- first try -----
// $password_status = User::where("email", $request->email)->where("password", Hash::check($request->password))->first();
// if password is correct ..
// ---- first try -----
// if(!is_null($password_status)) {
if(Hash::check($request->password, $email_status->password)) {
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
// Authentication passed ..
$authuser = auth()->user();
return response()->json(["status" => $this->status_code, "success" => true, "message" => "You have logged in successfully", "data" => $authuser]);
}
}else {
return response()->json(["status" => "failed", "success" => false, "message" => "Unable to login. Incorrect password."]);
}
}else{
return response()->json(["status" => "failed", "success" => false, "message" => "Email doesnt exist."]);
}
}
public function logout()
{
Auth::logout();
return response()->json(['message' => 'Logged Out'], 200);
}
I see that no answer has been accepted yet. I just had the problem that my sacntum auth did not work. The auth() helper always returned null.
To solve the problem I removed the comment in the kernel.php under the api key. It is about this class \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class. This is because it is commented out by default.
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
After that I had access to the User object with the auth() helper.
The simplest way to to that is to use auth helpers like
$user = auth('sanctum')->user();
Or you can get it by the request object
//SomeController.php
public function exampleMethod(Request $request)
{
$user = $request->user();
}
To get user by sactum token string like
2|bTNlKViqCkCsOJOXWbtNASDKF7SyHwzHOPLNH
Code be like
use Laravel\Sanctum\PersonalAccessToken;
//...
$token = PersonalAccessToken::findToken($sactumToken);
$user = $token->tokenable;
Note: The most way to pass token is from Authorization headers by bearer
Make sure the sanctum middleware is in api
I was in the same boat; migrated to Sanctum and wondered why all of my $request->user() were empty. The solution for me was to throw some middleware onto the stack to modify the request's user() resolver:
namespace App\Http\Middleware;
use Illuminate\Http\Request;
class PromoteSanctumUser
{
/**
* #param Request $request
* #param \Closure $next
*/
public function handle(Request $request, \Closure $next)
{
$sanctumUser = auth('sanctum')->user();
if ($sanctumUser) {
$request->setUserResolver(function() use ($sanctumUser) {
return $sanctumUser;
});
}
return $next($request);
}
}
It keeps giving failed via my controller when trying to make the post request. I'm trying to make a file upload and storing that file name associated with the user into my db. I'm not sure what I'm doing wrong here, I've tried many ways to fix this but to no avail as I've hit a wall. I believe it may be the way my code's written in my controller but I'm not too sure.
The error I'm getting in the logs is Call to a member function photos() on null which means
auth()->user() is not detecting the authenticated user and there lies the problem which begs the question - how? I'm logged in using correct credentials without issues. How come I can't validate in a separate controller?
What am I doing wrong and how can I fix this?
Note: My React.js and Laravel code bases are separated.
Here's my react form submission:
handleSubmit(e) {
e.preventDefault();
console.log("here in submitHandler()");
let access_token = Cookies.get("access_token").slice(13, -8);
const headers = {
Authorization: `Bearer ${access_token}`
}
console.log(this.state.file_path);
axios.post('http://myendpoint/api/auth/dashboard', this.state.file_path, {headers})
.then(response => {
console.log(response);
}).catch(error => {
console.log(error);
})
};
Here's my FileUploadController.php:
public function uploadTest(Request $request) {
if(auth()->user()) {
auth()->user()->photos()->create([
'file_path' => $request->file('fileToUpload')->getClientOriginalExtension()
]);
return response()->json($request->session()->get("user"));
}
return "failed";
}
Here's my User model:
public function photos() {
return $this->hasMany(Photo::class);
}
Here's my Photo model:
public function user() {
return $this->belongsTo(User::class);
}
Here's my auth for creating user and logging in (AuthController.php):
public function signup(Request $request) {
$request->validate([
'name' => 'required|string',
'email' => 'required|string|email|unique:users',
'password' => 'required|string|confirmed'
]);
$user = new User([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password)
]);
$user->save();
return response()->json([
'message' => 'Successfully created user!'
], 201);
}
public function login(Request $request) {
$request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
'remember_me' => 'boolean'
]);
$credentials = request(['email', 'password']);
if(!Auth::attempt($credentials))
return response()->json([
'message' => 'Unauthorized'
], 401);
$user = $request->user();
$tokenResult = $user->createToken('Personal Access Token');
$token = $tokenResult->token;
if ($request->remember_me)
$token->expires_at = Carbon::now()->addWeeks(1);
$token->save();
$request->session()->put("user", $user);
return response()->json([
'access_token' => $tokenResult->accessToken,
'token_type' => 'Bearer',
'expires_at' => Carbon::parse(
$tokenResult->token->expires_at
)->toDateTimeString(),
$user
]);
}
I tried lot to search about the problem. I couldn't find any solution. Please help me to understand what i am doing wrong.
I am attaching the code:
UserController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use Illuminate\Support\Facades\Auth;
class UserController extends Controller
{
public function signup(Request $request){
$this->validate($request,[
'name' => 'required',
'email' => 'required|unique:users',
'password' => 'required'
]);
$user = new User([
'name' => $request->input('name'),
'email' => $request->input('email'),
'password' => bcrypt($request->input('password ')),
]);
$user->save();
return response()->json([
'state' => 'success',
'message' => 'User created.'
],201);
}
public function signin(Request $request){
$credentials = $request->only('email', 'password');
dd(Auth::attempt($credentials));
if (!$token = $this->guard()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
}
And i have routes in api.php
Route::prefix('user')->group(function () {
Route::post('signup', 'UserController#signup');
Route::post('signin', 'UserController#signin');
});
I have
I have this in database
I sent the below json to signup first, but then when i sent to signin i am getting failed.
{
"name":"ironman",
"email":"ironman#yahoo.com",
"password":"avengers"
}
This is a brand new installation of laravel 5.4 (same with 5.5), Using detailt User migration and model came with it.
When i tried to diagnose the problem myself, i found that the password_very is returning false all the time in Auth package.
I am using default password field, hashing it while creating users as other similar questions answered.
I am using php artisan serv.
I am using postman to send this request.
Please help,
This is pulling null from the request:
$request->input('password '); // notice the space
'password' => bcrypt($request->input('password ')),
You probably did not intend to put a space at the end of the input name:
$request->input('password'); // no space
'password' => bcrypt($request->input('password')),
How to redirect back in laravel default authentication system.For example in Auth\RegisterController
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'contact_no' => 'required|unique:users',
'password' => 'required|string|min:6|confirmed',
]);
}
protected function create(array $data)
{
$email = $data['email'];
$token = $data['token'];
$checkUser = Invitation::where('email', $email)
->where('token', $token)
->first();
if (!$checkUser) {
return redirect()->back()->with('error', 'Credentials not matched !');
}
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'contact_no' => $data['contact_no'],
'password' => bcrypt($data['password']),
]);
before creating an user i want to check the user is exist or not with a token came from another table.I checkuser with a query and if i use this before the create method ,
if (!$checkUser) {
return redirect()->back()->with('error', 'Credentials not matched !');
}
it throws an error
Type error: Argument 1 passed to Illuminate\Auth\SessionGuard::login() must be an instance of Illuminate\Contracts\Auth\Authenticatable, instance of Illuminate\Http\RedirectResponse given,
problem occurred with the redirect inside the if condition.If i dd('error') inside if it shows error when check user return null
Use isset to check if a user exists.
if (!isset($checkUser)) {
return redirect()->back()->with('error', 'Credentials not matched !');
}
And lastly, I recommend using Laracasts/Flash for a more fluent flash messaging. So your code would be something like this.
flash()->error('Credentials not matched.');
return redirect()->back();
To sum up my suggestion:
if (!isset($checkUser)) {
flash()->error('Credentials not matched.');
return redirect()->back();
}
You can add your logic in the validator method of the RegisterController like this :
protected function validator(array $data)
{
\Validator::extend('has_invitation', function($attribute, $value, $parameters) use($data){
$checkUser = Invitation::where('email', $data['email'])
->where('token', $value)
->first();
return !$checkUser;
}, "Credentials not matched !");
return Validator::make($data, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed',
'token' => 'required|has_invitation'
]);
}
If there is an error in validation laravel will automaticly redirect to the registration page.
Make a change in if condition in protected function create(array $data) in RegisterController
if (!$checkUser) {
return null;
}
and overwrite the register method from trait RegistersUsers in RegisterController
public function register(Request $request)
{
$this->validator($request->all())->validate();
$user = $this->create($request->all());
if(!$user) return redirect()->back()->with('error','Credentials Not Matched! ');
event(new Registered($user));
$this->guard()->login($user);
return $this->registered($request, $user)
?: redirect($this->redirectPath());
}