Repository: Crossmint/crossmint-sdk
Pull request #1032: Auth: Adds support for custom storage solutions in auth sdk
Author: login: AlbertoElias
Created: 2025-04-08T18:27:30Z | Merged: 2025-04-08T21:44:38Z

## Description

We need non-cookie options for other environments such as react native

## Test plan

Tested with expo app and with smarter wallet

## Package updates

@client-auth: minor


Diff:
diff --git a/.changeset/famous-owls-swim.md b/.changeset/famous-owls-swim.md
new file mode 100644
index 000000000..e2f7191eb
--- /dev/null
+++ b/.changeset/famous-owls-swim.md
@@ -0,0 +1,5 @@
+---
+"@crossmint/client-sdk-auth": minor
+---
+
+Supports custom storage options
diff --git a/packages/client/auth/README.md b/packages/client/auth/README.md
index 83d81ee31..1d739ff30 100644
--- a/packages/client/auth/README.md
+++ b/packages/client/auth/README.md
@@ -36,6 +36,72 @@ const user = await crossmintAuth.getUser();
 crossmintAuth.logout();
 ```
 
+## Custom Storage Providers
+
+By default, the SDK uses browser cookies for storing authentication materials. For environments where cookies are not available, such as React Native, you can provide a custom storage provider.
+
+### React Native Storage Options
+
+The SDK provides several secure storage implementations for React Native:
+
+#### 1. Expo SecureStore (Recommended for Expo apps)
+
+**First install dependencies:**
+
+```bash
+npx expo install expo-secure-store
+```
+
+For Expo applications, use SecureStore which provides a secure encrypted storage solution:
+
+```typescript
+import * as SecureStore from 'expo-secure-store';
+import { ExpoSecureStorage, CrossmintAuth } from "@crossmint/client-sdk-auth";
+
+// Create a custom storage provider using Expo's SecureStore
+const storageProvider = new ExpoSecureStorage(SecureStore);
+
+// Initialize auth client with secure storage
+const crossmintAuth = CrossmintAuth.from(crossmint, {
+  storageProvider
+});
+```
+
+#### 2. React Native Encrypted Storage (Recommended for non-Expo apps)
+
+**First install dependencies:**
+
+```bash
+npm install react-native-encrypted-storage
+```
+
+For vanilla React Native apps, use EncryptedStorage:
+
+```typescript
+import EncryptedStorage from 'react-native-encrypted-storage';
+import { RNEncryptedStorage, CrossmintAuth } from "@crossmint/client-sdk-auth";
+
+// Create a custom storage provider using react-native-encrypted-storage
+const storageProvider = new RNEncryptedStorage(EncryptedStorage);
+
+// Initialize auth client with secure storage
+const crossmintAuth = CrossmintAuth.from(crossmint, {
+  storageProvider
+});
+```
+
+### Implementing Custom Storage
+
+You can implement any storage solution by implementing the `StorageProvider` interface:
+
+```typescript
+export interface StorageProvider {
+  get(key: string): string | undefined | Promise<string | undefined>;
+  set(key: string, value: string, expiresAt?: string): void | Promise<void>;
+  remove(key: string): void | Promise<void>;
+}
+```
+
 ## Secure setup using HttpOnly cookies
 
 To secure the authentication material, you can set up a custom endpoint in your backend that will handle refreshing the authentication material and storing it in HttpOnly cookies. This way, the authentication material is not accessible to JavaScript running in the browser.
@@ -96,4 +162,4 @@ const crossmintAuth = CrossmintAuth.from(crossmint, {
 }
 ```
 
-These callbacks allow you to perform custom actions when tokens are refreshed or when the user logs out.
\ No newline at end of file
+These callbacks allow you to perform custom actions when tokens are refreshed or when the user logs out.
diff --git a/packages/client/auth/src/CrossmintAuthClient.test.ts b/packages/client/auth/src/CrossmintAuthClient.test.ts
index 4e4609525..7c657111d 100644
--- a/packages/client/auth/src/CrossmintAuthClient.test.ts
+++ b/packages/client/auth/src/CrossmintAuthClient.test.ts
@@ -64,16 +64,16 @@ describe("CrossmintAuthClient", () => {
     });
 
     describe("storeAuthMaterial", () => {
-        it("should store auth material in cookies", () => {
+        it("should store auth material in cookies", async () => {
             const mockAuthMaterial: AuthMaterialWithUser = {
                 jwt: "mock.jwt.token",
                 refreshToken: { secret: "refresh-token", expiresAt: "2023-12-31T23:59:59Z" },
                 user: { id: "user123" },
             };
 
-            crossmintAuthClient.storeAuthMaterial(mockAuthMaterial);
+            await crossmintAuthClient.storeAuthMaterial(mockAuthMaterial);
 
-            expect(cookiesUtils.setCookie).toHaveBeenCalledWith("crossmint-jwt", mockAuthMaterial.jwt);
+            expect(cookiesUtils.setCookie).toHaveBeenCalledWith("crossmint-jwt", mockAuthMaterial.jwt, undefined);
             expect(cookiesUtils.setCookie).toHaveBeenCalledWith(
                 "crossmint-refresh-token",
                 mockAuthMaterial.refreshToken.secret,
diff --git a/packages/client/auth/src/CrossmintAuthClient.ts b/packages/client/auth/src/CrossmintAuthClient.ts
index 984ec2aad..b10403fc4 100644
--- a/packages/client/auth/src/CrossmintAuthClient.ts
+++ b/packages/client/auth/src/CrossmintAuthClient.ts
@@ -12,11 +12,13 @@ import {
 } from "@crossmint/common-sdk-auth";
 import type { Crossmint, CrossmintApiClient } from "@crossmint/common-sdk-base";
 import { type CancellableTask, queueTask } from "@crossmint/client-sdk-base";
-import { deleteCookie, getCookie, getJWTExpiration, setCookie, TIME_BEFORE_EXPIRING_JWT_IN_SECONDS } from "./utils";
+import { getJWTExpiration, TIME_BEFORE_EXPIRING_JWT_IN_SECONDS } from "./utils";
+import { type StorageProvider, getDefaultStorageProvider } from "./utils/storage";
 
 export type CrossmintAuthClientConfig = CrossmintAuthOptions & {
     callbacks?: CrossmintAuthClientCallbacks;
     logoutRoute?: string;
+    storageProvider?: StorageProvider;
 };
 
 export class CrossmintAuthClient extends CrossmintAuth {
@@ -24,11 +26,13 @@ export class CrossmintAuthClient extends CrossmintAuth {
     private refreshTask: CancellableTask | null = null;
     private refreshPromise: Promise<AuthMaterialWithUser> | null = null;
     private logoutRoute: string | null;
+    private storageProvider: StorageProvider;
 
     protected constructor(crossmint: Crossmint, apiClient: CrossmintApiClient, config: CrossmintAuthClientConfig = {}) {
         super(crossmint, apiClient, config);
         this.callbacks = config.callbacks ?? {};
         this.logoutRoute = config.logoutRoute ?? null;
+        this.storageProvider = config.storageProvider ?? getDefaultStorageProvider();
     }
 
     public static from(crossmint: Crossmint, config: CrossmintAuthClientConfig = {}): CrossmintAuthClient {
@@ -58,19 +62,29 @@ export class CrossmintAuthClient extends CrossmintAuth {
         }
     }
 
-    public storeAuthMaterial(authMaterial: AuthMaterialWithUser) {
-        setCookie(SESSION_PREFIX, authMaterial.jwt);
-        setCookie(REFRESH_TOKEN_PREFIX, authMaterial.refreshToken.secret, authMaterial.refreshToken.expiresAt);
+    public async storeAuthMaterial(authMaterial: AuthMaterialWithUser) {
+        await Promise.all([
+            this.storageProvider.set(SESSION_PREFIX, authMaterial.jwt),
+            this.storageProvider.set(
+                REFRESH_TOKEN_PREFIX,
+                authMaterial.refreshToken.secret,
+                authMaterial.refreshToken.expiresAt
+            ),
+        ]);
     }
 
     public async logout() {
-        // Store the old refresh token to pass it to the logout route before deleting the cookies
-        const oldRefreshToken = getCookie(REFRESH_TOKEN_PREFIX);
+        // Store the old refresh token to pass it to the logout route before deleting the storage
+        const oldRefreshToken = await this.storageProvider.get(REFRESH_TOKEN_PREFIX);
+
+        // Even if there's a server error, we want to clear the storage and we do it first to load faster
+        await Promise.all([
+            this.storageProvider.remove(REFRESH_TOKEN_PREFIX),
+            this.storageProvider.remove(SESSION_PREFIX),
+        ]);
 
-        // Even if there's a server error, we want to clear the cookies and we do it first to load faster
-        deleteCookie(REFRESH_TOKEN_PREFIX);
-        deleteCookie(SESSION_PREFIX);
         this.callbacks.onLogout?.();
+
         try {
             if (this.logoutRoute != null) {
                 await this.logoutFromCustomRoute();
@@ -83,13 +97,13 @@ export class CrossmintAuthClient extends CrossmintAuth {
     }
 
     public async handleRefreshAuthMaterial(refreshTokenSecret?: string): Promise<void> {
-        const refreshToken = refreshTokenSecret ?? getCookie(REFRESH_TOKEN_PREFIX);
-        // If there is a custom refresh route, that endpoint will fetch the cookies itself
-        if (refreshToken == null && this.refreshRoute == null) {
-            return;
-        }
-
         try {
+            const refreshToken = refreshTokenSecret ?? (await this.storageProvider.get(REFRESH_TOKEN_PREFIX));
+            // If there is a custom refresh route, that endpoint will fetch the cookies itself
+            if (refreshToken == null && this.refreshRoute == null) {
+                return;
+            }
+
             // Create new refresh promise if none exists
             if (this.refreshPromise == null) {
                 this.refreshPromise = this.refreshAuthMaterial(refreshToken);
@@ -98,7 +112,7 @@ export class CrossmintAuthClient extends CrossmintAuth {
 
             // If a custom refresh route is set, storing in cookies is handled in the server
             if (this.refreshRoute == null) {
-                this.storeAuthMaterial(authMaterial);
+                await this.storeAuthMaterial(authMaterial);
             }
 
             this.callbacks.onTokenRefresh?.(authMaterial);
@@ -106,7 +120,7 @@ export class CrossmintAuthClient extends CrossmintAuth {
             this.scheduleNextRefresh(authMaterial.jwt);
         } catch (error) {
             console.error(error);
-            this.logout();
+            await this.logout();
         } finally {
             this.refreshPromise = null;
         }
diff --git a/packages/client/auth/src/utils/index.ts b/packages/client/auth/src/utils/index.ts
index 35eee8ed1..3c31b9e8a 100644
--- a/packages/client/auth/src/utils/index.ts
+++ b/packages/client/auth/src/utils/index.ts
@@ -1,3 +1,4 @@
 export * from "./jwt";
 export * from "./cookies";
 export * from "./constants";
+export * from "./storage";
diff --git a/packages/client/auth/src/utils/storage.test.ts b/packages/client/auth/src/utils/storage.test.ts
new file mode 100644
index 000000000..445017f08
--- /dev/null
+++ b/packages/client/auth/src/utils/storage.test.ts
@@ -0,0 +1,54 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { CookieStorage, StorageProvider } from "./storage";
+
+describe("CookieStorage", () => {
+    let storage: StorageProvider;
+    let documentCookieSpy: ReturnType<typeof vi.spyOn>;
+
+    beforeEach(() => {
+        storage = new CookieStorage();
+        documentCookieSpy = vi.spyOn(document, "cookie", "get");
+        Object.defineProperty(document, "cookie", {
+            writable: true,
+            value: "",
+        });
+    });
+
+    afterEach(() => {
+        documentCookieSpy.mockRestore();
+    });
+
+    it("should get a value from cookie", async () => {
+        document.cookie = "test_key=test_value; path=/; SameSite=Lax;";
+        expect(await storage.get("test_key")).toBe("test_value");
+    });
+
+    it("should return undefined for non-existent cookie", async () => {
+        document.cookie = "";
+        expect(await storage.get("test_key")).toBeUndefined();
+    });
+
+    it("should set a cookie without expiration", async () => {
+        await storage.set("test_key", "test_value");
+        expect(document.cookie).toBe("test_key=test_value;  path=/; SameSite=Lax;");
+    });
+
+    it("should set a cookie with expiration", async () => {
+        const futureDate = new Date();
+        futureDate.setDate(futureDate.getDate() + 1);
+        const expiresAt = futureDate.toISOString();
+
+        await storage.set("test_key", "test_value", expiresAt);
+        expect(document.cookie).toContain("test_key=test_value;");
+        expect(document.cookie).toContain("expires=");
+        expect(document.cookie).toContain("path=/; SameSite=Lax;");
+    });
+
+    it("should remove a cookie", async () => {
+        document.cookie = "test_key=test_value; path=/; SameSite=Lax;";
+        await storage.remove("test_key");
+        // When a cookie is removed, it's set with an expiration in the past
+        expect(document.cookie).toContain("test_key=");
+        expect(document.cookie).toContain("expires=Thu, 01 Jan 1970 00:00:00 UTC");
+    });
+});
diff --git a/packages/client/auth/src/utils/storage.ts b/packages/client/auth/src/utils/storage.ts
new file mode 100644
index 000000000..0e30d5016
--- /dev/null
+++ b/packages/client/auth/src/utils/storage.ts
@@ -0,0 +1,34 @@
+import { deleteCookie, getCookie, setCookie } from "./cookies";
+
+export interface StorageProvider {
+    get(key: string): Promise<string | undefined>;
+    set(key: string, value: string, expiresAt?: string): Promise<void>;
+    remove(key: string): Promise<void>;
+}
+
+export class CookieStorage implements StorageProvider {
+    async get(key: string): Promise<string | undefined> {
+        if (typeof document === "undefined") {
+            return undefined;
+        }
+        return await getCookie(key);
+    }
+
+    async set(key: string, value: string, expiresAt?: string): Promise<void> {
+        if (typeof document === "undefined") {
+            return;
+        }
+        return await setCookie(key, value, expiresAt);
+    }
+
+    async remove(key: string): Promise<void> {
+        if (typeof document === "undefined") {
+            return;
+        }
+        return await deleteCookie(key);
+    }
+}
+
+export function getDefaultStorageProvider(): StorageProvider {
+    return new CookieStorage();
+}


Reviews:
user: login: afeight
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T18:54:56Z
user: login: afeight
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T18:56:08Z
user: login: afeight
body: This is quite nice 👌
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T18:56:53Z
user: login: AlbertoElias
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:17:08Z

Review comments:
diff_hunk: @@ -0,0 +1,36 @@
+export interface StorageProvider {
+    get(key: string): Promise<string | undefined>;
+    set(key: string, value: string, expiresAt?: string): Promise<void>;
+    remove(key: string): Promise<void>;
+}
+
+export class CookieStorage implements StorageProvider {
+    async get(key: string): Promise<string | undefined> {
+        if (typeof document === "undefined") {
+            return undefined;
+        }
+        const cookie = document.cookie.split("; ").find((row) => row.startsWith(key));
+        return cookie ? cookie.split("=")[1] : undefined;
+    }
+
+    // biome-ignore lint/suspicious/useAwait: The interface requires a Promise
+    async set(key: string, value: string, expiresAt?: string): Promise<void> {
+        if (typeof document === "undefined") {
+            return;
+        }
+        const expiresInUtc = expiresAt ? new Date(expiresAt).toUTCString() : "";
+        document.cookie = `${key}=${value}; ${expiresAt ? `expires=${expiresInUtc};` : ""} path=/; SameSite=Lax;`;
+    }
+
+    // biome-ignore lint/suspicious/useAwait: The interface requires a Promise
+    async remove(key: string): Promise<void> {
+        if (typeof document === "undefined") {
+            return;
+        }
+        document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
+    }
+}
+
+export function getDefaultStorageProvider(): StorageProvider {
+    return new CookieStorage();
+}
path: packages/client/auth/src/utils/storage.ts
user: login: afeight
body: I see the need for this now in order to not introduce a breaking change, but would ideally mark this as something to remove in the future. Might throw a comment saying that
created_at: 2025-04-08T18:54:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,20 +62,30 @@ export class CrossmintAuthClient extends CrossmintAuth {
         }
     }
 
-    public storeAuthMaterial(authMaterial: AuthMaterialWithUser) {
-        setCookie(SESSION_PREFIX, authMaterial.jwt);
-        setCookie(REFRESH_TOKEN_PREFIX, authMaterial.refreshToken.secret, authMaterial.refreshToken.expiresAt);
+    public async storeAuthMaterial(authMaterial: AuthMaterialWithUser) {
+        await Promise.all([
+            this.storageProvider.set(SESSION_PREFIX, authMaterial.jwt),
+            this.storageProvider.set(
+                REFRESH_TOKEN_PREFIX,
+                authMaterial.refreshToken.secret,
+                authMaterial.refreshToken.expiresAt
+            ),
+        ]);
     }
 
     public async logout() {
-        // Store the old refresh token to pass it to the logout route before deleting the cookies
-        const oldRefreshToken = getCookie(REFRESH_TOKEN_PREFIX);
-
-        // Even if there's a server error, we want to clear the cookies and we do it first to load faster
-        deleteCookie(REFRESH_TOKEN_PREFIX);
-        deleteCookie(SESSION_PREFIX);
-        this.callbacks.onLogout?.();
         try {
+            // Store the old refresh token to pass it to the logout route before deleting the storage
+            const oldRefreshToken = await this.storageProvider.get(REFRESH_TOKEN_PREFIX);
+
+            // Even if there's a server error, we want to clear the storage and we do it first to load faster
+            await Promise.all([
+                this.storageProvider.remove(REFRESH_TOKEN_PREFIX),
+                this.storageProvider.remove(SESSION_PREFIX),
+            ]);
path: packages/client/auth/src/CrossmintAuthClient.ts
user: login: afeight
body: since this could now throw an error, do we need to try/catch to make sure it continues even if `storageProvider.remove` fails?
created_at: 2025-04-08T18:56:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+export interface StorageProvider {
+    get(key: string): Promise<string | undefined>;
+    set(key: string, value: string, expiresAt?: string): Promise<void>;
+    remove(key: string): Promise<void>;
+}
+
+export class CookieStorage implements StorageProvider {
+    async get(key: string): Promise<string | undefined> {
+        if (typeof document === "undefined") {
+            return undefined;
+        }
+        const cookie = document.cookie.split("; ").find((row) => row.startsWith(key));
+        return cookie ? cookie.split("=")[1] : undefined;
+    }
+
+    // biome-ignore lint/suspicious/useAwait: The interface requires a Promise
+    async set(key: string, value: string, expiresAt?: string): Promise<void> {
+        if (typeof document === "undefined") {
+            return;
+        }
+        const expiresInUtc = expiresAt ? new Date(expiresAt).toUTCString() : "";
+        document.cookie = `${key}=${value}; ${expiresAt ? `expires=${expiresInUtc};` : ""} path=/; SameSite=Lax;`;
+    }
+
+    // biome-ignore lint/suspicious/useAwait: The interface requires a Promise
+    async remove(key: string): Promise<void> {
+        if (typeof document === "undefined") {
+            return;
+        }
+        document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
+    }
+}
+
+export function getDefaultStorageProvider(): StorageProvider {
+    return new CookieStorage();
+}
path: packages/client/auth/src/utils/storage.ts
user: login: AlbertoElias
body: why would this be temporary?
created_at: 2025-04-08T19:17:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-08T18:27:33Z
body: [vc]: #ku2iyhhooR63UPef5aJQ4qVIkUylJK18i65hZSE5dz8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzbWFydC13YWxsZXQtYXV0aC1kZW1vIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvd2FsbGV0cy9zbWFydC13YWxsZXQvbmV4dCIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9jcm9zc21pbnQvc21hcnQtd2FsbGV0LWF1dGgtZGVtby9BaHVyS1hNU1JDNDZlN3ZZcll0dUZ6OEJ5N0dmIiwicHJldmlld1VybCI6InNtYXJ0LXdhbGxldC1hdXRoLWRlbW8tZ2l0LWFsYmVydG9lLWN1c3RvbS1hdXQtYWU0MGFlLWNyb3NzbWludC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6ImNyb3NzbWludC1zZGstcmVtaXgtc3NyIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvcGF5bWVudHMvbmV4dGpzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2Nyb3NzbWludC9jcm9zc21pbnQtc2RrLXJlbWl4LXNzci9CWHZtVlRLa0xYeW9nbXEzNTZMUGZiWUQ5MkJhIiwicHJldmlld1VybCI6ImNyb3NzbWludC1zZGstcmVtaXgtc3NyLWdpdC1hbGJlcnRvZS1jdXN0b20tYXUtMzllMzI1LWNyb3NzbWludC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImNyb3NzbWludC1zZGstcmVtaXgtc3NyLWdpdC1hbGJlcnRvZS1jdXN0b20tYXUtMzllMzI1LWNyb3NzbWludC52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **crossmint-sdk-remix-ssr** | ✅ Ready ([Inspect](https://vercel.com/crossmint/crossmint-sdk-remix-ssr/BXvmVTKkLXyogmq356LPfbYD92Ba)) | [Visit Preview](https://crossmint-sdk-remix-ssr-git-albertoe-custom-au-39e325-crossmint.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/crossmint-sdk-remix-ssr-git-albertoe-custom-au-39e325-crossmint.vercel.app?via=pr-comment-feedback-link) | Apr 8, 2025 9:12pm |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **smart-wallet-auth-demo** | ⬜️ Ignored ([Inspect](https://vercel.com/crossmint/smart-wallet-auth-demo/AhurKXMSRC46e7vYrYtuFz8By7Gf)) | [Visit Preview](https://smart-wallet-auth-demo-git-albertoe-custom-aut-ae40ae-crossmint.vercel.app) |  | Apr 8, 2025 9:12pm |
</details>



author_association: NONE
user: login: changeset-bot[bot]
created_at: 2025-04-08T18:27:33Z
body: ###  🦋  Changeset detected

Latest commit: 70f6da8af80565a3e616a684e4f86da9d04e2839

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 6 packages</summary>

  | Name                                            | Type  |
| ----------------------------------------------- | ----- |
| @crossmint/client-sdk-auth                      | Minor |
| @crossmint/client-sdk-react-ui                  | Patch |
| @crossmint/auth-ssr-nextjs-demo                 | Patch |
| @crossmint/client-sdk-react-ui-starter          | Patch |
| @crossmint/client-sdk-nextjs-starter            | Patch |
| @crossmint/client-sdk-smart-wallet-next-starter | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/Crossmint/crossmint-sdk/new/albertoe/custom-auth-storage?filename=.changeset/young-mugs-travel.md&value=---%0A%22%40crossmint%2Fclient-sdk-auth%22%3A%20patch%0A---%0A%0AAuth%3A%20Adds%20support%20for%20custom%20storage%20solutions%20in%20auth%20sdk%0A)


author_association: NONE