Django Angular Authentication CSRF cached template - ajax

I am getting status code 403 when I try to log in after successfully being logged in and logged out.
Client side is written in Angular and server side is in Django.
This goes as follows:
Client requests url '/' fetches main HTML template with all required static files ( angular, bootstrap, jQuery sources and angular sources defined by me) with
<div ng-view></div> tag into which further templates will be inserted.
Via $location service is redirected to url '/#/login'
This rule from $routeProvider is executed once '/#/login' is hit:
$routeProvider.when('/login', {
templateUrl: 'login.html'
});
'login.html' is served by django view and form for logging in is rendered to the user
User logs in successfully providing proper credentials
Then user logs out, by clicking on a button, which fires '$http.get(
'/logout/'
);' and then is redirected to url '/#/login'
Here is the problem. When user fills in credential form and sends 'POST' request, 403 is returned. I thought that it is, because this routing is done only by angular and since 'login.html' template has already been requested it has been catched and can be served without hitting backend, but after logging out currently possesed CSRF cookie is stale, so that's why I am getting 403. So I tried to remove that template:
logout: function(){
var forceLoginTemplateRequest = function(){
if( $templateCache.get('login.html') !== 'undefined'){
$templateCache.remove('login.html');
}
};
var responsePromise = $http.get(
urls.logout
);
responsePromise.success(forceLoginTemplateRequest);
return responsePromise;
}
After doing that I could see client side requesting 'login.html' template always after logging out, so I thought I could provide CSRF cookie when serving that template from backend:
#urls.py
urlpatterns = patterns(
'',
...
url(r'^$', serve_home_template),
url(r'^login.html$', serve_login_template),
url(r'^login/', login_view, name='login'),
url(r'^logout/', logout_view, name='logout'),
...
)
#views.py
#ensure_csrf_cookie
def serve_login_template(request):
return render(request, "login.html")
#ensure_csrf_cookie
def serve_home_template(request):
return render(request, 'home.html')
But still it doesn't work and I am getting 403 when trying to log in after logging out. The only way I managed it to work is to simply refresh the page so that every single file, whether template or source file is requested again from the backend and CSRF cookie is updated with them.
Here is my app's run section for making sure CSRF cookie is sent with every request:
mainModule.run(['$http','$cookies', '$location', '$rootScope', 'AuthService', '$templateCache',
function($http, $cookies, $location, $rootScope, AuthService, $templateCache) {
$http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
if ( !(AuthService.isLoggedIn() == "true")){
$location.path('/login');
}
});
}]);

This could be a cache problem. Try to add the never_cache decorator to all your views:
from django.views.decorators.cache import never_cache
...
#ensure_csrf_cookie
#never_cache
def serve_login_template(request):
return render(request, "login.html")
...

I solved this problem by setting X-CSRFTOKEN header in $routeChangeStart event.
I don't exactly know how module.run phase works, but it seems that when certain event defined within it occurs everything what is defined outside this event's handler body isn't executed.
mainModule.run(['$http','$cookies', '$location', '$rootScope', 'AuthService',
function($http, $cookies, $location, $rootScope, AuthService) {
$http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
// Added this line
$http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
if ( !(AuthService.isLoggedIn() == "true")){
$location.path('/login');
}
});
}]);
This works together with removing 'login.html' template from $templateCache.
Instead of removing templates on client side with $templateCache service it is also possible to set your server to serve templates and set following headers:
Cache-Control: no-cache, no-store, must-revalidate
Pragma : no-cache
Expires : 0
Another way of dealing with this problem is to simply force page refresh, however I don't like this approach, since this is not pro-single-page-app approach. :)

One solution could be to read the current, fresh csrftoken directly from the cookie and then update the stale cookie using javascript.
var fresh_token = document.cookie.match('csrftoken=([a-zA-Z0-9]{32})

Related

how to solve cors Allow Access control in vue js and laravel application

I Have tried almost everything. My front end is developed in vue js . backend is in laravel. we have written api for another website from which we are trying to fetch data. If directly access that website Url it gives all the data but when i try to access it from my website with axios it gives me this error.
Access to XMLHttpRequest at 'https://example.com/api/tickets/fetch_tickets?page=undefined' from origin 'http://localhost:8000' has been blocked by CORS policy: Request header field x-requested-with is not allowed by Access-Control-Allow-Headers in preflight response.
that website form which i am trying to fetch data also build in laravel. i have created middleware and applied it on api routes. I added chrome extension Allow Cors with which it works fine but we cant ask every client to use that extension.
We access that url from other website which is accessing data nicely. only vue js app creating these issue.
Vue Code
getTickets() {
axios.get( 'example.com/api/tickets/fetch_tickets?page=' + this.pagination.current, {
}).then((response) => {
// console.log(res.data.data)
// this.desserts = res.data.data;
// this.loadingprop = false;
this.desserts = response.data.data;
this.pagination.current = response.data.current_page;
this.pagination.total = response.data.last_page;
console.log(response.data.data);
}).catch((err) => {
this.handleErrors(err.response.data.errors);
})
.then(() => {
this.loading = false;
});
}
other website's routes
Route::group(['middleware' => ['api','cors']], function () {
Route::group(['prefix' => 'tickets'], function () {
Route::post('/store_ticket_auth', 'TicketApiController#storeTicketAuth'); //enter ticket auth
Route::get('/fetch_tickets', 'TicketApiController#fetchTickets'); //get all tickets
Route::get('/fetch_replies/{ticket_id}', 'TicketApiController#fetchTicketReplies'); // get all replies by ticket id
Route::post('/send_reply', 'TicketApiController#sendTicketReply'); // Send reply
Route::post('/update_ticket', 'TicketApiController#updateTicketStatus'); // Update Status
});
});
Do I need to add this on my cuurent project too?
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
I think the issue is on client side but dont know why it is not working.
I tried all answers on stackoverflow but nothing works
I have to add these lines in my index.php file of laravel
header("Access-Control-Allow-Origin: *");
//header("Access-Control-Allow-Methods", "DELETE, POST, GET, OPTIONS");
header("Access-Control-Allow-Headers:*");
if ($_SERVER['REQUEST_METHOD'] == "OPTIONS") {//send back preflight request response
return "";
}
Solved my issues by commenting out:
// window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
in resources/js/bootstrap.js
The error is telling you that the server won't allow the client to use a x-requested-with header.
In php you can do this to allow the server to accept that header:
header('Access-Control-Allow-Headers: X-Requested-With');
If you want the easy way you can use laravel-cors
You can follow the installation step and add this code in your config/cors.php
'allow_origins' => [
'https://yourfrontendrequest.url',
],
Install Moesif Origin & CORS Changer Chrome extension and
Then go to resources/js/bootstrap.js and comment out this line // window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
you can disable same origin policy in chrome
press win + R
and then copy this :
chrome.exe --user-data-dir="C://Chrome dev session" --disable-web-security

Laravel Sanctum with SPA: Best practice for setting the CSRF cookie

Currently, my SPA is simple and has only a contact form. The form data is sent to the backend. I have installed Laravel Sanctum for that.
axios.get('/sanctum/csrf-cookie').then((response) => {
axios.post('api/contact')
.then((response) => {
//doing stuff
});
});
This works perfectly fine. However, I was wondering. Where and which time in your SPA do you fire the initial request to get the CSRF cookie? (axios.get('/sanctum/csrf-cookie'))
My first thoughts were:
Every time the page is mounted/loaded
Only when you receive a 419 and you attempt to refresh the token and retry the previous request
You don't fire any API requests, only when the user tries to log in and only then you are requesting a cookie (in my case I don't have any user authentification)
For each API request, you wrap it with axios.get('/sanctum/csrf-cookie') around
So for future readers, I chose:
Only when you receive a 419 and you attempt to refresh the token and retry the previous request
For that, I'm using the package axios-auth-refresh.
My settings look like that
//bootstrap.js
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
axios.defaults.withCredentials = true;
axios.defaults.baseURL = process.env.GRIDSOME_BACKEND_URL;
const refreshAuthLogic = (failedRequest) => axios.get('/sanctum/csrf-cookie').then((response) => Promise.resolve());
createAuthRefreshInterceptor(axios, refreshAuthLogic, { statusCodes: [419] });
window.axios = axios;

CORs error when accessing Square V2 API

I'm making a client-side request out to V2 of the Square API using Vue and Axios. My Vue component is as follows:
import axios from 'axios';
export default {
mounted() {
var instance = axios.create({
baseURL: 'https://connect.squareup.com/v2/',
timeout: 1000,
headers: {
'Authorization': 'Bearer xxxxxxxxxxxxxxxxx',
'Accepts': 'application/json',
'Content-Type': 'application/json'
}
});
instance.get('catalog/list')
.then(function (response) {
console.log(response);
}) ;
}
}
However, when I make that call, I receive the following error:
Failed to load https://connect.squareup.com/v2/catalog/list: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://local-env.dev' is therefore not allowed access. The response had HTTP status code 403.
That error suggests that there is some configuration that has to happen on the Square side, but I saw no opportunity to whitelist domains, etc.
Has anyone come across this error before, regardless of service, and if so, how did you resolve?
I don't think the Square API supports being called from a browser. I used Postman to do an OPTIONS request on https://connect.squareup.com/v2/catalog/list and the response was a NOT_FOUND. The OPTIONS request is needed for proper CORS support.
Plus, if you did this, I would think your auth token would need to be sent to the client -- thus exposing it to everyone. It looks like the Square API is only designed to be called from a server. But that is just based on me skimming the docs a bit. I have no experience using their API.
When doing OAuth authorization request you are not supposed to do it from your application. Create and URL with the parameters and open it in a new browser window or tab, Something like:
const grants='MERCHANT_PROFILE_READ CUSTOMERS_READ CUSTOMERS_WRITE PAYMENTS_READ PAYMENTS_WRITE PAYMENTS_WRITE_ADDITIONAL_RECIPIENTS PAYMENTS_WRITE_IN_PERSON';
const params = new HttpParams()
.set('scope', grants)
.set('client_id', <YourSquareApplicationId>)
.set('state', '1878789');
const requestUrl = `${<squareUrl>}/oauth2/authorize?${params.toString()}`;
window.open(requestUrl, "_blank");
That new window is supposed to ask the end user to login to his account and accept or deny the request.

Can Ajax make a Cross-Origin Login?

I'm trying to login from one of my servers to another in order to send cross-origin requests that requires being logged. is it possible?
I have two web servers, A and B. Lets say www.a.com and www.b.com.
B has an API that can be used only if the client is logged in. I need to use that API from A clients.
So, I send from A client an ajax (post) login request to B. B responses with CORS headers, the session cookie and a successful redirection to B's home/index.
But when I make a second ajax request (jsonp request) from A client to B server, this request doesn't send the previous session cookie received, therefore the login request failed.
If I login to www.b.com manually (in a second browser tab), all requests from A to B are successful detected as a logged user, so, the B API works from A.
I think that the session cookie received from my login requests is not being saved to the browser.
This is my login request:
$.post("www.b.com/login", { 'j_username': 'username', 'j_password': 'password' } );
Using:
jqXHR.withCredentials = true;
settings.crossDomain = true;
Response headers:
Access-Control-Allow-Headers:x-requested-with
Access-Control-Allow-Methods:POST, GET, OPTIONS
Access-Control-Allow-Origin:*
...
Location:http://www.b.com/home
...
Set-Cookie:JSESSIONID=tY++VWlMSxTTUkjvyaRelZ0o; Path=/
The cookie received is being saved to www.a.com or to www.b.com? How can I set this cookie to www.b.com from an A client ajax request? I think that is the problem.
As apsillers said, we can't use the wildcard Access-Control-Allow-Origin:*.
But this doesn't solved the problem.
I was setting jqXHR.withCredentials = true; inside a beforeSend handler function.
$.post({
...
beforeSend: function(xhr) {
xhr.withCredentials = true;
},
...
});
And for some reason, this doesn't work. I had to set the use of credentials directly:
$.post({
...
xhrFields: {
withCredentials: true
},
...
});
This code works perfectly !
Thanks you.

Flask error after redirect from POST method

I am using a combination of Flask and Javascript. After user input from a web page I send a JSON object back to the Flask server. ie:
var xhr = new XMLHttpRequest();
xhr.open('POST', '/completed/');
xhr.setRequestHeader('Content-Type', 'application/json');
var stringifiedObject = dataResultToJSON(data);
xhr.send(stringifiedObject);
Then in Flask:
#main_view.route('/completed/', methods=['POST'])
def completed():
if (request.headers['Content-Type'].startswith('application/json')):
#do stuff here
return redirect(url_for("main_view.home"))
#main_view.route('/')
def home():
logger.debug(">>home")
return render_template('home.html')
When flask redirects to 'home' asfter the Ajax POST I get the following console output:
DEBUG:myapp:>>home
INFO:werkzeug:127.0.0.1 - - [24/Apr/2016 20:13:15] "GET / HTTP/1.1" 200 -
INFO:werkzeug:127.0.0.1 - - [24/Apr/2016 20:13:15] "GET /%3C!DOCTYPE%20html%3E%3C!-- (... entire web page html)
The odd thing is the second INFO statement above - I don't get this line printed when I redirect to home from anywhere else - only occurs when I redirect from the 'completed' POST method. Werkzeug logs the entire home.html web page html and I get an error in the web client:
NetworkError: 404 NOT FOUND - http://127.0.0.1:5000/%3C!DOCTYPE%20html%3E%3C!-- (... entire web page html)
I also added code=307 to the redirect as per here: Make a POST request while redirecting in flask but still got the same 404 error.
I am stuck as to how to get around this.
I think your problem is that you're POSTing data as an AJAX request (i.e. not a browser navigation, but programatically from your client). It doesn't really make much sense to tell your AJAX client to redirect after the POST completes.
You're then trying to tell the client to redirect...but the redirect request is being returned to the XMLHttpRequest.
I'm not 100% sure what you want to happen, but you'd probably be better off using a regular form post if you want the client to redirect once you've posted the data.
I believe what you're trying to do is better illustrated by the answer to this question:
How to manage a redirect request after a jQuery Ajax call
I got this working following the comment and answer above. Specifically I did:
def completed():
#other code here
return url_for("main_view.home")
and in JS:
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
var OK = 200;
if (xhr.status === OK) {
window.location.href = xhr.responseText;
}
else {
console.log ('Error: ' + xhr.status);
}
}
};

Resources