Meteor Authentication from React Native


by Spencer Carli

Future updates will be made on Medium.

In this post I extend on my previous one in which I discussed how to easily connect a React Native app to a Meteor server. We’ll talk about the next component you’re likely to encounter - authentication. I’ll cover how to login with a username/password, email/password, or via a resume token (and how to store it).

Creating the Apps

I covered how to create and connect both the meteor app and the react native app last time so I won’t cover that here. If you need help getting started please refer to my previous post.

To get started simply clone the previous Github repo via git clone https://github.com/spencercarli/quick-meteor-react-native.

The code in this post will use that as a starting point. With that, let’s make a couple small adjustments before we dive in.

First, cd meteor-app && meteor add accounts-password. We’ll need to pull in the basic Meteor accounts package.

Then, create RNApp/app/ddp.js:

import DDPClient from 'ddp-client';
let ddpClient = new DDPClient();

export default ddpClient;

Then in RNApp/app/index.js replace

import DDPClient from 'ddp-client';
let ddpClient = new DDPClient();

with

import ddpClient from './ddp';

We’re doing this to keep our sign in/up logic out of the index.js file - keeping it a bit cleaner and organized.

Creating a User

Before diving into logging in we have to learn how to create a user. We’ll just hook into the Meteor core method createUser. We’ll be using it for email and password authentication - you can view the other options available in the Meteor docs.

In RNApp/app/ddp.js

import DDPClient from 'ddp-client';
let ddpClient = new DDPClient();

ddpClient.signUpWithEmail = (email, password, cb) => {
  let params = {
    email: email,
    password: password
  };

  return ddpClient.call('createUser', [params], cb);
};

ddpClient.signUpWithUsername = (username, password, cb) => {
  let params = {
    username: username,
    password: password
  };

  return ddpClient.call('createUser', [params], cb);
};

export default ddpClient;

We’ll wire up the UI a bit later.

Exploring the “login” Meteor method

Meteor core provides a method, login, that we can use to handle authorization of a DDP connection. This means that this.userId will now be available in Meteor methods and publications - allowing you to handle verification. This method handles all login services for Meteor. Logging in via email, username, resume token, and Oauth (though we won’t cover OAuth here).

When using the login method you pass an object as a single parameter to the function - the formatting of that object determines how you’re logging in. Here is how each looks.

For Email and Password:

{ user: { email: USER_EMAIL }, password: USER_PASSWORD }

For Username and Password:

{ user: { username: USER_USERNAME }, password: USER_PASSWORD }

For Resume Token:

{ resume: RESUME_TOKEN }

Signing In with Email and Password

In RNApp/app/ddp.js:

/*
 * Removed from snippet for brevity
 */

ddpClient.loginWithEmail = (email, password, cb) => {
  let params = {
    user: {
      email: email
    },
    password: password
  };

  return ddpClient.call("login", [params], cb)
}

export default ddpClient;

Signing In with Username and Password

In RNApp/app/ddp.js:

/*
 * Removed from snippet for brevity
 */

ddpClient.loginWithUsername = (username, password, cb) => {
  let params = {
    user: {
      username: username
    },
    password: password
  };

  return ddpClient.call("login", [params], cb)
}

export default ddpClient;

Storing the User Data

React Native has the (AsyncStorage)[https://facebook.github.io/react-native/docs/asyncstorage.html#content] API which we’ll be using to store the login token, login token expiration, and the userId. This data will be returned after successfully logging in or creating an account.

In RNApp/app/ddp:

import DDPClient from 'ddp-client';
import { AsyncStorage } from 'react-native';

/*
 * Removed from snippet for brevity
 */

ddpClient.onAuthResponse = (err, res) => {
  if (res) {
    let { id, token, tokenExpires } = res;

    AsyncStorage.setItem('userId', id.toString());
    AsyncStorage.setItem('loginToken', token.toString());
    AsyncStorage.setItem('loginTokenExpires', tokenExpires.toString());
  } else {
    AsyncStorage.multiRemove(['userId', 'loginToken', 'loginTokenExpires']);
  }
}

export default ddpClient;

This will give us persistent storage of those credentials, that way you can automatically login a user the next time they open the app.

Signing In with a Resume Token

In RNApp/app/ddp:

/*
 * Removed from snippet for brevity
 */

ddpClient.loginWithToken = (loginToken, cb) => {
  let params = { resume: loginToken };

  return ddpClient.call("login", [params], cb)
}

export default ddpClient;

Signing Out

In RNApp/app/ddp:

/*
 * Removed from snippet for brevity
 */

ddpClient.logout = (cb) => {
  AsyncStorage.multiRemove(['userId', 'loginToken', 'loginTokenExpires']).
    then((res) => {
      ddpClient.call("logout", [], cb)
    });
}

export default ddpClient;

The UI

First thing I want to do is break up RNApp/app/index a bit. It’ll make it easier to manage later on.

First, create RNApp/app/loggedIn.js:

import React, {
  View,
  Text
} from 'react-native';

import Button from './button';

import ddpClient from './ddp';

export default React.createClass({
  getInitialState() {
    return {
      posts: {}
    }
  },

  componentDidMount() {
    this.makeSubscription();
    this.observePosts();
  },

  observePosts() {
    let observer = ddpClient.observe("posts");
    observer.added = (id) => {
      this.setState({posts: ddpClient.collections.posts})
    }
    observer.changed = (id, oldFields, clearedFields, newFields) => {
      this.setState({posts: ddpClient.collections.posts})
    }
    observer.removed = (id, oldValue) => {
      this.setState({posts: ddpClient.collections.posts})
    }
  },

  makeSubscription() {
    ddpClient.subscribe("posts", [], () => {
      this.setState({posts: ddpClient.collections.posts});
    });
  },

  handleIncrement() {
    ddpClient.call('addPost');
  },

  handleDecrement() {
    ddpClient.call('deletePost');
  },

  render() {
    let count = Object.keys(this.state.posts).length;
    return (
      <View>
        <Text>Posts: {count}</Text>
        <Button text="Increment" onPress={this.handleIncrement}/>
        <Button text="Decrement" onPress={this.handleDecrement}/>
      </View>
    );
  }
});

You’ll notice this looks nearly identical to RNApp/app/index.js right now - and that’s true. We’re basically moving the entire existing app over to the loggedIn.js file in preparation for what’s next. With that, let’s update RNApp/app/index.js to use the newly created loggedIn.js file.

In RNApp/app/index.js:

import React, {
  View,
  StyleSheet
} from 'react-native';

import ddpClient from './ddp';
import LoggedIn from './loggedIn';

export default React.createClass({
  getInitialState() {
    return {
      connected: false
    }
  },

  componentDidMount() {
    ddpClient.connect((err, wasReconnect) => {
      let connected = true;
      if (err) connected = false;

      this.setState({ connected: connected });
    });
  },

  render() {
    let body;

    if (this.state.connected) {
      body = <LoggedIn />;
    }

    return (
      <View style={styles.container}>
        <View style={styles.center}>
          {body}
        </View>
      </View>
    );
  }
});

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#F5FCFF',
  },
  center: {
    alignItems: 'center'
  }
});

UI: Sign In

Now let’s mock up the (ugly) UI to sign in. We’ll only cover email but the exact same applies for username.

Create RNApp/app/loggedOut.js:

import React, {
  View,
  Text,
  TextInput,
  StyleSheet
} from 'react-native';

import Button from './button';
import ddpClient from './ddp';

export default React.createClass({
  getInitialState() {
    return {
      email: '',
      password: ''
    }
  },

  handleSignIn() {
    let { email, password } = this.state;
    ddpClient.loginWithEmail(email, password, (err, res) => {
      ddpClient.onAuthResponse(err, res);
      if (res) {
        this.props.changedSignedIn(true);
      } else {
        this.props.changedSignedIn(false);
      }
    });

    // Clear the input values on submit
    this.refs.email.setNativeProps({text: ''});
    this.refs.password.setNativeProps({text: ''});
  },

  handleSignUp() {
    let { email, password } = this.state;
    ddpClient.signUpWithEmail(email, password, (err, res) => {
      ddpClient.onAuthResponse(err, res);
      if (res) {
        this.props.changedSignedIn(true);
      } else {
        this.props.changedSignedIn(false);
      }
    });

    // Clear the input values on submit
    this.refs.email.setNativeProps({text: ''});
    this.refs.password.setNativeProps({text: ''});
  },

  render() {
    return (
      <View>
        <TextInput
          style={styles.input}
          ref="email"
          onChangeText={(email) => this.setState({email: email})}
          autoCapitalize="none"
          autoCorrect={false}
          placeholder="Email"
        />
        <TextInput
          style={styles.input}
          ref="password"
          onChangeText={(password) => this.setState({password: password})}
          autoCapitalize="none"
          autoCorrect={false}
          placeholder="Password"
          secureTextEntry={true}
        />

        <Button text="Sign In" onPress={this.handleSignIn} />
        <Button text="Sign Up" onPress={this.handleSignUp} />
      </View>
    )
  }
});

const styles = StyleSheet.create({
  input: {
    height: 40,
    width: 350,
    padding: 10,
    marginBottom: 10,
    backgroundColor: 'white',
    borderColor: 'gray',
    borderWidth: 1
  }
});

Now we actually need to display our logged out component.

In RNApp/app/index.js:

/*
 * Removed from snippet for brevity
 */
import LoggedOut from './loggedOut';

export default React.createClass({
  getInitialState() {
    return {
      connected: false,
      signedIn: false
    }
  },

  componentDidMount() {
    ddpClient.connect((err, wasReconnect) => {
      let connected = true;
      if (err) connected = false;

      this.setState({ connected: connected });
    });
  },

  changedSignedIn(status = false) {
    this.setState({signedIn: status});
  },

  render() {
    let body;

    if (this.state.connected && this.state.signedIn) {
      body = <LoggedIn changedSignedIn={this.changedSignedIn} />; // Note the change here as well
    } else if (this.state.connected) {
      body = <LoggedOut changedSignedIn={this.changedSignedIn} />;
    }

    return (
      <View style={styles.container}>
        <View style={styles.center}>
          {body}
        </View>
      </View>
    );
  }
});

Almost there! Just two steps left. Next, let’s give the user the ability to sign out.

In RNApp/app/loggedIn.js:

/*
 * Removed from snippet for brevity
 */

export default React.createClass({
  /*
   * Removed from snippet for brevity
   */
  handleSignOut() {
    ddpClient.logout(() => {
      this.props.changedSignedIn(false)
    });
  },

  render() {
    let count = Object.keys(this.state.posts).length;
    return (
      <View>
        <Text>Posts: {count}</Text>
        <Button text="Increment" onPress={this.handleIncrement}/>
        <Button text="Decrement" onPress={this.handleDecrement}/>

        <Button text="Sign Out" onPress={() => this.props.changedSignedIn(false)} />
      </View>
    );
  }
});

Last step! Let’s automatically log a user in if they’ve got a valid loginToken stored in AsyncStorage:

In RNApp/app/loggedOut.js:

import React, {
  View,
  Text,
  TextInput,
  StyleSheet,
  AsyncStorage // Import AsyncStorage
} from 'react-native';

import Button from './button';
import ddpClient from './ddp';

export default React.createClass({
  getInitialState() {
    return {
      email: '',
      password: ''
    }
  },

  componentDidMount() {
    // Grab the token from AsyncStorage - if it exists then attempt to login with it.
    AsyncStorage.getItem('loginToken')
      .then((res) => {
        if (res) {
          ddpClient.loginWithToken(res, (err, res) => {
            if (res) {
              this.props.changedSignedIn(true);
            } else {
              this.props.changedSignedIn(false);
            }
          });
        }
      });
  },

  handleSignIn() {
    let { email, password } = this.state;
    ddpClient.loginWithEmail(email, password, (err, res) => {
      ddpClient.onAuthResponse(err, res);
      if (res) {
        this.props.changedSignedIn(true);
      } else {
        this.props.changedSignedIn(false);
      }
    });

    // Clear the input values on submit
    this.refs.email.setNativeProps({text: ''});
    this.refs.password.setNativeProps({text: ''});
  },

  /*
   * Removed from snippet for brevity
   */
});

There we go! You should now be able to authenticate your React Native app with a Meteor backend. This gives you access to this.userId in Meteor Methods and Meteor Publications. Test it out by updating the addPost method in meteor-app/both/posts.js:

'addPost': function() {
  Posts.insert({
    title: 'Post ' + Random.id(),
    userId: this.userId
  });
},

Does userId exist on the newly created post?

Conclusion

I do want to drop a note about security here - it’s something I didn’t cover at all in this post. When in a production environment and users are passing real data make sure to set up SSL (same as with a normal Meteor app). Also, we’re not doing any password hashing on the client here so the password is being sent in plain text over the wire. This just increases the need for SSL. Covering password hashing would have made this post even longer - if you’re interested in seeing an implementation let me know @spencer_carli.

You can view the completed project on Github here: https://github.com/spencercarli/meteor-react-native-authentication

_Want to learn more about React Native and Meteor? Let me send you the latest post as soon as it comes out. Sign up here.

Share Share on Twitter Share on Facebook Share on LinkedIn

How Can We Help?

Reaching out doesn’t mean you’re ready to start a project, but we’d love to learn more about the challenge you’re facing, answer any questions, and see if we might be a good fit for working together.

Contact Us