Skip to content

Commit 13e3ddc

Browse files
authored
feat(clerk-expo): Add secure token cache implementation and rename secureStore (#5375)
1 parent 2452ae7 commit 13e3ddc

File tree

11 files changed

+134
-17
lines changed

11 files changed

+134
-17
lines changed

.changeset/fresh-hats-notice.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@clerk/clerk-expo': minor
3+
---
4+
5+
Adds a secure token cache implementation using `expo-secure-store` which encrypts the session token before storing it.
6+
7+
Usage:
8+
9+
```tsx
10+
// app/_layout.tsx
11+
import { ClerkProvider } from '@clerk/clerk-expo'
12+
import { tokenCache } from '@clerk/clerk-expo/token-cache'
13+
14+
export default function RootLayout() {
15+
return (
16+
<ClerkProvider
17+
publishableKey="your-publishable-key"
18+
tokenCache={tokenCache}
19+
>
20+
{/* Your app code */}
21+
</ClerkProvider>
22+
)
23+
}
24+
```

.changeset/gentle-insects-glow.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@clerk/clerk-expo': minor
3+
---
4+
5+
Mark `secureStore` as deprecated in favor of `resourceCache` from `@clerk/clerk-expo/resource-cache`.
6+
7+
Usage:
8+
9+
```tsx
10+
// app/_layout.tsx
11+
import { ClerkProvider } from '@clerk/clerk-expo'
12+
import { tokenCache } from '@clerk/clerk-expo/token-cache'
13+
// import { secureStore } from '@clerk/clerk-expo/secure-store'
14+
import { resourceCache } from '@clerk/clerk-expo/resource-cache'
15+
16+
export default function RootLayout() {
17+
return (
18+
<ClerkProvider
19+
publishableKey="your-publishable-key"
20+
tokenCache={tokenCache}
21+
// __experimental_resourceCache={secureStore}
22+
__experimental_resourceCache={resourceCache}
23+
>
24+
{...}
25+
</ClerkProvider>
26+
)
27+
}
28+
```

packages/expo/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@
4343
"./secure-store": {
4444
"types": "./dist/secure-store/index.d.ts",
4545
"default": "./dist/secure-store/index.js"
46+
},
47+
"./token-cache": {
48+
"types": "./dist/token-cache/index.d.ts",
49+
"default": "./dist/token-cache/index.js"
50+
},
51+
"./resource-cache": {
52+
"types": "./dist/resource-cache/index.d.ts",
53+
"default": "./dist/resource-cache/index.js"
4654
}
4755
},
4856
"main": "./dist/index.js",
@@ -53,7 +61,9 @@
5361
"web",
5462
"local-credentials",
5563
"passkeys",
56-
"secure-store"
64+
"secure-store",
65+
"resource-cache",
66+
"token-cache"
5767
],
5868
"scripts": {
5969
"build": "tsup",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"main": "../dist/resource-cache/index.js",
3+
"types": "../dist/resource-cache/index.d.ts"
4+
}

packages/expo/src/secure-store/__tests__/dummy-test-data.ts renamed to packages/expo/src/resource-cache/__tests__/dummy-test-data.ts

File renamed without changes.

packages/expo/src/secure-store/__tests__/secure-store.test.ts renamed to packages/expo/src/resource-cache/__tests__/secure-store.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
22

3-
import { createSecureStore } from '../secure-store';
3+
import { createResourceCacheStore } from '../resource-cache';
44
import { DUMMY_TEST_LARGE_JSON } from './dummy-test-data';
55

66
const KEY = 'key';
@@ -37,7 +37,7 @@ describe('SecureStore', () => {
3737
beforeEach(() => {
3838
vi.useFakeTimers();
3939

40-
const createSecureStoreMock = () => {
40+
const createResourceCacheStoreMock = () => {
4141
const _map = new Map();
4242
return {
4343
setItemAsync: (key: string, value: string): Promise<void> => {
@@ -53,7 +53,7 @@ describe('SecureStore', () => {
5353
},
5454
};
5555
};
56-
const secureStoreMock = createSecureStoreMock();
56+
const secureStoreMock = createResourceCacheStoreMock();
5757
mocks.setItemAsync.mockImplementation(secureStoreMock.setItemAsync);
5858
mocks.getItemAsync.mockImplementation(secureStoreMock.getItemAsync);
5959
mocks.deleteItemAsync.mockImplementation(secureStoreMock.deleteItemAsync);
@@ -64,19 +64,19 @@ describe('SecureStore', () => {
6464
});
6565

6666
test('sets a value correctly', async () => {
67-
const secureStore = createSecureStore();
67+
const secureStore = createResourceCacheStore();
6868
await secureStore.set(KEY, 'value');
6969
await vi.runAllTimersAsync();
7070
expect(await secureStore.get(KEY)).toBe('value');
7171
});
7272

7373
test('returns null for a non-existent key', async () => {
74-
const secureStore = createSecureStore();
74+
const secureStore = createResourceCacheStore();
7575
expect(await secureStore.get(KEY)).toBeNull();
7676
});
7777

7878
test('returns the last set value', async () => {
79-
const secureStore = createSecureStore();
79+
const secureStore = createResourceCacheStore();
8080
await secureStore.set(KEY, 'value1');
8181
await secureStore.set(KEY, 'value2');
8282
await vi.runAllTimersAsync();
@@ -90,7 +90,7 @@ describe('SecureStore', () => {
9090
describe('delayed write', () => {
9191
beforeEach(() => {
9292
vi.useFakeTimers();
93-
const createSecureStoreMock = () => {
93+
const createResourceCacheStoreMock = () => {
9494
const _map = new Map();
9595
return {
9696
setItemAsync: (key: string, value: string): Promise<void> => {
@@ -114,7 +114,7 @@ describe('SecureStore', () => {
114114
},
115115
};
116116
};
117-
const secureStoreMock = createSecureStoreMock();
117+
const secureStoreMock = createResourceCacheStoreMock();
118118
mocks.setItemAsync.mockImplementation(secureStoreMock.setItemAsync);
119119
mocks.getItemAsync.mockImplementation(secureStoreMock.getItemAsync);
120120
mocks.deleteItemAsync.mockImplementation(secureStoreMock.deleteItemAsync);
@@ -125,7 +125,7 @@ describe('SecureStore', () => {
125125
});
126126

127127
test('sets a value async', async () => {
128-
const secureStore = createSecureStore();
128+
const secureStore = createResourceCacheStore();
129129
void secureStore.set(KEY, 'value');
130130
await vi.runAllTimersAsync();
131131
const value = secureStore.get(KEY);
@@ -134,7 +134,7 @@ describe('SecureStore', () => {
134134
});
135135

136136
test('sets the correct last value when many sets happen almost at the same time', async () => {
137-
const secureStore = createSecureStore();
137+
const secureStore = createResourceCacheStore();
138138
void secureStore.set(KEY, 'value');
139139
void secureStore.set(KEY, 'value2');
140140
void secureStore.set(KEY, 'value3');
@@ -177,7 +177,7 @@ describe('SecureStore', () => {
177177
mocks.getItemAsync.mockImplementation(getItemAsync);
178178
mocks.deleteItemAsync.mockImplementation(deleteItemAsync);
179179

180-
const secureStore = createSecureStore();
180+
const secureStore = createResourceCacheStore();
181181
void secureStore.set(KEY, JSON.stringify(DUMMY_TEST_LARGE_JSON));
182182
await vi.runAllTimersAsync();
183183

@@ -225,7 +225,7 @@ describe('SecureStore', () => {
225225
mocks.getItemAsync.mockImplementation(getItemAsync);
226226
mocks.deleteItemAsync.mockImplementation(deleteItemAsync);
227227

228-
const secureStore = createSecureStore();
228+
const secureStore = createResourceCacheStore();
229229
void secureStore.set(KEY, JSON.stringify(DUMMY_TEST_LARGE_JSON));
230230
await vi.runAllTimersAsync();
231231
void secureStore.set(KEY, 'new value');
@@ -288,7 +288,7 @@ describe('SecureStore', () => {
288288
mocks.getItemAsync.mockImplementation(getItemAsync);
289289
mocks.deleteItemAsync.mockImplementation(deleteItemAsync);
290290

291-
const secureStore = createSecureStore();
291+
const secureStore = createResourceCacheStore();
292292
void secureStore.set(KEY, 'new value');
293293
await vi.runAllTimersAsync();
294294
const value = secureStore.get(KEY);
@@ -333,7 +333,7 @@ describe('SecureStore', () => {
333333
mocks.getItemAsync.mockImplementation(getItemAsync);
334334
mocks.deleteItemAsync.mockImplementation(deleteItemAsync);
335335

336-
const secureStore = createSecureStore();
336+
const secureStore = createResourceCacheStore();
337337
void secureStore.set(KEY, 'new value');
338338
await vi.runAllTimersAsync();
339339
const value = secureStore.get(KEY);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { createResourceCacheStore as resourceCache } from './resource-cache';

packages/expo/src/secure-store/secure-store.ts renamed to packages/expo/src/resource-cache/resource-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type Metadata = {
2727
* - key-{A/B}-complete -> 'true'/'false'
2828
*
2929
**/
30-
export const createSecureStore = (): IStorage => {
30+
export const createResourceCacheStore = (): IStorage => {
3131
let queue: KeyValuePair[] = [];
3232
let isProcessing = false;
3333

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
export { createSecureStore as secureStore } from './secure-store';
1+
import { resourceCache } from '../resource-cache';
2+
3+
/**
4+
* @deprecated Use `resourceCache` from `@clerk/clerk-expo/resource-cache` instead.
5+
*/
6+
const secureStore = resourceCache;
7+
8+
export { secureStore };
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as SecureStore from 'expo-secure-store';
2+
3+
import type { TokenCache } from '../cache';
4+
import { isNative } from '../utils';
5+
6+
/**
7+
* Create a token cache using Expo's SecureStore
8+
*/
9+
const createTokenCache = (): TokenCache => {
10+
return {
11+
getToken: async (key: string) => {
12+
try {
13+
const item = await SecureStore.getItemAsync(key);
14+
return item;
15+
} catch {
16+
await SecureStore.deleteItemAsync(key);
17+
return null;
18+
}
19+
},
20+
saveToken: (key: string, token: string) => {
21+
return SecureStore.setItemAsync(key, token);
22+
},
23+
};
24+
};
25+
26+
/**
27+
* Secure token cache implementation for Expo apps.
28+
*
29+
* Clerk stores the active user's session token in memory by default. In Expo apps, the
30+
* recommended way to store sensitive data, such as tokens, is by using `expo-secure-store`
31+
* which encrypts the data before storing it.
32+
*
33+
* To implement your own token cache, create an object that implements the `TokenCache` interface:
34+
* - `getToken(key: string): Promise<string | null>`
35+
* - `saveToken(key: string, token: string): Promise<void>`
36+
*
37+
* @type {TokenCache | undefined} Object with `getToken` and `saveToken` methods, undefined on web
38+
*/
39+
export const tokenCache = isNative() ? createTokenCache() : undefined;

0 commit comments

Comments
 (0)