PouchDB remote authentication password storage in browser - javascript

I'm playing around with in browser pouchdb and couchdb and and remote sync. This was all straight forward but now I'm at a roadblock that I'm not able to figure out myself.
For security reasons every user of the website has its own datbase that is only accessable via the respective couchdb user.
const localDB = new PouchDB('test');
const remoteDB = new PouchDB(process.env.REMOTE_DATABASE);
const username = process.env.REMOTE_USER;
const password = process.env.REMOTE_PASSWORD;
remoteDB.login(username, password, (error, response) => {
if (error) {
console.error('Login failure', error)
} else {
console.log('Login success');
dispatch('sync');
}
});
console.log('sync');
localDB.sync(remoteDB, {live: true})
.on('change', response => {
console.log(response);
})
.on('error', error => {
console.error('Sync failure', error);
});
To create a user I need a backend layer to handle user registration and creation. THis is now problem sind i can store the admin credentials "secure" on the server. Now my problem is how to handle the pouchdb user credentials in the browser. It would be possible to ask for the credentials on the first loading of the app and then storing it in the ram but this would lead to logging in on each reload of the page. Is there any way to use like cookie/token auth?
Or is there a way to proxy the pouchdb connection through a backend proxy that handles the authentication!?
Any tips would be appreciated.

The simplest way to use cookie authentication is by using the pouchdb-authentication plugin on GitHub. If you do not want to use this then you should read this previous question (#23986912) on Stackoverflow which discusses this in more detail.
Hope this helps.

Related

Want client-side Firebase logs to show up in StackDriver, not users' browsers

I'm using firebase-functions/lib/logger to log client-side firebase/firestore activity like
const { log, error } = require("firebase-functions/lib/logger");
export const addData = async (userId, dataId) => {
try {
const collectionRef = firestore
.collection("docs")
await collectionRef.add({
dataId,
});
log(`Data added`, { userId, dataId });
} catch (err) {
error(`Unable to add new data`, { userId, dataId });
throw new Error(err);
}
};
When I run this on my local, the log shows up in my browser console. Will this happen on non-local environments, ie for real users? Will these logs also show up automatically in Stackdriver, or are they stuck on the client side? I want to be able to view the logs either in Stackdriver or Firebase console but have them not show up in the browser for real users. How should I accomplish this?
Messages logged in Cloud Functions will not show up in the client app at all (that would probably be a security hole for your app). They will show up in the Cloud Functions console in the log tab, and in StackDriver.
Any messages logged in your app will not show up in any Google Cloud product. They are constrained to the device that generated them. If you want cloud logging, you'll need to implement some other solution. Cloud Functions does not support this - you will need to investigate other solutions or build something yourself.

How to Refresh Firebase Session Cookie

I'm developing a web application using Node.js/Express.js for the backend and I use Firebase for user authentication, and to manage user registration etc I use Firebase Admin SDK.
When a user want to login I sign him in using Firebase Client SDK like this:
// Handling User SignIn
$('#signin').on('click', function(e){
e.preventDefault();
let form = $('#signin-form'),
email = form.find('#email').val(),
pass = form.find('#password').val(),
errorWrapper = form.find('.error-wrapper');
if(email && pass){
firebase.auth().signInWithEmailAndPassword(email, pass)
.catch(err => {
showError(errorWrapper, err.code)
});
}else {
showError(errorWrapper, 'auth/required');
}
});
Below this code, I set an observer to watch for when the user successfully sign in, After a successfull sign in I get a Firebase ID token which I send to an endpoint on the server to exchange it for a session cookie that has the same claims the ID token since the later expires after 1 hour.
// POST to session login endpoint.
let postIdTokenToSessionLogin = function(url, idToken, csrfToken) {
return $.ajax({
type: 'POST',
url: url,
data: {
idToken: idToken,
csrfToken: csrfToken
},
contentType: 'application/x-www-form-urlencoded'
});
};
// Handling SignedIn Users
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
user.getIdToken().then(function(idToken) {
let csrfToken = getCookie('csrfToken');
return postIdTokenToSessionLogin('/auth/signin', idToken, csrfToken)
.then(() => {
location.href = '/dashboard';
}).catch(err => {
location.href = '/signin';
});
});
});
} else {
// No user is signed in.
}
});
Sign in endpoint on the server looks like this:
// Session signin endpoint.
router.post('/auth/signin', (req, res) => {
// Omitted Code...
firebase.auth().verifyIdToken(idToken).then(decodedClaims => {
return firebase.auth().createSessionCookie(idToken, {
expiresIn
});
}).then(sessionCookie => {
// Omitted Code...
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({
status: 'success'
}));
}).catch(err => {
res.status(401).send('UNAUTHORIZED REQUEST!');
});
});
I have created a middle ware to verify user session cookie before giving him access to protected content that looks like this:
function isAuthenticated(auth) {
return (req, res, next) => {
let sessionCookie = req.cookies.session || '';
firebase.auth().verifySessionCookie(sessionCookie, true).then(decodedClaims => {
if (auth) {
return res.redirect('/dashboard')
} else {
res.locals.user = decodedClaims;
next();
}
}).catch(err => {
if (auth) next();
else return res.redirect('/signin')
});
}
}
To show user information on the view I set the decoded claims on res.locals.user variable and pass it to the next middle ware where I render the view and passing that variable like this.
router.get('/', (req, res) => {
res.render('dashboard/settings', {
user: res.locals.user
});
});
So far everything is fine, now the problem comes after the user go to his dashboard to change his information (name and email), when he submits the form that has his name and email to an endpoint on the server I update his credentials using Firebase Admin SDK
// Handling User Profile Update
function settingsRouter(req, res) {
// Validate User Information ...
// Update User Info
let displayName = req.body.fullName,
email = req.body.email
let userRecord = {
email,
displayName
}
return updateUser(res.locals.user.sub, userRecord).then(userRecord => {
res.locals.user = userRecord;
return res.render('dashboard/settings', {
user: res.locals.user
});
}).catch(err => {
return res.status(422).render('dashboard/settings', {
user: res.locals.user
});
});
}
Now the view gets updated when the user submits the form because I set the res.locals.user variable to the new userRecord but once he refreshes the page the view shows the old credentials because before any get request for a protected content the middle ware isAuthenticated gets executed and the later gets user information from the session cookie which contains the old user credentials before he updated them.
So far these are the conclusions that I came to and what I tried to do:
If I want the view to render properly I should sign out and sign in again to get a new Firebase ID token to create a new session cookie which is not an option.
I tried to refresh the session cookie by creating a new ID token from the Admin SDK but it doesn't seem to have this option available and I can't do that through the client SDK because the user is already signed in.
Storing the ID token to use later in creating session cookies is not an option as they expire after 1 hour.
I Googled the hell out of this problem before posting here so any help is so much appreciated.
I am facing a very similar scenario with one of my apps. I think the answer lies in these clues.
From Firebase docs
Firebase Auth provides server-side session cookie management for traditional websites that rely on session cookies. This solution has several advantages over client-side short-lived ID tokens, which may require a redirect mechanism each time to update the session cookie on expiration:
So they're hinting here that you want to manage the session and it's lifetime from the server.
Second clue is in the docs
Assuming an application is using httpOnly server side cookies, sign in a user on the login page using the client SDKs. A Firebase ID token is generated, and the ID token is then sent via HTTP POST to a session login endpoint where, using the Admin SDK, a session cookie is generated. On success, the state should be cleared from the client side storage.
If you look at the example code, the even explicitly set persistence to None to clear state from the client using firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
So they are intending there to be no state on the client beyond the initial auth. They explicitly clear that state and expect an httponly cookie so the client can't grab the cookie (which really is just the ID token) and use it to get a new one.
It is odd that there is no clear way of refreshing the token client-side but there it is. You can only really create a session cookie with a super long lifetime and decide on the server when to delete the cookie or revoke the refresh token etc.
So that leaves the other option: manage state client-side. Some examples and tutorials simply send the ID token from the client to the server in a cookie. The satte sits on the client and the client can use the ID token to use all firebase features. The server can verify the user identity and use the token etc.
This scenario should work better. If the server needs to kick the user then it can delete the cookie revoke the refresh token (a bit harsh admittedly).
Hope that helps. Another scheme would be to build custom tokens, then you have complete control.

PouchDB and React-Native not replicating .to() but .from() is working

For some reason documents created on my app are not showing up on my remote couchdb database.
I am using the following
import PouchDB from 'pouchdb-react-native'
let company_id = await AsyncStorage.getItem('company_id');
let device_db = new PouchDB(company_id, {auto_compaction: true});
let remote_db = new PouchDB('https://'+API_KEY+'#'+SERVER+'/'+company_id, {ajax: {timeout: 180000}});
device_db.replicate.to(remote_db).then((resp) => {
console.log(JSON.stringify(resp));
console.log("Device to Remote Server - Success");
return resp;
}, (error) => {
console.log("Device to Remote Server - Error");
return false;
});
I get a successful response the response:
{
"ok":true,
"start_time":"2018-05-17T15:19:05.179Z",
"docs_read":0,
"docs_written":0,
"doc_write_failures":0,
"errors":[
],
"last_seq":355,
"status":"complete",
"end_time":"2018-05-17T15:19:05.555Z"
}
When I go to my remote database, document_id's that am able to search and grab on the application do not show up.
Is there something I am not taking into account?
Is there anything I can do to check why this might be happening?
This worked when I used the same scripting method in Ionic and when I switched to React-Native I noticed this is the case.
NOTE: When I do .from() and get data from remote to the device, I get the data. For some reason it just isn't pushing data out
"Is there anything I can do to check why this might be happening?"
I would try switching on debugging as outlined here.
PouchDB.debug.enable('*');
This should allow you to view debug messages in your browser's JavaScript console.

Authentication logic for sessions that last while tab is open

Assume you are working on a front end application that performs authentication through 3rd party api. Successful authentication returns a json web token.
What would be best practices to store such token and create some sort of session for user while he is active on the website i.e. didn't close a tab or browser, however refreshing / reloading a page should not destroy such session.
Also, how can this session be used to protect routes? I am working with a stack consisting of react / redux / node / express and quiet a few other libraries. I believe I can perform certain checks within my react-router, however wouldn't it be better to do these on the express side?
You can store the token in localStorage or sessionStorage, and include it in every API request.
Local storage outlives the tab, it's stored there until you explicitly delete from it, so refreshing a page won't be a problem. Even closing a tab and then coming back won't be.
Session storage allows you to store data. Page refreshes are fine, but tab closing isn't, which is closer to the behavior you want.
As for protecting routes, the server should obviously check the token on requests to all protected API routes.
On the browser side, you will probably want to show a login form if a user tries to visit a protected route but the token isn't there (or is invalid).
With react-router, you could do it like the official repo shows in the example, via onEnter hooks: https://github.com/reactjs/react-router/blob/master/examples/auth-flow/app.js
An alternative would be to create two top-level components, one for protected routes, one for public routes (like a landing page or the sign in/sign up forms). The protected handler will then in componentWillMount check if there's a token:
- PublicHandler
+ SignIn
+ SignUp
+ Index
- ProtectedHandler
+ Dashboard
+ MoneyWithdrawal
it may looks like that , with sessionStorage (JWT token is accesseble, untill browser or tab closed)
///action creator redux
export const signupUser = creds => dispatch =>{
dispatch(requestSignup());
return API.auth.signup(creds)
.then(res => {
sessionStorage.setItem('token', res.token);// <------------------
dispatch(receiveSignup(res));
return res;
})
.catch(err => {
dispatch(SignupError(err));
);
});
};
On client : handling auth through HOC redux-auth-wrapper
On server on server you can use passport-jwt strategy
passport.use('jwt',new JwtStrategy(opts, function(jwt_payload, done) {
User.findOne({where:{ id: jwt_payload.user.id }}).then(user=>{
if (user) {
done(null, jwt_payload.user);
} else {
done(null, false);
// or you could create a new account
}
},err=>{
console.log('Error ',err);
return done(err,false);
});
}));
then just add route handler
var checkJWT = passport.authenticate('jwt')
router.get('/protected',checkJWT, (req, res) =>{
res.json(req.user);
});
You don't need sessions on server for that

Use one user on both remote and local connections

I want to connect to a remote server and use that for logins. This was not particularly hard.
Remote = DDP.connect('http://somesite.com');
Accounts.connection = Remote;
Meteor.users = new Mongo.Collection('users', Remote);
However, when I call meteor methods on my local code (there are multiple servers, but one login), it does not recognize the user.
Meteor.methods({
'start': function () {
if (!this.userId) {
// ...
} else {
throw new Meteor.Error(401, 'Unauthorized');
}
}
});
This always results in an error, despite being logged in.
How can I set my local user to the same user as the remote user?
Let's rename the following:
Remote => Login Server
local => Default Server This is where you call the Methods
I think you are better served by logging into Default Server, which then relays the login attempt to the Login Server.
This way you will be logged in on Default Server (if Login Server confirms the credentials are valid) when you use the Meteor.Methods that are on Default Server.
Accounts.validateLoginAttempt allows you to run arbitrary code in a callback on a LoginAttempt, allowing you to pass the validation from Default Server to Login Server:
if (Meteor.isServer) {
Accounts.validateLoginAttempt(function(attempt) {
//psuedocode block
var res = LoginServer_Login(attempt.methodArguements)
if (res === true) return true; // Login Success
else return false; // Login Failed
});
}
I'm not sure of the best way to implement LoginServer_Login func, though I'd try using HTTP.post to communicate with Login Server first (and recommend using restivus on the Login Server, it will give you authentication routes out of the box).
I just came across this package: admithub:shared-auth
It requires a shared db between the two meteor apps though.
Beyond this, you probably need to look into full SSO solutions.

Categories

Resources