Laravel Sanctum SPA Logout Testing - laravel

I logout of my application via:
Auth::guard('web')->logout();
Please note, it's the SPA use of Sanctum so no tokens.
Then in my test, I check if the user is logged out via:
Sanctum::actingAs(User::first(), ['*']);
$response = $this->postJson(
route('logout')
); // runs auth logout
$this->assertGuest();
The above fails, yet, when I do not use Sanctum::actingAs(User::first(), ['*']); and instead do a call to my login:
$response = $this->postJson(
route('login'),
[
'email' => User::first()->email,
'password' => 'xyz',
]
);
The test passes.
Am I missing something with Sanctum::actingAs(User::first(), ['*']);, the docs say to use this for testing, does this not work when it's an SPA with cookie based authentication?

Change your logout middleware to web:
Route::middleware('auth:web')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
});
Then, in logout():
class AuthController extends Controller
{
/**
* #return JsonResponse
*/
public function logout(): JsonResponse
{
// If with tokens - delete them:
// Auth::user()->tokens()->delete();
Auth::guard('web')->logout();
return response()->json([
'message' => 'Tokens Revoked'
]);
}
}

Related

Tymon JWTAUTH exclude routes from token

I installed laravel 5.6 and configured tymon jwtauth token.
All working fine and done.
In laravel 5.1 i used this function to exclude function from jwt authentication:
public function __construct() {
$this->middleware('jwt.auth', ['except' => ['login']]);
}
tried also :
public function __construct() {
$this->middleware('jwt.auth')->except([
'login'
]);
}
It's not working in laravel 5.6, any idea? I want to exclide the login function so the user can login, take the token and send it with each request.
protected $routeMiddleware = [
'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',
]
I was putting this in
protected $Middleware
in api.php I put my routes not in web.php
Route::post('user/register', 'APIRegisterController#register');
Route::post('user/login', 'APILoginController#login');
Route::middleware('jwt.auth')->get('testFunc', 'testController#testFunc');

Laravel Guest Middleware and user authentication

I just created a simple login with guest middleware that allows the user to access one account at a time, but I am just worried if this is the right way to do it.
/** Routes **/
Route::group(['middleware' => 'guest'], function () {
Route::get('/', 'LoginController#index')->name('login');
Route::post('/', 'LoginController#post')->name('login.post');
});
/** login.post controller **/
public function post(Request $request){
$this->rules($request);
$rules = array(
'username' => $request->username,
'password' => $request->password,
);
if(Auth::attempt($rules)) {
if(Auth::user()->is_active == true){
/** IF THE USER IS CURRECTLY LOGIN **/
if(Auth::user()->is_login == true){
Auth::logout();
Session::flash('multilog', 'Your account is log-in to another device!!');
return redirect('/')->withInput();
}
$user = user::find(Auth::user()->id);
$user->is_login = true;
$user->save();
return redirect('admin/home');
}
Session::flash('unactivated', 'Your account is not activated!!');
return redirect('/')->withInput();
}
Session::flash('unmatch', 'Invalid username or password!!');
return redirect('/')->withInput();
}
/** **/
If you are not sure, you can use Laravel to create authentication. Write in command line:
php artisan make:auth
Then just look how the logic works in files.
More you can read here:
https://laravel.com/docs/5.5/authentication

Laravel Passport Scopes

I am a bit confused on the laravel scopes part.
I have a user model and table.
How can I assign a user the role of user, customer and/or admin.
I have a SPA with vue and laravel api backend. I use https://laravel.com/docs/5.3/passport#consuming-your-api-with-javascript
Passport::tokensCan([
'user' => 'User',
'customer' => 'Customer',
'admin' => 'Admin',
]);
How can i assign which user model has which scope(s)?
Or are scopes not the same as roles?
How would you implement this?
Thanks in advance!
Or are scopes not the same as roles?
The biggest difference between the two is the context they apply to. Role-based Access Control (RBAC) governs the access control of a user when using the web application directly, while Oauth-2 scope governs the access to the API resources for an external client on behalf of a user.
How can i assign which user model has which scope(s)?
In general Oauth flow, a user (as a resource owner) is requested to authorize a client on things that it can and cannot do on his/her behalf, these are what you called scope. On successful authorization the scope being requested by the client will be assigned to the generated token not to the user per se.
Depending on which Oauth grant flow that you choose, the client should include the scope on its request. In Authorization code grant flow the scope should be included on HTTP GET query parameter when redirecting the user to authorization page, while on Password grant flow the scope must be included in HTTP POST body parameter to request a token.
How would you implement this?
This is an example with Password grant flow, with assumption that you completed the laravel/passport setup beforehand
Define scopes for both admin and user role. Be specific as you can, for example: admin can manage-order and user only read it.
// in AuthServiceProvider boot
Passport::tokensCan([
'manage-order' => 'Manage order scope'
'read-only-order' => 'Read only order scope'
]);
Prepare the REST controller
// in controller
namespace App\Http\Controllers;
class OrderController extends Controller
{
public function index(Request $request)
{
// allow listing all order only for token with manage order scope
}
public function store(Request $request)
{
// allow storing a newly created order in storage for token with manage order scope
}
public function show($id)
{
// allow displaying the order for token with both manage and read only scope
}
}
Assign the route with api guard and scope
// in api.php
Route::get('/api/orders', 'OrderController#index')
->middleware(['auth:api', 'scopes:manage-order']);
Route::post('/api/orders', 'OrderController#store')
->middleware(['auth:api', 'scopes:manage-order']);
Route::get('/api/orders/{id}', 'OrderController#show')
->middleware(['auth:api', 'scopes:manage-order, read-only-order']);
And when issuing a token check the user role first and grant the scope based on that role. To achieve this, we need an extra controller that use AuthenticatesUsers trait to provide login endpoint.
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
class ApiLoginController extends Controller
{
use AuthenticatesUsers;
protected function authenticated(Request $request, $user)
{
// implement your user role retrieval logic, for example retrieve from `roles` database table
$role = $user->checkRole();
// grant scopes based on the role that we get previously
if ($role == 'admin') {
$request->request->add([
'scope' => 'manage-order' // grant manage order scope for user with admin role
]);
} else {
$request->request->add([
'scope' => 'read-only-order' // read-only order scope for other user role
]);
}
// forward the request to the oauth token request endpoint
$tokenRequest = Request::create(
'/oauth/token',
'post'
);
return Route::dispatch($tokenRequest);
}
}
Add route for api login endpoint
//in api.php
Route::group('namespace' => 'Auth', function () {
Route::post('login', 'ApiLoginController#login');
});
Instead of doing POST to /oauth/token route, POST to the api login endpoint that we provided before
// from client application
$http = new GuzzleHttp\Client;
$response = $http->post('http://your-app.com/api/login', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'username' => 'user#email.com',
'password' => 'my-password',
],
]);
return json_decode((string) $response->getBody(), true);
Upon successful authorization, an access_token and a refresh_token based on scope that we define before will be issued for the client application. Keep that somewhere and include the token to the HTTP header whenever making a request to the API.
// from client application
$response = $client->request('GET', '/api/my/index', [
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer '.$accessToken,
],
]);
The API now should return
{"error":"unauthenticated"}
whenever a token with under privilege is used to consumed restricted endpoint.
Implement the Raymond Lagonda response and it works very well, just to be careful with the following.
You need to override some methods from AuthenticatesUsers traits in ApiLoginController:
/**
* Send the response after the user was authenticated.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
protected function sendLoginResponse(Request $request)
{
// $request->session()->regenerate(); // coment this becose api routes with passport failed here.
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user())
?: response()->json(["status"=>"error", "message"=>"Some error for failes authenticated method"]);
}
/**
* Get the failed login response instance.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\RedirectResponse
*/
protected function sendFailedLoginResponse(Request $request)
{
return response()->json([
"status"=>"error",
"message"=>"Autentication Error",
"data"=>[
"errors"=>[
$this->username() => Lang::get('auth.failed'),
]
]
]);
}
If you changed the login: username field to a custom username field eg: e_mail. You must refine the username method as in your LoginController.
Also you have to redefine and edit the methods: validateLogin, attemptLogin, credentials since once the login is validated, the request is forwarded to passport and must be called username.
I know this is a little late, but if you're consuming a backend API in an SPA using the CreateFreshApiToken in web middleware, then you can simply add an 'admin' middleware to your app:
php artisan make:middleware Admin
Then in \App\Http\Middleware\Admin do the following:
public function handle($request, Closure $next)
{
if (Auth::user()->role() !== 'admin') {
return response(json_encode(['error' => 'Unauthorised']), 401)
->header('Content-Type', 'text/json');
}
return $next($request);
}
Make sure you have added the role method to \App\User to retrieve the users role.
Now all you need to do is register your middleware in app\Http\Kernel.php $routeMiddleware, like so:
protected $routeMiddleware = [
// Other Middleware
'admin' => \App\Http\Middleware\Admin::class,
];
And add that to your route in routes/api.php
Route::middleware(['auth:api','admin'])->get('/customers','Api\CustomersController#index');
Now if you try to access the api without permission you will receive a "401 Unauthorized" error, which you can check for and handle in your app.
I've managed to get this into working, with #RaymondLagonda solution, for Laravel 5.5 with Sentinel, but it should, also work, without Sentinel.
The solution needs some class methods overriding (so please keep that in mind, for future updates), and adds some protection to your api routes (not exposing client_secret for example).
First step, is to modify your ApiLoginController in order to add construct function:
public function __construct(Request $request){
$oauth_client_id = env('PASSPORT_CLIENT_ID');
$oauth_client = OauthClients::findOrFail($oauth_client_id);
$request->request->add([
'email' => $request->username,
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client->secret]);
}
In this example, you need to define var ('PASSPORT_CLIENT_ID') in your .env and create OauthClients Model, but you can safely skip this by putting your proper test values here.
One thing to notice, is that we are setting $request->email value to username, just to stick to Oauth2 convention.
Second step is, to override, sendLoginResponse method which is causing errors like Session storage not set, we don't need sessions here, cause it is api.
protected function sendLoginResponse(Request $request)
{
// $request->session()->regenerate();
$this->clearLoginAttempts($request);
return $this->authenticated($request, $this->guard()->user())
?: redirect()->intended($this->redirectPath());
}
Third step is, to modify your authenticated methods as suggested by #RaymondLagonda. You need to write your own logic here, and especially configure your scopes.
And final step (in case you are using Sentinel) is to modify AuthServiceProvider. Add
$this->app->rebinding('request', function ($app, $request) {
$request->setUserResolver(function () use ($app) {
return \Auth::user();
// return $app['sentinel']->getUser();
});
});
just after $this->registerPolicies(); in boot method.
After these steps you should be able, to get your api working, by providing username ('this will always be email, in this implementation'), password and grant_type='password'
At this point, you can add to middlewares scopes scopes:... or scope:... to protect your routes.
I hope, it is going to really help...
With #RaymondLagonda solution. If you are getting a class scopes not found error, add the following middleware to the $routeMiddleware property of your app/Http/Kernel.php file:
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,`
Also, if you are getting the error Type error: Too few arguments to function, you should be able to get the $user from the request like below.
(I am using laratrust for managing roles)
public function login(Request $request)
{
$email = $request->input('username');
$user = User::where('email','=',$email)->first();
if($user && $user->hasRole('admin')){
$request->request->add([
'scope' => 'manage-everything'
]);
}else{
return response()->json(['message' => 'Unauthorized'],403);
}
$tokenRequest = Request::create(
'/oauth/token',
'post'
);
return Route::dispatch($tokenRequest);
}
Thanks for this, this question was riddling my mind for a while! I took Raymond Lagonda's solution customised it a little for Laravel 5.6, using the built-in rate limiting, using a single thirdparty client (or be more custom if needed), while still giving each user a list of permissions (scopes).
Uses Laravel Passport password grant and follows Oauth flow
Gives you ability to set roles (scopes) for different users
don't expose/release client ID or client secret, only the user's username (email) and password, pretty much a password grant, minus the client/grant stuff
Examples at bottom
routes/api.php
Route::group(['namespace' => 'ThirdParty', 'prefix' => 'thirdparty'], function () {
Route::post('login', 'ApiLoginController#login');
});
ThirdParty/ApiLoginController.php
<?php
namespace App\Http\Controllers\ThirdParty;
use Hash;
use App\User;
use App\ThirdParty;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class ApiLoginController extends Controller
{
use AuthenticatesUsers;
/**
* Thirdparty login method to handle different
* clients logging in for different reasons,
* we assign each third party user scopes
* to assign to their token, so they
* can perform different API tasks
* with the same token.
*
* #param Request $request
* #return Illuminate\Http\Response
*/
protected function login(Request $request)
{
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
$user = $this->validateUserLogin($request);
$client = ThirdParty::where(['id' => config('thirdparties.client_id')])->first();
$request->request->add([
'scope' => $user->scopes,
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret
]);
return Route::dispatch(
Request::create('/oauth/token', 'post')
);
}
/**
* Validate the users login, checking
* their username/password
*
* #param Request $request
* #return User
*/
public function validateUserLogin($request)
{
$this->incrementLoginAttempts($request);
$username = $request->username;
$password = $request->password;
$user = User::where(['email' => $username])->first();
abort_unless($user, 401, 'Incorrect email/password.');
$user->setVisible(['password']);
abort_unless(Hash::check($password, $user->password), 401, 'Incorrect email/password.');
return $user;
}
}
config/thirdparties.php
<?php
return [
'client_id' => env('THIRDPARTY_CLIENT_ID', null),
];
ThirdParty.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ThirdParty extends Model
{
protected $table = 'oauth_clients';
}
.env
## THIRDPARTIES
THIRDPARTY_CLIENT_ID=3
php artisan make:migration add_scope_to_users_table --table=users
// up
Schema::table('users', function (Blueprint $table) {
$table->text('scopes')->nullable()->after('api_access');
});
// down
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('scopes');
});
(note: api_access is a flag which decides whether a user can login to the website/frontend portion of the app, to view dashboards/records etc.),
routes/api.php
Route::group(['middleware' => ['auth.client:YOUR_SCOPE_HERE', 'throttle:60,1']], function () {
...routes...
});
MySQL - Users scopes
INSERT INTO `users` (`id`, `created_at`, `updated_at`, `name`, `email`, `password`, `remember_token`, `api_access`, `scopes`)
VALUES
(5, '2019-03-19 19:27:08', '2019-03-19 19:27:08', '', 'hello#email.tld', 'YOUR_HASHED_PASSWORD', NULL, 1, 'YOUR_SCOPE_HERE ANOTHER_SCOPE_HERE');
MySQL - ThirdParty Oauth Client
INSERT INTO `oauth_clients` (`id`, `user_id`, `name`, `secret`, `redirect`, `personal_access_client`, `password_client`, `revoked`, `created_at`, `updated_at`)
VALUES
(3, NULL, 'Thirdparty Password Grant Client', 'YOUR_SECRET', 'http://localhost', 0, 1, 0, '2019-03-19 19:12:37', '2019-03-19 19:12:37');
cURL - Logging in/requesting a token
curl -X POST \
http://site.localhost/api/v1/thirdparty/login \
-H 'Accept: application/json' \
-H 'Accept-Charset: application/json' \
-F username=hello#email.tld \
-F password=YOUR_UNHASHED_PASSWORD
{
"token_type": "Bearer",
"expires_in": 604800,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciO...",
"refresh_token": "def502008a75cd2cdd0dad086..."
}
Use longlived access_token/refresh_token as normal!
Accessing forbidden scope
{
"data": {
"errors": "Invalid scope(s) provided."
},
"meta": {
"code": 403,
"status": "FORBIDDEN"
}
}

Laravel login redirected you too many times

I have been struggling with this from quiet a time now, what i am trying is to redirect all the url's hit by non-logged in users to login page and it gives me this error, which I am sure is because it is creating a loop on /login URL. authentication is checking for authorized user in login page also. however I wish the login page should be an exception when checking the auth. I may be doing something wrong which I am not able to get. here goes my code.
routes.php
Route::post('login', 'Auth\AuthController#login');
Route::get('login' , 'Auth\AuthController#showLoginForm');
Route::get('/' , 'Auth\AuthController#showLoginForm');
kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'can' => \Illuminate\Foundation\Auth\Access\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'acl' => \App\Http\Middleware\CheckPermission::class,
];
Authenticate class
class Authenticate
{
public function handle($request, Closure $next, $guard = null) {
if (Auth::guard($guard)->guest()) {
if ($request->ajax() || $request->wantsJson()) {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('login');
}
}
return $next($request);
}
}
AuthController class
class AuthController extends Controller {
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
protected $redirectTo = '/dashboard';
protected $loginPath = '/login';
protected $redirectPath = '/dashboard';
public function __construct(){
$this->middleware('auth', ['except' =>'login']);
/* I have been trying these many things to fix this, all in loss.
// $this->middleware('acl'); // To all methods
// $this->middleware('acl', ['only' => ['create', 'update']]);
// $this->middleware('guest', ['only' => ['/login']]);
// echo "Message"; exit;
// $this->middleware('auth');
// $this->middleware('auth', ['only' => ['login']]);
// $this->middleware('auth', ['only' => ['/login']]);
// $this->middleware('auth', ['except' => 'login']);
// $this->middleware('guest');
// $this->middleware('guest', ['only' => ['logout' , 'login', '/login', '/']]);
}
Please help me, It going all above my head, seems some sort of rocket science to me. well btw I am new to laravel and may be doing some silly thing around, apologies for that. Thanks in Advance.
You need add route login outside Laravel group:
routes.php
Route::auth();
Route::group(['middleware' => 'auth'], function () {
// All route your need authenticated
});
Aditionally, you can see yours route list using:
php artisan route:list
Why you are doing all this just to redirect every non-logged in user to login form?
i think you can just do this
Routes.php
Route::post('login', 'Auth\AuthController#login');
Route::get('login' , 'Auth\AuthController#showLoginForm');
Route::get('/' , 'Auth\AuthController#showLoginForm');
Route::group(['middleware' => 'auth'], function () {
// any route here will only be accessible for logged in users
});
and auth controller construct should be like this
AuthController
public function __construct()
{
$this->middleware('guest', ['except' => 'logout']);
}
The problem is with your routes.
When I enter and I am not logged out you send me to login(get) route. And as you are specifying the middleware in the construct function in the AuthController, every time a method of the AuthController is called, construct function is called again and sends you back at login.. and it repeats indefinitely.
like #mkmnstr say
The problem is with your routes.
When I enter and I am not logged out you send me to login(get) route. And as you are specifying the middleware in the construct function in the AuthController, every time a method of the AuthController is called, construct function is called again and sends you back at login.. and it repeats indefinitely.
to fix that u should add
Auth::logout();
Here
...
} else {
Auth::logout(); // user must logout before redirect them
return redirect()->guest('login');
}
...
If your working with custom middleware you must follow it's all rules
in my case, I have to define a custom route class in the web middleware group.
In the world of copy-paste sometime we make mistakes.
Middleware :
public function handle($request, Closure $next)
{
if(!isset(session('user'))){
return redirect('login');
}
return $next($request);
}
}
My Mistake in Kernel.php
if custom middleware class present in web $middlewareGroups will check condition 2 times so it will give error as: redirected you too many times
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\webUser::class, //Remove
],
protected $routeMiddleware = [
'webUser'=> \App\Http\Middleware\webUser::class //Keepit
]
I had same problem after creating my own route service provider. The problem was that when I tried to login, in first time login page showed and after entering credentials I encountered "redirected too many times" and redirected to my admin dashboard and login route!
the solution was: adding middleware "web" into my routes:
Route::middleware('web')->group(base_path('Admin/routes.php'));

Laravel 5 Auth - Change Login Route

I'm trying to redirect my user to 'homepage' after successful login.
I've been able to redirect after logout by adding the following to AuthController:
protected $redirectAfterLogout = 'homepage';
However, adding the following to AuthController does not work after login. It directs me to 'home."
protected $redirectPath = 'homepage';
I then changed the default redirect in the handle() function in RedirectIfAuthenticated to:
return redirect('homepage');
Not only does that not work, it gives me the following error:
This webpage has a redirect loop
ERR_TOO_MANY_REDIRECTS
Does anyone know how I can achieve redirecting to 'homepage' after login?
Edit - Adding Routes:
Route::get('/', function () {
return view('welcome');
});
Route::get('home', 'HomepageController#getIndex');
Route::get('homepage', 'HomepageController#getIndex');
Thanks for any guidance!
Dude the attribute should be named as $redirectTo:
$redirectTo = "homepage";
now if you got more than one rule like admin and user, stored within your user model as type field, you may override the value of this attribute within postLogin() function, override the function first then do your changes. i.e:
// AuthController.php
/**
* #param Request $request
* #return $this|\Illuminate\Http\RedirectResponse
*/
public function postLogin(Request $request)
{
$this->validate($request, [
'email' => 'required|email', 'password' => 'required',
]);
$credentials = $request->only('email', 'password');
if ($this->auth->attempt($credentials, $request->has('remember')))
{
$user=User::find($this->auth->user()->id);
if($user->type == "ADMIN")
$this->redirectTo = "/dashboard";
return redirect()->intended($this->redirectPath());
}
return redirect($this->loginPath())
->withInput($request->only('email', 'remember'))
->withErrors([
'email' => $this->getFailedLoginMessage(),
]);
}
I got around this by simply modifying the existing Route::get('/') to the following:
Route::get('/', 'HomepageController#getIndex');
I really didn't need the Welcome View any longer, so this solution made the most sense.
Also, after modifying this Route, I was able to remove my previous 'home' and 'homepage' Routes.

Resources