about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-06-15 20:26:41 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-06-15 20:26:41 -0500
commit07b92a2180ca6600f09e03a85c8ca7a06d24cbfc (patch)
tree1f7fd65f7cbaf59ff93c92595dc04a22b0a079a7
parent81441c3c265ae6e733365dcba01f7da650f5b1f9 (diff)
downloadvoidsky-07b92a2180ca6600f09e03a85c8ca7a06d24cbfc.tar.zst
Implement full auth flow in iOS
-rw-r--r--README.md11
-rw-r--r--src/platform/urls.tsx27
-rw-r--r--src/state/auth.ts54
-rw-r--r--src/state/index.ts7
4 files changed, 75 insertions, 24 deletions
diff --git a/README.md b/README.md
index 785060147..8ebb51f5e 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,11 @@ Uses:
 - [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/)
 - [Async Storage](https://github.com/react-native-async-storage/async-storage)
 
+## TODOs
+
+- Handle the "unauthed" state better than changing route definitions
+  - Currently it's possible to get a 404 if the auth state changes
+
 ## Build instructions
 
 - Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
@@ -55,4 +60,8 @@ For native builds, we must provide a polyfill of `webcrypto`. We use [react-nati
 `./platform/polyfills.*.ts` adds polyfills to the environment. Currently this includes:
 
 - webcrypto
-- TextEncoder / TextDecoder
\ No newline at end of file
+- TextEncoder / TextDecoder
+
+### Auth flow
+
+The auth flow is based on a browser app which is specified by the `REACT_APP_AUTH_LOBBY` env var. The app redirects to that location with the UCAN request, and then waits for a redirect back. In the native platforms with proper support, it will do this using an in-app browser. In native without in-app browser, or in the Web platform, it will handle this with redirects. The ucan is extracted from the hash fragment of the "return url" which is provided either by the in-app browser in response or detected during initial setup in the case of redirects.
\ No newline at end of file
diff --git a/src/platform/urls.tsx b/src/platform/urls.tsx
index 958b5232d..048c92f2e 100644
--- a/src/platform/urls.tsx
+++ b/src/platform/urls.tsx
@@ -1,4 +1,5 @@
-import {isIOS, isAndroid} from './detection'
+import {Linking} from 'react-native'
+import {isIOS, isAndroid, isNative, isWeb} from './detection'
 
 export function makeAppUrl(path = '') {
   if (isIOS) {
@@ -10,3 +11,27 @@ export function makeAppUrl(path = '') {
     return `${window.location.origin}${path}`
   }
 }
+
+export function extractHashFragment(url: string): string {
+  return url.split('#')[1] || ''
+}
+
+export async function getInitialURL(): Promise<string> {
+  if (isNative) {
+    const url = await Linking.getInitialURL()
+    if (url) {
+      return url
+    }
+    return makeAppUrl()
+  } else {
+    // @ts-ignore window exists -prf
+    return window.location.toString()
+  }
+}
+
+export function clearHash() {
+  if (isWeb) {
+    // @ts-ignore window exists -prf
+    window.location.hash = ''
+  }
+}
diff --git a/src/state/auth.ts b/src/state/auth.ts
index b49a11d90..ee0fe981d 100644
--- a/src/state/auth.ts
+++ b/src/state/auth.ts
@@ -3,7 +3,12 @@ import * as auth from '@adxp/auth'
 import * as ucan from 'ucans'
 import {InAppBrowser} from 'react-native-inappbrowser-reborn'
 import {isWeb} from '../platform/detection'
-import {makeAppUrl} from '../platform/urls'
+import {
+  getInitialURL,
+  extractHashFragment,
+  clearHash,
+  makeAppUrl,
+} from '../platform/urls'
 import * as storage from './storage'
 import * as env from '../env'
 
@@ -20,24 +25,26 @@ export async function logout(authStore: ReactNativeStore) {
   await authStore.reset()
 }
 
-export async function parseUrlForUcan() {
-  if (isWeb) {
-    // @ts-ignore window is defined -prf
-    const fragment = window.location.hash
-    if (fragment.length < 1) {
-      return undefined
-    }
-    try {
-      const ucan = await auth.parseLobbyResponseHashFragment(fragment)
-      // @ts-ignore window is defined -prf
-      window.location.hash = ''
-      return ucan
-    } catch (err) {
-      return undefined
+export async function parseUrlForUcan(fragment: string) {
+  try {
+    return await auth.parseLobbyResponseHashFragment(fragment)
+  } catch (err) {
+    return undefined
+  }
+}
+
+export async function initialLoadUcanCheck(authStore: ReactNativeStore) {
+  let wasAuthed = false
+  const fragment = extractHashFragment(await getInitialURL())
+  if (fragment) {
+    const ucan = await parseUrlForUcan(fragment)
+    if (ucan) {
+      await authStore.addUcan(ucan)
+      wasAuthed = true
+      clearHash()
     }
-  } else {
-    // TODO
   }
+  return wasAuthed
 }
 
 export async function requestAppUcan(authStore: ReactNativeStore) {
@@ -53,6 +60,7 @@ export async function requestAppUcan(authStore: ReactNativeStore) {
   }
 
   if (await InAppBrowser.isAvailable()) {
+    // use in-app browser
     const res = await InAppBrowser.openAuth(url, returnUrl, {
       // iOS Properties
       ephemeralWebSession: false,
@@ -62,12 +70,20 @@ export async function requestAppUcan(authStore: ReactNativeStore) {
       enableDefaultShare: false,
     })
     if (res.type === 'success' && res.url) {
-      Linking.openURL(res.url)
+      const fragment = extractHashFragment(res.url)
+      if (fragment) {
+        const ucan = await parseUrlForUcan(fragment)
+        if (ucan) {
+          await authStore.addUcan(ucan)
+          return true
+        }
+      }
     } else {
-      console.error('Bad response', res)
+      console.log('Not completed', res)
       return false
     }
   } else {
+    // use system browser
     Linking.openURL(url)
   }
   return true
diff --git a/src/state/index.ts b/src/state/index.ts
index fa7c9518d..460815d13 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -7,6 +7,7 @@ import {
 import {Environment} from './env'
 import * as storage from './storage'
 import * as auth from './auth'
+import * as urls from '../platform/urls'
 
 const ROOT_STATE_STORAGE_KEY = 'root'
 
@@ -32,9 +33,9 @@ export async function setupState() {
   if (env.authStore) {
     const isAuthed = await auth.isAuthed(env.authStore)
     rootStore.session.setAuthed(isAuthed)
-    const ucan = await auth.parseUrlForUcan()
-    if (ucan) {
-      await env.authStore.addUcan(ucan)
+
+    // handle redirect from auth
+    if (await auth.initialLoadUcanCheck(env.authStore)) {
       rootStore.session.setAuthed(true)
     }
   }