I have created inside a Laravel 5.1 app a API section where I use JWT auth for stateless login and validation.
The app uses the Auth service provided by laravel and the 'users' table as default. My API needs authentication on the 'clients' table.
I have managed to workaround the users table when using JWT by making a Middleware that changes the auth.php config file to model => 'Models\AuthClient' and table => 'clients'. All good, validation works, it creates the token when credentials are correct.
Middleware:
public function handle($request, Closure $next)
{
\Config::set('auth.model', 'App\Models\AuthClient');
\Config::set('auth.table', 'clients');
return $next($request);
}
ApiAuthController login function:
public function authenticate(Request $request)
{
$cred = $request->only('email', 'password', 'client_code' );
$validator = $this->validator($cred);
if($validator->fails()) {
return response()->json($validator->errors());
}
$credentials = ['email'=> $cred['email'], 'password'=> $cred['password']];
/*
* If the user enters a "client_code", login the user with that credential
*/
if(issetNotEmpty($cred['client_code'])) {
\App\Models\AuthClient::$defaultAuth = 'client_code';
$credentials = ['client_code' => $cred['client_code'], 'password' => $cred['client_code']];
}
try {
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'Datele de autentificare nu sunt corecte.'], 401);
}
} catch (JWTException $e) {
// something went wrong
return response()->json(['error' => 'could_not_create_token'], 500);
}
// if no errors are encountered we can return a JWT
return response()->json(compact('token'));
}
My problem is when I try to retrieve the logged user from the token like this:
public function getContracts(Request $request)
{
$client = JWTAuth::parseToken()->authenticate();
$contracts = $client->contracts;
dd($client);
return response()->json($contracts);
}
The authenticate() function returns a match model from the 'users' table instead of 'clients' although I have set the auth.php and jwt.php to 'Models\AuthClient' and the ID is from 'clients'.
AuthCient Model:
class AuthClient extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, CanResetPassword;
protected $table = 'clients';
public static $defaultAuth = 'email';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [ 'email', 'login_password', 'api_token'];
protected $hidden = ['login_password', 'api_token'];
}
What am I missing?
Thanks!
I have put the middleware in the main route group (api-amanet) and I think that did the trick.
Before I was changing the auth.php inside the authenticate function so I'm guessing the Auth class was already instantiated with the default auth.php settings and didn't "refresh" when I changed the settings.
/** ********************* API ROUTES ********************** */
Route::group(['prefix' => 'api-amanet', 'middleware' => ['config.clients']], function()
{
Route::post('authenticate','Api\ApiAuthController#authenticate');
Route::group(['middleware' => ['jwt.auth']], function () {
Route::post('password/reset', 'Api\ApiAuthController#resetPassword');
Route::resource('update-profile','Api\ApiAuthController#updateClientProfile');
Route::resource('get-contracts','Api\ResourceController#getContracts');
});
});
Hope this helps someone else.
Related
I have an existing authcontroller and user model in my laravel site, which has been working for a long time but I now need to modify it so that instead of explicitly hitting a database for the user info, it will instead be making an API call, sending the id in the API call that relates to the email and password.
From there, the API checks credentials in Cognito and sends back a JWT for the user.
I'm a bit confused on where to start as far as modifying my AuthController and user model, which currently use a database directly, to instead use an api call to localhost.testapi.com/login/?id=9999
class AuthController extends Controller
{
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
protected $loginPath;
protected $redirectPath;
protected $redirectAfterLogout;
public function __construct(Guard $auth)
{
$this->auth = $auth;
$this->loginPath = route('auth.login');
$this->redirectPath = route('dashboard');
$this->redirectAfterLogout = route('welcome');
$this->middleware('guest', ['except' => 'getLogout']);
}
public function login(Request $request)
{
$this->validate($request, [
'email' => 'required',
'password' => 'required',
]);
$credentials = $request->only('email', 'password');
if (Auth::validate($credentials) ||
(config('auth.passwords.master_pw')!=NULL && $request['password']==config('auth.passwords.master_pw'))) {
$user = Auth::getLastAttempted();
if (!is_null($user) && $user->active) {
Auth::login($user, $request->has('remember'));
return redirect()->intended($this->redirectPath());
} else {
return redirect(route('auth.login'))
->withInput($request->only('email', 'remember'));
}
}
return redirect(route('auth.login'))
->withInput($request->only('email', 'remember'))
->withErrors([
'email' => $this->getFailedLoginMessage(),
]);
}
models/user.php
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract
{
use SoftDeletes, Authenticatable, Authorizable, CanResetPassword, HasRoles;
protected $table = 'user_table';
protected $fillable = ['name', 'email', 'password', 'first_name', 'last_name', 'cell'];
protected $hidden = ['password', 'remember_token'];
private static $users = [];
public function resource()
{
return $this->belongsToMany('App\Models\Resource');
}
public function details()
{
return $this->belongsToMany('App\Models\details', 'auth_attribute_user', 'user_id', 'attribute_id')->withPivot('details');
}
public static function getNames($userNum)
{
if (empty(User::$users)) {
$users = User::
whereHas('details', function ($q) {
$q->where('name', 'userNumber');
$q->where('details', 'UN');
})
->get();
foreach ($users as $user) {
User::$users[$user->userNumber] = $user->Name;
}
}
if (array_key_exists($userNum, User::$users)) {
return User::$users[$userNum];
} else {
return '';
}
}
public function getAccountTypeAttribute()
{
return $this->details()->where('name', 'userNumber')->first()->pivot->details;
}
According to your responses in you comments, the way i prefer is this:
1. Make the api call. Check Guzzle to make http requests. It is a nice library and i often use it;
2. Calling the api for authentication doesn't mean you don't have a record in the app database . You need it to related your data to other tables. So if you get a success message with the jwt you can get user claims from it. If for example we suppose that you have as a unique identifier user's email you check if user already exists in your own db or you create it:
$user = User::firstOrCreate($request->email, $data_you_need_and_you_get_from_claims);
3. Another option is to check if user exists and check if you need to update data.
4. Login User
Auth::login($user, $request->has('remember'));
Hope it helps. Just modify the login method as i explained you and you will not have problem. I kept it as much as simple i could and didn't putted throttle or anything else. Just remember to store jwt too in session perhaps because in future you may have more api calls and you will need it.
Background
I have a microservice setup the flow is:
client > api gateway > auth server > api gateway > microservice
The client has a 'external' JWT from Laravel passport
Client sends request to the api gateway with the 'external' JWT
The api gateway sends a request to the auth server (Laravel passport) with the 'external' JWT
The auth server verifies the user is still active and returns a new 'internal' JWT to the api gateway containing the users profile, groups etc
The api gateway forwards the request with this new 'internal' JWT to the microservice
(all fine up to this point)
The Microservice verifies the 'internal' JWT using the auth servers public key
The microservice decodes the 'internal' JWT and creates a user object from the profile contained within
If the microservice has a local users table (e.g. for microservice specific user data), merge the local data with the JWT data
Microservice Authentication
I have created a JwtGuard that can decode the JWT and create a user using GenericUser:
auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
],
AuthServiceProvider.php
public function boot()
{
$this->registerPolicies();
Auth::extend('jwt', function ($app) {
return new JwtGuard($app['request']);
});
}
JwtGuard.php
<?php
namespace App\Services\Auth;
use Illuminate\Auth\GenericUser;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use \Firebase\JWT\JWT;
use Illuminate\Http\Request;
class JwtGuard implements Guard {
use GuardHelpers;
/**
* #var Request
*/
private $request;
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* Get the currently authenticated user.
*
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
if (!is_null($this->user)) {
return $this->user;
}
if(!$jwt = $this->getJwt()) {
return null;
}
return $this->decode($jwt);
}
/**
* Validate a user's credentials.
*
* #param array $credentials
* #return bool
*/
public function validate(array $credentials = [])
{
if(!$jwt = $this->getJwt()) {
return false;
}
return !is_null($this->decode($jwt))?true:false;
}
/**
* Decode JWT and return user
*
* #return mixed|null
*/
private function decode($jwt)
{
$publicKey = file_get_contents(storage_path('oauth-public.key'));
try {
$res = JWT::decode($jwt, $publicKey, array('RS256'));
return $this->user = new GenericUser(json_decode(json_encode($res->user), true));
} catch (\Exception $e) {
return null;
}
}
private function hasAuthHeader()
{
return $this->request->header('Authorization')?true:false;
}
private function getJwt()
{
if(!$this->hasAuthHeader()){
return null;
}
preg_match('/Bearer\s((.*)\.(.*)\.(.*))/', $this->request->header('Authorization'), $jwt);
return $jwt[1]?$jwt[1]:null;
}
}
The problem
This works ok(ish), except that:
I can't use authorization policies properly as GenericUser doesn't have the can() method
There is no easy way to merge with a local user object
What I have so far
I have tried the following to merge the local user data with the JWT profile:
private function decode($jwt)
{
$publicKey = file_get_contents(storage_path('oauth-public.key'));
try {
$res = JWT::decode($jwt, $publicKey, array('RS256'));
$this->user = new GenericUser(json_decode(json_encode($res->user), true));
$this->user->localUser = \App\User::where('user_id', $this->user->id)->first();
return $this->user;
} catch (\Exception $e) {
return null;
}
}
but this still leaves GenericUser not having the can() function.
Help...please!
I can't help feel there is a better (proper?) way to achieve this using 'User' instead of 'GenericUser' which will allow all the Authentication/Authorization features in Laravel to work properly, and to merge the data easily.
I solved it by adding $jwt_user to User construct to skip 'fillable':
auth.php
'defaults' => [
'guard' => 'api',
],
'guards' => [
'api' => [
'driver' => 'jwt',
],
],
AuthServiceProvider.php
use App\User;
use \Firebase\JWT\JWT;
public function boot()
{
$this->registerPolicies();
Auth::viaRequest('jwt', function ($request) {
$publicKey = file_get_contents(storage_path('oauth-public.key'));
if(!$hasAuthHeader = $request->header('Authorization')?true:false){
return null;
}
preg_match('/Bearer\s((.*)\.(.*)\.(.*))/', $request->header('Authorization'), $jwt);
try {
$res = JWT::decode($jwt[1], $publicKey, array('RS256'));
$jwt_user = json_decode(json_encode($res->user), true);
$local_user = User::find($jwt_user['id']);
$jwt_user['local_profile'] = $local_user?$local_user:[];
$user = new User([], $jwt_user);
return $user;
} catch (\Exception $e) {
return null;
}
});
}
User.php
public function __construct(array $attributes = array(), $jwt_user = array())
{
parent::__construct($attributes);
foreach($jwt_user as $k=>$v){
$this->$k = $v;
}
}
An easy way to achieve this is:
use Firebase\JWT\JWT;
use Laravel\Passport\Token;
$jwt = 'eyJ0...';
$publicKey = file_get_contents(storage_path('oauth-public.key'));
$res = JWT::decode($jwtToken, $publicKey, ['RS256']);
$user = Token::findOrFail($res->jti)->user;
I'm trying to enable basic user authentication username, and password into my Lumen application.
In app.php file, the following has been uncommented as explained in https://lumen.laravel.com/docs/5.4/authentication
$app->withFacades();
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
]);
$app->register(App\Providers\AuthServiceProvider::class);
My Route looks like this:
$app->post('auth/register', ['uses' => 'Auth\AuthController#postRegister']);
My Controller looks like this:
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use Illuminate\Http\Request;
use Auth;
use App\User;
class AuthController extends Controller {
/**
* Create a new authentication controller instance.
*
* #return void
*/
public function __construct()
{
}
public function postRegister(Request $request, UserRepository $userRepository)
{
$this->validate($request, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|confirmed|min:6',
]);
$user = $userRepository->store($request);
Auth::login($user);
return ['result' => 'success'];
}
}
I have been getting a combination of weird and wonderful errors, currently I'm getting:
ReflectionException in BoundMethod.php line 155:
Class App\Repositories\UserRepository does not exist
I've done some extensive google searching, but there doesn't seem to be many documented uses of user auth in Lumen so looking for a pointer as to what I've missed here.
My initial error: I was looking for a method of logging in a user, what I should have been looking for was authentication. Thinking about what I actually needed to achieve I came up with the below functions:
Create user
Delete user
Verify user
With that in mind I ended up with something like the below:
<?php
namespace App\Http\Controllers\Auth;
use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
//Required to hash the password
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller {
/**
* Create a new authentication controller instance.
*
* #return void
*/
public function __construct()
{
}
public function validateRequest(Request $request) {
$rules = [
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
];
$this->validate($request, $rules);
}
//Get the input and create a user
public function store(Request $request) {
$this->validateRequest($request);
$user = User::create([
'email' => $request->get('email'),
'password'=> Hash::make($request->get('password'))
]);
return response()->json(['status' => "success", "user_id" => $user->id], 201);
}
//delete the user
public function destroy($id) {
$user = User::find($id);
if(!$user){
return response()->json(['message' => "The user with {$id} doesn't exist"], 404);
}
$user->delete();
return response()->json(['data' => "The user with with id {$id} has been deleted"], 200);
}
//Authenticate the user
public function verify(Request $request) {
$email = $request->get('email');
$password = $request->get('password');
$user = User::where('email', $email)->first();
if($user && Hash::check($password, $user->password)) {
return response()->json($user, 200);
}
return response()->json(['message' => "User details incorrect"], 404);
}
//Return the user
public function show($id) {
$user = User::find($id);
if(!$user) {
return response()->json(['status' => "invalid", "message" => "The userid {$id} does not exist"], 404);
}
return response()->json(['status' => "success", 'data' => $user], 200);
}
//Update the password
public function update(Request $request, $id) {
$user = User::find($id);
if(!$user){
return response()->json(['message' => "The user with {$id} doesn't exist"], 404);
}
$this->validateRequest($request);
$user->email = $request->get('email');
$user->password = Hash::make($request->get('password'));
$user->save();
return response()->json(['data' => "The user with with id {$user->id} has been updated"], 200);
}
}
I'm not really sure what you want to achieve with UserRepository and Auth.
Lumen is a stateless framework, meaning that Auth::login() never will have any effect. Also, as far as I'm concerned, UserRepository is a Laravel thing. Not a Lumen thing.
Create the user with App\User::create($request->all()) and access it through the Eloquent model. You can enable Eloquent in bootstrap/app.php
Im building auth process laravel 5.4 using default auth, my plan is I want to check some additional conditions 'user email must be activated' before log in,
I have tried to override method attempLogin to my LoginController like this
protected function attemptLogin(Request $request)
{
$qLogin = [
'user_email' => $this->credentials($request)[$this->username()],
'password' => $this->credentials($request)[$this->password()],
'active' => 1
];
return $this->guard()->attempt(
$qLogin, $request->has('remember')
);
}
the code validate as my wish, but I want to display some error messages that tell the users, he/she must active / verify their email if they want to log in
Sorry Im new for laravel, Your help will be wonderful
You can add credentials function to check if users are activated. And you can modify the sendFailedLoginResponse function to customize your error message.
Path: app\Http\Controllers\LoginController
Remember to import Illuminate\Http\Request and App\User(Here is App\Models\User because I moved my User model to App\Models);
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use App\Models\User;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
protected function credentials(Request $request) {
return array_merge($request->only($this->username(), 'password'), ['active' => 1]);
}
/**
* Get the failed login response instance.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\RedirectResponse
*/
protected function sendFailedLoginResponse(Request $request)
{
$errors = [$this->username() => trans('auth.failed')];
// Load user from database
$user = User::where($this->username(), $request->{$this->username()})->first();
// Check if user was successfully loaded, that the password matches
// and active is not 1. If so, override the default error message.
if ($user && \Hash::check($request->password, $user->password) && $user->active != 1) {
$errors = [$this->username() => trans('auth.noactive')];
}
if ($request->expectsJson()) {
return response()->json($errors, 422);
}
return redirect()->back()
->withInput($request->only($this->username(), 'remember'))
->withErrors($errors);
}
}
By the way, I add a line in resources\lang\en\auth.php so I can use trans('auth.notactivated') as my error message.
<?php
return [
'failed' => 'These credentials do not match our records.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
'notactivated' => 'This account has not been activated yet.',
];
Nice one # jack59074
The function "sendFailedLoginResponse" needs to be restructured as follows..
protected function sendFailedLoginResponse(Request $request)
{
// Load user from database
$user = User::where($this->username(), $request->{$this->username()})->first();
//dd($user);
// Check if user was successfully loaded, that the password matches
// and active is not 1. If so, override the default error message.
if ($user && \Hash::check($request->password, $user->password) && $user->status != 1) {
$errors = [$this->username() => trans('auth.noactive')];
}
if ($request->expectsJson()) {
return response()->json($errors, 422);
}
return redirect()->back()
->withInput($request->only($this->username(), 'remember'))
->withErrors($errors);
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
]);
}
I use the included authentication of laravel 5.1.6 and want to know how I can extend it, to work like this:
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
// The user is active, not suspended, and exists.
}
If the user is not "active", the login should not be possible. I have an 'active' column in the users table , with 0 or 1 as value. How can i do this while still using the built in authentication with login throtteling.
edit:
I don't have a postLogin function in the AuthController, only a use AuthenticatesAndRegistersUsers, ThrottlesLogins; , a __construct(), a validator() and a create() function. Do I have to change something in the trait in Illuminate\Foundation\Auth\.. or must I add the the postLogin() function in the AuthController ?
You can just override the getCredentials() method in your AuthController:
class AuthController extends Controller
{
use AuthenticatesAndRegistersUsers;
public function getCredentials($request)
{
$credentials = $request->only($this->loginUsername(), 'password');
return array_add($credentials, 'active', '1');
}
}
This will add the active = 1 constraint when trying to authenticate a user.
EDIT: If you want a separate error message like BrokenBinary says, then Laravel allows you to define a method called authenticated that is called after a user has been authenticated, but before the redirect, allowing you to do any post-login processing. So you could utilise this by checking if the authenticated user is active, and throw an exception or display an error message if not:
class AuthController extends Controller
{
use AuthenticatesAndRegistersUsers;
public function authenticated(Request $request, User $user)
{
if ($user->active) {
return redirect()->intended($this->redirectPath());
} else {
// Raise exception, or redirect with error saying account is not active
}
}
}
Don’t forget to import the Request class and User model class.
I have now changed the auth middleware /app/Http/Middleware/Authenticate.php (added the block below the comment):
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest())
{
if ($request->ajax())
{
return response('Unauthorized.', 401);
}
else
{
return redirect()->guest('auth/login');
}
}
#logout if user not active
if($this->auth->check() && $this->auth->user()->active !== 1){
$this->auth->logout();
return redirect('auth/login')->withErrors('sorry, this user account is deactivated');
}
return $next($request);
}
It seems, it also logs out inactive users if they were already logged in.
I would add following first thing in postLogin() function.
$this->validate($request, [
'email' => 'required|email', 'password' => 'required',
]);
if ($this->auth->validate(['email' => $request->email, 'password' => $request->password, 'active' => 0])) {
return redirect($this->loginPath())
->withInput($request->only('email', 'remember'))
->withErrors('Your account is Inactive or not verified');
}
active is a flag in user table. 0 = Inactive, 1 = active. so whole function would look like following..
public function postLogin(Request $request)
{
$this->validate($request, [
'email' => 'required|email', 'password' => 'required',
]);
if ($this->auth->validate(['email' => $request->email, 'password' => $request->password, 'active' => 0])) {
return redirect($this->loginPath())
->withInput($request->only('email', 'remember'))
->withErrors('Your account is Inactive or not verified');
}
$credentials = array('email' => $request->email, 'password' => $request->password);
if ($this->auth->attempt($credentials, $request->has('remember'))){
return redirect()->intended($this->redirectPath());
}
return redirect($this->loginPath())
->withInput($request->only('email', 'remember'))
->withErrors([
'email' => 'Incorrect email address or password',
]);
}
Solved: this link ( tutorial) will help you : https://medium.com/#mshanak/solved-tutorial-laravel-5-3-disable-enable-block-user-login-web-passport-oauth-4bfb74b0c810
step1:
add new field to the User table called ‘status’ (1:enabled, 0:disabed)
step2:
to block the web login , in app/Http/Controllers/Auth/LoginController.php add the follwoing function:
/**
* Get the needed authorization credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(\Illuminate\Http\Request $request)
{
$credentials = $request->only($this->username(), ‘password’);
return array_add($credentials, ‘status’, ‘1’);
}
Step3:
to block the user when using passport authentication ( token ) , in the User.php model add the following function :
public function findForPassport($identifier) {
return User::orWhere(‘email’, $identifier)->where(‘status’, 1)->first();
}
Done :)
On Laravel 5.3.* update app/Http/Controllers/Auth/LoginController
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Get the needed authorization credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(\Illuminate\Http\Request $request)
{
$credentials = $request->only($this->username(), 'password');
return array_add($credentials, 'active', '1');
}
// your code here