Handling an authentication page returned by an axios request in vue - vue.js

I have a vue app that sits behind a firewall, which controls authentication. When you first access the app you need to authenticate after which you can access the app and all is well until the authentication expires. From the point of view of my app I only know that the user needs to re-authenticate when I use axios to send off an API request and instead of the expected payload I receive a 403 error, which I catch with something like the following:
import axios from 'axios'
var api_url = '...'
export default new class APICall {
constructor() {
this.axios = axios.create({
headers: {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
},
withCredentials: true,
baseURL: api_url
});
}
// send a get request to the API with the attached data
GET(command) {
return this.axios.get(command)
.then((response) => {
if (response && response.status === 200) {
return response.data; // all good
} else {
return response; // should never happen
}
}).catch((err) => {
if (err.message
&& err.message=="Request failed with status code 403"
&& err.response && err.response.data) {
// err.response.data now contains HTML for the authentication page
// and successful authentication on this page resends the
// original axios request, which is in err.response.config
}
})
}
}
Inside the catch statement, err.response.data is the HTML for the authentication page and successfully authenticating on this page automatically re-fires the original request but I can't for the life of me see how to use this to return the payload I want to my app.
Although it is not ideal from a security standpoint, I can display the content of err.response.data using a v-html tag when I do this I cannot figure out how to catch the payload that comes back when the original request is fired by the authentication page, so the payload ends up being displayed in the browser. Does anyone know how to do this? I have tried wrapping everything inside promises but I think that the problem is that I have not put a promise around the re-fired request, as I don't have direct control of it.
Do I need to hack the form in err.response.data to control how the data is returned? I get the feeling I should be using an interceptor but am not entirely sure how they work...
EDIT
I have realised that the cleanest approach is to open the form in error.response.data in a new window, so that the user can re-authenticate, using something like:
var login_window = window.open('about:blank', '_blank');
login_window.document.write(error.response.data)
Upon successful re-authentication the login_window now contains the json for the original axios get request. So my problem now becomes how to detect when the authentication fires and login_window contains the json that I want. As noted in Detect form submission on a page, extracting the json from the formatting window is also problematic as when I look at login_window.document.body.innerText "by hand" I see a text string of the form
JSON
Raw Data
Headers
Save
Copy
Collapse All
Expand All
status \"OK\"
message \"\"
user \"andrew\"
but I would be happy if there was a robust way of determining when the user submits the login form on the page login_window, after which I can resend the request.

I would take a different approach, which depends on your control over the API:
Option 1: you can control (or wrap) the API
have the API return 401 (Unauthorized - meaning needs to authenticate) rather than 403 (Forbidden - meaning does not have appropriate access)
create an authentication REST API (e.g. POST https://apiserver/auth) which returns a new authentication token
Use an Axios interceptor:
this.axios.interceptors.response.use(function onResponse(response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// no need to do anything here
return response;
}, async function onResponseError(error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
if ("response" in error && "config" in error) { // this is an axios error
if (error.response.status !== 401) { // can't handle
return error;
}
this.token = await this.axios.post("auth", credentials);
error.config.headers.authorization = `Bearer ${this.token}`;
return this.axios.request(config);
}
return error; // not an axios error, can't handler
});
The result of this is that the user does not experience this at all and everything continues as usual.
Option 2: you cannot control (or wrap) the API
use an interceptor:
this.axios.interceptors.response.use(function onResponse(response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// no need to do anything here
return response;
}, async function onResponseError(error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
if ("response" in error && "config" in error) { // this is an axios error
if (error.response.status !== 403) { // can't handle
return error;
}
if (!verifyLoginHtml(error.response.data)) { // this is not a known login page
return error;
}
const res = await this.axios.post(loginUrl, loginFormData);
return res.data; // this should be the response to the original request (as mentioned above)
}
return error; // not an axios error, can't handler
});

One solution is to override the <form>'s submit-event handler, and then use Axios to submit the form, which gives you access to the form's response data.
Steps:
Query the form's container for the <form> element:
// <div ref="container" v-html="formHtml">
const form = this.$refs.container.querySelector('form')
Add a submit-event handler that calls Event.preventDefault() to stop the submission:
form.addEventListener('submit', e => {
e.preventDefault()
})
Use Axios to send the original request, adding your own response handler to get the resulting data:
form.addEventListener('submit', e => {
e.preventDefault()
axios({
method: form.method,
url: form.action,
data: new FormData(form)
})
.then(response => {
const { data } = response
// data now contains the response of your original request before authentication
})
})
demo

Related

Managing Gcal Response + express and header issue

I'm new to node and banging my head against a wall on what should be a simple node+express+googlecal+pug issue
node/express route accepts requests and calls controller
controller ensures validation of auth and then...
executes a successful gcal function...console.log has the data i need
trying to directly (in controller function) returns "Cannot set headers after they are sent to the client"....why is a call to Gcal API forcing a response back to client?
Trying to make it more micro via individual calls to each function results in same result
What am I missing here?
getcalendars: async function(oAuth2Client, res) {
const calendar = google.calendar({ version: "v3", auth: oAuth2Client });
cal = await calendar.calendarList.list(
{},
(err, result) => {
//console.log("HEADERS SENT1?: "+res.headersSent);
if (err) {
console.log('The API returned an error: ' + err);
return;
}
console.log(JSON.stringify(result));
message2 = JSON.stringify(result)
res.render('schedules', {message2: message2})
return
});
},
EDIT: Calling function
router.route('/dashboard/schedules')
.get(async function(req, res) {
if (req.session.loggedin) {
//x = gcalController.getcalendars(req, res);
token = await gcalController.gettoken(req, res);
isAuth = await gcalController.calauth(token);
listcalendars = await gcalController.getcalendars(isAuth,res);
} else {
res.redirect("/?error=failedAuthentication")
//res.send('Please login to view this page!');
}
});
Can't set headers already sent happens when you're sending a response more than once. Usually you can terminate the function by returning your res.send() call.
It looks like the express middleware that created the res object is sending a response by the time your res.render() gets pulled out of the microtask queue.
Can you show the full code? It seems that this is probably originating in the scope where getcalendars is called.

Vue axios doesnt change header

I am quite new to vue and I am trying to send a request to my api using axios.
I build an interceptor which seems to work (logging is happening)
export default function setup() {
console.log('Http interceptor starting...')
Axios.interceptors.request.use((request) => {
const token = store.getters.token;
if (token) {
request.headers.Authorization = `Bearer ${token}`;
}
console.log(request);
return request
}, (err) => {
return Promise.reject(err);
});
}
If I check the console I can see the request including the token. If I check my network tab in the browser i can see the same request without the token. If I check the console of my api the token is null. Any Ideas?
Edit: If I use postman with the same request and the same token it is working as it shuld

Accessing the response from one GET request within another

I'm working with Vue to interact with an external API on a Drupal website, but in order to do so dynamically per Drupal user, I need to get a token from Drupal first. To do so, I'm trying to do two GET requests. The first gets me a bearer token out of Drupal, and the second uses it to authenticate the third-party API request.
Below is what I'm trying – I'm able to get the token successfully in the first request's response, but not when I try to use it in the header of my second request. If I try hardcoding the token that I see in the console log, it does work, so I know none of that is the issue. It's just that this.jwt['data']['token'] in the second request's headers seems to not pull back the token.
What do I need to adjust in order to access the token from the first response as part of the headers of my second request?
created() {
axios
.get('/jwt/token')
.then(response => {
this.jwt = response
console.log(this.jwt['data']['token']) // this does show what I want to access later
})
},
mounted() {
axios
.get('/comment/doc/' + this.id, {
headers: { Authorization: "Bearer " + this.jwt['data']['token'] } // ...but this doesn't work
})
.then(response => {
this.comments = response
})
},
It's likely the response to the token request has not finished by the time the component mounts, at which point this.jwt is not yet assigned.
I would move the token request into the mounted hook, fetching comments only when the token request succeeds:
export default {
mounted() {
axios
.get('/jwt/token')
.then(tokenResp => {
this.jwt = tokenResp
axios
.get('/comment/doc/' + this.id, {
headers: { Authorization: 'Bearer ' + this.jwt['data']['token'] }
})
.then(commentsResp => {
this.comments = commentsResp
})
})
}
}

How to force axios GET request to send headers?

Even though my code is inside one of my components in Vue, the problem is with Axios, let me explain why. So, I'm trying to get some information, like this:
axios.get('http://localhost:8181/roles/1',
{
headers: {
'Api-Token': 'tokenTOKEN',
'Content-Type': 'application/json'
}
}
)
.then(response => {console.log(response)})
.catch(response => {
console.log(response);
})
So, yes, I'm importing Axios correctly. Yes, I know we should not be sending a Content-Type header in a GET request. However, I already read the RFC 7231 and it doesn't say is impossible, is just not common. So, we want to send a Content-Type header in my request.
So, how do I know it doesn't work? Well, one of my middlewares in my Lumen API goes like this:
<?php
namespace App\Http\Middleware;
use Closure;
class JsonVerifier
{
public function handle($request, Closure $next)
{
if($request->isJson())
{
return $response = $next($request);
}
else
{
return response('Unauthorized.', 401);
}
}
}
I tried to use Postman to send that specific GET request, and it works. I tried to use fetch() like this:
var miInit = { method: 'GET',
headers: {
'Api-Token': 'tokenTOKEN',
'Content-Type': 'application/json'
},
mode: 'cors',
cache: 'default' };
fetch('http://localhost:8181/roles/1',miInit)
.then(function(response) {
console.log(response);
})
and it works! In both cases (with Postman and fetch()) my API returns the desire data.
However, when I try with Axios, I get a 401 response with the "Unauthorized" word, meaning that Axios didn't send the header correctly.
Now, the question. Is there any other way to send headers in an axios GET request? How can I force Axios to send the headers no matter what as it seem to be case with fetch() and Postman?
Axios automatically (as it should) removes the Content-Type header if you're sending a request without a body, as you do with any GET request.
https://github.com/axios/axios/blob/2ee3b482456cd2a09ccbd3a4b0c20f3d0c5a5644/lib/adapters/xhr.js#L112
// Add headers to the request
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
// Remove Content-Type if data is undefined
delete requestHeaders[key];
} else {
// Otherwise add header to the request
request.setRequestHeader(key, val);
}
});
}
You're probably looking for the Accepts header and $request->wantsJson() (or acceptsJson()) instead.

vue-resource not passing token in request headers

I'm new to Vuejs 2, currently using vue-resource to retrieve data from the server. However, I would need a token passed in the request header at the same time in order to retrieve the data from the server.
So the problem is, I am unable to retrieve data because the token is not passed into the request header, using vue-resource.
Here is the method that uses the vue-resource's interceptor (to pass in the token) to intercept the GET request:
test () {
this.$http.interceptors.push((request) => {
var accessToken = window.localStorage.getItem('access_token')
request.headers.set('x-access-token', accessToken)
return request
})
this.$http.get(staffUrl)
.then(response => {
console.log(response)
}, (response) => {
console.log(response)
})
}
Documentation for vue-resource, HTTP: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md
When I try to GET the data, i end up with an error 403 (forbidden) and after checking the request headers in the dev tools, I also could not find the token in the request headers.
Please tell me where I went wrong because I'm really new to this so i appreciate any help! Thank you!
Setting interceptors inside the component using $http doesn't work, or at least it doesn't in my testing. If you examine/log this.$http.interceptors right after your push in the test method, you'll note that the interceptor was not added.
If you add the interceptor before you instantiate your Vue, however, the interceptor is added properly and the header will be added to the request.
Vue.http.interceptors.push((request, next) => {
var accessToken = "xyxyxyx"
request.headers.set('x-access-token', accessToken)
next()
})
new Vue({...})
Here is the test code I was using.
Also note, if you are using a version prior to 1.4, you should always call the next method that is passed to the interceptor. This does not appear to be necessary post version 1.4.
please go through this code
import vueResource from "vue-resource";
import { LocalStorage } from 'quasar'
export default ({
app,
router,
Vue
}) => {
Vue.use(vueResource);
const apiHost = "http://192.168.4.205:8091/";
Vue.http.options.root = apiHost;
Vue.http.headers.common["content-type"] = "application/json";
Vue.http.headers.common["Authorization"] = "Bearer " + LocalStorage.get.item("a_t");
Vue.http.interceptors.push(function(request, next) {
console.log("interceptors", request);
next(function(response) {
});
});
}