diff --git a/.changeset/fresh-hats-notice.md b/.changeset/fresh-hats-notice.md new file mode 100644 index 00000000000..df93e2c4066 --- /dev/null +++ b/.changeset/fresh-hats-notice.md @@ -0,0 +1,24 @@ +--- +'@clerk/clerk-expo': minor +--- + +Adds a secure token cache implementation using `expo-secure-store` which encrypts the session token before storing it. + +Usage: + +```tsx +// app/_layout.tsx +import { ClerkProvider } from '@clerk/clerk-expo' +import { tokenCache } from '@clerk/clerk-expo/token-cache' + +export default function RootLayout() { + return ( + + {/* Your app code */} + + ) +} +``` \ No newline at end of file diff --git a/.changeset/gentle-insects-glow.md b/.changeset/gentle-insects-glow.md new file mode 100644 index 00000000000..2be48d963ae --- /dev/null +++ b/.changeset/gentle-insects-glow.md @@ -0,0 +1,28 @@ +--- +'@clerk/clerk-expo': minor +--- + +Mark `secureStore` as deprecated in favor of `resourceCache` from `@clerk/clerk-expo/resource-cache`. + +Usage: + +```tsx +// app/_layout.tsx +import { ClerkProvider } from '@clerk/clerk-expo' +import { tokenCache } from '@clerk/clerk-expo/token-cache' +// import { secureStore } from '@clerk/clerk-expo/secure-store' +import { resourceCache } from '@clerk/clerk-expo/resource-cache' + +export default function RootLayout() { + return ( + + {...} + + ) +} +``` diff --git a/packages/expo/package.json b/packages/expo/package.json index e37ebce9b2d..0f3c891a0ae 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -43,6 +43,14 @@ "./secure-store": { "types": "./dist/secure-store/index.d.ts", "default": "./dist/secure-store/index.js" + }, + "./token-cache": { + "types": "./dist/token-cache/index.d.ts", + "default": "./dist/token-cache/index.js" + }, + "./resource-cache": { + "types": "./dist/resource-cache/index.d.ts", + "default": "./dist/resource-cache/index.js" } }, "main": "./dist/index.js", @@ -53,7 +61,9 @@ "web", "local-credentials", "passkeys", - "secure-store" + "secure-store", + "resource-cache", + "token-cache" ], "scripts": { "build": "tsup", diff --git a/packages/expo/resource-cache/package.json b/packages/expo/resource-cache/package.json new file mode 100644 index 00000000000..6a242788a1c --- /dev/null +++ b/packages/expo/resource-cache/package.json @@ -0,0 +1,4 @@ +{ + "main": "../dist/resource-cache/index.js", + "types": "../dist/resource-cache/index.d.ts" +} diff --git a/packages/expo/src/secure-store/__tests__/dummy-test-data.ts b/packages/expo/src/resource-cache/__tests__/dummy-test-data.ts similarity index 100% rename from packages/expo/src/secure-store/__tests__/dummy-test-data.ts rename to packages/expo/src/resource-cache/__tests__/dummy-test-data.ts diff --git a/packages/expo/src/secure-store/__tests__/secure-store.test.ts b/packages/expo/src/resource-cache/__tests__/secure-store.test.ts similarity index 93% rename from packages/expo/src/secure-store/__tests__/secure-store.test.ts rename to packages/expo/src/resource-cache/__tests__/secure-store.test.ts index c8471fc8294..6904aa3d256 100644 --- a/packages/expo/src/secure-store/__tests__/secure-store.test.ts +++ b/packages/expo/src/resource-cache/__tests__/secure-store.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { createSecureStore } from '../secure-store'; +import { createResourceCacheStore } from '../resource-cache'; import { DUMMY_TEST_LARGE_JSON } from './dummy-test-data'; const KEY = 'key'; @@ -37,7 +37,7 @@ describe('SecureStore', () => { beforeEach(() => { vi.useFakeTimers(); - const createSecureStoreMock = () => { + const createResourceCacheStoreMock = () => { const _map = new Map(); return { setItemAsync: (key: string, value: string): Promise => { @@ -53,7 +53,7 @@ describe('SecureStore', () => { }, }; }; - const secureStoreMock = createSecureStoreMock(); + const secureStoreMock = createResourceCacheStoreMock(); mocks.setItemAsync.mockImplementation(secureStoreMock.setItemAsync); mocks.getItemAsync.mockImplementation(secureStoreMock.getItemAsync); mocks.deleteItemAsync.mockImplementation(secureStoreMock.deleteItemAsync); @@ -64,19 +64,19 @@ describe('SecureStore', () => { }); test('sets a value correctly', async () => { - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); await secureStore.set(KEY, 'value'); await vi.runAllTimersAsync(); expect(await secureStore.get(KEY)).toBe('value'); }); test('returns null for a non-existent key', async () => { - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); expect(await secureStore.get(KEY)).toBeNull(); }); test('returns the last set value', async () => { - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); await secureStore.set(KEY, 'value1'); await secureStore.set(KEY, 'value2'); await vi.runAllTimersAsync(); @@ -90,7 +90,7 @@ describe('SecureStore', () => { describe('delayed write', () => { beforeEach(() => { vi.useFakeTimers(); - const createSecureStoreMock = () => { + const createResourceCacheStoreMock = () => { const _map = new Map(); return { setItemAsync: (key: string, value: string): Promise => { @@ -114,7 +114,7 @@ describe('SecureStore', () => { }, }; }; - const secureStoreMock = createSecureStoreMock(); + const secureStoreMock = createResourceCacheStoreMock(); mocks.setItemAsync.mockImplementation(secureStoreMock.setItemAsync); mocks.getItemAsync.mockImplementation(secureStoreMock.getItemAsync); mocks.deleteItemAsync.mockImplementation(secureStoreMock.deleteItemAsync); @@ -125,7 +125,7 @@ describe('SecureStore', () => { }); test('sets a value async', async () => { - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); void secureStore.set(KEY, 'value'); await vi.runAllTimersAsync(); const value = secureStore.get(KEY); @@ -134,7 +134,7 @@ describe('SecureStore', () => { }); test('sets the correct last value when many sets happen almost at the same time', async () => { - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); void secureStore.set(KEY, 'value'); void secureStore.set(KEY, 'value2'); void secureStore.set(KEY, 'value3'); @@ -177,7 +177,7 @@ describe('SecureStore', () => { mocks.getItemAsync.mockImplementation(getItemAsync); mocks.deleteItemAsync.mockImplementation(deleteItemAsync); - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); void secureStore.set(KEY, JSON.stringify(DUMMY_TEST_LARGE_JSON)); await vi.runAllTimersAsync(); @@ -225,7 +225,7 @@ describe('SecureStore', () => { mocks.getItemAsync.mockImplementation(getItemAsync); mocks.deleteItemAsync.mockImplementation(deleteItemAsync); - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); void secureStore.set(KEY, JSON.stringify(DUMMY_TEST_LARGE_JSON)); await vi.runAllTimersAsync(); void secureStore.set(KEY, 'new value'); @@ -288,7 +288,7 @@ describe('SecureStore', () => { mocks.getItemAsync.mockImplementation(getItemAsync); mocks.deleteItemAsync.mockImplementation(deleteItemAsync); - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); void secureStore.set(KEY, 'new value'); await vi.runAllTimersAsync(); const value = secureStore.get(KEY); @@ -333,7 +333,7 @@ describe('SecureStore', () => { mocks.getItemAsync.mockImplementation(getItemAsync); mocks.deleteItemAsync.mockImplementation(deleteItemAsync); - const secureStore = createSecureStore(); + const secureStore = createResourceCacheStore(); void secureStore.set(KEY, 'new value'); await vi.runAllTimersAsync(); const value = secureStore.get(KEY); diff --git a/packages/expo/src/resource-cache/index.ts b/packages/expo/src/resource-cache/index.ts new file mode 100644 index 00000000000..6c7d59d6685 --- /dev/null +++ b/packages/expo/src/resource-cache/index.ts @@ -0,0 +1 @@ +export { createResourceCacheStore as resourceCache } from './resource-cache'; diff --git a/packages/expo/src/secure-store/secure-store.ts b/packages/expo/src/resource-cache/resource-cache.ts similarity index 98% rename from packages/expo/src/secure-store/secure-store.ts rename to packages/expo/src/resource-cache/resource-cache.ts index 0d99aa9cdb7..14009d3900e 100644 --- a/packages/expo/src/secure-store/secure-store.ts +++ b/packages/expo/src/resource-cache/resource-cache.ts @@ -27,7 +27,7 @@ type Metadata = { * - key-{A/B}-complete -> 'true'/'false' * **/ -export const createSecureStore = (): IStorage => { +export const createResourceCacheStore = (): IStorage => { let queue: KeyValuePair[] = []; let isProcessing = false; diff --git a/packages/expo/src/secure-store/index.ts b/packages/expo/src/secure-store/index.ts index 540c7a1b36d..4ae933093f6 100644 --- a/packages/expo/src/secure-store/index.ts +++ b/packages/expo/src/secure-store/index.ts @@ -1 +1,8 @@ -export { createSecureStore as secureStore } from './secure-store'; +import { resourceCache } from '../resource-cache'; + +/** + * @deprecated Use `resourceCache` from `@clerk/clerk-expo/resource-cache` instead. + */ +const secureStore = resourceCache; + +export { secureStore }; diff --git a/packages/expo/src/token-cache/index.ts b/packages/expo/src/token-cache/index.ts new file mode 100644 index 00000000000..4a67114e009 --- /dev/null +++ b/packages/expo/src/token-cache/index.ts @@ -0,0 +1,39 @@ +import * as SecureStore from 'expo-secure-store'; + +import type { TokenCache } from '../cache'; +import { isNative } from '../utils'; + +/** + * Create a token cache using Expo's SecureStore + */ +const createTokenCache = (): TokenCache => { + return { + getToken: async (key: string) => { + try { + const item = await SecureStore.getItemAsync(key); + return item; + } catch { + await SecureStore.deleteItemAsync(key); + return null; + } + }, + saveToken: (key: string, token: string) => { + return SecureStore.setItemAsync(key, token); + }, + }; +}; + +/** + * Secure token cache implementation for Expo apps. + * + * Clerk stores the active user's session token in memory by default. In Expo apps, the + * recommended way to store sensitive data, such as tokens, is by using `expo-secure-store` + * which encrypts the data before storing it. + * + * To implement your own token cache, create an object that implements the `TokenCache` interface: + * - `getToken(key: string): Promise` + * - `saveToken(key: string, token: string): Promise` + * + * @type {TokenCache | undefined} Object with `getToken` and `saveToken` methods, undefined on web + */ +export const tokenCache = isNative() ? createTokenCache() : undefined; diff --git a/packages/expo/token-cache/package.json b/packages/expo/token-cache/package.json new file mode 100644 index 00000000000..cdcf5453a5e --- /dev/null +++ b/packages/expo/token-cache/package.json @@ -0,0 +1,4 @@ +{ + "main": "../dist/token-cache/index.js", + "types": "../dist/token-cache/index.d.ts" +}