I am currently trying to create a simple SPA using Vue and Laravel. I've got the basics to work - users can register and login.
I just can't figure out how to create a logout function.
This is what I currently have:
AuthController.php:
public function logout()
{
$accessToken = auth()->user()->token();
$refreshToken = DB::table('oauth_refresh_tokens')
->where('access_token_id', $accessToken->id)
->update([
'revoked' => true
]);
$accessToken->revoke();
return response()->json(['status' => 200]);
}
routes/api.php:
Route::middleware('auth:api')->group(function () {
Route::post('/logout', 'API\AuthController#logout');
Route::get('/get-user', 'API\AuthController#getUser');
});
Right now, this is what I have tried to do:
Layout.vue:
methods: {
logout() {
axios.post('/api/logout').then(response => {
this.$router.push("/login")
}).catch(error => {
location.reload();
});
}
}
Which calls my logout function in Auth.js:
logout() {
localStorage.removeItem('token')
localStorage.removeItem('expiration')
}
However, when users click on the logout function, they are not logged out immediately (redirected to the login page) - they can still browse "user only pages".
I have to refresh the page before I am properly logged out.
Can anyone assist me with this? Is this even the right approach to a "secure" logout function?
That's a bit old, however, I've just started with Laravel/Vue and managed to do this quite simple. Using the integrated auth from Laravel, you could just simulate the logout from app.blade.php like so:
<b-dropdown-item href="#" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Sign Out</b-dropdown-item> //that's instead of a regular <a> tag
<b-form id="logout-form" action="logout" method="POST" style="display: none;">
<input type="hidden" name="_token" :value="csrf">
</b-form>
You'll need to have the csrf token passed through data in your script in order for it to work like so:
export default {
data() {
return {
csrf: document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
},
methods: {
submit : function(){
this.$refs.form.submit();
}
}
}
As well as adding a meta csrf in your head (blade.php file) like so:
<meta name="csrf-token" content="{{ csrf_token()}}">
I'm assuming you'll be using the logout in your navbar .vue file
Never used Laravel myself, but you should be able to handle logouts client side without needing to do anything in your backend. At the moment you remove auth token from local storage, so the user loses access to data that requires you to be logged in to get.
You probably call your getUser when you refresh the page and that's why you are only logged out then - you send empty token to your backend server, it can't find a user that's associated to it and returns an empty/default guest object. What's left to do is clear your user state after removing the token in your logout() function or send a request to your /get-user endpoint.
To logout install axios and do the rest code in Laravel 6* / 7* / 8*
npm install axios
the trigger this code when you click on logout
axios.post("logout").then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
For my logout links I'd like to make a request to Laravel to invalidate the current user's (Passport JWT) token.
Here's how I'm doing it:
In my backend:
AuthController.php
I have a logout method:
...
public function logout(Request $request) {
$request->user()->token()->revoke();
return response()->json([
'message' => 'Successfully logged out'
]);
}
routes/api.php
I have a route that can only be accessed by being authenticated.
Route::group(['middleware' => 'auth:api'], function() {
...
Route::get('/logout', 'AuthController#logout');
});
My frontend:
For this I'm using Vue's single file components
App.vue
<template>
<nav>
<ul>
...
<li v-if="isLoggedIn">
<a id="logout-link" href="#" #click.prevent="logout">Logout</a>
</li>
</ul>
</nav>
...
</template>
<script>
export default {
...
methods: {
logout(evt) {
if(confirm("Are you sure you want to log out?")) {
axios.get('api/logout').then(response => {
localStorage.removeItem('auth_token');
// remove any other authenticated user data you put in local storage
// Assuming that you set this earlier for subsequent Ajax request at some point like so:
// axios.defaults.headers.common['Authorization'] = 'Bearer ' + auth_token ;
delete axios.defaults.headers.common['Authorization'];
// If using 'vue-router' redirect to login page
this.$router.go('/login');
})
.catch(error => {
// If the api request failed then you still might want to remove
// the same data from localStorage anyways
// perhaps this code should go in a finally method instead of then and catch
// methods to avoid duplication.
localStorage.removeItem('auth_token');
delete axios.defaults.headers.common['Authorization'];
this.$router.go('/login');
});
}
}
}
}
</script>
The point of this approach then is to invalidate the token on the back end upon logging out. Nevertheless, it might not be necessary to do this if the token has short expiration dates.
Try redirecting with javascript on a successful logout:
window.location.replace("desiredURL");
Related
I have a button that deletes the record in table, and it works on purpose. But I need to add a message after deleting (not alert). When I was doing that in Laravel only, it worked perfectly. But now I have Vue.js in my project and it doesn't work.
It should return a green box that says 'successfully deleted record', when I write the same code in Laravel, it was working amazing. But with Vue.js, it doesn't work.
My blade file, it extends vue component. The message is before that in code.
<body>
<div id="app">
<div class="container">
#if(session('alert_delete'))
<div class="alert alert-success">
{{ session('alert_delete') }}
</div>
#endif
<students>
</students>
</div>
</div>
<script src="{{asset('js/app.js')}}">
</script>
My Controller
public function destroy($id)
{
$student = Student::findOrFail($id);
$student->delete();
return redirect('/')->with('alert_delete', 'Selected query is deleted successfully.');
}
Vue method to delete record
deleteStudent(id){
axios.delete(`api/student/`+id)
location.reload();
},
I was also using that to show validation errors. But now, it doesn't even show simple success message.
It's reloading the page by location.reload(); function on Vue method. But it doesn't work when I try to redirect from controller.
Ajax requests dont return with session data so you have to make a response alternative for ajax request. So your code should look something like the following:
Controller
public function destroy($id)
{
$student = Student::findOrFail($id);
$student->delete();
if (\request()->wantsJson()) {
return response()->json([
'alert_delete' => 'Selected query is deleted successfully.'
]);
}
return redirect('/')->with('alert_delete', 'Selected query is deleted successfully.');
}
Vue
deleteStudent(id){
axios.delete(`api/student/`+id).then(response => {
// Message will now be available here:
response.data.alert_delete
// You can now display the message to your users using JS notification
// libraries like SweetAlert2 or toastr.
// You don't really need this
location.reload();
})
},
or
// Set this in data() method of your vue component.
// alert: null
deleteStudent(id){
axios.delete(`api/student/`+id).then(response => {
// Message will now be available here:
response.data.alert_delete
this.alert = response.data.alert_delete
// You don't really need this
location.reload();
})
},
And still in the same vue component, display the message like so:
<div v-if="alert" class="alert alert-success" v-text="alert"></div>
// You can use a close button to set "alert" back to null if the
// users wants to clear alert.
So with the above code, you can display notifications or alert when making ajax request and session when making a regular HTTP request...
The message you've used relies on data being flashed to the session so you'd need to use a web route to handle the delete, flash the message to the session and redirect back to the original page.
If you want to raise a message after handling the delete with an axios request you'd need to place an element in the template that reacts to an event raised by the axios request.
deleteStudent(id){
axios.delete(`api/student/`+id)
.then(response => {
trigger_message();
});
},
Trigger alert would need to raise an event that your message element subscribes to.
The easiest way would be to wrap the entire content in a Vue component and use a v-if on the message element
Vue Component
<template>
<div v-if="deleted" class="alert alert-success">
Student {{ student.id }} Deleted
</div>
<student #click="deleteStudent"></student>
</template>
<script>
data () {
return {
deleted: false
}
},
methods: {
deleteStudent(){
axios.delete(`api/student/`+id)
.then(response => {
this.deleted = true;
});
},
},
</script>
That should be a good starting point. There's more you might want to do with your alert like set a timed fade-out on it. Bootstrap-Vue's Alert component is a great option for this and the documentation has some excellent examples to get you started.
I'm setting up a laravel, vue project and i am using JWT auth for the user authentication. I am trying to protect the Routes with Vue Router and it is getting token from the local storage and giving access to the authentic user to specific route, but once on another route if i click on any other route or refresh the page it redirects me on the "/login" page again. The token remains same all the time but it is considering the token as the user is not authentic. Please help as i am new to laravel and vue
I have tried using meta info but that didn't work as well. Moreover, i have tried deleting the token from local storage and created it again but nothing works for me.
routes.js file
export const routes = [
{
path: '/', component: Home, name: 'home'
},
{
path: '/dashboard', component: Dashboard, name: 'dashboard', meta:{requiresAuth: true}
},
{
path: '/login', component: Login, name: 'login'
}
];
App.js file
import VueRouter from 'vue-router';
import { routes } from './routes.js';
window.Vue = require('vue');
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history',
routes
});
router.beforeEach((to, from, next) => {
console.log(to)
if (to.meta.requiresAuth) {
const authUser = JSON.stringify(window.localStorage.getItem('usertoken'))
if(authUser && authUser.accessToken){
console.log("here")
next();
}else{
next({name: 'login'});
}
}
next();
});
const app = new Vue({
el: '#app',
router
});
I expect the output to be like when the user is authentic and the router.beforeEach method finds a token, the user can get to any route until the token gets deleted or changed. Moreover, the user should not be taken to '/login' everytime a <router-link> is clicked or page is refreshed.
I was just trying to solve it and it is solved...the problem was with the line if(authUser && authUser.accessToken) . I added authUser.accessToken as a condition which was not fulfilled, so it was redirecting on every click. I removed that condition and just left with if(authUser) and now it is working perfectly. Also I have added JSON.stringify to change my object to text and then authenticate with JSON.parse by passing a variable.
My final code looks like:-
router.beforeEach((to, from, next) => {
console.log(to)
if (to.meta.requiresAuth) {
var usertoken = JSON.stringify(window.localStorage.getItem('usertoken'))
const authUser = JSON.parse(usertoken)
if(authUser){
console.log("here")
next();
}else{
next({name: 'login'});
}
}
next();
});
I get TokenMismatchException when using nested AJAX calls. The first AJAX call works fine but the second always goes to error instead of success.
What I'm trying to do is that when the user registers from the button in the nav bar I want him to go to the dashboard or /home - this works okay. But, when the user fills the form (to buy something) on the index page, I want him to:
Have his input checked for validity, then, check if he's logged in, if not then the registration modal pops up. After he's registered I want him to be redirected to the checkout page.
However, what happens is that when the user fills the buying form and hits submit, the first ajax checks if the input in the buying form is valid, if it is, then check if he's logged in if not return 401 error.
401 gets picked up by the first ajax and directs the flow to 401 handling where the registration modal pops up to register, that's when the 2nd ajax pop up. After he's registered the back-end keeps returning 500 because of CSRF token mismatch.
First, this is the nested ajax:
<script type="text/javascript">
$(document).ready(function(){
$('#topup-form').submit(function(e){
e.preventDefault();
var topup_info = $('form').serialize();
//FIRST AJAX
$.ajax({
url: $('form').attr('action'),
method: 'post',
data: topup_info,
type: 'json',
//if success show success message for user
success: function(result){
alert(result.responseJSON.code);
$('.alert.error').slideUp(200);
$('.alert.success').append("<p class='lead'>Thanks! To checkout we go!</p>").slideDown(200);
},
//for error check if it's 400 (validation) or 401(authentication)
error: function(errorData){
// alert(errorData.responseJSON.code);
if(errorData.responseJSON.code === 400){
var error = errorData.responseJSON.message;
$('.alert.error').text('');
$('.alert.success').slideUp(200);
for (var i in error){
for (var j in error[i]) {
var message = error[i][j];
$('.alert.error').append("<p class='lead'>" + message + "<p>");
}
}
$('.alert.error').slideDown(00);
}//end error 400
//for authentication failure, show registeration modal
else if (errorData.responseJSON.code === 401) {
//change somethings in registeration modal
$('#myModalLabel').html('Please Login First');
$('#register').trigger('click');
document.getElementById('formRegister').action = "{{ route('user.regtopup') }}";
//when registeration form is submitted..
$('#formRegister').submit(function(e){
e.preventDefault();
//fire 2nd ajax
$.ajax({
url: $('#formRegister').attr('action'),
method: 'post',
data: $('form').serialize(),
type: 'json',
success: function(result){
alert('success!!!');
},
//it keeps going to error! complaining about csrf token mismatch
error: function(result){
console.log(JSON.stringify(result));
},
})//end of 2nd ajax
});//end of 2nd submit
}//end of 401
}//end of error
});//end of first ajax
});//end of first submit
$.ajaxSetup({
headers: {
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
}
})
});
</script>
Second, this is the controller that checks input validity and return 401 when not registered:
public function etiPost(Request $request) {
$validator = [
'topupAmount'=> 'required|integer|between:10,500',
'phonenumber'=> 'required|regex:/^05[602][0-9]{7}$/',
];
$inputs = $request->all();
Log::info($inputs);
$validator = Validator::make($inputs, $validator);
if($validator->fails()){
return Response::json([
'error' => true,
'message' => $validator->messages(),
'code' => 400
], 400);
}
elseif (Auth::check()) {
return view('pages.checkout', compact('inputs'));
}
else {
return Response::json([
'error' => true,
'message' => "Please login first",
'code' => 401
], 401);
}
}
This is the overloaded register method that returns JSON when registration is successful. Here is where 500 is returned! When I Log the returned JSON it comes out as normal 200 response but it arrives at the "Whoops" 500 error to the 2nd ajax! The user is registered successfully in the database but this method returns 500 which is caught by the error part of the ajax call.
/**
* Handle a registration request for the application (overloaded).
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function register(Request $request)
{
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
$this->guard()->login($this->create($request->all()));
// return response()->json();
return response()->json(['msg' => 'Success! You have been registered!'], 200);
}
I won't include the forms for brevity but rest assured I added all the CSRF input tags and the meta tag in the head of the HTML.
What should I do differently to avoid this? The first ajax works but the second doesn't.
Set header token for each ajax call
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') },
Also note that you have to add mete token in your template
you can add meta token like this in template
<meta name="csrf-token" content="{{ csrf_token() }}">
if you still want to disable csrf token then
Excluding URIs From CSRF Protection
Sometimes you may wish to exclude a set of URIs from CSRF protection. For example, if you are using Stripe to process payments and are utilizing their webhook system, you will need to exclude your Stripe webhook handler route from CSRF protection since Stripe will not know what CSRF token to send to your routes.
Typically, you should place these kinds of routes outside of the web middleware group that the RouteServiceProvider applies to all routes in the routes/web.php file. However, you may also exclude the routes by adding their URIs to the $except property of the VerifyCsrfToken middleware:
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* #var array
*/
protected $except = [
'stripe/*',
];
}
Instead of 'stripe/*', if you give '/*' then it will disable token for all
For more detail :
https://laravel.com/docs/5.5/csrf#csrf-x-csrf-token
please follow code in your jquery before ajax call
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
it's not advisable to add CSRF token in meta because all pages are not contain form to submit value so use this solution so you can use CSRF only for your js perspective.
Thank you
So I am working on an Angular JS x Laravel project. I decided to use JWT tokens and I have some routes like register and login that must be accessible only if the user is not authenticated. Here is are my routes:
Route::group(['middleware' => ['before' => 'jwt.auth']], function () {
Route::resource('api/user', 'UsersController');
Route::resource('api/group', 'GroupsController');
Route::resource('api/project', 'ProjectsController');
Route::resource('api/lesson', 'LessonsController');
Route::get('api/authenticate/user', 'AuthenticationController#getAuthenticatedUser');
Route::get('/{any}', function ($any) {
return view('index');
})->where('any', '.*');
});
Route::post('api/register', 'AuthenticationController#register');
Route::post('api/login', 'AuthenticationController#login');
Route::get('/{any}', function ($any) {
return view('guest');
})->where('any', '.*');
How can I make a route group for the routes outside the jwt.auth route group to be accessible only if the user in not authenticated?
You could do this on the frontend side with your angular controllers. Here is how I did it
//Controller used for the login page
whimAppControllers.controller('LoginController', ['userService', '$location', '$scope', '$http', function (userService, $location, $scope, $http) {
//Upon clicking the login button this function attempts to login in the user through the API
$scope.login = function() {
//Send the user supplied values to the userService in services.js when then attempts to POST to the API
userService.login(
$scope.login, $scope.userName, $scope.password,
//Upon response, if successful return the user to the main page, else return error
function(response){
$location.path('/');
},
function(response){
alert('Something went wrong with the login process. Try again later!');
}
);
}
//Clear out the fields on the login form
$scope.email = '';
$scope.userName = '';
$scope.password = '';
//If the user is already logged, redirect to the main page
if(userService.checkIfLoggedIn())
$location.path('/');
}]);
At the bottom it checks to see if the user has already been assigned a JWT and if so it redirects to the homepage.
In my in my service.js file, the userService has this method to check for the JWT:
//Checks to see if token is present or not
function checkIfLoggedIn() {
if(localStorageService.get('token'))
return true;
else
return false;
};
I'm using LocalStorageModule to make the call to localStorageService:
var whimAppServices = angular.module('whimAppServices', [
'restangular',
'LocalStorageModule',
'ngFileUpload'
]);
I'm using angular-local-storage to store the JWT on the client: https://github.com/grevory/angular-local-storage
I have a route defined in routes.php file but when i make an ajax request from my angular app, i get this error
{"error":{"type":"Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException","message":"Controller method not found.","file":"C:\\xampp\\htdocs\\tedxph\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Controllers\\Controller.php","line":290}}
this is my routes file
/*
|--------------------------------------------------------------------------
| Api Routes
|--------------------------------------------------------------------------
*/
Route::group(array('prefix' => 'api'), function() {
//Auth Routes
Route::post('auth/login', 'ApiUserController#authUser');
Route::post('auth/signup', 'ApiUserController#registerUser');
/* Persons */
Route::group(array('prefix' => 'people'), function() {
Route::get('{id}', 'ApiPeopleController#read');
Route::get('/', 'ApiPeopleController#read');
});
/* Events */
Route::group(array('prefix' => 'events'), function() {
Route::get('{id}', 'ApiEventsController#read');
Route::get('/','ApiEventsController#read');
});
});
Accessing the same url (http://localhost/site/public/api/auth/signup) from a rest client app on chrome does not give any errors, what could be wrong?
this is the angular code from my controller
$rootScope.show('Please wait..registering');
API.register({email: email, password: password})
.success(function (data) {
if(data.status == "success") {
console.log(data);
$rootScope.hide();
}
})
.error(function (error) {
console.log(error)
$rootScope.hide();
})
more angular code
angular.module('tedxph.API', [])
.factory('API', function ($rootScope, $http, $ionicLoading, $window) {
//base url
var base = "http://localhost/tedxph/public/api";
return {
auth: function (form) {
return $http.post(base+"/auth/login", form);
},
register: function (form) {
return $http.post(base+"/auth/signup", form);
},
fetchPeople: function () {
return $http.get(base+"/people");
},
fetchEvents: function() {
return $http.get(base+"/events");
},
}
});
It'd help to see the code you're using to make the angular request, as well as the header information from Chrome's Network -> XHR logger, but my first guess would be Angular is sending the AJAX request with the GET method instead of the POST method. Try changing Angular to send an explicit POST or change routes.php so auth/signup responds to both GET and POST requests.
Update looking at your screen shots, the AJAX request is returning an error 500. There should be information logged to either your laravel.log file or your PHP/webserver error log as to why the error is happening. My guess if your Angular request sends different information that your Chrome/REST-app does, and that triggers a code path where there's an error.
Fixed the problem, turns my controller was calling an undefined method in the controller class.
Renamed the method correctly and the request now works, thanks guys for the input.