I'm trying to make a logout test for my api with tymon/jwt-auth package. Here I have defined the api routes, controller, and a unit test.
In api.php:
Route::group(['middleware' => 'api', 'prefix' => 'auth'], function ($router) {
Route::post('login', 'AuthController#login');
Route::post('logout', 'AuthController#logout');
Route::post('refresh', 'AuthController#refresh');
Route::post('me', 'AuthController#me');
Route::post('me/profile', 'AuthController#profile');
});
In AuthController.php:
/**
* Log the user out (Invalidate the token).
*
* #return \Illuminate\Http\JsonResponse
*/
public function logout()
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
In tests/Unit/AuthenticationTest.php:
/**
* Test if user can login trough internal api.
*
* #return void
*/
public function testLogin()
{
$response = $this->post('api/auth/login', [
'email' => 'admin#xscriptconnect.com',
'password' => 'password'
]);
$response->assertStatus(200)
->assertJsonStructure(['access_token', 'token_type', 'expires_in']);
$this->assertAuthenticated('api');
}
/**
* Test if user can logout trough internal api.
*
* #return void
*/
public function testLogout()
{
$user = User::first();
$user = $this->actingAs($user, 'api');
$user->post('api/auth/logout')
->assertStatus(200)
->assertJsonStructure(['message']);
$this->assertUnauthenticatedAs($user, 'api');
}
The login test works fine but when it starts the logout test, the assertion fails. It shows me this error:
There was 1 failure:
1) Tests\Unit\AuthenticationTest::testLogout
Expected status code 200 but received 500.
Failed asserting that false is true.
And when I tested it using this method:
public function testLogout()
{
$user = User::first();
$this->actingAs($user, 'api');
$response = auth()->logout();
$response->assertStatus(200);
$response->assertJsonStructure(['message']);
}
I got this error:
There was 1 error:
1) Tests\Unit\AuthenticationTest::testLogout
Tymon\JWTAuth\Exceptions\JWTException: Token could not be parsed from the request
What is the proper way to test a logout trough this package? Please help.
According to the this comment in it's github page, I have found the solution for this problem. I changed my script like this and it works.
/**
* Test if user can logout trough internal api.
*
* #return void
*/
public function testLogout()
{
$user = User::first();
$token = \JWTAuth::fromUser($user);
$this->post('api/auth/logout?token=' . $token)
->assertStatus(200)
->assertJsonStructure(['message']);
$this->assertGuest('api');
}
Please feel free to post another answer regarding to this question if any. Thank you very much.
Override the method be() in your TestCase to set the authorization header when use actingAs() aka be() method
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
abstract class TestCase extends BaseTestCase
{
public function be(UserContract $user, $driver = null)
{
$token = auth()->fromUser($user);
return parent::be($user, $driver)->withHeader('Authorization', "Bearer {$token}");
}
}
Related
I'm making an API in laravel with tymon/jwt-auth package. based on https://jwt-auth.readthedocs.io/en/develop/
Login works fine.
Sending GET request with right token works fine.
BUT when I send a GET request with incorrect/no token it redirects me to POST login (wants me to login) rather than send a 401
GET /api/reviews with no Auth:
The GET method is not supported for this route. Supported methods: POST.
api.php
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::group([
'prefix' => 'auth',
], function ($router) {
Route::post('login', [AuthController::class, 'login'])->name('login');
});
Route::get('reviews',[ReviewController::class, 'index']);
AuthController.php
class AuthController extends Controller
{
/**
* Create a new AuthController instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth:api', ['except' => ['login']]);
}
/**
* Get a JWT via given credentials.
*
* #return \Illuminate\Http\JsonResponse
*/
public function login()
{
$credentials = request(['email', 'password']);
if (!$token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return response()->json([
"user" => auth()->user(),
"access_token" => $token,
]);
}
/**
* Get the authenticated User.
*
* #return \Illuminate\Http\JsonResponse
*/
public function me()
{
return response()->json(auth()->user());
}
/**
* Log the user out (Invalidate the token).
*
* #return \Illuminate\Http\JsonResponse
*/
public function logout()
{
auth()->logout();
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh a token.
*
* #return \Illuminate\Http\JsonResponse
*/
public function refresh()
{
return $this->respondWithToken(auth()->refresh());
}
}
ReviewController.php
class ReviewController extends Controller
{
public function __construct()
{
$this->middleware('auth:api');
}
public function index()
{
$reviews = Review::all();
return response()->json([
'status' => 200,
'reviews' => $reviews
]);
}
}
Should I remove the auth middleware and do it manually?
I'm new to Laravel, so here I want to make a view for login and register, and then I change the default view login and register into my own view, I changed it in the route and then it work, and then I try to run this code: php artisan ui:auth and then my login view before, it changes to the default view of Laravel. How to change it again into my login and view design ? Thank you.
This is my route web.php:
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('pages/home');
});
Route::get('/register','Auth\AuthController#register');
Route::post('/register','Auth\AuthController#postRegister')->name('register');
Route::get('/login','AuthController#login');
Route::post('/login','AuthController#postLogin')->name('login');
and this is my AuthController
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
class AuthController extends Controller
{
public function register()
{
return view('login_register/register');
}
public function postRegister(Request $request )
{
User::create([
'name'=> $request->nama,
'email'=> $request->email,
'password'=> bcrypt($request->password)
]);
return redirect('/login');
}
public function login()
{
return view('login_register/login');
}
public function postLogin(Request $request)
{
if(!\Auth::attempt(['email' => $request->email, 'password' => $request->password ])){
return redirect()->back();
}
return redirect('/galangdana/list');
}
public function redirectToProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
/**
* Obtain the user information from provider. Check if the user already exists in our
* database by looking up their provider_id in the database.
* If the user exists, log them in. Otherwise, create a new user then log them in. After that
* redirect them to the authenticated users homepage.
*
* #return Response
*/
public function handleProviderCallback($provider)
{
$user = Socialite::driver($provider)->user();
$authUser = $this->findOrCreateUser($user, $provider);
Auth::login($authUser, true);
return redirect('/');
}
/**
* If a user has registered before using social auth, return the user
* else, create a new user object.
* #param $user Socialite user object
* #param $provider Social auth provider
* #return User
*/
public function findOrCreateUser($user, $provider)
{
$authUser = User::where('provider_id', $user->id)->first();
if ($authUser) {
return $authUser;
}
else{
$data = User::create([
'name' => $user->name,
'email' => !empty($user->email)? $user->email : '' ,
'provider' => $provider,
'provider_id' => $user->id
]);
return $data;
}
}
}
?>
You can override the showLoginForm() method, in you LoginController.php:
public function showLoginForm()
{
return view('my.view');
}
It overrides the function showLoginForm defined in the trait Illuminate\Foundation\Auth\AuthenticatesUsers.
For registration, you showRegistrationForm method, defined in Illuminate\Foundation\Auth\RegistersUsers trait:
public function showRegistrationForm()
{
return view('my.register.view');
}
Hope it helps.
solved, that becasue i put Auth::routes() so that make multiple login, thank you for the answer 🙏
First off, let me admit that I'm new to APIs, and right now I'm working on JWT with Laravel. I'm using tymon\jwt-auth (tymon/jwt-auth:dev-develop --prefer-source to be specific). I went through some tutorials and was able to generate JWT access token.
Here is my login code:
public function login() {
$credentials = request(['email', 'password']);
if (!$token = auth('api')->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return response()->json([
'status' => 'success',
'message' => 'Login successful',
'data' => [
'access_token' => $token,
],
]);
}
I also need to get refresh token along with the access token, and cannot find code that works in my case.
I tried adding these lines in the code:
$refresh_token = JWTAuth::refresh($token);
but the postman returns with this error:
A token is required in file
/var/www/brochill-api/vendor/tymon/jwt-auth/src/JWT.php on line 331
I can also provide other configuration snippets I used if needed. Please help!
Let's start with creating a /refresh route:
Route::post('refresh', 'AuthController#refresh');
Now, in the AuthController,
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
class AuthController extends Controller
{
/**
* Create a new AuthController instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth:api', ['except' => ['login']]);
}
/**
* Get a JWT via given credentials.
*
* #return \Illuminate\Http\JsonResponse
*/
public function login()
{
//
}
/**
* Get the authenticated User.
*
* #return \Illuminate\Http\JsonResponse
*/
public function me()
{
//
}
/**
* Log the user out (Invalidate the token).
*
* #return \Illuminate\Http\JsonResponse
*/
public function logout()
{
//
}
/**
* Refresh a token.
*
* #return \Illuminate\Http\JsonResponse
*/
public function refresh()
{
return $this->respondWithToken(auth()->refresh());
}
/**
* Get the token array structure.
*
* #param string $token
*
* #return \Illuminate\Http\JsonResponse
*/
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
]);
}
}
The refresh() function refreshes the access token and invalidates the current one.
For more info on these, you can checkout the official documentation of tymon/jwt-auth, which can be found here.
I am using jwtAuth package from Tymon to handle Auth from my laravel backend to vue spa front end, I am creating AuthController that pretty much I take from the documentation and just tweak a little of it to meet my needs. And everything works fine from login to logout and when token expires.
The question is I do see there is a token refresh function on that controller that if my guess is right, it is to refresh the current token that the client already has. But how to do that? how do I handle that refresh token on my front end? Since it is quite annoying that every 60 minutes (by default the token lifetime) then it will throw 401.
What I want is maybe every time the user doing request to backend then it will refresh the token or increase the lifetime of the token. So the token will only expire if the user idle for the entire 60 minutes.
Can we do that? and is it the best practice? I am quite new on the entire jwt and token thing, in the past, I only rely on laravel token to expire since I am not working with spa, but a blade front-end so mostly don't need to mess around with the way laravel authenticate user.
For the added information here is every file that i think have anything to do with the entire auth thing.
Here is my authcontroller
<?php
namespace App\Http\Controllers;
use DB;
use Hash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Input;
use App\Http\Controllers\Controller;
use App\User;
use Response;
class Authcontroller extends Controller
{
/**
* Create a new AuthController instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth:api', ['except' => ['login']]);
}
/**
* Get a JWT via given credentials.
*
* #return \Illuminate\Http\JsonResponse
*/
public function login()
{
$credentials = request(['username', 'password']);
if (! $token = auth('api')->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
/**
* Get the authenticated User.
*
* #return \Illuminate\Http\JsonResponse
*/
public function me()
{
return response()->json(auth('api')->user());
}
/**
* Log the user out (Invalidate the token).
*
* #return \Illuminate\Http\JsonResponse
*/
public function logout()
{
auth('api')->logout();
return response()->json(['message' => 'Successfully logged out']);
}
/**
* Refresh a token.
*
* #return \Illuminate\Http\JsonResponse
*/
public function refresh()
{
return $this->respondWithToken(auth('api')->refresh());
}
/**
* Get the token array structure.
*
* #param string $token
*
* #return \Illuminate\Http\JsonResponse
*/
protected function respondWithToken($token)
{
$id = auth('api')->user()->getId();
$kelas = User::with('pus','cu')->findOrFail($id);
return response()->json([
'access_token' => $token,
'user' => $kelas,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60
]);
}
public function guard()
{
return Auth::Guard('api');
}
}
Here is my API route
Route::group(['prefix' => 'auth'],function($router){
Route::post('/login', 'AuthController#login');
Route::post('/logout', 'AuthController#logout');
Route::post('/refresh', 'AuthController#refresh');
Route::get('/me', 'AuthController#me');
});
And here is on my vue general.js file that handles route and also give the header to Axios
export function initialize(store, router) {
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
const currentUser = store.state.auth.currentUser;
if(requiresAuth && !currentUser) {
next('/login');
} else if(to.path == '/login' && currentUser) {
next('/');
} else {
next();
}
});
axios.interceptors.response.use(null, (error) => {
if (error.response.status == 401) {
store.dispatch('auth/logout');
router.push('/login');
}
return Promise.reject(error);
});
if (store.state.auth.currentUser) {
setAuthorization(store.state.auth.currentUser.token);
}
}
export function setAuthorization(token) {
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`
}
And here is auth.js that handle login
import { setAuthorization } from "./general";
export function login(credentials){
return new Promise((res,rej) => {
axios.post('/api/auth/login', credentials)
.then((response) => {
setAuthorization(response.data.access_token);
res(response.data);
})
.catch((err) => {
rej("Username atau password salah");
})
})
}
You could tweak the token expiry (from .env as JWT_TTL) and refresh times (JWT_REFRESH_TTL) to suit your needs. And check if the token is valid and/or needs to be refreshed in the middleware so the token is refreshed soon as it needs to be.
As to whether this is a good practice, see the comments to JWT_REFRESH_TTL in your Laravel project's config/jwt.php.
The solution that has worked well for me was to use a custom Middleware that extends Tymon\JWTAuth\Http\Middleware\BaseMiddleware. The boilerplate would look something like this:
Class TryTokenRefresh extends BaseMiddleware
{
public function handle($request, Closure $next)
{
$newToken = $this->tryRefresh($request);
if ($newToken) {
// in case there's anything further to be done with the token
// we want that code to have a valid one
$request->headers->set('Authorization', 'Bearer ' . $newToken);
}
...
...
$response = $next($request);
...
...
if ($newToken) {
// send new token back to frontend
$response->headers->set('Authorization', $newToken);
}
return $response;
}
// Refresh the token
protected function tryRefresh()
{
try {
$token = $this->auth->parseToken()->refresh();
return $token;
} catch (JWTException $e) {
// token expired? force logout on frontend
throw new AuthenticationException();
}
return null;
}
On the frontend, it's as simple as look for a Authorization header in the response:
// check for the `Authorization` header in each response - refresh on frontend if found
axios.interceptors.response.use((response) => {
let headers = response.headers
// your 401 check here
// token refresh - update client session
if (headers.authorization !== undefined) {
setAuthorization(headers.authorization);
}
return response
})
Hope this helps.
I am created a Laravel 5.2 application; I need to limit failure login attempts.I am created a AuthController with following code; But not working logging attempt lock.
<?php
namespace App\Http\Controllers\Auth;
use App\User;
use Validator;
use Auth;
use URL;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
class AuthController extends Controller
{
use AuthenticatesAndRegistersUsers;
protected $maxLoginAttempts=5;
protected $lockoutTime=300;
/**
* Create a new authentication controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'getLogout']);
$this->loginPath = URL::route('login');
$this->redirectTo = URL::route('dashboard'); //url after login
$this->redirectAfterLogout = URL::route('home');
}
public function index()
{
return 'Login Page';
}
/**
* Handle a login request to the application.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function postLogin(Request $request)
{
$this->validate($request, [
'username' => 'required', 'password' => 'required',
]);
$throttles = $this->isUsingThrottlesLoginsTrait();
if ($throttles && $this->hasTooManyLoginAttempts($request)) {
return $this->sendLockoutResponse($request);
}
$credentials = $this->getCredentials($request);
if (Auth::attempt($credentials, $request->has('remember'))) {
return redirect()->intended($this->redirectPath());
}
if ($throttles) {
$this->incrementLoginAttempts($request);
}
return redirect($this->loginPath)
->withInput($request->only('username', 'remember'))
->withErrors([
'username' => $this->getFailedLoginMessage(),
]);
}
/**
* Get the needed authorization credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function getCredentials(Request $request)
{
return $request->only('username', 'password');
}
/**
* Create a new user instance after a valid registration.
*
* #param array $data
* #return User
*/
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'username' => $data['username'],
'password' => bcrypt($data['password']),
]);
}
}
After many failure login their is no error message displayed. I am added some line to display error in login.blade.php file
Assuming you have implemented the make:auth artisan command of laravel.
Inside of the loginController, change the properties:
protected $maxLoginAttempts=5; to protected $maxAttempts = 5;
and
protected $lockoutTime=300; to protected $decayMinutes = 5; //in minutes
you need to use ThrottlesLogins trait in your controller
....
use AuthenticatesAndRegistersUsers, ThrottlesLogins ;
...
take a look here https://github.com/GrahamCampbell/Laravel-Throttle
and here https://mattstauffer.co/blog/login-throttling-in-laravel-5.1
Second link is for L5.1, but I think shouldnt be different for L5.2
Hope it helps!
Have a nice day.
Just overriding the following 2 functions maxAttempts and decayMinutes will be good to go. This 2 functions belong to Illuminate\Foundation\Auth\ThrottlesLogins.php file. I have tested on Laravel 5.6 version and working fine.
public function maxAttempts()
{
//Lock on 4th Failed Login Attempt
return 3;
}
public function decayMinutes()
{
//Lock for 2 minutes
return 2;
}