I have a node/express app with an ember front end. I'm new to the all of it, so please excuse the (hopefully) simple question.
I have ember talking to node (with cors properly set up). I am able to log a user in and create a session on the server, and return the session ID to ember. I then store the session ID in a cookie using the same cookie name that the server would set. I'm aware that ember and node are using different ports, so the cookies can no be read by the other party. I'm using ember-simple-auth for the authorization middleware. That part is all working as it should currently.
My problem is on subsequent api calls, the server isn't able to get the session ID to identify the user. I need to know how I can pass the session ID back to the server via ajax api calls. I've tried a few things with trying to pass it in the header, but I'm doing something wrong as its not registering. What's the proper way to send the session via the header?
//app/authorizers/custom.js
import Ember from 'ember';
import Base from 'ember-simple-auth/authorizers/base';
export default Base.extend({
authorize(sessionData, block) {
if (!Ember.isEmpty(sessionData.access_token)) {
block('X-Authorization', 'Token: ' + this.get('sessionData.access_token'));
}
}
});
//app/controllers/application.js
this.get('session').authorize('authorizer:custom', (headerName, headerValue) => {
$.ajax({
dataType: "json",
method: 'GET',
url: ENV.APP.apiHost,
data: {p: 'logout'},
beforeSend: function(xhr){
xhr.setRequestHeader(`${headerName}`, headerValue);
},
success: function( response ){
if( response.success ){
this.get('session').invalidate();
this.transitionToLoginRoute();
} else {
console.log('something went wrong with server log out. json returned: ', response );
}
}
});
});
For others having a hard time with same issue, here is what I did to solve it:
1.) on the client side (Ember) ajax call, add
beforeSend: function(xhr){
xhr.setRequestHeader(`${headerName}`, headerValue);
},
where header name is 'Authorization' and headerValue is the session ID
On the server side (Node) in your main above all other app.get/post/etc, add
// CORS && client session id magic for API calls
app.all('/api/*', function( req, res, next ){
corsIndex = $.inArray( req.headers.origin, config.CORS_WHITELIST );
if( corsIndex > -1 ){
res.header( 'Access-Control-Allow-Origin', config.CORS_WHITELIST[ corsIndex ]);
res.header( 'Access-Control-Allow-Headers', 'Authorization');
}
// check for session id in authorize header. if true, set headers.cookie with signed session id
var sid = req.headers.authorization;
if(sid){
var cookie = require('cookie'),
signature = require('cookie-signature');
var signed = 's:' + signature.sign(sid, config.SESS_SECRET);
var data = cookie.serialize(config.SESS_COOKIE_NAME, signed);
req.headers.cookie = data;
}
next();
});
You'll need to update '/api/*' if you're using a different path for your api. You'll also want to swap out the config.CORS_WHITELIST for an array of white listed clients, and the config.SESS_SECRET for your session secret.
Related
I am currently developing an API in NodeJS and a WebClient in VueJS.
I want to create a simple login/logout mechanism. The Webclient should send the request to the API and the API should handle the sessions of the different users and serving the data from its mongoDB.
Recently I came across a strange problem. When I want to login via WebClient, the browser shows me that it sends two different headers to the API. One "OPTIONS" header and one "POST" header. The POST header is sent due to my POST-Request (WebClient), which is clear. Due to Mozillas explenation I also understand the OPTION header part since the browser wants to know if the API's CORS-configuration has been configured contrary for the WebClient or not (or something like this).
But the problem now is the following:
Due to the two different header-methods, my API creates two session-IDs with just one login-post action (via WebClient), whereas one of these two sessions gets detached from the WebClient, unnecessarily consuming valuable space. This only happens through the WebClient. Using PostMan does not show this behaviour, only one session will be created since only one header is sent.
What I want to know is:
Since there is a reason for why the OPTIONS-header is sent, I want to know how I can prevent my API to create the second session via the WebClient.
Since this problem happened after testing my WebClient, it is clear to me that the WebClient is not configured or written properly, but I cannot tell where or how to prevent this since WebDev at this level is new to me. Like: Do I have to configure my WebClient or the API?
If more code is needed just tell me what you need and I will edit this post and attach the neede code.
//////////////////// Code:
//// API:
// src/main.js:
const corsOptions = {
origin: "http://localhost:8080",
credentials:true,
methods: "GET,HEAD,POST",
preflightContinue: false,
optionsSuccessStatus: 204
};
// src/routes/LoginRoute.js:
router.post("/login", function(req,res,next){
console.log("// Routes/Login");
if(!req.user){
console.log("---- Info: User is not logged in");
passport.authenticate("local", (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json({success:false,errors:["User not found"]});
}
req.login(user, (err) => {
if(err){
return next(err);
}
console.log("---- Info: Routing success");
return res.status(200).json({success:true});
});
})(req, res, next);
}else{
console.log("---- Info: User is already logged in");
return res.status(403).json({success:false,errors:["Already logged in"]});
}
});
//// VueJS
// src/store/index.js
actions:{
authenticate({commit},formData){
console.log("---- Info: Vuex/Store/Action/Authenticate - Trying to log in");
var url = "http://localhost:3000/api/login";
console.log(formData);
return Vue.axios.post(url,formData)
.then((res)=>{
console.log("---- Info: Vuex/Store/Action/Authenticate - Success");
commit('login',res.data);
return res.data;
})
.catch((err)=>{
console.log("---- Error: Vuex/Store/Action/Authenticate");
console.log(err);
// commit('logout');
return err;
});
}
}
//////////////////// FireFox Network Analysis:
I would believe that the cors package you apparently use would resolve this problem.
In any case this is not a problem with your frontend, it's handled by your backend and it's typical that the browser creates problems that aren't present with Postman. Postman is built to not care about browser CORS issues. You can however set a session in Postman for testing: https://blog.postman.com/sessions-faq/
Back to the problem: One approach is a middleware function for all or specific routes to filter out requests that already contain a session.
app.use((req, res, next) => {
if (req.session) {
// bypass new session creation, re-route or other solution
}
next()
})
Another and more flexible approach is to target the OPTIONS header directly. I solved a similar problem in a serverless proxy function with a request handler that targets the OPTIONS header specifically. It filters such requests out and returns an "OK signal" and generous headers to tell the browser it can go ahead with the real request, the POST.
You could try something like this as a general middleware or add it as a response to certain endpoints (code not tested, just freestyling the Express syntax here):
const optionsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type'
}
exports.reqHandler = async(req, res) => {
if (req.method === 'OPTIONS') {
return {
res.set(optionsHeaders)
res.status(200)
}
}
req.session.user = {}
req.session.user.id = uuid.v4()
// etc. ...
return res.status(200).json({
success: true,
data: req.session.user
msg: 'Session ID set'
})
}
So I figured out what to do in order to prevent the browser to send two headers to the API server.
I had to configure the axios.post() overgive a header option to the funciton:
const axiosHeaders = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
Vue.axios.post(url,QueryString.stringify({username:username}),axiosHeaders)
.then(..)
[...]
Setting the "Configure-Type" as "application/x-www-form-urlencoded" will do the work, but the form-data has to be wrapped with the QueryString.stringify(..) function.
With this, the browser stops to send multiple headers with one post-request and in my case, it stops to create multiple sessions on my API.
I hope this will be usefull for anyone else too.
Everythings works but I keep getting: Error while running getDataFromTree Network error: localStorage is not defined. I tried conditionals to only send req from client but it did'nt work. All the requests seem to be coming from server.
I'm using google Oauth and passport for authentication in NextJS
Should I create authLink from a different file? Any clue?
Usefull links:
https://www.apollographql.com/docs/react/recipes/authentication/
https://github.com/zeit/next.js/tree/canary/examples/with-apollo
I've tried to conditional logic to only send req from client but then, logic stops working. Looks like most of nextJS reqest are from serverside.
let apolloClient = null
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? Bearer ${token} : "",
}
}
});
function create(initialState) {
// Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
const isBrowser = typeof window !== 'undefined'
console.log(isBrowser);
return new ApolloClient({
connectToDevTools: isBrowser,
ssrMode: !isBrowser, // Disables forceFetch on the server (so queries are only run once)
link: authLink.concat(new createHttpLink({
uri: 'http://localhost:3000/graphql', // Server URL (must be absolute)
credentials: 'same-origin', // Additional fetch() options like credentials or headers
// Use fetch() polyfill on the server
fetch: !isBrowser && fetch
})),
cache: new InMemoryCache().restore(initialState || {})
})
}
export default function initApollo(initialState) {
// Make sure to create a new client for every server-side request so that data
// isn't shared between connections (which would be bad)
if (typeof window === 'undefined') {
return create(initialState)
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState)
}
return apolloClient
}
I need apollo to send cookies with every request, so my react component can find if the user is logged in.
It's breaking my head, any help appreciated. Thank you in advance.
I am trying to build a token system to allow authentication via an email link. The flow I am thinking might work is...
click email link (of the form site.com/login?token=hf74hf64&email=m#email.com) -> server checks the token is valid and the email is registered -> server redirects to '/' with a session cookie -> client acknowledges session cookie and authenticates the user
The last step is where I'm having trouble. How do I detect from within my component that a session cookie is present?
I was thinking of something like this in my React auth component:
class AuthenticatedComponent extends Component {
componentWillMount() {
if (cookie) {
this.props.dispatch(authenticateUser())//.....
}
}
}
Might this work, or do I need to make a separate fetch to the server and trigger the dispatch depending on the response?
We've implemented a very similar approach for our app. For this to work, we handle all the login in Node and not in the actual components.
Check if token is provided in query string
Pass token to server to validate
If token is valid, create the cookie as you would for a normal user/pass login
Redirect call to original url, sans the token
server.js
// I abstracted the login functionality into one call since it's the same for us
var handleAuthRequest = function handleAuthRequest(auth_body, req, res, next) {
request ({
method: 'POST',
uri: Constants.API_LOGIN_URL,
body: auth_body,
json: true
}, (error, response, body) => {
if (response.statusCode === 200) {
// this makes a cookie with the response of the body (auth token)
ssoUtils.generateCookies(body, res)
// this redirects to the initial url, with the provided cookie.
// Assuming your router already doesn't allow certain components to be accessed
// without a cookie, it would work the same for the login_token auth too.
res.redirect(req.url)
}
else {
next();
}
})
}
// this needs to come before any other routes
app.use((req, res, next) => {
// check if login_token query string was provided
if (req.query.hasOwnProperty('login_token')) {
var {login_token} = req.query
// API call to server to validate token
var jwtToken = jwt.sign({
sub: login_token
}, Constants.API_JWT_SECRET)
// modify the redirect url to remove the token
let parsed = url.parse(req.url)
delete req.query['login_token']
let newUrl = parsed.pathname + '?' + qs.stringify(req.query)
req.url = newUrl
// call the generic login handler
return handleAuthRequest({link_token: jwtToken}, req, res, next)
}
Assuming your server will return the same response from logging in or a valid link token, this would just redirect the call back to whatever your existing process is so no separate functionality client side is needed. As you can see, we also sign the token in a JWT to ensure it's only accepted by the server if sent from our app.
We use React Router to handle our client side routing. Your onEnter check for the initial route would look like this.
routes.js
// token is passed in from our cookie by both the client and server
module.exports = function (token, userAgent, originalUrl) {
function isToken() {
return token !== undefined && token !== null;
}
function ifNoTokenRedirect(nextState, replaceState) {
// check if token is set from cookie
if (!isToken()) {
replaceState({ nextPathname: nextState.location.pathname}, '/signup? redirect=' + originalUrl.pathname);
}
}
return (
// the actual routes
)
}
What is the best way (most secure and easiest) to authenticate a user for a server side route?
Software/Versions
I'm using the latest Iron Router 1.* and Meteor 1.* and to begin, I'm just using accounts-password.
Reference code
I have a simple server side route that renders a pdf to the screen:
both/routes.js
Router.route('/pdf-server', function() {
var filePath = process.env.PWD + "/server/.files/users/test.pdf";
console.log(filePath);
var fs = Npm.require('fs');
var data = fs.readFileSync(filePath);
this.response.write(data);
this.response.end();
}, {where: 'server'});
As an example, I'd like to do something close to what this SO answer suggested:
On the server:
var Secrets = new Meteor.Collection("secrets");
Meteor.methods({
getSecretKey: function () {
if (!this.userId)
// check if the user has privileges
throw Meteor.Error(403);
return Secrets.insert({_id: Random.id(), user: this.userId});
},
});
And then in client code:
testController.events({
'click button[name=get-pdf]': function () {
Meteor.call("getSecretKey", function (error, response) {
if (error) throw error;
if (response)
Router.go('/pdf-server');
});
}
});
But even if I somehow got this method working, I'd still be vulnerable to users just putting in a URL like '/pdf-server' unless the route itself somehow checked the Secrets collection right?
In the Route, I could get the request, and somehow get the header information?
Router.route('/pdf-server', function() {
var req = this.request;
var res = this.response;
}, {where: 'server'});
And from the client pass a token over the HTTP header, and then in the route check if the token is good from the Collection?
In addition to using url tokens as the other answer you could also use cookies:
Add in some packages that allow you to set cookies and read them server side:
meteor add mrt:cookies thepumpinglemma:cookies
Then you could have something that syncs the cookies up with your login status
Client Side
Tracker.autorun(function() {
//Update the cookie whenever they log in or out
Cookie.set("meteor_user_id", Meteor.userId());
Cookie.set("meteor_token", localStorage.getItem("Meteor.loginToken"));
});
Server Side
On the server side you just need to check this cookie is valid (with iron router)
Router.route('/somepath/:fileid', function() {
//Check the values in the cookies
var cookies = new Cookies( this.request ),
userId = cookies.get("meteor_user_id") || "",
token = cookies.get("meteor_token") || "";
//Check a valid user with this token exists
var user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)
});
//If they're not logged in tell them
if(!user) return this.response.end("Not allowed");
//Theyre logged in!
this.response.end("You're logged in!");
}, {where:'server'});
I think I have a secure and easy solution for doing this from within IronRouter.route(). The request must be made with a valid user ID and auth token in the header. I call this function from within Router.route(), which then gives me access to this.user, or responds with a 401 if the authentication fails:
// Verify the request is being made by an actively logged in user
// #context: IronRouter.Router.route()
authenticate = ->
// Get the auth info from header
userId = this.request.headers['x-user-id']
loginToken = this.request.headers['x-auth-token']
// Get the user from the database
if userId and loginToken
user = Meteor.users.findOne {'_id': userId, 'services.resume.loginTokens.token': loginToken}
// Return an error if the login token does not match any belonging to the user
if not user
respond.call this, {success: false, message: "You must be logged in to do this."}, 401
// Attach the user to the context so they can be accessed at this.user within route
this.user = user
// Respond to an HTTP request
// #context: IronRouter.Router.route()
respond = (body, statusCode=200, headers) ->
this.response.statusCode statusCode
this.response.setHeader 'Content-Type', 'text/json'
this.response.writeHead statusCode, headers
this.response.write JSON.stringify(body)
this.response.end()
And something like this from the client:
Meteor.startup ->
HTTP.get "http://yoursite.com/pdf-server",
headers:
'X-Auth-Token': Accounts._storedLoginToken()
'X-User-Id': Meteor.userId()
(error, result) -> // This callback triggered once http response received
console.log result
This code was heavily inspired by RestStop and RestStop2. It's part of a meteor package for writing REST APIs in Meteor 0.9.0+ (built on top of Iron Router). You can check out the complete source code here:
https://github.com/krose72205/meteor-restivus
Because server-side routes act as simple REST endpoints, they don't have access to user authentication data (e.g. they can't call Meteor.user()). Therefore you need to devise an alternative authentication scheme. The most straightforward way to accomplish this is with some form of key exchange as discussed here and here.
Example implementation:
server/app.js
// whenever the user logs in, update her apiKey
Accounts.onLogin(function(info) {
// generate a new apiKey
var apiKey = Random.id();
// add the apiKey to the user's document
Meteor.users.update(info.user._id, {$set: {apiKey: apiKey}});
});
// auto-publish the current user's apiKey
Meteor.publish(null, function() {
return Meteor.users.find(this.userId, {fields: {apiKey: 1}});
});
lib/routes.js
// example route using the apiKey
Router.route('/secret/:apiKey', {name: 'secret', where: 'server'})
.get(function() {
// fetch the user with this key
// note you may want to add an index on apiKey so this is fast
var user = Meteor.users.findOne({apiKey: this.params.apiKey});
if (user) {
// we have authenticated the user - do something useful here
this.response.statusCode = 200;
return this.response.end('ok');
} else {
// the key is invalid or not provided so return an error
this.response.statusCode = 403;
return this.response.end('not allowed');
}
});
client/app.html
<template name="myTemplate">
{{#with currentUser}}
secret
{{/with}}
</template>
Notes
Make /secret only accessible via HTTPS.
While it's very likely that the user requesting /secret is currently connected, there is no guarantee that she is. The user could have logged in, copied her key, closed the tab, and initiated the request sometime later.
This is a simple means of user authentication. I would explore more sophisticated mechanisms (see the links above) if the server-route reveals high-value data (SSNs, credit cards, etc.).
See this question for more details on sending static content from the server.
I truly believe using HTTP headers are the best solution to this problem because they're simple and don't require messing about with cookies or developing a new authentication scheme.
I loved #kahmali's answer, so I wrote it to work with WebApp and a simple XMLHttpRequest. This has been tested on Meteor 1.6.
Client
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
// Skipping ahead to the upload logic
const xhr = new XMLHttpRequest();
const form = new FormData();
// Add files
files.forEach((file) => {
form.append(file.name,
// So BusBoy sees as file instead of field, use Blob
new Blob([file.data], { type: 'text/plain' })); // w/e your mime type is
});
// XHR progress, load, error, and readystatechange event listeners here
// Open Connection
xhr.open('POST', '/path/to/upload', true);
// Meteor authentication details (must happen *after* xhr.open)
xhr.setRequestHeader('X-Auth-Token', Accounts._storedLoginToken());
xhr.setRequestHeader('X-User-Id', Meteor.userId());
// Send
xhr.send(form);
Server
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { Roles } from 'meteor/alanning:roles'; // optional
const BusBoy = require('connect-busboy');
const crypto = require('crypto'); // built-in Node library
WebApp.connectHandlers
.use(BusBoy())
.use('/path/to/upload', (req, res) => {
const user = req.headers['x-user-id'];
// We have to get a base64 digest of the sha256 hashed login token
// I'm not sure when Meteor changed to hashed tokens, but this is
// one of the major differences from #kahmali's answer
const hash = crypto.createHash('sha256');
hash.update(req.headers['x-auth-token']);
// Authentication (is user logged-in)
if (!Meteor.users.findOne({
_id: user,
'services.resume.loginTokens.hashedToken': hash.digest('base64'),
})) {
// User not logged in; 401 Unauthorized
res.writeHead(401);
res.end();
return;
}
// Authorization
if (!Roles.userIsInRole(user, 'whatever')) {
// User is not authorized; 403 Forbidden
res.writeHead(403);
res.end();
return;
}
if (req.busboy) {
// Handle file upload
res.writeHead(201); // eventually
res.end();
} else {
// Something went wrong
res.writeHead(500); // server error
res.end();
}
});
I hope this helps someone!
Since Meteor doesn't use session cookies, client must explicitly include some sort of user identification when making a HTTP request to a server route.
The easiest way to do it is to pass userId in the query string of the URL. Obviously, you also need to add a security token that will prove that the user is really who the claim they are. Obtaining this token can be done via a Meteor method.
Meteor by itself doesn't provide such mechanism, so you need some custom implementation. I wrote a Meteor package called mhagmajer:server-route which was thoroughly tested. You can learn more about it here: https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94
I am using Express 3.x and connect-mongo and the request module
My App has some middleware that ensures the external request has an access_token.
The access_token is checked and a some data is stored in the session.
I then want to make an internal call to a url within the application, but the internal call gets issued a new session (as its a separate request from the users browser request).
What I want to do is somehow copy the Express signed cookies across into the internal request() so that the middleware performs actions based on the original external session id.
I have tried passing a cookie jar into the request object but it doesnt seem to support signed cookies very well. Any ideas how I can do this?
/* Middleware to check access tokens */
app.all("/*", requireAuth, function(req, res, next) {
next();
});
function requireAuth(req,res,next) {
if ( req.query.access_token && !req.session ) {
// Check the access token and popualte stuff in the session
req.session.somedata = 'test';
// Then call a url internall to do something
// My issue is that this INTERNAL request below gets a new session Id
// so it itself returns Not Authorised as its hits the same code
request({
url: 'someurlinside-this-node-app',
method: "GET"
}, function _callback(err, serviceres, body) {
next();
});
}else{
res.send('Not Authorised');
}
}
Cookies are just another header so if you want to pass it along you should be able to do this:
var cookies = req.header('Set-Cookie');
request({
url: 'someurlinside-this-node-app',
method: "GET",
headers: { 'Set-Cookie' : cookies}
}