So, finally my MMORTS game built on Sails is going to Kongregate.
Had few obstacles, like connecting websockets, but solved now.
Probably the last obstacle is to keep authenticated session. I was using frameworks everywhere and i have no idea how does the authentication sessions work under the hood.
The main problem is probably the CSRF or CORS. I am using Sails v1.0.
So, i start with HTML, which I upload to kongregate. I'm taking the simplest possible example:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<script src="jquery.js"></script>
<script src='https://cdn1.kongregate.com/javascripts/kongregate_api.js'></script>
<script src="sails.io.js"
autoConnect="false"
environment="production"
headers='{ "x-csrf-token": "" }'
></script>
<script type="text/javascript">
io.sails.url = 'https://my-secret-game.com'; // or where you want
</script>
</head>
<script src="main.js"></script>
</html>
And this is the main.js , which I also upload to kongregate
kongregateAPI.loadAPI(function(){
window.kongregate = kongregateAPI.getAPI();
var username = kongregate.services.getUsername();
var id = kongregate.services.getUserId();
var token = kongregate.services.getGameAuthToken();
$.get("https://my-secret-game.com/csrfToken", function (data, jwres) {
var params = {
username: username,
id: id,
token: token,
_csrf: data._csrf
};
$.post("https://my-secret-game.com/kong", params, function(data, jwr, xhr){
// cant set the cookie - because of chrome. this doesnt work
document.cookie = document.cookie + ';authenticated=true;sails.sid=' + data.id;
$.get("https://my-secret-game.com/csrfToken", function (data, jwres) {
var msg = {
testing_authentication: true,
_csrf: data._csrf
};
$.post("https://my-secret-game.com/test", msg, function(data, status){
// getting the 403 Forbidden, CSRF mismatch. trying to access
// the play/test route, which is protected by sessionAUTH
console.log('data.response', data.response)
});
});
});
});
});
The problem is, that i am getting 403 Forbidden whenever I try to POST my Sails backend with sessionAUTH. I also cant set cookies - probably because of Chrome. What can I do? When i get CSRF token, on the next request my Sails app responds about CSRF mismatch. It becomes wrong.
And this is the controller on my Sails backend server
module.exports = {
kong: function (req, res, next) {
var url = 'https://api.kongregate.com/api/authenticate.json';
var kong_api_key = 'my-secred-api-key';
var params = req.allParams();
var request = require('request');
var req_form = {
"api_key": kong_api_key,
"user_id": params.id,
"game_auth_token": params.token
};
request({
url: url,
method: "POST",
json: true,
body: req_form,
timeout: 5000
}, function (err, response, body){
if(err) { console.log(err, 'ERR43'); return res.ok(); }
else {
if(!response.body.success) {
console.log('unsuccessful login from kongregate')
return res.ok();
}
// trying to use a existing user and authenticate to it
User.find({username: 'admin-user'}).exec(function(err, users) {
var user = users[0];
req.session.authenticated = true;
req.session.user = { id: user.id };
// trying to send session_id, so that i could hold it on kongregates cookies as `sid`
return res.send({ user: user, id: req.session.id });
});
}
});
},
Could somoene please help to fix authentication and CSRF of my app?
In case needs more info about my configs, this is the config/session.js
var prefixes = 'dev';
module.exports.session = {
secret: 'my-secret',
cookie: {
secure: false
},
adapter: 'redis',
host: 'localhost',
port: 6379,
ttl: 3000000,
db: 0,
prefix: prefixes + 'sess:',
};
config/policies.js
module.exports.policies = {
user: {
'new': 'flash',
'create': 'flash',
'edit': 'rightUser',
'update': 'rightUser',
'*': 'sessionAuth'
},
play: {
'*': 'sessionAuth'
}
};
api/policies/sessionAuth.js
module.exports = function(req, res, next) {
if (req.session.authenticated) {
return next();
} else {
var requireLoginErr = [
{ name: 'requireLogin', message: 'You must be signed in' }
];
req.session.flash = {
err: requireLoginErr
};
res.redirect('/');
return;
}
};
config/security.js
module.exports.security = {
csrf: true,
cors: {
allowRequestMethods: 'GET,PUT,POST,OPTIONS,HEAD',
allowRequestHeaders: 'content-type,Access-Token',
allowResponseHeaders: '*',
allRoutes: true,
allowOrigins: '*',
allowCredentials: false,
},
};
Okey, since i had no answers (obviously - the question was bad), answering with my solution which i have solved by my self - so the next time i can read my self. Not sure how good it is, but at least it works.
Sails CORS is taken form Express.js and allows to connect sockets to kongregate if i allow it inside configs.
But it does not allow to authenticate in a normal way, by sending sails.sid (authentication token) via cookies.
Chrome does not allow to set cookies with javascript (i dont have backend on Kongregate) to headers at all due to security. So, if i can't send cookies with headers, Sails can't authenticate the requests in a normal way. Even if i allow CORS to accept the 'cookie' header - it's not allowed by browsers to set cookie headers with javascript.
I can make some unique header like "authentication" and set the sails.sid there, extend some core functionality of Sails to take this new header instead of cookie header. But the problem - on Sails backed i was not able to get at all this sails.sid and send it to my external frontend.. Where it is created? How can i get sails.sid on Sails backend? Not sure - can't google it.
So, i just did authentication in a most simple way possible - on account login/register, i just create a session key by my self - with bcrypt hashing user_id+secret_token (taken from sails config secrets). and sending to the frontend { user_id: 'abcd', secret_token: 'a2dw412515...' }
I have made my policies in Sails to authenticate on every POST/GET request - take the request's session_id and user_id, and compare using bcrypt, does the session_id is the same as encrypted user_id+secret_token. I hope its secure enough.
So, it worked. I just had to disable CSRF. Maybe some day i will implement it again, I just have to write it in my way, not leave the Sails defaults.
The working code:
FRONTEND
// you upload this HTML file to kongregate
// also you upload additionally ZIP named "kongregate_uploaded_folder" with libraries like sails.io, jquery
<!DOCTYPE html>
<html>
<head>
<script src="kongregate_uploaded_folder/jquery.js"></script>
<script src='https://cdn1.kongregate.com/javascripts/kongregate_api.js'></script>
<script src="kongregate_uploaded_folder/sails.io.js"
autoConnect="false"
environment="production"
></script>
</head>
<body style="padding:0; margin:0; overflow:hidden;">
<div style="position:absolute; margin: 0px; padding: 0px;">
<canvas id="main_canvas" style="position:absolute;" width="640" height="480" >Best initial resolution to have in Kongregate</canvas>
</div>
<script>
// the first thing happends - you try to connect your frontend with Kongregate
kongregateAPI.loadAPI(function(){
window.kongregate = kongregateAPI.getAPI();
if(!kongregate.services.isGuest()) {
var params = {
username: kongregate.services.getUsername(),
id: kongregate.services.getUserId(),
token: kongregate.services.getGameAuthToken(),
};
// call your backend to create a new session and give you session_id
$.post("https://your_game_server.com/kong", params, function(data, jwr, xhr){
var kong_session_id = data.kong_session_id;
var kong_user_id = data.kong_user_id;
var user = data.user;
// connect your sockets with the server in this way
io.socket = io.sails.connect("https://your_game_server.com", { useCORSRouteToGetCookie: false, reconnection: true });
// subscribe to the global sockets channel. You have to make this route and code, but here is a example
io.socket.get('/subscribe', { kong_session_id: kong_session_id, kong_user_id: kong_user_id }, function(data, jwr){
if (jwr.statusCode == 200){
io.socket.on(data.room, function(event){
// on any server-side event, you will get this "event" message. At this part you decide what to do with this data
incoming_socket_event(event); // i wont write this function
});
// your game continues here:
$.get("https://your_game_server.com/player_data?kong_session_id=" + kong_session_id + "&kong_user_id=" + kong_user_id, params, function(data, jwr, xhr){
// you will get authenticated "current_user"
}
});
})
}
});
</script>
</html>
BACKEND
// SAILS BACKEND: home_controller.js
module.exports = {
kong: function (req, res, next) {
// player has already opened your game in kongregate.com and frontend requests this endpoint POST /kong { id: 84165456, token: 'as54..' }
// you need to create a new session for this player, or register this player. This is your own session creation, since default sails session wont work with external website frontend.
var req_params = {
url: "https://api.kongregate.com/api/authenticate.json", // default URL to validate kongregate user
method: "POST",
json: true,
body: {
api_key: 'gg000g00-c000-4c00-0000-c00000f2de8', // kongregate will provide this api-key for server-side connection (this one has some letters replaced)
user_id: 84165456, // when frontend requests POST /kong { id=84165456 } , this 84165456 is provided by kongregate in the frontend
game_auth_token: "as54a45asd45fs4aa54sf" // when frontend requests POST /kong { token = 'as54..' }, this is provided by kongregate in the frontend
},
timeout: 20000
}
// request kongregate that this is the real player and you will get a username
request(req_params, function (err, response, body){
var response_params = response.body; // response from kongregate API
// search for user with this kongregate_id inside your database, maybe he is already registered, and you need just to create a new session.
User.find({ kongregate_id: response_params.user_id }).exec(function(err, usr) {
var user = usr[0]
// if player already has an account inside your online game
if(user) {
// create new session for this user.
require('bcryptjs').hash("your_own_random_secret_key" + user.id, 10, function sessionCreated(err, kong_session_id) {
// send this info to frontend, that this player has been connected to kongregate
return res.send({
user: user,
kong_session_id: kong_session_id,
kong_user_id: user.id
});
});
//if this is new user, you need to register him
} else {
var allowedParams = {
username: response_params.username, // kongregate will give you this player username
email: 'you_have_no_email#kong.com', // kongregate does not provide email
password: 'no_password_from_kongregate',
kongregate_id: response_params.user_id // kongregate will give you this player id
};
User.create(allowedParams, function(err, new_user) {
// create new session for this user.
require('bcryptjs').hash("your_own_random_secret_key" + new_user.id, 10, function sessionCreated(err, kong_session_id) {
// send this info to frontend, that this player has been connected to kongregate
return res.send({
user: new_user,
kong_session_id: kong_session_id,
kong_user_id: new_user.id
});
});
});
}
});
});
},
};
ROUTES
// config/routes.js
module.exports.routes = {
'GET /player_data': 'PlayController.player_data',
'GET /subscribe': 'PlayController.subscribe',
'POST /kong': {
action: 'home/kong',
csrf: false // kongregate is a external website and you will get CORS error without this
},
};
SECURITY
// config/security.js
module.exports.security = {
csrf: false,
cors: {
allowOrigins: ['https://game292123.konggames.com'], // your game ID will be another, but in this style
allowRequestMethods: 'GET,POST',
allowRequestHeaders: 'Content-Type',
allowResponseHeaders: '',
allRoutes: true,
allowCredentials: false,
},
};
SOCKETS
// config/sockets.js
module.exports.sockets = {
grant3rdPartyCookie: true,
transports: ["websocket"],
beforeConnect: function(handshake, cb) { return cb(null, true); },
};
CONFIG POLICIES
// /config/policies.js
module.exports.policies = {
play: {'*': 'sessionAuth'},
};
API POLICIES
// /app/sessionAuth.js
module.exports = function(req, res, next) {
var params = req.allParams();
// your own session handling way to get the user from session token
require('bcryptjs').compare("your_own_random_secret_key" + params.kong_user_id, params.kong_session_id, function(err, valid) {
req.session.authenticated = true;
req.session.user_id = params.kong_user_id;
return next();
});
};
CONTROLLER
// /api/controllers/PlayController.js
module.exports = {
player_data: async function (req, res, next) {
var users = await User.find(req.session.user_id);
return res.send({ current_user: users[0] });
},
subscribe: async function (req, res, next) {
var users = await User.find(req.session.user_id);
var roomName = String(users[0].id);
sails.sockets.join(req.socket, roomName);
res.json({ room: roomName });
},
}
Related
I have get a login page code on the web based on express nodeJS.
I have many backend app running on different port. The goal is, when a user is authentificated in the nodeJS server, he's automaticaly redirected to his app.
But, if i can mount a http-proxy separated and run it properly, i would want to include proxying in this loggin JS code when user is connected.
There is the part of the code below.
const httpProxy = require('http-proxy');
const proxy = httpProxy.createProxyServer();
(...)
// http://localhost:3000/auth
app.post('/auth', function(request, response) {
// Capture the input fields
let username = request.body.username;
let password = request.body.password;
// Ensure the input fields exists and are not empty
if (username && password) {
// Execute SQL query that'll select the account from the database based on the specified username and password
connection.query('SELECT * FROM accounts WHERE username = ? AND password = ?', [username, password], function(error, results, fields) {
// If there is an issue with the query, output the error
if (error) throw error;
// If the account exists
if (results.length > 0) {
// Authenticate the user
request.session.loggedin = true;
request.session.username = username;
// Redirect to home page
proxy.web(req, res, { target: 'http://127.0.0.1:1881' });
//response.redirect('/home');
} else {
response.send('Incorrect Username and/or Password!');
}
response.end();
});
} else {
response.send('Please enter Username and Password!');
response.end();
}
});
app.listen(3000);
At the response.redirect('/home');i want to replace it by proxying, but nothing append.
I don't know if this is possible, because running 2 servers on the same instance.
Thank you for your help.
Regard
I think it is better to use this package if you use express: https://www.npmjs.com/package/express-http-proxy
You can use it like this:
const { createProxyMiddleware, fixRequestBody, responseInterceptor } = require( 'http-proxy-middleware' );
const proxyMiddleware = createProxyMiddleware({
target: 'foo',
onProxyReq: fixRequestBody,
logLevel: 'debug',
changeOrigin: true,
secure: false,
xfwd: true,
ws: true,
hostRewrite: true,
cookieDomainRewrite: true,
headers: {
"Connection": "keep-alive",
"Content-Type": "text/xml;charset=UTF-8",
"Accept": "*/*"
},
});
app.use( '/youRoute/**', proxyMiddleware );
And then when you are loged in you redirect to you you proxified route :
res.redirect('/youRoute/');
The problem that I am facing is that after setting a Firebase session cookie, I go to another endpoint that needs to be secure, but req cannot find the cookie.
I am following a Firebase tutorial called Manage Session Cookies that describes how to generate a cookie after a user has logged in using the Firebase signInWithEmailAndPassword function. The user idToken is then passed over to a POST request:
First, I am generating a token and sending it to an endpoint:
function signIn() {
var email = document.getElementById('email').value;
var password = document.getElementById('password').value;
firebase.auth().signInWithEmailAndPassword(email, password).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
if (errorCode === 'auth/wrong-password') {
alert('Wrong password.');
} else {
alert(errorMessage);
}
document.getElementById('quickstart-sign-in').disabled = false;
}).then(user => {
// Get the user's ID token as it is needed to exchange for a session cookie.
return user.getIdToken().then(idToken => {
if (firebase.auth().currentUser) {
$.ajax({
method: 'POST',
url: '/login',
data: {'email':firebase.auth().currentUser.email,idToken},
success: function(data) {
//do other things...
}
});
}
});
})
}
The URL endpoint, which is getting the idToken from the prior POST request, creating a session cookie with createSessionCookie, then setting the cookie using res.cookie('session', sessionCookie, options):
exports.postLogin = (req, res, next) => {
// Get the ID token passed
const idToken = req.body.idToken.toString();
// Set session expiration to 14 days.
const expiresIn = 60 * 60 * 24 * 14 * 1000;
var exampleDB = admin.database().ref('exampleDB');
exampleDB.once('value', function(snapshot) {
//unrelated things happening...
//generate randomData...
}).then(function() {
admin.auth().createSessionCookie(idToken, {expiresIn})
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.status(200).send(randomData).end();
}, error => {
res.status(401).send('UNAUTHORIZED REQUEST!');
});
});
};
The problem that I am facing starts here, when I go to another endpoint, /dashboard. The cookie that I had supposedly set cannot be found, and instead I get an error message stating TypeError: Cannot read property 'session' of undefined for my session cookie:
exports.dashboard = (req, res, next) => {
const sessionCookie = req.cookies.session || '';
// a bunch of other code around the session cookie that doesn't even get triggered because of the break in req.cookies.session
}
Am I retrieving the cookie incorrectly? Or have I not set the cookie properly? Or is the cookie somehow not being carried over to this new endpoint, /dashboard, from the page on which the POST to /login happens?
After logging the req to /dashboard I see that I have the following in there, but I don't know if it's from another session for something. If it is from Firebase, I don't know how to access it correctly:
sessionID: 'ublahxARQVljyGRblahPQrei',
session:
Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true },
returnTo: '/dashboard',
flash: {},
_csrfSecret: 'r46blahE+kOzblah5==' },
I believe that you need to use the __session cookie.
When using Firebase Hosting together with Cloud Functions or Cloud
Run, cookies are generally stripped from incoming requests.
source
This has been encountered before in a different form: Firebase Functions : How to store simple cookies to remember an authenticated user
im trying to get array edges from this url
`https://www.instagram.com/graphql/query/?query_id=17851374694183129&variables={"id":"500818047","first":10}
In the frontend, I can get this array with fetch and credentials: 'include'.
But I can't work with him because of CORS. So, I try to do this in app.js using node, but array is still hidden.
That's what I get:
{
edge_followed_by:
{
count: 18213,
page_info: { has_next_page: false, end_cursor: null },
edges: []
}
}
How to enable the parameter credentials in node request? Or, if I do something wrong, how to get this array?
var cors = require('cors');
app.use(cors());
var request = require("request");
const url = 'https://www.instagram.com/graphql/query/?query_id=17851374694183129&variables={"id":"500818047","first":10}';
request({
url: url,
json: true,
}, function (error, response, body) {
if (!error && response.statusCode === 200) {
console.log(body['data']['user'])
}
});
This is because when you hit the URL from frontend you may have already logged into Instagram so the User Access Token is present in your browser's cookies and hence you are able to access all the information in edges
Try the same thing after logging out of Instagram or try it with incognito mode and you would find that edges are empty because the browser no longer holds the User Access Token needed to check for permissions and provide you with the information.
To do it from a backend service follow the steps provided here,
https://developers.facebook.com/docs/instagram-api/getting-started
Hope this helps. :)
Have you taken a look at the request docs?
You have the following options to authenticate your request:
request.get('http://some.server.com/').auth('username', 'password', false);
// or
request.get('http://some.server.com/', {
'auth': {
'user': 'username',
'pass': 'password',
'sendImmediately': false
}
});
// or
request.get('http://some.server.com/').auth(null, null, true, 'bearerToken');
// or
request.get('http://some.server.com/', {
'auth': {
'bearer': 'bearerToken'
}
});
My application is a Node.js API with a client inside the same application.
I'm trying to implement a simple auth login that uses a JWT token generated by a Node.js API.
My logic is as follows:
Client: User submits login information to /auth/login route.
$.ajax({
url: "/auth/login",
type: "POST",
data: formData,
dataType: "json",
success: function(data, textStatus, jqXHR) {
if (typeof data.redirect == "string") {
window.location = data.redirect;
}
},
error: function(data) {
if (typeof fail === "function") fail(data);
}
});
API: Verify user and on success generates JWT and sends back to the client.
router.post("/login", async (req, res) => {
var login = { UID: req.body.UID, password: req.body.password };
AU.manualLogin(login)
.then(result => {
res.header("x-auth-token", result.token).json({
status: 200,
message: "success",
data: result.data,
redirect: "/dashboard"
});
})
.catch(err => next({ status: 400, message: err.message }));
});
Client: Saves JWT to the header and checks for redirect - In this case, I use window.location to direct to /dashboard after successful login. (this part I'm not sure about)
API: Middleware checks valid JWT on protected routes.
module.exports = function auth(req, res, next) {
const token = req.headers["x-auth-token"];
if (!token)
return res.status(401).send("Access denied. No token provided.");
try {
const decoded = jwt.verify(token, "jwtPrivateKey");
req.user = decoded;
next(); //pass control to next middleware
} catch (ex) {
res.status(400).send("Invalid token.");
}
};
The Problem:
The token is definitely being sent from API -> Client. But I have no idea how to handle the token from the client-side. I think the issue might be to do with the window.location redirect as at this point it does not seem to be sending the x-auth-token to the API.
What I have tried
I have tested the solution with Postman from end-to-end and it works fine. That probably proves that it isn't the API side that has the issue.
I've also tried these sources:
Pass request headers in a jQuery AJAX GET call
Adding custom header in HTTP before redirect
How to add header to request in Jquery Ajax?
jwt on node - how does the client pass the token back to the server
You need kind of a storage to keep the token. Otherwise the user has always to login again after he closes the browser/tab. So it's quite common to keep the token in local or session storage.
Approach 1: Use a single page application (SPA) framework like angular, vue.js, react etc. to protect your routes client-side
Approach 2: You can request only html and css (view) from your backend and then store the token after a login procedure. With a valid token, fetch the (protected) data with ajax requests. Redirect to the login page if a ajax request returns the status code 401 (unauthorized) or a user wants to access the protected route without having a token stored. This is perhaps the most suitable for you.
Approach 3: Use Node.js with a backend framework like express and store auth information in a server side session
index.js
const express = require('express');
const session = require('express-session');
const app = express();
app.use(require("cookie-parser")());
app.use(session({ secret: 'aslwezoweasdfasdlkfalksdfhweelaerfcv', resave: false, saveUninitialized: true}));
routes/protectedRoutes.js
const express = require('express');
const router = express.Router();
router.all("/*", util.handleAuthenticate); // check auth on every request
// other routes
indexController.js (login functionality)
module.exports.login = function(req, res) {
if(!req.session.name) {
// check username/password --> db lookup
// if valid:
req.session.name = ...
// redirect to home or backref
// else: redirect to login
}
}
util/security.js
function isLoggedIn(req) {
return !!req.session.name;
}
function handleAuthenticate(req, res, next) {
if(isLoggedIn(req))
{
next();
}
else
{
// redirect to login page
}
}
I'm having trouble scraping a website that needs authentication, and is using session cookies. The session requires a request with POST, and the authentication then approves. But when I want to GET the webpage that need authentication, it returns "Unauthorized". I guess I need a way to bring the session cookie with the GET-request, but I don't know how! My dependencies is request-promise(https://www.npmjs.com/package/request-promise).
The code looks like this:
var rp = require("request-promise");
var options = {
method: "POST",
uri: "http://website.com/login",
form: {
username: "user",
password: "pass",
},
headers: {},
simple: false
};
rp(options).then(function(response) {
console.log(response); // --> "Redirecting to login/AuthPage"
request("http://website.com/login/AuthPage", function(err, res, body) {
console.log(body); // --> "Unauthorized"
})
}).catch(function(e) {
console.log(e)
})
I'm guessing you have to put the request in a "Jar" (https://github.com/request/request#requestjar), to be able to reach the next request-URL, but how can I set the request-promise to create a cookie-jar?
Your problem is how to keep the session after authentication.
That means, after logging in by using username and password, the server will return a cookie with an identifier. Then you need to attach that cookie to all your feature requests.
It's simple with request-promise. Just keep tracking session by enabling jar option then use the same request object for all requests.
Let take a look
var request = require("request-promise").defaults({ jar: true });
var options = {
method: "POST",
uri: "http://website.com/login",
form: {
username: "user",
password: "pass",
},
headers: {},
simple: false
};
request(options).then(function(response) {
request("http://website.com/login/AuthPage", function(err, res, body) {
console.log(body);
})
}).catch(function(e) {
console.log(e)
})
Use the following object while making rest calls.
var request = require("request-promise").defaults({jar: true});
To add your own cookies
var tough = require('tough-cookie');
// Easy creation of the cookie - see tough-cookie docs for details
let cookie = new tough.Cookie({
key: "some_key",
value: "some_value",
domain: 'api.mydomain.com',
httpOnly: true,
maxAge: 31536000
});
// Put cookie in an jar which can be used across multiple requests
var cookiejar = rp.jar();
cookiejar.setCookie(cookie, 'https://api.mydomain.com');
// ...all requests to https://api.mydomain.com will include the cookie
var options = {
uri: 'https://api.mydomain.com/...',
jar: cookiejar // Tells rp to include cookies in jar that match uri
};
and then make the call. More details about request-promise:
https://www.npmjs.com/package/request-promise