inProgress-team / react-native-meteor

Meteor Reactivity for your React Native application :)
MIT License
693 stars 210 forks source link

Resume token not set for third party auth providers #239

Open georgobermayr opened 7 years ago

georgobermayr commented 7 years ago

Hi,

in our app, we have classic password accounts as well as Facebook and Twitter accounts. For that we implemented our own login handlers for the oAuth process. We discovered an issue with react-native-meteor, that with Facebook and Twitter login handlers the resume token is not written to the async store. This caused some issues with Meteor.userId() returning null after changes to the connection. So we added the key to the async store on our own. The code for this looks something like this:

export const authFacebook = (error, result) => {
  return (dispatch) => {
    if (error) {
      // Error handling
    } else if (result.cancelled) {
      // Error handling
    } else {
      const Data = Meteor.getData()
      AccessToken.getCurrentAccessToken()
        .then((res) => {
          if (res) {
            Meteor.call('login', { facebook: res }, (err, result) => {
              if (!err) {
                AsyncStorage.setItem(USER_TOKEN_KEY, result.token, (err) => {
                  if (err) console.log('token write error: ', err)
                });
                Data._tokenIdSaved = result.token
                Meteor._userIdSaved = result.id
                dispatch(authSetUserId())
                dispatch(authSuccess())
              } else {
                LoginManager.logOut()
                // Error handling
              }
            })
          }
        })
    }
  }
}

This works fine and userId is set.

However we have some very rare cases now, in which the userId got lost later in the process. We don't know the exact circumstances when this happens, but after some time in the app (and probably some connection changes) the userId is null again. Is there any scenario where the the userId is cleared by react-native-meteor? Is there any way we could work around this? Is there a fix?

Thank you! Georg

conorstrejcek commented 7 years ago

This issue has been occurring for us as well.

Regardless of the form of authentication, it seems that in some instances Meteor.userId() will return null. It does not seem very predictable, and when I use Meteor.collection('users').find(), the user exists in the collection with the correct _id.

Edit:

I have actually been able to reproduce the error on my end. When you use Accounts.createUser to create a new user, Meteor automatically logs in the new account. This works as intended, except that Meteor.userId() will return as null on the client even though the user is logged in. I fixed this by using a block similar to this:

Accounts.createUser({email, password}, (err) => {
    if (Meteor.userId() === null) {
        Meteor.loginWithPassword(email, password, (e) => {
            if (e) {
                console.log(e);
            } else {
                // do stuff here
            }
        });
    } else {
        if (err) {
            console.log(err);
        } else {
            // do stuff here
        }
    }
});

I also have the issue with userId when Meteor reconnects -- it is set back to null on reconnect.

conorstrejcek commented 7 years ago

I have found a way around the issue of reconnects where the userId is set to null:

let wasDisconnected = false;
if (!Meteor.status().connected) {
    wasDisconnected = true;
}
Meteor.waitDdpConnected(() => {
    if (wasDisconnected) {
        Meteor._loadInitialUser().then(() => {
            // your code here which requires userId to be set correctly
        });
    } else {
        // your code here which requires userId to be set correctly
    }
});

_loadInitialUser() comes from the Meteor.connect() function.

If you omit this section:

Meteor._loadInitialUser().then(() => { ... });

Then Meteor.userId() will be set correctly on the client, but any Meteor.call()s will return null for this.userId on the server.

georgobermayr commented 6 years ago

I was now able to circle back to the issue and do some more digging. I'm still not able to reproduce the issue so I implemented a fix based an my analysis of react-native-meteor. In my startup saga I do something like this:

const selectUser = (state) => {
  return Selectors.getUser(state, state.auth.userId)
}
const selectUserToken = (state) => {
  return Selectors.getUserToken(state)
}

const USER_TOKEN_KEY = 'reactnativemeteor_usertoken'

export function * startup (api, action) {
  const user = yield select(selectUser)

  if (user) {
    const userToken = yield select(selectUserToken)
    const storedToken = yield call(getStoredToken)

    if (userToken !== storedToken) {
      const setToken = yield call(setUserToken, userToken)
    }
  }
}

const getStoredToken = () => {
  return new Promise(function (resolve, reject) {
    AsyncStorage.getItem(USER_TOKEN_KEY, (error, result) => {
      if (error) resolve(error)
      else resolve(result)
    })
  })
}

const setUserToken = (token) => {
  return new Promise(function (resolve, reject) {
    AsyncStorage.setItem(USER_TOKEN_KEY, token, (error) => {
      if (error) resolve(error)
    })
  })
}

So I store the Meteor userToken in my own Auth store too. On startup I check, if the reactnativemeteor_usertoken is still present. If not, I set it again from my copy.

I'm not 100% sure if this a reliable fix, but from my tests so far it looks fine.