Skip to content

Commit

Permalink
docs: explain use of SecureStore with Expo/React Native (supabase#1…
Browse files Browse the repository at this point in the history
…7643)

* docs: explain use of `SecureStore` with Expo/React Native

* fix: cipher

* docs: update react native storage guidance.

* docs: misc improvements.

* blog: cleanup.

---------

Co-authored-by: thorwebdev <[email protected]>
  • Loading branch information
hf and thorwebdev authored Sep 26, 2023
1 parent 7aee78a commit da2d6e9
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 48 deletions.
140 changes: 113 additions & 27 deletions apps/docs/pages/guides/getting-started/tutorials/with-expo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,43 +36,129 @@ Then let's install the additional dependencies: [supabase-js](https://github.com
```bash
npm install @supabase/supabase-js
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
npx expo install expo-secure-store
```

Now let's create a helper file to initialize the Supabase client.
We need the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
These variables will be exposed on the browser, and that's completely fine since we have
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.

```ts lib/supabase.ts
import 'react-native-url-polyfill/auto'
import * as SecureStore from 'expo-secure-store'
import { createClient } from '@supabase/supabase-js'
<Tabs
scrollable
size="large"
type="underlined"
defaultActiveId="async-storage"
queryGroup="auth-store"
>
<TabPanel id="async-storage" label="AsyncStorage">

```ts lib/supabase.ts
import 'react-native-url-polyfill/auto'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
})
```

const ExpoSecureStoreAdapter = {
getItem: (key: string) => {
return SecureStore.getItemAsync(key)
},
setItem: (key: string, value: string) => {
SecureStore.setItemAsync(key, value)
},
removeItem: (key: string) => {
SecureStore.deleteItemAsync(key)
},
}
</TabPanel>
<TabPanel id="secure-store" label="SecureStore">

const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
If you wish to encrypt the user's session information, you can use `aes-js` and store the encryption key in [Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore). The [`aes-js` library](https://github.com/ricmoo/aes-js) is a reputable JavaScript-only implementation of the AES encryption algorithm in CTR mode. A new 256-bit encryption key is generated using the `react-native-get-random-values` library. This key is stored inside Expo's SecureStore, while the value is encrypted and placed inside AsyncStorage.

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: ExpoSecureStoreAdapter as any,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
})
```
Please make sure that:
- You keep the `expo-secure-storage`, `aes-js` and `react-native-get-random-values` libraries up-to-date.
- Choose the correct [`SecureStoreOptions`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestoreoptions) for your app's needs. E.g. [`SecureStore.WHEN_UNLOCKED`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestorewhen_unlocked) regulates when the data can be accessed.
- Carefully consider optimizations or other modifications to the above example, as those can lead to introducing subtle security vulnerabilities.

Install the necessary dependencies in the root of your Expo project:

```bash
npm install @supabase/supabase-js
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
npm install aes-js react-native-get-random-values
npx expo install expo-secure-store
```

Implement a `LargeSecureStore` class to pass in as Auth storage for the `supabase-js` client:

```ts lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
import * as aesjs from 'aes-js';
import 'react-native-get-random-values';

// As Expo's SecureStore does not support values larger than 2048
// bytes, an AES-256 key is generated and stored in SecureStore, while
// it is used to encrypt/decrypt values stored in AsyncStorage.
class LargeSecureStore {
private async _encrypt(key: string, value: string) {
const encryptionKey = crypto.getRandomValues(new Uint8Array(256 / 8));

const cipher = new aesjs.ModeOfOperation.ctr(encryptionKey, new aesjs.Counter(1));
const encryptedBytes = cipher.encrypt(aesjs.utils.utf8.toBytes(value));

await SecureStore.setItemAsync(key, aesjs.utils.hex.fromBytes(encryptionKey));

return aesjs.utils.hex.fromBytes(encryptedBytes);
}

private async _decrypt(key: string, value: string) {
const encryptionKeyHex = await SecureStore.getItemAsync(key);
if (!encryptionKeyHex) {
return encryptionKeyHex;
}

const cipher = new aesjs.ModeOfOperation.ctr(aesjs.utils.hex.toBytes(encryptionKeyHex), new aesjs.Counter(1));
const decryptedBytes = cipher.decrypt(aesjs.utils.hex.toBytes(value));

return aesjs.utils.utf8.fromBytes(decryptedBytes);
}

async getItem(key: string) {
const encrypted = await AsyncStorage.getItem(key);
if (!encrypted) { return encrypted; }

return await this._decrypt(key, encrypted);
}

async removeItem(key: string) {
await AsyncStorage.removeItem(key);
await SecureStore.deleteItemAsync(key);
}

async setItem(key: string, value: string) {
const encrypted = await this._encrypt(key, value);

await AsyncStorage.setItem(key, encrypted);
}
}

const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY

const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: new LargeSecureStore(),
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});
```

</TabPanel>
</Tabs>

### Set up a Login component

Expand Down
24 changes: 5 additions & 19 deletions apps/www/_blog/2023-08-01-react-native-storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,14 @@ npx create-expo-app@latest cloudApp --template tabs@49

# Install dependencies
npm i @supabase/supabase-js
npm i react-native-url-polyfill base64-arraybuffer react-native-loading-spinner-overlay
npm i react-native-url-polyfill base64-arraybuffer react-native-loading-spinner-overlay @react-native-async-storage/async-storage

# Install Expo packages
npx expo install expo-secure-store
npx expo install expo-image-picker
npx expo install expo-file-system
```

We will use the [Expo Secure Store](https://docs.expo.dev/versions/latest/sdk/securestore/) to store the Supabase session, and the [Expo Image Picker](https://docs.expo.dev/versions/latest/sdk/imagepicker/) to select images from the device. We also need the [Expo File System](https://docs.expo.dev/versions/latest/sdk/filesystem/) to read the image from the device and upload its data.
We will use the [Expo AsyncStorage](https://docs.expo.dev/versions/latest/sdk/async-storage/) to store the Supabase session, and the [Expo Image Picker](https://docs.expo.dev/versions/latest/sdk/imagepicker/) to select images from the device. We also need the [Expo File System](https://docs.expo.dev/versions/latest/sdk/filesystem/) to read the image from the device and upload its data.

You can now already run your project with `npx expo` and then select a platform to run on.

Expand All @@ -116,37 +115,24 @@ EXPO_PUBLIC_SUPABASE_ANON_KEY=
We can now simply read those values from the environment variables and initialize the Supabase client, so create a file at `config/initSupabase.ts` and add the following code:

```ts
import * as SecureStore from 'expo-secure-store'
import AsyncStorage from '@react-native-async-storage/async-storage'
import 'react-native-url-polyfill/auto'

import { createClient } from '@supabase/supabase-js'

// Use a custom secure storage solution for the Supabase client to store the JWT
const ExpoSecureStoreAdapter = {
getItem: (key: string) => {
return SecureStore.getItemAsync(key)
},
setItem: (key: string, value: string) => {
SecureStore.setItemAsync(key, value)
},
removeItem: (key: string) => {
SecureStore.deleteItemAsync(key)
},
}

const url = process.env.EXPO_PUBLIC_SUPABASE_URL
const key = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY

// Initialize the Supabase client
export const supabase = createClient(url, key, {
auth: {
storage: ExpoSecureStoreAdapter as any,
storage: AsyncStorage,
detectSessionInUrl: false,
},
})
```

We are using the SecureStore from Expo to handle the session of our Supabase client and add in the `createClient` function.
We are using the AsyncStorage from Expo to handle the session of our Supabase client and add in the `createClient` function.

Later we can import the `supabase` client from this file and use it in our app whenever we need to access Supabase.

Expand Down
76 changes: 74 additions & 2 deletions spec/supabase_js_v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ functions:
`supabase-js` uses the [`cross-fetch`](https://www.npmjs.com/package/cross-fetch) library to make HTTP requests,
but an alternative `fetch` implementation can be provided as an option.
This is most useful in environments where `cross-fetch` is not compatible (for instance Cloudflare Workers).
- id: react-native-options
name: React Native options
- id: react-native-options-async-storage
name: React Native options with AsyncStorage
code: |
```js
import { createClient } from '@supabase/supabase-js'
Expand All @@ -112,6 +112,78 @@ functions:
```
description: |
For React Native we recommend using `AsyncStorage` as the storage implementation for Supabase Auth.
- id: react-native-options-secure-storage
name: React Native options with Expo SecureStore
code: |
```js
import { createClient } from '@supabase/supabase-js'
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
import * as aesjs from 'aes-js';
import 'react-native-get-random-values';
// As Expo's SecureStore does not support values larger than 2048
// bytes, an AES-256 key is generated and stored in SecureStore, while
// it is used to encrypt/decrypt values stored in AsyncStorage.
class LargeSecureStore {
private async _encrypt(key: string, value: string) {
const encryptionKey = crypto.getRandomValues(new Uint8Array(256 / 8));
const cipher = new aesjs.ModeOfOperation.ctr(encryptionKey, new aesjs.Counter(1));
const encryptedBytes = cipher.encrypt(aesjs.utils.utf8.toBytes(value));
await SecureStore.setItemAsync(key, aesjs.utils.hex.fromBytes(encryptionKey));
return aesjs.utils.hex.fromBytes(encryptedBytes);
}
private async _decrypt(key: string, value: string) {
const encryptionKeyHex = await SecureStore.getItemAsync(key);
if (!encryptionKeyHex) {
return encryptionKeyHex;
}
const cipher = new aesjs.ModeOfOperation.ctr(aesjs.utils.hex.toBytes(encryptionKeyHex), new aesjs.Counter(1));
const decryptedBytes = cipher.decrypt(aesjs.utils.hex.toBytes(value));
return aesjs.utils.utf8.fromBytes(decryptedBytes);
}
async getItem(key: string) {
const encrypted = await AsyncStorage.getItem(key);
if (!encrypted) { return encrypted; }
return await this._decrypt(key, encrypted);
}
async removeItem(key: string) {
await AsyncStorage.removeItem(key);
await SecureStore.deleteItemAsync(key);
}
async setItem(key: string, value: string) {
const encrypted = await this._encrypt(key, value);
await AsyncStorage.setItem(key, encrypted);
}
}
const supabase = createClient("https://xyzcompany.supabase.co", "public-anon-key", {
auth: {
storage: new LargeSecureStore(),
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});
```
description: |
If you wish to encrypt the user's session information, you can use `aes-js` and store the encryption key in Expo SecureStore. The `aes-js` library, a reputable JavaScript-only implementation of the AES encryption algorithm in CTR mode. A new 256-bit encryption key is generated using the `react-native-get-random-values` library. This key is stored inside Expo's SecureStore, while the value is encrypted and placed inside AsyncStorage.
Please make sure that:
- You keep the `expo-secure-storage`, `aes-js` and `react-native-get-random-values` libraries up-to-date.
- Choose the correct [`SecureStoreOptions`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestoreoptions) for your app's needs. E.g. [`SecureStore.WHEN_UNLOCKED`](https://docs.expo.dev/versions/latest/sdk/securestore/#securestorewhen_unlocked) regulates when the data can be accessed.
- Carefully consider optimizations or other modifications to the above example, as those can lead to introducing subtle security vulnerabilities.
- id: auth-api
title: 'Overview'
notes: |
Expand Down

0 comments on commit da2d6e9

Please sign in to comment.