├── .gitignore
├── .npmignore
├── README.md
├── bun.lockb
├── package.json
├── src
├── SolanaAuthProvider.tsx
└── index.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | solana-react-auth-test/
4 | # Logs
5 |
6 | logs
7 | _.log
8 | npm-debug.log_
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 | .pnpm-debug.log*
13 |
14 | # Caches
15 |
16 | .cache
17 |
18 | # Diagnostic reports (https://nodejs.org/api/report.html)
19 |
20 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
21 |
22 | # Runtime data
23 |
24 | pids
25 | _.pid
26 | _.seed
27 | *.pid.lock
28 |
29 | # Directory for instrumented libs generated by jscoverage/JSCover
30 |
31 | lib-cov
32 |
33 | # Coverage directory used by tools like istanbul
34 |
35 | coverage
36 | *.lcov
37 |
38 | # nyc test coverage
39 |
40 | .nyc_output
41 |
42 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
43 |
44 | .grunt
45 |
46 | # Bower dependency directory (https://bower.io/)
47 |
48 | bower_components
49 |
50 | # node-waf configuration
51 |
52 | .lock-wscript
53 |
54 | # Compiled binary addons (https://nodejs.org/api/addons.html)
55 |
56 | build/Release
57 |
58 | # Dependency directories
59 |
60 | node_modules/
61 | jspm_packages/
62 |
63 | # Snowpack dependency directory (https://snowpack.dev/)
64 |
65 | web_modules/
66 |
67 | # TypeScript cache
68 |
69 | *.tsbuildinfo
70 |
71 | # Optional npm cache directory
72 |
73 | .npm
74 |
75 | # Optional eslint cache
76 |
77 | .eslintcache
78 |
79 | # Optional stylelint cache
80 |
81 | .stylelintcache
82 |
83 | # Microbundle cache
84 |
85 | .rpt2_cache/
86 | .rts2_cache_cjs/
87 | .rts2_cache_es/
88 | .rts2_cache_umd/
89 |
90 | # Optional REPL history
91 |
92 | .node_repl_history
93 |
94 | # Output of 'npm pack'
95 |
96 | *.tgz
97 |
98 | # Yarn Integrity file
99 |
100 | .yarn-integrity
101 |
102 | # dotenv environment variable files
103 |
104 | .env
105 | .env.development.local
106 | .env.test.local
107 | .env.production.local
108 | .env.local
109 |
110 | # parcel-bundler cache (https://parceljs.org/)
111 |
112 | .parcel-cache
113 |
114 | # Next.js build output
115 |
116 | .next
117 | out
118 |
119 | # Nuxt.js build / generate output
120 |
121 | .nuxt
122 | dist
123 |
124 | # Gatsby files
125 |
126 | # Comment in the public line in if your project uses Gatsby and not Next.js
127 |
128 | # https://nextjs.org/blog/next-9-1#public-directory-support
129 |
130 | # public
131 |
132 | # vuepress build output
133 |
134 | .vuepress/dist
135 |
136 | # vuepress v2.x temp and cache directory
137 |
138 | .temp
139 |
140 | # Docusaurus cache and generated files
141 |
142 | .docusaurus
143 |
144 | # Serverless directories
145 |
146 | .serverless/
147 |
148 | # FuseBox cache
149 |
150 | .fusebox/
151 |
152 | # DynamoDB Local files
153 |
154 | .dynamodb/
155 |
156 | # TernJS port file
157 |
158 | .tern-port
159 |
160 | # Stores VSCode versions used for testing VSCode extensions
161 |
162 | .vscode-test
163 |
164 | # yarn v2
165 |
166 | .yarn/cache
167 | .yarn/unplugged
168 | .yarn/build-state.yml
169 | .yarn/install-state.gz
170 | .pnp.*
171 |
172 | # IntelliJ based IDEs
173 | .idea
174 |
175 | # Finder (MacOS) folder config
176 | .DS_Store
177 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | solana-react-auth-test/
2 | # Ignore source code if you're publishing only the build output
3 | src/
4 | # Ignore Bun's lockfile
5 | bun.lockb
6 |
7 | # Ignore TypeScript configs and cache
8 | tsconfig.json
9 |
10 | # Ignore build tools and scripts
11 | scripts/
12 | .vscode/
13 | .env
14 | .DS_Store # macOS specific
15 | Thumbs.db # Windows specific
16 |
17 | # Ignore test files and coverage
18 | tests/
19 | test/
20 | *.test.*
21 | coverage/
22 | .jest/
23 | .nyc_output/
24 |
25 | # Ignore Git files and metadata
26 | .git/
27 | .gitignore
28 |
29 | # Ignore unnecessary files
30 | npm-debug.log
31 | yarn.lock
32 | package-lock.json
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Solana React Auth Provider
2 |
3 | A React context provider that allows users to authenticate using their Solana wallet. It uses browser local storage to store the authentication data. which includes the signature, public key, and the timestamp of the signature in seconds.
4 | It verifies the signature using the public key and the signature itself and also checks the timestamp to ensure that the signature is not expired.
5 |
6 | # Installation
7 |
8 | ### bun
9 |
10 | ```bash
11 | bun add solana-react-auth
12 | ```
13 |
14 | ### yarn
15 |
16 | ```bash
17 | yarn add solana-react-auth
18 | ```
19 |
20 | ### npm
21 |
22 | ```bash
23 | npm install solana-react-auth
24 | ```
25 |
26 | # Exposed Objects
27 |
28 | ### `SolanaAuthProvider`
29 |
30 | A React component that wraps the application and provides the authentication context. It takes the following props:
31 |
32 | - **`wallet`**: WalletContext from useWallet hook
33 | - **`message`**: A string or object that represents the message to be signed by the user.
34 | - **`authTimeout`**: A number that represents the timeout of the authentication in seconds.
35 |
36 | > Note: This provider should be a child of WalletProvider from @solana/wallet-adapter-react.
37 |
38 | ```tsx
39 |
44 |
45 |
46 | ```
47 |
48 | ### `useSolanaAuth`
49 |
50 | A React hook that exposes the following methods:
51 |
52 | - **`checkIsAuthenticated`**: Returns a boolean value indicating if the user is authenticated.
53 | - **`authenticate`**: A function that tries to authenticate the user.
54 | - **`getAuthData`**: Returns the authentication data, including the signature, public key, and the timestamp of the signature in seconds.
55 |
56 | ```tsx
57 | const { checkIsAuthenticated, authenticate, getAuthData } = useSolanaAuth();
58 | ```
59 |
60 | ### `AuthStorage`
61 |
62 | An object that represents the authentication data stored in the local storage of the browser.
63 |
64 | ```typescript
65 | type AuthStorage = {
66 | signature: string;
67 | pubkey: string;
68 | signedAt: number;
69 | };
70 | ```
71 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagaprasadvr/solana-react-auth/f7567278ffd52ee982bdafe823458d2fb280d149/bun.lockb
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "solana-react-auth",
3 | "description": "Solana React Auth",
4 | "license": "MIT",
5 | "author": {
6 | "name": "Nagaprasad V R",
7 | "email": "nagaprasadvr246@gmail.com"
8 | },
9 | "type": "module",
10 | "main": "dist/index.js",
11 | "types": "dist/index.d.ts",
12 | "version": "2.0.5",
13 | "scripts": {
14 | "build": "bun build src/index.ts --outdir ./dist --minify --format esm --external react --external react-dom "
15 | },
16 | "devDependencies": {
17 | "@types/bun": "latest",
18 | "@types/react": "^18.3.11"
19 | },
20 | "peerDependencies": {
21 | "typescript": "^5.0.0"
22 | },
23 | "dependencies": {
24 | "@solana/web3.js": "^1.95.4",
25 | "bs58": "^6.0.0",
26 | "buffer": "^6.0.3",
27 | "react": "^18.3.1",
28 | "react-dom": "^18.3.1",
29 | "tweetnacl": "^1.0.3"
30 | },
31 | "publishConfig": {
32 | "access": "public"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "https://github.com/NagaprasadVr/solana-react-auth.git"
37 | },
38 | "keywords": [
39 | "solana",
40 | "react",
41 | "auth",
42 | "web3"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/src/SolanaAuthProvider.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | useCallback,
4 | useContext,
5 | useEffect,
6 | useState,
7 | type ReactNode,
8 | } from "react";
9 |
10 | import { type WalletContextState } from "@solana/wallet-adapter-react";
11 |
12 | import bs58 from "bs58";
13 | import solanaCrypto from "tweetnacl";
14 |
15 | export type SolanaAuthProviderProps = {
16 | children: ReactNode;
17 | message: object | string;
18 | wallet: WalletContextState;
19 | authTimeout: number; // in seconds
20 | };
21 |
22 | export interface SolanaAuthContextType {
23 | checkIsAuthenticated: (wallet: WalletContextState) => boolean;
24 | authenticate: (wallet: WalletContextState) => void;
25 | getAuthData: () => AuthStorage | null;
26 | }
27 |
28 | const defaultContext: SolanaAuthContextType = {
29 | checkIsAuthenticated: () => false,
30 | authenticate: () => {},
31 | getAuthData: () => null,
32 | };
33 |
34 | const SolanaAuthContext = createContext(defaultContext);
35 |
36 | export type AuthStorage = {
37 | signature: string;
38 | pubkey: string;
39 | signedAt: number; // timestamp in seconds
40 | };
41 |
42 | export const SolanaAuthProvider = ({
43 | children,
44 | message,
45 | wallet,
46 | authTimeout,
47 | }: SolanaAuthProviderProps) => {
48 | const [pubkey, setPubkey] = useState(null);
49 |
50 | // set this when the pubkey is available
51 | useEffect(() => {
52 | if (wallet?.connected && wallet?.publicKey) {
53 | setPubkey(wallet.publicKey.toBase58());
54 | }
55 |
56 | if (!wallet?.connected) {
57 | setPubkey(null);
58 | }
59 | }, [wallet?.connected, wallet?.publicKey]);
60 |
61 | // if pubkey changes, re-authenticate
62 | useEffect(() => {
63 | authenticate();
64 | }, [pubkey]);
65 |
66 | const checkIsAuthenticated = useCallback((): boolean => {
67 | try {
68 | if (!wallet.connected) {
69 | return false;
70 | }
71 |
72 | if (!pubkey) {
73 | return false;
74 | }
75 |
76 | const storedAuth = getstoredAuth();
77 | if (!storedAuth) {
78 | return false;
79 | }
80 |
81 | const isSignatureValid = verifySignature(
82 | storedAuth.signature,
83 | pubkey,
84 | getJsonMessage(pubkey, message)
85 | );
86 |
87 | if (!isSignatureValid) {
88 | return false;
89 | }
90 |
91 | const now = getTimeNowInSeconds();
92 | const signedAt = storedAuth.signedAt;
93 | const elapsed = now - signedAt;
94 |
95 | return elapsed < authTimeout;
96 | } catch (e) {
97 | console.error(e);
98 | return false;
99 | }
100 | }, [pubkey]);
101 |
102 | const authenticate = useCallback(async () => {
103 | try {
104 | console.log("authenticating...", pubkey);
105 | if (!pubkey) {
106 | return;
107 | }
108 |
109 | // if already authenticated, no need to re-authenticate
110 | if (checkIsAuthenticated()) {
111 | return;
112 | }
113 |
114 | // sign the message
115 | const signedMessage = await signMessage(
116 | getJsonMessage(pubkey, message),
117 | wallet
118 | );
119 |
120 | // store the signature on local storage
121 | storeSignature(signedMessage, pubkey);
122 | } catch (e) {
123 | console.error(e);
124 | }
125 | }, [pubkey]);
126 |
127 | const getAuthData = () => {
128 | return getstoredAuth();
129 | };
130 |
131 | return (
132 |
139 | {children}
140 |
141 | );
142 | };
143 |
144 | export const useSolanaAuth = () => {
145 | return useContext(SolanaAuthContext);
146 | };
147 |
148 | function minimizePubkey(pubkey: string) {
149 | return pubkey.slice(0, 5) + "..." + pubkey.slice(-5);
150 | }
151 |
152 | function encodeWithBase58(sig: Uint8Array) {
153 | return bs58.encode(sig);
154 | }
155 |
156 | function decodeWithBase58(sig: string) {
157 | return bs58.decode(sig);
158 | }
159 |
160 | function getMessageToSign(jsonMessage: object) {
161 | return new TextEncoder().encode(JSON.stringify(jsonMessage));
162 | }
163 |
164 | function verifySignature(
165 | signature: string, // base58 encoded
166 | pubkey: string, // base58 encoded
167 | jsonMessage: object
168 | ) {
169 | return solanaCrypto.sign.detached.verify(
170 | getMessageToSign(jsonMessage),
171 | decodeWithBase58(signature),
172 | decodeWithBase58(pubkey)
173 | );
174 | }
175 |
176 | async function signMessage(jsonMessage: object, wallet: WalletContextState) {
177 | if (!wallet.publicKey) {
178 | throw new Error("Wallet public key is not available");
179 | }
180 |
181 | if (!wallet.signMessage) {
182 | throw new Error("Wallet does not support signing messages");
183 | }
184 |
185 | const encodedMessage = getMessageToSign(jsonMessage);
186 | const signature = await wallet.signMessage(encodedMessage);
187 |
188 | return encodeWithBase58(signature);
189 | }
190 |
191 | function getJsonMessage(pubkey: string, jsonMessage: any): object {
192 | if (typeof jsonMessage === "string") {
193 | const message = {
194 | message: jsonMessage,
195 | pubkey: minimizePubkey(pubkey),
196 | };
197 |
198 | return message;
199 | }
200 |
201 | if (!jsonMessage["pubkey"]) {
202 | jsonMessage["pubkey"] = minimizePubkey(pubkey);
203 | }
204 | return jsonMessage;
205 | }
206 |
207 | function storeSignature(signature: string, pubkey: string) {
208 | const authStorage: AuthStorage = {
209 | signature,
210 | pubkey,
211 | signedAt: getTimeNowInSeconds(),
212 | };
213 |
214 | localStorage.setItem("authStorage", JSON.stringify(authStorage));
215 | }
216 |
217 | function getstoredAuth(): AuthStorage | null {
218 | const authStorage = localStorage.getItem("authStorage");
219 | if (!authStorage) {
220 | return null;
221 | }
222 |
223 | return JSON.parse(authStorage);
224 | }
225 |
226 | function getCheckedAuthTimeout(authTimeout: number) {
227 | const secondsInDay = 24 * 60 * 60;
228 | if (authTimeout > secondsInDay) {
229 | return secondsInDay;
230 | }
231 | return authTimeout;
232 | }
233 |
234 | function getTimeNowInSeconds() {
235 | return Math.floor(Date.now() / 1000);
236 | }
237 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | SolanaAuthProvider,
3 | useSolanaAuth,
4 | AuthStorage,
5 | } from "./SolanaAuthProvider";
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "declaration": true,
5 | "emitDeclarationOnly": true,
6 | "lib": ["ESNext", "DOM"],
7 | "target": "ESNext",
8 | "module": "ESNext",
9 | "moduleDetection": "force",
10 | "jsx": "react-jsx",
11 | "outDir": "./dist",
12 | "allowJs": true,
13 | "moduleResolution": "Node",
14 |
15 | // Best practices
16 | "strict": true,
17 | "skipLibCheck": true,
18 | "noFallthroughCasesInSwitch": true,
19 | "allowSyntheticDefaultImports": true,
20 |
21 | // Some stricter flags (disabled by default)
22 | "noUnusedLocals": false,
23 | "noUnusedParameters": false,
24 | "noPropertyAccessFromIndexSignature": false
25 | },
26 |
27 | "include": ["src/**/*"]
28 | }
29 |
--------------------------------------------------------------------------------