Pass a file download from protected route to browser - node.js

I have a react frontend and a nodejs/express backend. The backend can serve files (downloads) via a protected route:
GET /api/file/:id
When the frontend wants to start a file download, it sends a request to this endpoint using this code (and the Authorization header is set with a valid token):
axios.get(`${apiURL}/file/${id}`)
.then(file => {
...
})
The backend responds with this code:
router.get('/file/:id', requireAuth, = (req, res, next) => {
...
res.set('Content-Type', 'application/pdf');
res.download(file.path, file.filename);
};);
This works fine: the download starts and the binary data is present in the file object of the .then() case of the axios call.
My question:
Instead of downloading the complete file I would like to pass the file to the browser BEFORE the file download starts, so that the browser handles the download like a usual browser-triggered-download, where the browser prompts wether it should download or display the file. How can this be done...?
The solution:
Thanks to #FakeRainBrigand tip I added token based authorization via request parameters to the route (using passport strategy):
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromHeader('authorization'),
ExtractJwt.fromUrlQueryParameter('token'),
]),
secretOrKey: secretKey,
};
Now I can pass the download directly to the browser to handle it:
window.location.href = `${apiURL}/file/${_id}?token=${token}`;

The Authorization header is going to cause issues here. You should use cookies, at least for this feature. Otherwise, you'll have to include a token in the URL.
The two solutions are window.open in response to an event, or navigating to the url (e.g. location.href = '...').
At least in some browsers, they won't actually navigate when there's a file download, despite you changing the location.

Related

NodeJs - res.redirect changes the content displayed but not the URL

i have a react native frontend and a nodejs backend. In one of my API calls i am getting a redirectHTML from a gateway to display to the users. The redirectHTML obtained is used in react native Webview to get displayed. my problem is the only way to know that the transaction is success or not is from the url. I have tried res.redirect and res.writeHead and both change the content of the screen but the URL still remains the same.
Server side
router.get(
"/pay/authenticate/result",
async(req, res) => {
console.log(req.query)
// res.redirect(302,"/");
res.writeHead(302,{'Location':'https://www.google.com/'});
res.end("");
});
client Side
<WebView
style={{flex:1}}
source={{html:`${authenticateWebViewUrl}`}} //this is the redirectHTML that came from the response
onNavigationStateChange={(navState) => {
console.log(navState)
}}
scalesPageToFit={false}
javaScriptEnabled={true}
/>
res.redirect with 302 will add the temporary redirection, if you want a permanent redirection, prefer HTTP 301. Reference
res.writeHead + res.end does not, on its own, cause the redirection to a new URL.
Edit: Typo req and res.

Create cookies on other domains via javascript

I'm working on a project where I need to create a cookie in another domain of my application, I know that doing it directly is not possible because that would be a security flaw.
Example scenario:
Frontend: epictv.cf
Streaming Backend: ipv4-epictv001.infra-imm.epictv.cf
Reason: I need a <video> tag to be pre-authenticated in the streaming domain, since you can't pass any type of header in the tag.
For that I thought of making the frontend make a request to the backend and pass in the "body" of this request (GET) the value of the cookie I want to create, but for some reason when axios makes this request the cookie is not created, in however, if i open the route manually works fine!
How could I get the frontend to place a cookie on the backend?
My attempt code:
async createCookie(req, res){
const session = req.params.session;
res.cookie("sessao", session, { maxAge: 1000000 });
res.status(200).json({
"status": "ok"
});
},
PS: I know that in the video tag I could pass the url with the authentication in the src itself, but I would rather not do it.

Redirect from React to an Express route from my API server on the same domain

I have both a React APP and a Express API server on the same server/domain. Nginx is serving the React APP and proxying the express server to /api.
Nginx configuration
https://gist.github.com/dvriv/f4cff6e07fe6f0f241a9f57febd922bb
(Right now I am using the IP directly instead of a domain)
From the React APP, when the user does something I want him to download a file. I used a express route on my API server that serve the file. This works fine when the user put the URL.
This is my express route:
donwloadFile.route('/')
.get((req, res) => {
const file = '/tmp/PASOP180901.txt';
res.download(file);
});
This is my react redirect:
if (this.state.downloadFile === true) {
this.setState({ downloadFile: false });
setTimeout(() => {
window.location.href = '/api/downloadFile';
}, 100);
}
The address changes but the download don't start. If I press F5 then the download starts just fine. If I use a external URL to host my file, the download start just fine too.
Thanks
First things first. Don't use setTimeout, but rather use the callback function of setState to execute code after the state is set ensuring it has been modified. Calling the callback function will guarantee the state is changed before that code in the callback is executed.
from the official docs:
setState() enqueues changes to the component state and tells React
that this component and its children need to be re-rendered with the
updated state. This is the primary method you use to update the user
interface in response to event handlers and server responses.
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied.
setState(stateChange[, callback])
The second parameter to setState() is an optional callback function
that will be executed once setState is completed and the component is
re-rendered. Generally we recommend using componentDidUpdate() for
such logic instead.
So, instead of:
if (this.state.downloadFile === true) {
this.setState({ downloadFile: false });
setTimeout(() => {
// execute code, or redirect, or whatever
}, 100);
}
you should do:
if (this.state.downloadFile === true) {
this.setState({ downloadFile: false }, () => {
// execute code, or redirect, or whatever
});
}
Now, for your specific problem
Set headers in your server side
You can set the Content-Disposition header to tell the browser to download the attachment:
from here:
In a regular HTTP response, the Content-Disposition response header is
a header indicating if the content is expected to be displayed inline
in the browser, that is, as a Web page or as part of a Web page, or as
an attachment, that is downloaded and saved locally.
Set it like this:
('Content-Disposition: attachment; filename="/tmp/PASOP180901.txt"');
Force download from the client
There are multiple ways to force the download from the client, but this is the only one I've tried.
For this to work, you have to have the content of text somehow in the client (your express route can return it for example) and create a filename for the file that will be downloaded.
let element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
Basically you are creating an empty link component, setting the data attribute to it with the text's content, attaching the link to the body, clicking the link and then removing the link from the body.
Open the link in a new tab
Opening the link in a new tab will trigger the download as well:
window.open('/api/downloadFile');
Redirect programatically
Have a look at this question in SO
You can do this:
this.props.history.push("/api/downloadFile")?
If cannot access this.props.history you can import { withRouter } from 'react-router-dom'; and export default withRouter(yourComponent); to access it.

Trying to redirect client to Google OAuth, getting 405 error

I'm try to create a Web Service that connects to the Google Calendar API. In attempting to authorize my app, I have generated a url with necessary scopes. The problem is when I try to redirect the client to the generated url, I am getting a 405 error with the following message:
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://localhost:8080' is therefore not allowed
access. The response had HTTP status code 405.
For the most part I have been following this guide: https://developers.google.com/identity/protocols/OAuth2WebServer using the node.js client library.
From what I understand, it seems that Google has not configured their server to accept cross origin requests, what I don't understand is how I am supposed to redirect my users to their authorization page if I cannot send a request from my domain.
Here's the relevant code:
export function authorize(req, res, callback): any {
let auth = new googleAuth();
let oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
if (// Check if we have previously stored a token.) {
oauth2Client.credentials = //get token;
callback(oauth2Client);
} else {
//redirect to google authentication page
let authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
state: '/',
scope: SCOPES
});
res.redirect(authUrl);
}
}
Turns out when AJAX receives a 300 status code, the browser will automatically send another GET request to the returned location on behalf of the client. Even if I had gotten around my CORS issue I would have then had to manually load the html page myself since the request was made on behalf of the client. What I really want is for the browser to make the request to the auth uri.
So my solution to this problem is to detect redirects on the front end and have the browser issue a completely new GET request. Still seems like a work around so if anyone else has a better solution I'd like to hear it.
If some one use React or similar SPA Framework to solve this problem related to OAuth 2 read this:
Do not redirect on the server side. Don not do this (node.js/express as backend example):
router.get('/go', errorHandler(async (req, res, next) => {
res.redirect(authenticationUrl);
})
);
DO THIS:
router.get('/go', errorHandler(async (req, res, next) => {
res.json({
redirectUrl: 'https://google.com'
})
})
);
And then on your frontend client, get the response and make redirect. Example:
getUrl = async () => {
try {
const res = await API.get(`/go`);
window.location = res.data.redirectUrl;
} catch (err) {
console.log(err);
}
}
Work in React. Checked!
As per the flow, it is shown like Browser (APP) -> API (Server) -> Google auth (redirection), It is a very common trail with Identity provider servers like AWS, Google Auth, etc.. as they never send an additional header of ALLOW ORIGIN that's why browser reject the request when it comes to the point of redirection
Now comes the workout solution.
The App domain (http://localhost:3000) should be added as a trusted domain at the IDP server
Request to API server for Auth initiation should be
window.location.href = 'http://localhost:5000/api/v1/auth/login-google';

Express res.redirect shows content but not downloads

I have and API link what automatically starts downloading file if I follow it in address bar. Let's call it my-third-party-downloading-link-com.
But when in Express framework I set res.redirect(my-third-party-downloading-link-com). I get status code 301 and can see file content in Preview tab in developer tools. But I can't make Browser downloading this file.
My appropriate request handler is following:
downloadFeed(req, res) {
const { jobId, platform, fileName } = req.query;
const host = platform === 'production' ? configs.prodHost :
configs.stageHost;
const downloadLink = `${host}/api/v1/feedfile/${jobId}`;
// I also tried with these headers
// res.setHeader('Content-disposition', 'attachment;
// filename=${fileName}.gz');
// res.setHeader('Content-Type', 'application/x-gzip');
res.redirect(downloadLink)
}
P.S Now, to solve this problem, I build my-third-party-downloading-link-com on back-end, send it with res.end and then:
window.open(**my-third-party-downloading-link-com**, '_blank').
But I don't like this solution. How can I tell browser to start downloading content form this third-party API ?
According to the documentation, you should use res.download() to force a browser to prompt the user for download.
http://expressjs.com/es/api.html#res.download

Resources