Hybrid App Developers: Don’t Store Your User’s Passwords
I almost titled this post something clickbaity like 'The one MASSIVE security mistake hybrid developers are making'. It is a big issue, and it is a mistake I see pop up on a weekly basis (and I'm sure it extends outside of the hybrid mobile app development sphere). I don't want the tone of this article to come across as accusatory, in fact, the mistake is completely understandable. But it's important that if you are currently making this mistake that you read this, and stop doing it (and preferably, update any existing applications where you are doing this).
In a nutshell, this is the issue: developers want a user to be logged back in automatically the second time they visit an application. So, after logging in the first time, they store the user's username and password in local storage, and then retrieve the values from there next time rather than requiring the user to enter it themselves.
This totally works, which is why it's understandable that it is so often used, but it is a very bad security practice that puts your user's credentials at risk.
Outline
Source codeWhy Is Storing Users Passwords Bad?
Passwords are obviously super sensitive pieces of information, so the general rule with passwords is that you don't store them anywhere, ever (unless you're doing something like developing a password manager, in which case you should have some serious security experts on your team). The only place you should ever "store" a password is on the server that you are authenticating against, and even then it should, at the very least, be a hash of the original password – so the original password is still never actually stored anywhere.
And remember, hashing is different to encryption. Encryption is two-way, meaning that it can be reversed, a hash (theoretically) can not. The natural extension to this issue, which also happens very frequently, is that the developer will realise a password stored in plain text in local storage is not secure, so they decide to use some more secure form of storage and encrypt it. This is still not acceptable. Since it has been encrypted it can also be decrypted, and if your application can decrypt that password to authenticate against a server then so could an attacker.
So, What Do We Do Then?
Hopefully, I've established that we can't store a user's password in local storage (or anywhere) to authenticate them automatically. However, the ability to log a user in automatically is a desirable and convenient function, so how do we do it securely?
Generally, rather than storing the user's credentials in local storage, we can instead use some kind of token to authenticate them. There are many different ways you could go about doing this, but I am going to walk through one particular way of doing it using a JWT (JSON Web Token) in an Ionic application.
In short, a JWT encodes some data that we can sign with a key. We can then store that token and later send it back to the server that created it, and that server can verify its authenticity (i.e. that it hasn't been modified). The only way to create or modify a JWT that will pass this test for authenticity is to have the secret key, which is hopefully locked up safe on your server and readable by nobody. This means that if someone tampers with a JWT that your server creates, you will know about it – this is the critical part of how we are able to use a JWT for authentication without requiring a password.
I don't intend to provide a complete tutorial about JSON Web Tokens in this article, because I have already covered it here: Using JSON Web Tokens (JWT) for Custom Authentication in Ionic. If you don't have a basic understanding of JWT already, I would recommend reading that first.
IMPORTANT: One misconception I want to make very clear is that a JWT is not a secure way to store sensitive data – that is not its purpose. A JWT is just base64
encoded, which can very easily be decoded. The point isn't that the contents of a JWT can't be viewed, it's that they can't be modified. Keeping this in mind as you read this tutorial should help clarify the concept.
Authenticating a User using a JWT
In this example I am going to set up a simple Node server to authenticate a user, send the user back a JWT, and then reauthenticate that user later by using the same JWT.
We will be using the jsonwebtoken npm package. This will allow us to create and verify a JWT in Node, but there are plenty of libraries out there to do this for many different server side languages. Here's one for PHP, Ruby, Python, Java.
IMPORTANT: Please do not use this tutorial as an example of how to create an authentication system. This is an unrealistic example that is used solely to demonstrate the point that a JWT can be used for authentication without requiring you to store a user's password. The example authentication system we use will be built in Node, you can use the same concept with just about any server-side language.
A Note on Using Local Storage to Store Tokens
Before we begin, I wanted to address something that comes up quite often which is: "You should never use local storage to store a token as it is insecure and can be stolen by attackers. You should only ever use an HTTP only cookie to store a token."
People have strong opinions on this, and you will find articles that warn against using local storage for storing tokens - there is no consensus on this, however. With nothing else to consider, storing tokens in cookies is generally better, but local storage is an option that may be preferable under certain circumstances.
The argument against the security of using local storage is that it is vulnerable to XSS attacks. That is, if a malicious person can manage to execute JavaScript on your app/website they could steal the tokens of users from local storage. This is true, but people having the ability to execute arbitrary scripts on your app/website is a huge security vulnerability in the first place. Cookie based tokens are also not completely immune to these types of attacks either. If a malicious user can execute arbitrary code on your app/website, then they can also launch their own HTTP requests from that app/website which would contain the user's token.
1. Implementing the Server
Here's the example Node server I have set up for this example:
// Set up
var express = require('express');
var app = express();
var logger = require('morgan');
var bodyParser = require('body-parser');
var cors = require('cors');
var jwt = require('jsonwebtoken');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(logger('dev'));
app.use(cors());
app.set('jwtSecret', 'ekjwhf9832j98fh9wefew08');
// Routes
app.post('/api/auth', function (req, res) {
if (req.body.username === 'Josh' && req.body.password === 'password') {
var token = jwt.sign({ userId: 57 }, app.get('jwtSecret'));
res.json({
success: true,
message: 'Authenticated as Admin',
token: token,
});
} else {
res.json({
success: false,
message: 'Invalid login',
});
}
});
app.post('/api/checkToken', function (req, res) {
var token = req.body.token;
if (token) {
jwt.verify(token, app.get('jwtSecret'), function (err, validToken) {
if (err) {
res.json({
success: false,
message: 'Invalid token',
});
} else {
if (validToken.userId === 57) {
res.json({
success: true,
message: 'Authenticated as Admin',
});
}
if (validToken.userId === 1) {
res.json({
success: true,
message: 'Authenticated as Super Admin',
});
}
}
});
} else {
res.json({
success: false,
message: 'No token provided',
});
}
});
// Listen
app.listen(process.env.PORT || 8080);
We have two endpoints set up: /api/auth
and /api/checkToken
.
The auth
route will be used to authenticate the user normally using a username and password. This is clearly not a proper authentication system – it's only checking for a single username/password combination, but how authentication occurs in terms of the database etc. isn't really relevant to this tutorial.
In the case of a successful authentication, we create a new JWT using the jsonwebtoken
package. We give it a payload of:
{userId: 57}
and sign it with the secret key. This token is then sent back in the authentication response (which will allow the client to store it for use later). Since we know the JWT can not be modified, when a user sends this token back to us later we can trust that they are indeed the user with an id of 57
, because we gave them a token that says so (it's kind of like issuing them a driver's license or passport).
The other route we have is for checking the validity of an existing token. First, we check if the token is valid by calling the verify
method, and if it's not valid we reject it right away (we will talk a little more about why a token might be rejected later).
If it is valid, we check the userId
contained in that JWT and immediately authenticate the user. Again, this is just a simple example and it's only checking against two possible options. In a real system, you would likely perform some lookup in the database using the provided userId
and do whatever you need to do to authenticate them as that user.
We've set up a second check in the checkToken
route for a userId
of 1
as well as the 57
we created before. We're not assigning any tokens for a user with an id of 1
, but a little later we are going to try and hack our way into being authenticated as the Super Admin by modifying the JWT (carrying on with the passport example, this would be equivalent to us trying to create a fake passport to impersonate someone else).
2. Authenticating and Assigning the Token
Now we need to handle things on the client side. I'll walk through the code I've written for Ionic, but it's going to be the same concept for anything you're using.
First, I created this authentication provider:
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class AuthProvider {
url: string = 'http://localhost:8080/'
constructor(public http: Http) {
}
login(username, password){
let headers = new Headers();
headers.append('Content-Type', 'application/json');
let credentials = {
username: username,
password: password
};
return this.http.post(this.url + 'api/auth', JSON.stringify(credentials), {headers: headers});
}
reauthenticate(token){
let headers = new Headers();
headers.append('Content-Type', 'application/json');
let credentials = {
token: token
};
return this.http.post(this.url + 'api/checkToken', JSON.stringify(credentials), {headers: headers});
}
}
This just sends an HTTP request to either of the two routes that were created. In the case of an initial login we will be sending the username and password that the user supplies, but on subsequent logins, we will be sending the JWT.
IMPORTANT: In a production environment, make sure that you are sending requests to your server over HTTPS, not HTTP. If you send sensitive information via HTTP an attacker could intercept that request and view the user's password.
Then we make use of this provider as part of the login process:
import { Component } from '@angular/core';
import { NavController, NavParams, LoadingController } from 'ionic-angular';
import { Storage } from '@ionic/storage';
import { AuthProvider } from '../../providers/auth/auth';
import { HomePage } from '../home/home';
@Component({
selector: 'page-login',
templateUrl: 'login.html',
})
export class LoginPage {
loading: any;
username: string;
password: string;
constructor(public navCtrl: NavController, public navParams: NavParams, public storage: Storage,
public loadingCtrl: LoadingController, public auth: AuthProvider){
}
ionViewDidLoad() {
this.showLoader();
this.storage.get('token').then((token) => {
if(typeof(token) !== 'undefined'){
console.log("authenticating with token: ", token);
this.auth.reauthenticate(token).map(res => res.json()).subscribe((res) => {
console.log(res);
this.loading.dismiss();
if(res.success){
this.navCtrl.setRoot(HomePage);
}
}, (err) => {
console.log(err);
this.loading.dismiss();
});
} else {
this.loading.dismiss();
}
});
}
login(){
this.showLoader();
this.auth.login(this.username, this.password).map(res => res.json()).subscribe((res) => {
this.loading.dismiss();
if(res.success){
console.log("received token: ", res.token);
this.storage.set('token', res.token);
this.navCtrl.setRoot(HomePage);
}
}, (err) => {
this.loading.dismiss();
});
}
showLoader(){
this.loading = this.loadingCtrl.create({
content: 'Authenticating...'
});
this.loading.present();
}
}
We're using the ionViewDidLoad
hook, which triggers as soon as this page loads, to launch a request to authenticate the token if one exists in local storage. If we get a success
response back from the server in response to that JWT, we grant the user access to the application.
In the case of a successful initial login, the token is contained in the response from the server. So, we take that token and add it to local storage.
In a real-world scenario you will also likely add some kind of expiry onto the token and revalidate it by issuing new tokens, but I want to keep this example as simple as possible.
3. Attempting to Modify the Token
When you first look at JSON Web Tokens to perform automatic authentication of users, it can be a little hard for it to "click". When discussing this concept with some people, they often get stuck on the idea of authenticating the user without any form of credentials. The JWT tells us that the user's id is 57
and we're supposed to just say "sure, no problem, welcome to the app user 57". Perhaps surprisingly, yes, that's exactly what we are doing, but let's walk through an example to show why this is secure.
NOTE: It is technically a possibility for someone's JWT to be stolen, and then that token could potentially be used to authenticate the person who stole the token as whoever they stole it from. There are additional safeguards you could implement against this (e.g. having a JWT with a short expiry time and using refresh tokens, requiring re-authentication for sensitive actions), but having the token stolen from storage certainly beats having a plain text password being stolen from storage.
If we were a nefarious user, how might we go about exploiting this JWT setup?
You can easily decode and encode with base64
by using the Javascript function btoa
and atob
. After authenticating against my server, the JWT that I was assigned was as follows:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU3LCJpYXQiOjE0OTY2Mzk1NjJ9.JZKf2L3usAQWsC1plSPCRcHMoSST_3_BYtF6_-rVk80
You may notice the string above is separated by three periods .
. The middle section contains the payload, which contains the identifying information for the user.
If you go to jwt.io they have a handy little tool for decoding and verifying JSON Web Tokens (you could also just use the atob
function manually if you want). If you put the above JWT into the box you will see that the payload is as follows:
{
"userId": 57,
"iat": 1496639562
}
So, by having this JWT I am entitled to access the server as the user with an id of 57
. That's great, but let's say I don't want to be user 57
, I want to be user 1
, who happens to be a "Super Admin". Well, I've got the token right here, and I know it's just base64 encoded, so why don't I just overwrite it but modify the userId
to be 1
instead of 57
? Let's try that.
We will take this string:
{"userId": 1,"iat": 1496639562}
and base64
encode it using btoa
, which will give us this value:
eyJ1c2VySWQiOjEsImlhdCI6MTQ5NjYzOTU2Mn0=
now we will just modify the payload of the JWT to use this instead of the original value:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTQ5NjYzOTU2Mn0=.JZKf2L3usAQWsC1plSPCRcHMoSST_3_BYtF6_-rVk80
Let's modify the login page to use this fake token instead of the one from storage:
this.storage.get('token').then((token) => {
if (typeof token !== 'undefined') {
console.log('authenticating with token: ', token);
// Attempt to hack token
token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTQ5NjYzOTU2Mn0=.JZKf2L3usAQWsC1plSPCRcHMoSST_3_BYtF6_-rVk80';
this.auth
.reauthenticate(token)
.map((res) => res.json())
.subscribe(
(res) => {
console.log(res);
this.loading.dismiss();
if (res.success) {
this.navCtrl.setRoot(HomePage);
}
},
(err) => {
console.log(err);
this.loading.dismiss();
}
);
} else {
this.loading.dismiss();
}
});
The token from storage is being overwritten by our fake token, so now we will try to refresh the application and have it authenticate us as the Super Admin.
You can see in the image above, we aren't successfully logged in, we get a message saying that the token is invalid. This is because since the payload of the application has been changed, the signature created using the jwtSecret
on the server is no longer valid. If anything at all changes in the JWT, the validation will fail.
Once again with the passport example, what I'm attempting to do here is pretty much like taking my passport, crossing out "Joshua Morony" and writing in "Roger Federer". When I hand my passport over it's going to be obvious that it is fake, and I'm probably getting detained.
Summary
This is just one example of how you might go about authenticating a user automatically without having to store their password, and although the concept of a JWT may be a bit hard to grok initially they are the perfect tool for the job. On a conceptual level they can be a bit tricky, but using them is super easy. There's plenty more useful things to know about JSON Web Tokens, but I don't want to over-complicate this tutorial.
Usually, I don't necessarily have anything against just taking the approach of going with whatever works and getting the job done, but this is one particular example where I feel we all have a responsibility to make sure we are taking the security of our user's passwords seriously.