expogoogle-signin

"Provided value to SecureStore is larger than 2048 bytes" while trying to store Google OAuth token


I'm implementing Google login to my Expo app using Supabase, I set it up to store the tokens on expo-secure-store, but when I login I get the following warning:

Provided value to SecureStore is larger than 2048 bytes. An attempt to store such a value will throw an error in SDK 35.

I a using Expo 48. The token I'm trying to store is the following (redacted):

{"access_token":"<REDACTED>","token_type":"<REDACTED>","expires_in":<REDACTED>,"refresh_token":"<REDACTED>","user":{"id":"<REDACTED>","aud":"<REDACTED>","role":"<REDACTED>","email":"<REDACTED>","email_confirmed_at":"<REDACTED>","phone":"<REDACTED>","confirmed_at":"<REDACTED>","last_sign_in_at":"<REDACTED>","app_metadata":{"provider":"<REDACTED>","providers":["<REDACTED>"]},"user_metadata":{"avatar_url":"<REDACTED>","email":"<REDACTED>","email_verified":<REDACTED>,"full_name":"<REDACTED>","iss":"<REDACTED>","name":"<REDACTED>","picture":"<REDACTED>","provider_id":"<REDACTED>","sub":"<REDACTED>"},"identities":[{"id":"<REDACTED>","user_id":"<REDACTED>","identity_data":{"avatar_url":"<REDACTED>","email":"<REDACTED>","email_verified":<REDACTED>,"full_name":"<REDACTED>","iss":"<REDACTED>","name":"<REDACTED>","picture":"<REDACTED>","provider_id":"<REDACTED>","sub":"<REDACTED>"},"provider":"<REDACTED>","last_sign_in_at":"<REDACTED>","created_at":"<REDACTED>","updated_at":"<REDACTED>"}],"created_at":"<REDACTED>","updated_at":"<REDACTED>"},"expires_at":<REDACTED>}

I suppose I'm not the first trying to implement Google login through Expo so I was wondering what's the commonly accepted solution to this problem?


Solution

  • We've clarified SecureStore usage and outlined an option for encrypting the session data here: https://supabase.com/docs/guides/getting-started/tutorials/with-expo?auth-store=secure-store#initialize-a-react-native-app

    This uses regular async-storage for actual value storage, but encrypts the values stored there.

    Then we associate the values with a randomized and encrypted key, which is then saved to SecureStore. The encryption key will always be small enough for SecureStore, and by storing the key securely we also protect the encrypted data in async-store

    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,
      },
    });