userbase icon indicating copy to clipboard operation
userbase copied to clipboard

Use userbase in React Native

Open Fubinator opened this issue 3 years ago • 12 comments

I asked on Twitter if there was a way to use userbase in React Native. After some time of playing around, however, I still haven't managed to get userbase working. I am opening this issue to share my experience so far and maybe someone can help me find the solution (This is also too much for Twitter :smile:).

The proposed approach was to use userbase-js-node with rn-nodeify. In the process, I ran into a few problems, which I'll explain below.

So first things first: I've had massive problems with using expo in conjunction with rn-nodeify (See here). I ended up ditching expo and used the react-native-cli for creating the project.

After setting up the initial project and installing rn-nodeify, I tried to get userbase-js-node working. The problem here is that with userbase-js-node, node-localstorage is used. node-localstorage simulates localstorage by creating a file on disk, which is not possible in React Native. You could possibly somehow polyfill with react-native-fs, but that also seems a little awkward. @peculiar/webcrypto doesn't seem to work well in React Native either.

I ended up playing around with userbase's JavaScript SDK, which took me the furthest. I was able to polyfill the localstorage with react-native-sync-localstorage to the point that userbase stopped complaining.

Initialising the app does not throw any errors. I suspect this is because no session exists. However, as soon as I call userbase.signUp I get the following error: WebCryptoUnavailable: The WebCrypto API is unavailable. Please make sure your website uses https. According to userbase's source code this error is thrown when window.crypto.subtle is not available. When I logged that in my app it was indeed undefined. So I figured I needed a polyfill for window.crypto.subtle and came across react-native-webview-crypto. This does polyfill crypto.subtle, but now I get the following error:

Userbase error. Please report this to [email protected].

 stringify error InvalidAccessError: key is not extractable

I am stuck at this point. I think the error comes directly from window.crypto.subtle, but I don't find much on google when I search for it directly. Also, I'm not sure if I'm too naive about polyfilling. Replacing everything that is not available with different libraries doesn't seem to be the best approach.

If anyone has any further suggestions or possibly even userbase in React Native running, I would be very happy if they could point me in the right direction.

EDIT: According to this a key is not extractable, when there is a boolean flag set to false when calling window.crypto.subtle.generateKey(). However, when searching in the repo, I can't find any occurrences where the flag is set to false.

EDIT2: After some debugging, it seems like the error occurs here: https://github.com/smallbets/userbase/blob/74d09f77aaf2f4721de15c4c239ab7a46eefaacc/src/userbase-js/src/Crypto/aes-gcm.js#L182-L188 For testing purposes I've set KEY_IS_EXTRACTABLE to true. As soon as I set the flag, I can create a user. Now it seems that the websocket is broken, as I get the following error: WebSocket error: Can't find variable: DOMException

EDIT3: The last error was a little bit misleading. It actually occurs here: https://github.com/smallbets/userbase/blob/74d09f77aaf2f4721de15c4c239ab7a46eefaacc/src/userbase-js/src/Crypto/hmac.js#L14-L26 Again, after I set the key to be extractable, the error is gone. I can even log in and get a session. I won't try any further from here, as I don't know what cryptographic complications this presents either. However, maybe someone knows if the problem is with the implementation of react-native-webview-crypto.

Fubinator avatar Apr 10 '21 08:04 Fubinator

Oh man, I'm sorry for this rabbit hole you've gone down. That twitter response was me. The most challenging aspects of getting userbase-js-node working were dealing with similar things. My recommendation would be to turn back unless you're prepared to keep putting time into this. You're making good progress even if it feels crappy to run into error after error.

On the crypto issue: to me, this looks like an implementation error with react-native-webview-crypto mishandling the extraction somehow, I'm not exactly sure. I ran into an implementation error with the node web crypto polyfill as well, and submitted the issue (can see I linked to it in that comment in your EDIT3), so definitely not out of the question. In any case, that boolean isn't a big deal. It's saying the raw key can be loaded into memory shared with the executing code via the function exportKey. But usernames and passwords (and the user's seed) is already loaded into shared memory anyway, and those are functionally what enable anyone to retrieve all of a user's cryptographic keys. So extractability, or lack thereof, is really not providing meaningful protection imo.

On the websocket polyfill: perhaps react-native-websocket will work?

I believe in the end, would need polyfills for all of these to make sure everything works. Could help serve as a guide maybe.

j-berman avatar Apr 12 '21 09:04 j-berman

No worries about the rabbit hole :smile: It actually gave me a deeper understanding of how userbase works internally. I think I'll continue to look into it a bit more as I have some time to do so. I will definitely take a closer look at the react-native-webview-crypto implementation, as it seems to be the biggest problem. If I find a solution or encounter any other problems that someone could help me with, I would comment in here again. Maybe it would help someone else for future reference.

Fubinator avatar Apr 12 '21 14:04 Fubinator

@Fubinator I would definitely be interested in React Native support for Userbase. I'm currently playing around with Userbase in Ionic. I like React Native significantly more than Ionic, so if you need help with this let me know, I'd for sure want to assist when I have the time!

jneterer avatar Apr 14 '21 23:04 jneterer

@jneterer I'll briefly describe my current evaluation of the problem and what we need to make it work: The main problem is the polyfill of the WebCrypto API. I have tried several polyfills, all of which failed in different places:

  1. isomorphic-webcrypto: This exposes the Microsoft Research Library. It throws Error: Could not find an appropriate alg. I trusted the error message and think that some algorithm is simply not implemented here.

  2. react-native-webview-crypto: I described that in the initial post. Here errors are thrown when deriveKey is executed. This could possibly be a bug in the library or in webview-crypto, on which react-native-webview-crypto is based.

  3. @peculiar/webcrypto + rn-nodeify: This library is also used in userbase-js-node to polyfill the Crypto Web API. Errors are thrown here that I haven't quite located yet.

So what is the current status? I think there are two options: Either we continue to look for bugs in the libraries and try to reproduce them minimally so that we eventually write bug reports, or we find another library that works out of the box. Another problem is that all libraries so far have been very slow.

My current polyfill setup looks like this:

import './shim.js'; // rn-nodeify shims

const {Crypto} = require('@peculiar/webcrypto');
import localStorage from 'react-native-sync-localstorage';

export default async function initialize(): Promise<void> {
  // crypto
  global.crypto = new Crypto();

  // localStorage
  await localStorage.getAllFromLocalStorage();
  global.localStorage = localStorage;

  // sessionStorage
  // https://gist.github.com/juliocesar/926500#gistcomment-1620487
  global.sessionStorage = {
    _data: {},
    setItem: function (id: string, val: any) {
      return (this._data[id] = String(val));
    },
    getItem: function (id: string) {
    return Object.prototype.hasOwnProperty.call(this._data, 'id')
        ? this._data[id]
        : undefined;
    },
    removeItem: function (id: string) {
      return delete this._data[id];
    },
    clear: function () {
      return (this._data = {});
    },
  };

  // DOMException
  global.DOMException = require('domexception');
}

I hope this is a good overview to try out for yourself.

Fubinator avatar Apr 18 '21 10:04 Fubinator

So after a little bit of further investigation I managed to sign in using the @peculiar/webcrypto lib. There is one problem with the crypto.createSign function in conjunction with rn-nodeify. rn-nodeify uses browserify-sign. It calls the crypto.createSign function with SHA256 as algorithm name. browserify-sign then looks for the algorithm and throws an error if it is not found (See here). Adding an uppercase definition for the algorithm here lets me sign in.

In my opinion, browserify-sign should allow the algorithm name to be uppercase, as in node the following works:

const crypto = require('crypto');

console.log(crypto.createSign('SHA256'));

I've created an issue on browserify-sign and I might provide a PR. We might be near to a solution for the crypto problem over here.

Fubinator avatar Apr 18 '21 12:04 Fubinator

@Fubinator this is really encouraging to hear! How was the speed of signing in?

jneterer avatar Apr 18 '21 13:04 jneterer

@j-berman I do have one question when it comes to mobile apps built with Userbase. If I've set remember me to local storage and the user at some point deletes the app, then they want to sign into the web app but they've forgotten their password, will they be able to since they've deleted the app? Or will they have to reinstall and reset it there? The question comes from this line in the Forgot Password docs "Recovery will not be possible if the user loses access to all previously used devices". Is it the physical device that is important, or the browser/app?

jneterer avatar Apr 18 '21 13:04 jneterer

@jneterer Sign in speed is not good. I stopped the time and it's taking ~28 seconds.

BTW: It is possible to change the name of the SHA-256 algorithm in userbase.js directly so that it works. Just change the constant to "sha-256" over here: https://github.com/smallbets/userbase/blob/74d09f77aaf2f4721de15c4c239ab7a46eefaacc/src/userbase-js/src/Crypto/sha-256.js#L5 Maybe that would be a possibility if everything else doesn't work.

Fubinator avatar Apr 18 '21 14:04 Fubinator

I've created a repository with a little bit of explanation and the current status: https://github.com/Fubinator/react-native-userbase-poc

I think this is better than spamming my intermediate steps in here all the time :smile:

Fubinator avatar Apr 18 '21 18:04 Fubinator

@Fubinator great idea, I'll check your repo out!

jneterer avatar Apr 19 '21 01:04 jneterer

@j-berman I do have one question when it comes to mobile apps built with Userbase. If I've set remember me to local storage and the user at some point deletes the app, then they want to sign into the web app but they've forgotten their password, will they be able to since they've deleted the app? Or will they have to reinstall and reset it there? The question comes from this line in the Forgot Password docs "Recovery will not be possible if the user loses access to all previously used devices". Is it the physical device that is important, or the browser/app?

For apps that are end-to-end encrypted, if the user deletes the app and all the app's data, this will likely delete the user's encryption key too which is stored locally when you use the local storage option. If the user loses both their password and the encryption key, the user wouldn't be able to access their encrypted data.

j-berman avatar Apr 20 '21 05:04 j-berman

Any updates?

dancomanlive avatar Jun 05 '22 22:06 dancomanlive