Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor authentication logic. Fix re-authentication. #3250

Merged
merged 28 commits into from
Jan 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b48c4e0
Implement ILoginProvider, BypassStore. WIP.
bengtan Jan 9, 2019
a961b6f
Use ILoginProvider for static typing.
bengtan Jan 10, 2019
61d7890
Invert logout flow. Rename some logout()s to onLogout().
bengtan Jan 10, 2019
f581e71
Split wocky.logout() into two
bengtan Jan 10, 2019
b02777b
Move Router.login() into store.
bengtan Jan 10, 2019
382e0ca
Make firebaseStore a iLoginProvider. Other tidy-up.
bengtan Jan 10, 2019
c975854
Make getLoginCredentials a flow(function*())
bengtan Jan 10, 2019
581f87f
Fix up background re-auth. Refresh firebase token.
bengtan Jan 10, 2019
7079e0f
Hand-edit mockStore.ts to make test pass.
bengtan Jan 10, 2019
4e1a9db
Implement simple provider registry. More stable names.
bengtan Jan 14, 2019
f6a2bfb
Revert f581e711, remove errorReporting.js.
bengtan Jan 14, 2019
61168dd
Strong typing for generateWockyToken()
bengtan Jan 15, 2019
e7c4030
Refine JWT fields.
bengtan Jan 15, 2019
c9b0c81
Move BypassStore into wocky-client.
bengtan Jan 15, 2019
f7dc298
Fix up (bypass) login for unit tests.
bengtan Jan 15, 2019
9ba87b1
Reduce registerProvider params.
bengtan Jan 15, 2019
c7dea5c
Merge branch 'master' into 3223-background-reauth
bengtan Jan 18, 2019
58eea75
one-way credential data flow to wocky (no need for LoginProvider)
southerneer Jan 18, 2019
ca7adb0
wocky is a part of PersistableModel, remove it here
southerneer Jan 19, 2019
12e86c4
WIP: reauth improvements
southerneer Jan 19, 2019
b78f132
separate auth strategies from auth store
southerneer Jan 21, 2019
395294f
cleanup connectivity. add todos
southerneer Jan 21, 2019
4b43901
cleanup
southerneer Jan 21, 2019
38eac1b
fix persistableModel
southerneer Jan 21, 2019
564c1da
fix connectivity (prevent infinite retries)
southerneer Jan 21, 2019
d2026f9
fix logout
southerneer Jan 21, 2019
b88f9b0
cleanup
southerneer Jan 21, 2019
07e4a20
Merge pull request #3267 from hippware/3223-eric
aksonov Jan 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion __tests__/utils/mockStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,18 @@ export default {
warn: jest.fn(),
firebaseStore: {
phone: '1234567890',
token: null,
token: undefined as any,
resource: null,
inviteCode: null,
buttonText: 'Verify',
registered: false,
errorMessage: '',
providerName: 'firebaseStore',
setState: jest.fn(),
reset: jest.fn(),
setInviteCode: jest.fn(),
afterAttach: jest.fn(),
getLoginCredentials: jest.fn(),
logout: jest.fn(),
beforeDestroy: jest.fn(),
verifyPhone: jest.fn(),
Expand Down
22 changes: 9 additions & 13 deletions src/components/Connectivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import {reaction, observable} from 'mobx'
import {inject} from 'mobx-react/native'
import {Actions} from 'react-native-router-flux'
import * as log from '../utils/log'

// TODO: need to export declaration file to make this work as expected?
import {IWocky} from 'wocky-client'
import {IHomeStore} from '../store/HomeStore'
import {ILocationStore} from '../store/LocationStore'
import {IAuthStore} from 'src/store/AuthStore'

type Props = {
wocky?: IWocky
Expand All @@ -17,9 +16,10 @@ type Props = {
locationStore?: ILocationStore
log?: any
analytics?: any
authStore?: IAuthStore
}

@inject('wocky', 'homeStore', 'notificationStore', 'locationStore', 'log', 'analytics')
@inject('wocky', 'homeStore', 'notificationStore', 'locationStore', 'log', 'analytics', 'authStore')
export default class Connectivity extends React.Component<Props> {
@observable lastDisconnected = Date.now()
retryDelay = 1000
Expand All @@ -35,12 +35,14 @@ export default class Connectivity extends React.Component<Props> {
this.props.log('NETINFO INITIAL:', reach, {level: log.levels.INFO})
this._handleConnectionInfoChange(reach)
})
// todo: refactor. Since interval is hardcoded here exponential backoff won't work...it checks every second regardless of changes to retryDelay
this.intervalId = setInterval(async () => {
const model = this.props.wocky!
if (
this.isActive &&
!model.connected &&
!model.connecting &&
this.props.authStore!.canLogin &&
Date.now() - this.lastDisconnected >= this.retryDelay
) {
await this.tryReconnect(`retry: ${this.retryDelay}`)
Expand Down Expand Up @@ -69,26 +71,20 @@ export default class Connectivity extends React.Component<Props> {
tryReconnect = async reason => {
const info = {reason, currentState: AppState.currentState}
const model = this.props.wocky!
if (
AppState.currentState === 'active' &&
!model.connected &&
!model.connecting &&
(model.phoneNumber || model.accessToken) &&
model.username &&
model.password &&
model.host
) {
const {authStore} = this.props
if (AppState.currentState === 'active' && !model.connected && !model.connecting) {
try {
this.props.analytics.track('reconnect_try', {
...info,
delay: this.retryDelay,
connectionInfo: this.connectionInfo,
})
await model.login({})
await authStore!.login()
this.props.analytics.track('reconnect_success', {...info})
this.retryDelay = 1000
} catch (e) {
this.props.analytics.track('reconnect_fail', {...info, error: e})
// todo: error message will be different with GraphQL (?)
if (e.toString().indexOf('not-authorized') !== -1 || e.toString().indexOf('invalid')) {
this.retryDelay = 1e9
Actions.logout()
Expand Down
23 changes: 7 additions & 16 deletions src/components/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { IStore } from 'src/store'
import { IPersistable } from 'src/store/PersistableModel'
import OnboardingSwiper from './Onboarding/OnboardingSwiper'
import ChatTitle from './Chats/ChatTitle'
import { IAuthStore } from 'src/store/AuthStore';
const iconClose = require('../../images/iconClose.png')
const sendActive = require('../../images/sendActive.png')

Expand All @@ -62,11 +63,12 @@ type Props = {
iconStore?: IconStore
store?: IStore & IPersistable
onceStore?: IOnceStore
authStore?: IAuthStore
analytics?: any
log?: any
}

@inject('store', 'wocky', 'locationStore', 'iconStore', 'analytics', 'navStore', 'log', 'onceStore')
@inject('store', 'wocky', 'locationStore', 'iconStore', 'analytics', 'navStore', 'log', 'onceStore', 'authStore')
@observer
class TinyRobotRouter extends React.Component<Props> {
componentDidMount() {
Expand Down Expand Up @@ -98,19 +100,19 @@ class TinyRobotRouter extends React.Component<Props> {
}

render() {
const {store, iconStore, wocky, navStore, onceStore} = this.props
const {store, iconStore, wocky, navStore, onceStore, authStore} = this.props

return (
<Router onStateChange={() => navStore!.setScene(Actions.currentScene)} {...navBarStyle} uriPrefix={uriPrefix} onDeepLink={this.onDeepLink}>
<Tabs hideNavBar hideTabBar>
<Stack hideNavBar lightbox type="replace">
<Scene key="load" component={Launch} on={store!.hydrate} success="checkCredentials" failure="preConnection" />
<Scene key="checkCredentials" on={() => wocky!.username && wocky!.password && wocky!.host} success="checkProfile" failure="preConnection" />
<Scene key="connect" on={this.login} success="checkHandle" failure="preConnection" />
<Scene key="checkCredentials" on={() => authStore!.canLogin} success="checkProfile" failure="preConnection" />
<Scene key="connect" on={authStore!.login} success="checkHandle" failure="preConnection" />
<Scene key="checkProfile" on={() => wocky!.profile} success="checkHandle" failure="connect" />
<Scene key="checkHandle" on={() => wocky!.profile!.handle} success="checkOnboarded" failure="signUp" />
<Scene key="checkOnboarded" on={() => onceStore!.onboarded} success="logged" failure="onboarding" />
<Scene key="logout" on={store!.logout} success="preConnection" />
<Scene key="logout" on={authStore!.logout} success="preConnection" />
</Stack>
<Lightbox>
<Stack initial hideNavBar key="main">
Expand Down Expand Up @@ -211,17 +213,6 @@ class TinyRobotRouter extends React.Component<Props> {
this.props.store!.searchStore.setGlobal('')
Actions.pop()
}

// TODO: Move it outside
login = async (data = {}) => {
try {
await this.props.wocky!.login(data) // Remove that after new typings for MST3
return true
} catch (error) {
this.props.analytics.track('error_connection', {error})
}
return false
}
}

export default TinyRobotRouter
8 changes: 6 additions & 2 deletions src/components/TestRegister.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@ import {k, width} from './Global'
import {colors} from '../constants'
import {INavStore} from '../store/NavStore'
import {IWocky} from 'wocky-client'
import {IAuthStore} from 'src/store/AuthStore'

type Props = {
wocky?: IWocky
analytics?: any
navStore?: INavStore
name: string
warn?: any
authStore: IAuthStore
}

type State = {
text: string
}

@inject('wocky', 'analytics', 'warn', 'navStore')
@inject('wocky', 'analytics', 'warn', 'navStore', 'authStore')
@observer
class TestRegister extends React.Component<Props, State> {
state: State = {
Expand All @@ -30,7 +32,9 @@ class TestRegister extends React.Component<Props, State> {
if (this.props.navStore!.scene !== this.props.name) {
return
}
Actions.connect({phoneNumber: `+1555${this.state.text}`})

this.props.authStore!.register(`+1555${this.state.text}`, 'bypass')
Actions.connect()
}

render() {
Expand Down
55 changes: 55 additions & 0 deletions src/store/AuthStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {types, Instance, getParent, flow, applySnapshot} from 'mobx-state-tree'
import analytics from 'src/utils/analytics'
import strategies, {AuthStrategy, Strategy} from './authStrategies'

const AuthStore = types
.model('AuthStore', {
phone: types.maybe(types.string),
strategyName: types.optional(types.enumeration(['firebase', 'bypass']), 'firebase'),
})
.views(self => ({
get canLogin(): boolean {
return !!self.phone
},
}))
.actions(self => {
const store = getParent<any>(self)
const {wocky, homeStore, locationStore} = store
let strategy: AuthStrategy | null = null

return {
register: (phone: string, s: Strategy) => {
self.phone = phone
self.strategyName = s
},

login(): Promise<boolean> {
try {
if (!self.canLogin) {
throw new Error('Phone number must be set before login.')
}
strategy = strategies[self.strategyName]
return strategy.login(store)
} catch (error) {
analytics.track('error_connection', {error})
}
return Promise.resolve(false)
},

logout: flow(function*() {
homeStore.logout()
locationStore.logout()
if (strategy) {
yield strategy.logout(store)
}
applySnapshot(self, {})
strategy = null
yield wocky.logout()
return true
}),
}
})

export default AuthStore

export interface IAuthStore extends Instance<typeof AuthStore> {}
45 changes: 30 additions & 15 deletions src/store/FirebaseStore.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import {types, getEnv, flow, getParent} from 'mobx-state-tree'
import {when} from 'mobx'
import {IWocky} from 'wocky-client'
import {IWocky, Credentials} from 'wocky-client'
import {IEnv} from '.'
import {settings} from '../globals'
import {RNFirebase} from 'react-native-firebase'
import {IAuthStore} from './AuthStore'

type State = {
phone?: string
token?: string
resource?: string
buttonText?: string
registered?: boolean
errorMessage?: string
token?: string
}

const codeUrlString = '?inviteCode='

const FirebaseStore = types
.model('FirebaseStore', {
phone: '',
token: types.maybeNull(types.string),
resource: types.maybeNull(types.string),
inviteCode: types.maybeNull(types.string),
token: types.maybeNull(types.string),
})
.volatile(() => ({
phone: '',
buttonText: 'Verify',
registered: false,
errorMessage: '', // to avoid strange typescript errors when set it to string or null,
Expand All @@ -42,6 +43,7 @@ const FirebaseStore = types
}))
.actions(self => {
const {firebase, auth, logger, analytics}: IEnv = getEnv(self)
const {authStore} = getParent<any>(self)
let wocky: IWocky
let confirmResult: any
let unsubscribe: any
Expand Down Expand Up @@ -95,9 +97,10 @@ const FirebaseStore = types
if (user) {
try {
await auth!.currentUser!.reload()
const token = await auth!.currentUser!.getIdToken(true)
self.setState({token})
// await firebase.auth().currentUser.updateProfile({phoneNumber: user.providerData[0].phoneNumber, displayName: '123'});)
// self.token = await auth!.currentUser!.getIdToken(true)
self.setState({
token: await auth!.currentUser!.getIdToken(true),
})
} catch (err) {
logger.warn('Firebase onAuthStateChanged error:', err)
analytics.track('auth_error_firebase', {error: err})
Expand All @@ -110,6 +113,20 @@ const FirebaseStore = types
}
}

const getLoginCredentials = flow(function*() {
if (!self.token) {
return null
}
// Refresh firebase token if less than 5 minutes from expiry
const tokenResult: RNFirebase.IdTokenResult = yield auth.currentUser!.getIdTokenResult(false)
// todo: use moment instead of Date
if (tokenResult.claims.exp - Date.now() / 1000 < 300) {
self.token = yield auth.currentUser!.getIdToken(true)
}

return {typ: 'firebase', sub: self.token, phone_number: self.phone}
}) as () => Promise<Credentials | null>

const logout = flow(function*() {
analytics.track('logout')
if (self.token) {
Expand All @@ -121,12 +138,7 @@ const FirebaseStore = types
logger.warn('firebase logout error', err)
}
}
try {
yield wocky.logout()
} catch (err) {
analytics.track('error_wocky_logout', {error: err})
logger.warn('wocky logout error', err)
}

self.reset()
confirmResult = null
return true
Expand Down Expand Up @@ -197,7 +209,8 @@ const FirebaseStore = types
const registerWithToken = flow(function*() {
try {
self.setState({buttonText: 'Connecting...'})
yield wocky.login({accessToken: self.token!})
authStore.register(self.phone, 'firebase')
yield (authStore as IAuthStore).login()
self.setState({buttonText: 'Verify', registered: true})
} catch (err) {
logger.warn('RegisterWithToken error', err)
Expand All @@ -206,6 +219,7 @@ const FirebaseStore = types
} finally {
self.setState({
buttonText: 'Verify',
// todo: shouldn't overwrite errorMessage here
errorMessage: '',
})
}
Expand Down Expand Up @@ -249,6 +263,7 @@ const FirebaseStore = types

return {
afterAttach,
getLoginCredentials,
logout,
beforeDestroy,
verifyPhone,
Expand Down
4 changes: 2 additions & 2 deletions src/store/LocationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import DeviceInfo from 'react-native-device-info'
import {settings} from '../globals'
import {Location, IWocky} from 'wocky-client'
import _ from 'lodash'
import {IStore} from '.'

export const BG_STATE_PROPS = [
'elasticityMultiplier',
Expand Down Expand Up @@ -299,7 +298,8 @@ const LocationStore = types
}
})
.actions(self => {
const {wocky, onceStore} = getRoot<IStore>(self)
// const {wocky, onceStore} = getRoot<IStore>(self)
const {wocky, onceStore} = getRoot<any>(self)
let reactions: IReactionDisposer[] = []
const {logger} = getEnv(self)

Expand Down
Loading