Implement full auth flow in iOS
parent
81441c3c26
commit
07b92a2180
|
@ -10,6 +10,11 @@ Uses:
|
||||||
- [MobX](https://mobx.js.org/README.html) and [MobX State Tree](https://mobx-state-tree.js.org/)
|
- [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)
|
- [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
|
## Build instructions
|
||||||
|
|
||||||
- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
|
- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
|
||||||
|
@ -56,3 +61,7 @@ For native builds, we must provide a polyfill of `webcrypto`. We use [react-nati
|
||||||
|
|
||||||
- webcrypto
|
- webcrypto
|
||||||
- TextEncoder / TextDecoder
|
- 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.
|
|
@ -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 = '') {
|
export function makeAppUrl(path = '') {
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
|
@ -10,3 +11,27 @@ export function makeAppUrl(path = '') {
|
||||||
return `${window.location.origin}${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 = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,12 @@ import * as auth from '@adxp/auth'
|
||||||
import * as ucan from 'ucans'
|
import * as ucan from 'ucans'
|
||||||
import {InAppBrowser} from 'react-native-inappbrowser-reborn'
|
import {InAppBrowser} from 'react-native-inappbrowser-reborn'
|
||||||
import {isWeb} from '../platform/detection'
|
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 storage from './storage'
|
||||||
import * as env from '../env'
|
import * as env from '../env'
|
||||||
|
|
||||||
|
@ -20,26 +25,28 @@ export async function logout(authStore: ReactNativeStore) {
|
||||||
await authStore.reset()
|
await authStore.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseUrlForUcan() {
|
export async function parseUrlForUcan(fragment: string) {
|
||||||
if (isWeb) {
|
try {
|
||||||
// @ts-ignore window is defined -prf
|
return await auth.parseLobbyResponseHashFragment(fragment)
|
||||||
const fragment = window.location.hash
|
} catch (err) {
|
||||||
if (fragment.length < 1) {
|
return undefined
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const ucan = await auth.parseLobbyResponseHashFragment(fragment)
|
|
||||||
// @ts-ignore window is defined -prf
|
|
||||||
window.location.hash = ''
|
|
||||||
return ucan
|
|
||||||
} catch (err) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wasAuthed
|
||||||
|
}
|
||||||
|
|
||||||
export async function requestAppUcan(authStore: ReactNativeStore) {
|
export async function requestAppUcan(authStore: ReactNativeStore) {
|
||||||
const did = await authStore.getDid()
|
const did = await authStore.getDid()
|
||||||
const returnUrl = makeAppUrl()
|
const returnUrl = makeAppUrl()
|
||||||
|
@ -53,6 +60,7 @@ export async function requestAppUcan(authStore: ReactNativeStore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await InAppBrowser.isAvailable()) {
|
if (await InAppBrowser.isAvailable()) {
|
||||||
|
// use in-app browser
|
||||||
const res = await InAppBrowser.openAuth(url, returnUrl, {
|
const res = await InAppBrowser.openAuth(url, returnUrl, {
|
||||||
// iOS Properties
|
// iOS Properties
|
||||||
ephemeralWebSession: false,
|
ephemeralWebSession: false,
|
||||||
|
@ -62,12 +70,20 @@ export async function requestAppUcan(authStore: ReactNativeStore) {
|
||||||
enableDefaultShare: false,
|
enableDefaultShare: false,
|
||||||
})
|
})
|
||||||
if (res.type === 'success' && res.url) {
|
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 {
|
} else {
|
||||||
console.error('Bad response', res)
|
console.log('Not completed', res)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// use system browser
|
||||||
Linking.openURL(url)
|
Linking.openURL(url)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
import {Environment} from './env'
|
import {Environment} from './env'
|
||||||
import * as storage from './storage'
|
import * as storage from './storage'
|
||||||
import * as auth from './auth'
|
import * as auth from './auth'
|
||||||
|
import * as urls from '../platform/urls'
|
||||||
|
|
||||||
const ROOT_STATE_STORAGE_KEY = 'root'
|
const ROOT_STATE_STORAGE_KEY = 'root'
|
||||||
|
|
||||||
|
@ -32,9 +33,9 @@ export async function setupState() {
|
||||||
if (env.authStore) {
|
if (env.authStore) {
|
||||||
const isAuthed = await auth.isAuthed(env.authStore)
|
const isAuthed = await auth.isAuthed(env.authStore)
|
||||||
rootStore.session.setAuthed(isAuthed)
|
rootStore.session.setAuthed(isAuthed)
|
||||||
const ucan = await auth.parseUrlForUcan()
|
|
||||||
if (ucan) {
|
// handle redirect from auth
|
||||||
await env.authStore.addUcan(ucan)
|
if (await auth.initialLoadUcanCheck(env.authStore)) {
|
||||||
rootStore.session.setAuthed(true)
|
rootStore.session.setAuthed(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue