How to use U2F with NextJS - node.js

I am trying to implement U2F in my NextJS project. Currently I am using NextJS 13 (beta).
I already have the server side code working with the u2f library but how do I implement it on the client side?
const U2F = require("u2f");
const Express = require("express");
const BodyParser = require("body-parser");
const Cors = require("cors");
const HTTPS = require("https");
const FS = require("fs");
const session = require("express-session");
const APP_ID = "https://localhost:2015";
const server = Express();
server.use(session({ secret: "123456", cookie: { secure: true, maxAge: 60000 }, saveUninitialized: true, resave: true }));
server.use(BodyParser.json());
server.use(BodyParser.urlencoded({ extended: true }));
server.use(Cors({ origin: [APP_ID], credentials: true }));
let user;
server.get("/register", (request, response, next) => {
request.session.u2f = U2F.request(APP_ID);
response.send(request.session.u2f);
});
server.post("/register", (request, response, next) => {
const registration = U2F.checkRegistration(request.session.u2f, request.body.registerResponse);
if(!registration.successful) {
return response.status(500).send({ message: "error" });
}
user = registration;
response.send({ message: "The hardware key has been registered" });
});
server.get("/login", (request, response, next) => {
request.session.u2f = U2F.request(APP_ID, user.keyHandle);
response.send(request.session.u2f);
});
server.post("/login", (request, response, next) => {
const success = U2F.checkSignature(request.session.u2f, request.body.loginResponse, user.publicKey);
response.send(success);
});
HTTPS.createServer({
key: FS.readFileSync("server.key"),
cert: FS.readFileSync("server.cert")
}, server).listen(443, () => {
console.log("Listening at :443...");
});
Edit:
What I found out is that you should use WebAuthn these days. Do any of you have a good tutorial that explains how to use it with nextjs?

EDIT: maybe this will suit you better as it is already implemented in react: https://github.com/csalas-yubico/webauthn-client
Well, i couldn't do a better demo than already exists and is open source at https://webauthn.org, you can check the source code here https://github.com/webauthn-open-source/webauthn-yubiclone and find more info about the WebAuthn reference implementation here https://github.com/webauthn-open-source/webauthn-simple-app/ I think that will provide you with a good start to implement the client side of it. It has nothing to do with the framework you are using, e.g. Next.js or whatever, there is an ES6 branch for that reference implementation here https://github.com/webauthn-open-source/webauthn-simple-app/tree/es6-module so you could go from there when using a modern web framework.
But in short, you basically initialize the library and do either register your U2F key or login with an already registered key, very simplified you have to implement these two scenarios into your client-side component/frontend:
// register a new device / account
var waApp = new WebAuthnApp()
waApp.username = "me";
waApp.register()
.then(() => {
alert("You are now registered!");
})
.catch((err) => {
alert("Registration error: " + err.message);
});
// log in to a previously registered account
var waApp = new WebAuthnApp()
waApp.username = "me";
waApp.login()
.then(() => {
alert("You are now logged in!");
})
.catch((err) => {
alert("Log in error: " + err.message);
});

Related

How to authenticate keycloak token using node js that calls postgraphile?

I'm new on node js, and the company that i work for needs a proof of concept about postgraphile, the situation is this:
I created a node js mini server that uses postgraphile to access the data on postgres
The mini server works fine and can return data and also can use mutations.
I used keycloak-connect to try to access keycloak to authenticate the token from the request that is sent by postman but there is a problem.
If the token is valid or not it does not matter for the mini server, the only thing that seems to matter is that is a bearer token.
I tried to use other plugins (like keycloak-nodejs-connect, keycloak-verify, etc) but the result is the same, i also changed my code to use the examples in the documentation of those plugins but nothing.
This is my code: (keycloak-config.js file)
var session = require('express-session');
var Keycloak = require('keycloak-connect');
let _keycloak;
var keycloakConfig = {
clientId: 'type credential',
bearerOnly: true,
serverUrl: 'our company server',
realm: 'the test realm',
grantType: "client_credentials",
credentials: {
secret: 'our secret'
}
};
function initKeycloak(){
if(_keycloak){
console.warn("Trying to init Keycloak again!");
return _keycloak;
}
else{
console.log("Initializing Keycloak...");
var memoryStore = new session.MemoryStore();
_keycloak = new Keycloak({store: memoryStore}, keycloakConfig);
return _keycloak;
}
}
function getKeycloak(){
if(!_keycloak){
console.error('Keycloak has not been initialized. Please called init first');
}
return _keycloak;
}
module.exports = {
initKeycloak,
getKeycloak
};
My Index.js file:
const express = require('express')
const bodyParser = require('body-parser')
const postgraphile = require('./postgraphile')
const app = express()
const keycloak = require('../config/keycloak-config').initKeycloak()
var router = express.Router();
app.set( 'trust proxy', true );
app.use(keycloak.middleware());
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(postgraphile);
app.get('/', keycloak.checkSso(), (req, res) => {
res.send('success');
} );
var server = app.listen(8080, () => console.log(`Server running on port ${8080}`));
Also I used this code to get the token and use the keycloak-verify plugin but got nothing:
router.get('/',keycloak.protect(),function(req, res, next) {
var token=req.headers['authorization'];
console.log(token);
try {
let user = keycloak.jwt.verify(token);
console.log(user.isExpired());
} catch (error) {
console.error(error);
}
})
I know that I lack the knowledge because I am a backend (C#) developer, can somebody help me with this?, thanks in advance.
I found the answer to my problem:
const express = require("express");
const request = require("request");
var keycloakConfig = require('../AuthOnly/config/keycloak-config').keycloakConfig;
const postgraphile = require('./postgraphile');
const app = express();
const keycloakHost = keycloakConfig.serverUrl;
const realmName = keycloakConfig.realm;
// check each request for a valid bearer token
app.use((req, res, next) => {
// assumes bearer token is passed as an authorization header
if (req.headers.authorization) {
// configure the request to your keycloak server
const options = {
method: 'GET',
url: `${keycloakHost}/auth/realms/${realmName}/protocol/openid-connect/userinfo`,
headers: {
// add the token you received to the userinfo request, sent to keycloak
Authorization: req.headers.authorization,
},
};
// send a request to the userinfo endpoint on keycloak
request(options, (error, response, body) => {
if (error) throw new Error(error);
// if the request status isn't "OK", the token is invalid
if (response.statusCode !== 200) {
res.status(401).json({
error: `unauthorized`,
});
}
// the token is valid pass request onto your next function
else {
next();
}
});
} else {
// there is no token, don't process request further
res.status(401).json({
error: `unauthorized`,
});
}});
app.use(postgraphile);
app.listen(8080);

How can I maintain Session in reactJs NodeJs web APP?

I'm developing a register/login website which includes all features to make it work in an efficient and secure way using reactJS, NodeJS and Mysql.
Everything was working fine until I used express-session. In fact, when a user logs in, he will be redirected to a home page (obviously a session will be created) but when the user refreshes the page, It is expected to stay on the home page but the behavior I got is losing the session, thus being redirected to login page.
I looked for a fix and I already tried enabling credentials with Axios in the frontEnd and Cors in the backEnd but the problem is persisting.
This is my code:
server.js
const express = require('express');
const app = express();
const mysql = require('mysql2');
const cors = require('cors');
const validator = require('validator');
const {body, validationResult} = require('express-validator');
const session = require('express-session');
const cookieParser = require('cookie-parser');
app.use(express.json());
app.use(cors({
origin: ['http://localhost:3000'],
methods: ['GET', 'POST'],
credentials: true,
}
));
app.use(express.urlencoded({extended: true}));
app.use(cookieParser());
app.use(session({
name: 'session',
secret: 'crud',
resave: false,
saveUninitialized: false,
cookie: {
expires: 60 * 30,
sameSite: 'strict',
}
}
app.post('/login', (req, res) => {
const mail = validator.escape(req.body.mail);
const pass = validator.escape(req.body.pass);
const sqlSelect = 'SELECT * FROM login WHERE mail = ? AND pass = ?';
db.query(sqlSelect, [mail, pass], (err, result) => {
if (err) {
console.log(err);
}
if (result.length > 0) {
req.session.user = result;
req.session.loggedIn = true;
console.log(req.session.user);
res.send({message: 'success', session: req.session});
}
else {
res.send({message: 'Wrong combination Email/Password !'});
}
})
});
app.get('/login', (req, res) => {
console.log(req.session.user);
if (req.session.user){
res.send({
session: req.session,
message: 'logged'
});
}
else {
res.send({
message: 'Not logged'
});
}
});
app.js (login page)
Axios.defaults.withCredentials = true;
const onSubmit = () => {
Axios.post('http://localhost:9001/login', {
mail,
pass,
}).then((response) => {
console.log(response.data.message);
if (response.data.message === 'success') {
history.push('/home');
}
else {
setMessage(response.data.message);
}
});
};
home.js
export default function Home() {
const [user, setUser] = useState('');
const history = useHistory();
useEffect(() => {
Axios.get('http://localhost:9001/login', {withCredentials: true}).then((response) => {
console.log(response.data.message);
if (response.data.message === 'logged'){
setUser(response.data.session.user[0].mail);
}
else {
history.push('/');
}
})
//eslint-disable-next-line
}, []);
return (
<div>
<p>{user}</p>
</div>
)
}
I hope someone is able to suggest some fix to this. I know I can use localStorage but I want to use the session instead.

Express-session req.session in Websocket Socket.io?

I try to exchange sessions between express and socket.io -
When I connect to the Websocket, I can set userData in the session and also get it again on another Websocket event!
Server.ts
const session = require("express-session");
const sessionData = {
name: COOKIE_NAME,
secret:COOKIE_SECRET,
resave: false,
saveUninitialized: false,
store: store
}
const expressSession = expressSession(sessionData);
const socketSession = require("express-socket.io-session");
const sharedsession = socketSession(expressSession, {
autoSave:true
})
const io = new Server(httpsServer,{
cors: {
origin: "https://localhost:3000",
methods: ["GET", "POST"]
}
})
io.use(sharedsession);
io.on("connection", (socket: Socket) => {
socket.on("message",userdata=>{
var data = socket.handshake || socket.request;
(data as any).session.userdata = userdata;
(data as any).session.save();
console.log("From Session:",(data as any).session.userdata)
})
socket.on("getUser",()=>{
var data = socket.handshake || socket.request;
console.log("Got it: ",(data as any).session.userdata)// good!
})
});
But now... when I set a session in a express route like this:
app.get('/login', function (req, res) {
req.session.user = {_id: '', firstname:'Hide', lastname:'myPain',email:'Harold#hiden.com', hasPain: false, };
res.send('You are now logged in! => isLoggedIn:' + JSON.stringify(req.session.user));
});
And now I want to access this req.session.user in my Websocket event:
socket.on("getUser",()=>{
var data = socket.handshake || socket.request;
console.log("Got it: ",(data as any).session.user) // undefined!
})
the session.user which I set in express route on req.session.user is undefined.. why? Is it meant to work like this, or is it just for exchanging inbetween Websocket events?
Or do I just have to trigger the
(data as any).session.user= newUserDataFromExpressReq;
(data as any).session.save();
on my socketio when the user call my express endpoint for login? So its not really an automated exchange?
Ok, everything what was needed was to add "withCredentials" option to the client. This is needed when you have your client and server running on different servers/ports.
// client-side
const io = require("socket.io-client");
const socket = io("https://api.example.com", {
withCredentials: true,
extraHeaders: {
"my-custom-header": "abcd"
}
});

Why can’t I send specific mongoose.js errors to the client when they enter a username already in use?

I am working on implementing custom error handling in a MongoDB MERN application. I’m also using Mongoose and passport-local-mongoose.
When a user registers for a new account, they should get a specific error message in the chrome console saying, ‘username already exists’. I know it is a bad idea to show all the server error messages to the user, but I would like to show a select few.
Links that I have tried:
https://github.com/saintedlama/passport-local-mongoose
Passport-local-mongoose : Authenticate user right after registration
https://www.geeksforgeeks.org/nodejs-authentication-using-passportjs-and-passport-local-mongoose/
Here is the code:
server/controller/auth.js
const { User } = require('../models');
const register = async function (req, res) {
try {
const user = new User({ username: req.body.username });
await user.setPassword(req.body.password);
await user.save();
}
catch (err) {
console.log(`error inside save ${err}`);
res.status(500).send(err);
}
};
const login = async function (req, res) {
//code block under construction
console.log(`login!!!!!`);
};
const logout = function (req, res) {
req.session.destroy();
res.end();
};
exports.login = login;
exports.register = register;
exports.logout = logout;
server/models/User.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const passportLocalMongoose = require('passport-local-mongoose');
const userSchema = new Schema({
username: { type: String, unique: true },
password: { type: String}
});
userSchema.plugin(passportLocalMongoose);
const User = mongoose.model('User', userSchema);
module.exports = User;
server/routes/api/auth/index.js
const router = require('express').Router();
const passport = require('../../../config/passport');
const authController = require('../../../controllers/auth');
router.route('/logout').get(authController.logout);
router.route('/register').post(authController.register);
router.use(passport.authenticate('local', {
session: true
}));
// Matches with '/api/auth'
router.route('/login').post(authController.login);
module.exports = router;
server/server.js
const path = require('path');
const express = require('express');
const passport = require('./config/passport');
const mongoose = require('mongoose');
const cors = require('cors');
const session = require('express-session');
const helmet = require('helmet');
const morgan = require('morgan');
const corsOptions = require('./config/cors.js');
const routes = require('./routes');
const { v1: uuidv1 } = require('uuid');
// console.log(uuidv1());
const PORT = process.env.PORT || 3001;
const app = express();
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost/puzzlegallery', {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false
});
mongoose.set("useCreateIndex", true);
// Define middleware here
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(helmet({ contentSecurityPolicy: false }));
app.use(session({ secret: 'sassy', resave: false, saveUninitialized: true }));
app.use(passport.initialize());
app.use(passport.session());
app.use(cors(corsOptions));
app.use(morgan('dev'));
app.use(routes);
// for Reactjs ##################
// Serve up static assets (usually on heroku)
if (process.env.NODE_ENV === 'production') {
app.use(express.static('client/build'));
}
// #################################################
if (process.env.NODE_ENV === 'production') {
app.get('*', (_, res) => {
res.sendFile(path.join(__dirname, '../client/build/index.html'));
});
}
app.listen(PORT, (err) => {
if (err) throw err;
console.log(
`🌎 Server is Ready and Listening on http://localhost:${PORT}`
); // eslint-disable-line no-console
});
Login page:
Server console:
Here is a link to the repo also for more context: https://github.com/BenjDG/puzzle-gallery
Thanks for any help you can offer!!!
If you want the caller to handle errors gracefully, you might consider returning a 200 response, like:
{
"success": true
}
or...
{
"success": false,
"errorCode": "UsernameAlreadyExists",
"message": "This username already exists."
}
It will be the responsibility of the caller to check the success field to make sure the request succeeded. If you want, you can display the friendly message directly in your app, or you can use an "error code" to determine what to display. This is helpful for an API shared across multiple apps, and you want to display different messages, or if you support a multi-lingual UI, and want to translate the message.
Quick aside:
I know it is a bad idea to show all the server error messages to the user, but I would like to show a select few.
I'm sure you've seen articles that warn against this, so just a bit of clarification. You don't want to pass internal error messages and stack traces to your callers, as this exposes more information about your system than most clients should know. Attackers might use this information to learn more about your implementation, and use that to exploit your system.
In general, there is little harm in returning a friendly error message, or a sub-status code, esp for 4xx errors, to help the caller understand how they need to re-submit the request to get a successful response. The important thing is to abstract away all underlying implementation details, so don't just pass an error message directly from Mongoose to your caller, catch the exception, and send an appropriate response.
Because you're dealing with authentication - you also need to be careful about exposing too much to your caller. For example - exposing a "Check Username" endpoint will make it easy for someone to brute force your API to get a handful of valid users in your app.
When you send a status code that is not in the range 200, it is considered as an "exception" in the client code. For Axios specifically, the catch block is executed. In your code, it is
.catch((err) => {
console.error(err);
});
So you have the error message in the console.
Recommend: You can use the status 200. And the better is to check if the email is already in the database before adding a new user.
I think the error 500 is used in case you aren't aware of the error. In this case, we can handle the duplication error by checking before.

Node Express setting cookies

I may be misunderstanding here.
I have a node server running at localhost:3000, and a React app running at localhost:8080.
The React app is making a get request to the node server - my server code for this looks like:
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(cookieParser());
app.get('/', function (req, res) {
let user_token = req.cookies['house_user']; // always empty
if (user_token) {
// if the token exists, great!
} else {
crypto.randomBytes(24, function(err, buffer) {
let token = buffer.toString('hex');
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080');
res.cookie('house_user', token, {maxAge: 9000000000, httpOnly: true, secure: false });
res.send(token);
});
}
});
app.listen(3000, () => console.log('Example app listening on port 3000!'))
I'm trying to set the house_user token, so that I can later keep track of requests from users.
However, the token is not being set on the user (request from localhost:8080) - the house_user token is always empty (in fact, req.cookies is entirely empty). Do I need to do something else?
I just tried the code below (and it worked). As a reminder, you can just paste this in myNodeTest.js, then run node myNodeTest.js and visit http://localhost:3003. If it does work, then it probably means you're having CORS issues.
[EDIT] withCredentials:true should do the trick with axios.
axios.get('localhost:3000', {withCredentials: true}).then(function (res) { console.log(res) })
const express = require('express')
const cookieParser = require('cookie-parser')
const crypto = require('crypto');
const port = 3003
app.use(cookieParser());
app.get('/', function (req, res) {
let user_token = req.cookies['house_user']; // always empty
if (user_token) {
// if the token exists, great!
} else {
crypto.randomBytes(24, function(err, buffer) {
let token = buffer.toString('hex');
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080');
res.cookie('house_user', token, {maxAge: 9000000000, httpOnly: true, secure: true });
res.append('Set-Cookie', 'house_user=' + token + ';');
res.send(token);
});
}
});
app.get('/', (request, response) => {
response.send('Hello from Express!')
})
app.listen(port, (err) => {
if (err) {
return console.log('something bad happened', err)
}
console.log(`server is listening on ${port}`)
})
Making my comment into an answer since it seemed to have solved your problem.
Since you are running on http, not https, you need to remove the secure: true from the cookie as that will make the cookie only be sent over an https connection which will keep the browser from sending it back to you over your http connection.
Also, remove the res.append(...) as res.cookie() is all that is needed.

Resources