Today I'm reviewing another Ionic Native feature, the Secure Storage wrapper. As the plugin docs explain, this is a plugin that allows for encrypted storage of sensitive data. It follows an API similar to that of WebStorage, with a few differences.
First, the plugin lets you define a 'bucket' for your data. So your app could have multiple different sets of data that are separated from each other. (The plugin refers to it as 'namespaced storage', but buckets just made more sense to me.)
Second, you can't get all the keys like you can with WebStorage. That's probably related to the whole 'secure' thing, but in general, I can't imagine needing that functionality in a real application. You could also use a key that represents a list of keys.
Secure Storage is a key/value storage system, and like WebStorage, you can only store strings, but you can use JSON to get around that.
With that out of the way - let's build a simple demo. I created a simple two page app to represent a login screen and main page.
Let's start by looking at the first page, our login screen.
<ion-header>
<ion-navbar>
<ion-title>
Secure Storage Example
</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-list>
<ion-item>
<ion-label fixed>Username</ion-label>
<ion-input type="text" [(ngModel)]="username"></ion-input>
</ion-item>
<ion-item>
<ion-label fixed>Password</ion-label>
<ion-input type="password" [(ngModel)]="password"></ion-input>
</ion-item>
</ion-list>
<button primary block (click)="login()">Login</button>
</ion-content>
Now we'll look at the code behind this.
import {Component} from '@angular/core';
import {NavController} from 'ionic-angular';
import {LoginProvider} from '../../providers/login-provider/login-provider';
import { Dialogs } from 'ionic-native';
import {MainPage} from '../main-page/main-page';
@Component({
templateUrl: 'build/pages/home/home.html',
providers:[LoginProvider]
})
export class HomePage {
public username:string;
public password:string;
private loginService:LoginProvider;
constructor(public navCtrl: NavController) {
this.loginService = new LoginProvider();
}
login() {
console.log('login',this.username,this.password);
this.loginService.login(this.username,this.password).subscribe((res) => {
console.log(res);
if(res.success) {
//thx mike for hack to remove back btn
this.navCtrl.setRoot(MainPage, null, {
animate: true
});
} else {
Dialogs.alert("Bad login. Use 'password' for password.","Bad Login");
}
});
}
}
All we've got here is a login handler that calls a provider to verify the credentials. There's one interesting part - the setRoot
call you see there is used instead of navCtrl.push as it lets you avoid having a back button on the next view. Finally, let's look at the provider, even though it's just a static system.
import { Injectable } from '@angular/core';
import 'rxjs/add/operator/map';
import {Observable} from 'rxjs';
//import 'rxjs/Observable/from';
@Injectable()
export class LoginProvider {
constructor() {}
public login(username:string,password:string) {
let data = {success:1};
if(password !== 'password') data.success = 0;
return Observable.from([data]);
}
}
Basically - any login with "password" as the password will be a succesful login. That's some high quality security there!
You can view this version of the code here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/securestorage_ionicnative/app_v1
Ok, so let's kick it up a notch. My plan with Secure Storage is to modify the code as such:
- When you login, JSON encode the username and password and store it as one value.
- When the app launches, first create the 'bucket' for the system, which will only actually create it one time.
- See if pre-existing data exists, and if so, get it, decode it, put the values in the form, and automatically submit the form.
Since I'm using a plugin, I know now that my app has to wait for Cordova's deviceReady to fire. I've got a login button in my view that I can disable until that happens. So one small change to the view is to show/hide it based on a value I'll use based on the ready status. Here is the new login button:
<button primary block (click)="login()" *ngIf="readyToLogin">Login</button>
Now let's look at the updated script. I'll share the entire update and then I'll point out the updates.
import {Component} from '@angular/core';
import {NavController,Platform} from 'ionic-angular';
import {LoginProvider} from '../../providers/login-provider/login-provider';
import { Dialogs } from 'ionic-native';
import {MainPage} from '../main-page/main-page';
import {SecureStorage} from 'ionic-native';
@Component({
templateUrl: 'build/pages/home/home.html',
providers:[LoginProvider]
})
export class HomePage {
public username:string;
public password:string;
private loginService:LoginProvider;
public readyToLogin:boolean;
private secureStorage:SecureStorage;
constructor(public navCtrl: NavController, platform:Platform ) {
this.loginService = new LoginProvider();
this.readyToLogin = false;
platform.ready().then(() => {
this.secureStorage = new SecureStorage();
this.secureStorage.create('demoapp').then(
() => {
console.log('Storage is ready!');
this.secureStorage.get('loginInfo')
.then(
data => {
console.log('data was '+data);
let {u,p} = JSON.parse(data);
this.username = u;
this.password = p;
this.login();
},
error => {
// do nothing - it just means it doesn't exist
}
);
this.readyToLogin = true;
},
error => console.log(error)
);
});
}
login() {
this.loginService.login(this.username,this.password).subscribe((res) => {
console.log(res);
if(res.success) {
//securely store
this.secureStorage.set('loginInfo', JSON.stringify({u:this.username, p:this.password}))
.then(
data => {
console.log('stored info');
},
error => console.log(error)
);
//thx mike for hack to remove back btn
this.navCtrl.setRoot(MainPage, null, {
animate: true
});
} else {
Dialogs.alert('Bad login. Use \'password\' for password.','Bad Login','Ok');
this.secureStorage.remove('loginInfo');
}
});
}
}
So let's start at the top. Don't forget that your Ionic views can fire before the Cordova deviceReady event has fired. I still wish there was a simple little flag I could give to my Ionic code to say "Don't do anything until then", but until then, you can use the Platform
class and the ready event.
I create my Secure Storage bucket "demoapp", and in the success handler, I immediately look for the key loginInfo
. Obviously on the first run it won't exist, but the bucket will be created. On the second (and onward) run, the bucket will already exist, and the data may or may not exist.
If it does - I decode it, set the values, and login. That last operation was optional of course. Maybe your app will just default the values. There's a few different ways of handling this.
Finally, in the login handler I both set the value (after encoding it) and clear it based on the result of the login attempt. Notice that both calls are asynchronous, but I really don't need to wait for them, right? Therefore I treat them both as 'fire and forget' calls.
They could, of course, error. And there is a very good reason why it could. In the docs, they mention that this plugin works just fine on iOS, but on Android it will only work if the user has a secure pin setup. That's unfortunate, but the plugin actually provides an additional API to bring up that setting for Android users, which is pretty cool I think.
You can find the code for this version here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/securestorage_ionicnative/app
How about a few final thoughts?
- While you can store a username and password, and the docs even say this, I still feel a bit wonky about doing so. I'd maybe consider storing a token instead that could be used to automatically login just that user. And it could have an automatic timeout of some sort.
- If you read the blog post, Ionic Native: Enabling Login with Touch ID for iOS, then this plugin would be a great addition to that example.
- A bit off topic, but I would normally have added a "loading" indicator on login to let the user know what's going on. And of course, Ionic has one. I was lazy though and since my login provider was instantaneous, I didn't feel like it was crucial.
As always - let me know what you think in the comments below.
p.s. I'm loving Ionic 2, and Angular 2, and TypeScript, but wow, it is still a struggle. For this demo, I'd say 80% of my time was spent just building the first version. I'm still struggling with Observables, still struggling with Angular 2 syntax. Heck, it took me a few minutes to even just bind the form fields to values. That being said, and I know I've said this before, I still like the code in v2 more than my Angular 1 code.
Archived Comments
have you had any issues running this with IOS?
I'm having issues when I try and set, get the created storage it errors.
The only error I get back is the following:
{"line":71,"column":45,"sourceURL":"http://192.168.1.3:8100/plugins/cordova-plugin-secure-storage/www/securestorage.js"}
You don't see anything else in the console? You said it errors, so therefore the error must show up.
Error message is "Failure in SecureStorage.set() - Refer to SecBase.h for description"
https://github.com/Crypho/c... I think it's related to emulator issue. I'll have to try and test out on a real device and see if I reproduce.
Ok - and if you can - you should file that w/ the plugin repo.
Yep confirmed it's a bug in the emulator. Was able to progress when deployed to a real device.
It doesn't works on some devices like HTC , Samsung. But working on Asus. What I do?
my code is
this.secureStorage.create('storeroom')
.then(
()=>{
this.secureStorage.set('userId',this.username)
.then(
data=>{
loading.dismiss();
},
error=>{
loading.dismiss();
console.log('Your device issue');
}
)
},
error=>{
loading.dismiss();
console.log(error.error);
}
);
Report it as a bug to the plugin author.
Question what is the main difference between ionic native storage and ionic secure storage?
There is no Ionic Native Storage. Ionic Native is a group name for plugins that have been 'wrapped' and made more 'Angular friendly'.
Can we see encrypted data in console ?
Did you try?
No, I can't see because when I try to get data using this.secureStorage.get(.......) then it returns me decrypted data.
So how we can see encrypted data.
I'd ask the plugin author - I believe it is stored in a platform specific manner. But yea - check on the plugin site.
I just looked and the plugin docs talk about this.
Thanks.
Thanks for the nice blog
ive got the problem, when try to login, then ive got a runtime error
with Cannot read proberty 'set' of undefindet
the same happens if i typ the wrong pass, but then i got insteadt of 'set' 'remove'
TypeError: Cannot read property 'set' of undefined
at SafeSubscriber._next (http://localhost:8100/build/main.js:146:30)
at SafeSubscriber.__tryOrSetError (http://localhost:8100/build/vendor.js:15697:16)
at SafeSubscriber.next (http://localhost:8100/build/vendor.js:15637:27)
at Subscriber._next (http://localhost:8100/build/vendor.js:15575:26)
at Subscriber.next (http://localhost:8100/build/vendor.js:15539:18)
at ArrayObservable._subscribe (http://localhost:8100/build/vendor.js:28853:28)
at ArrayObservable.Observable._trySubscribe (http://localhost:8100/build/vendor.js:211:25)
at ArrayObservable.Observable.subscribe (http://localhost:8100/build/vendor.js:199:27)
at HomePage.webpackJsonp.227.HomePage.login (http://localhost:8100/build/main.js:143:63)
at Object.eval [as handleEvent] (ng:///AppModule/HomePage.ngfactory.js:517:24)
Ionic Framework: 3.6.0
Ionic App Scripts: 2.1.3
Angular Core: 4.1.3
Angular Compiler CLI: 4.1.3
Node: 6.11.2
OS Platform: Windows 10
Navigator Platform: Win32
User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
Not sure I understand. You type the wrong password for my login service?
i mean, in your example, you coded the password to 'password'
so if i write anything other then 'passwort' i get the error with 'remove'
What remove call though? If you put in a bad password it should keep you on the login page, right?
Oh wait - I think I see the issue. My logic assumes that you had already logged in once. So this is a bug in my code. It should say (in pseudo-code)
if login failed, AND if I had stored info before, then remove it.
how to create storage instance just once?
I believe (stress, BELIEVE) the create API only makes it once and after that it's just opened.
Of course, but the documentation is too light and doesn't specify it
Then I'd file a bug report so they know the docs are lacking in this regard.
Hi, thanks for the good summary!
Do you have any idea how to properly unit test this.
Is there a way I can run automated tests to make sure my credentials are saved securely?
Thanks in advance :)
Sorry - I haven't used this in a while.
what solve this issue, same issue
I'd offer the same advice, to report it to the plugin.