Implement authentication with SIWE #4

Merged
nabarun merged 6 commits from nv-siwe into main 2024-10-18 12:47:12 +00:00
13 changed files with 55 additions and 45 deletions
Showing only changes of commit 5ea8479c62 - Show all commits

View File

@ -16,11 +16,11 @@
"apollo-server-core": "^3.13.0", "apollo-server-core": "^3.13.0",
"apollo-server-express": "^3.13.0", "apollo-server-express": "^3.13.0",
"cookie-session": "^2.1.0", "cookie-session": "^2.1.0",
"cors": "2.8.5", "cors": "^2.8.5",
"debug": "^4.3.1", "debug": "^4.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"express-session": "1.18.0", "express-session": "^1.18.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"luxon": "^3.4.4", "luxon": "^3.4.4",
@ -51,7 +51,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/cookie-session": "^2.0.49", "@types/cookie-session": "^2.0.49",
"@types/express-session": "1.17.10", "@types/express-session": "^1.17.10",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"better-sqlite3": "^9.2.2", "better-sqlite3": "^9.2.2",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",

View File

@ -53,7 +53,7 @@ router.post('/validate', async (req, res) => {
}); });
if (!success) { if (!success) {
return res.send({ success, error: 'SIWE verifcation failed' } ); return res.send({ success });
} }
const service: Service = req.app.get('service'); const service: Service = req.app.get('service');
const user = await service.getUserByEthAddress(data.address); const user = await service.getUserByEthAddress(data.address);
@ -66,6 +66,7 @@ router.post('/validate', async (req, res) => {
subOrgId: '', subOrgId: '',
turnkeyWalletId: '', turnkeyWalletId: '',
}); });
// SIWESession from the web3modal library requires both address and chain ID
req.session.address = newUser.id; req.session.address = newUser.id;
req.session.chainId = data.chainId; req.session.chainId = data.chainId;
} else { } else {

View File

@ -22,6 +22,9 @@ import { Service } from './service';
const log = debug('snowball:server'); const log = debug('snowball:server');
// Set cookie expiration to 1 month in milliseconds
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
declare module 'express-session' { declare module 'express-session' {
interface SessionData { interface SessionData {
address: string; address: string;
@ -86,7 +89,7 @@ export const createAndStartServer = async (
saveUninitialized: true, saveUninitialized: true,
cookie: { cookie: {
secure: new URL(appOriginUrl).protocol === 'https:', secure: new URL(appOriginUrl).protocol === 'https:',
maxAge: 30 * 24 * 60 * 60 * 1000, maxAge: COOKIE_MAX_AGE,
domain: domain || undefined, domain: domain || undefined,
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax', sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
} }

View File

@ -38,7 +38,7 @@
"@snowballtools/smartwallet-alchemy-light": "^0.2.0", "@snowballtools/smartwallet-alchemy-light": "^0.2.0",
"@snowballtools/types": "^0.2.0", "@snowballtools/types": "^0.2.0",
"@snowballtools/utils": "^0.1.1", "@snowballtools/utils": "^0.1.1",
"@tanstack/react-query": "5.22.2", "@tanstack/react-query": "^5.22.2",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",

View File

@ -12,7 +12,7 @@ import Index from './pages';
import AuthPage from './pages/AuthPage'; import AuthPage from './pages/AuthPage';
import { DashboardLayout } from './pages/org-slug/layout'; import { DashboardLayout } from './pages/org-slug/layout';
import Web3Provider from 'context/Web3Provider'; import Web3Provider from 'context/Web3Provider';
import { baseUrl } from 'utils/constants'; import { BASE_URL } from 'utils/constants';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -56,7 +56,7 @@ function App() {
// Hacky way of checking session // Hacky way of checking session
// TODO: Handle redirect backs // TODO: Handle redirect backs
useEffect(() => { useEffect(() => {
fetch(`${baseUrl}/auth/session`, { fetch(`${BASE_URL}/auth/session`, {
credentials: 'include', credentials: 'include',
}).then((res) => { }).then((res) => {
const path = window.location.pathname; const path = window.location.pathname;

View File

@ -21,7 +21,7 @@ import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts'; import { useMediaQuery } from 'usehooks-ts';
import { SIDEBAR_MENU } from './constants'; import { SIDEBAR_MENU } from './constants';
import { UserSelect } from 'components/shared/UserSelect'; import { UserSelect } from 'components/shared/UserSelect';
import { baseUrl } from 'utils/constants'; import { BASE_URL } from 'utils/constants';
interface SidebarProps { interface SidebarProps {
mobileOpen?: boolean; mobileOpen?: boolean;
@ -86,7 +86,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
}, [orgSlug]); }, [orgSlug]);
const handleLogOut = useCallback(async () => { const handleLogOut = useCallback(async () => {
await fetch(`${baseUrl}/auth/logout`, { await fetch(`${BASE_URL}/auth/logout`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
}); });

View File

@ -4,6 +4,7 @@ import { SiweMessage, generateNonce } from 'siwe';
import { WagmiProvider } from 'wagmi'; import { WagmiProvider } from 'wagmi';
import { arbitrum, mainnet } from 'wagmi/chains'; import { arbitrum, mainnet } from 'wagmi/chains';
import axios from 'axios'; import axios from 'axios';
import { createWeb3Modal } from '@web3modal/wagmi/react'; import { createWeb3Modal } from '@web3modal/wagmi/react';
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'; import { defaultWagmiConfig } from '@web3modal/wagmi/react/config';
import { createSIWEConfig } from '@web3modal/siwe'; import { createSIWEConfig } from '@web3modal/siwe';
@ -13,16 +14,16 @@ import type {
} from '@web3modal/core'; } from '@web3modal/core';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { VITE_WALLET_CONNECT_ID, baseUrl } from 'utils/constants'; import { VITE_WALLET_CONNECT_ID, BASE_URL } from 'utils/constants';
if (!VITE_WALLET_CONNECT_ID) { if (!VITE_WALLET_CONNECT_ID) {
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set'); throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
} }
assert(baseUrl, 'VITE_SERVER_URL is not set in env'); assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: baseUrl, baseURL: BASE_URL,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',

View File

@ -14,7 +14,7 @@ import { GQLClientProvider } from './context/GQLClientContext';
import { SERVER_GQL_PATH } from './constants'; import { SERVER_GQL_PATH } from './constants';
import { Toaster } from 'components/shared/Toast'; import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error'; import { LogErrorBoundary } from 'utils/log-error';
import { baseUrl } from 'utils/constants'; import { BASE_URL } from 'utils/constants';
import Web3ModalProvider from './context/Web3Provider'; import Web3ModalProvider from './context/Web3Provider';
@ -24,8 +24,8 @@ const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement, document.getElementById('root') as HTMLElement,
); );
assert(baseUrl, 'VITE_SERVER_URL is not set in env'); assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`; const gqlEndpoint = `${BASE_URL}/${SERVER_GQL_PATH}`;
const gqlClient = new GQLClient({ gqlEndpoint }); const gqlClient = new GQLClient({ gqlEndpoint });

View File

@ -1,9 +1,9 @@
import { baseUrl } from './constants'; import { BASE_URL } from './constants';
export async function verifyAccessCode( export async function verifyAccessCode(
accesscode: string, accesscode: string,
): Promise<boolean | null> { ): Promise<boolean | null> {
const res = await fetch(`${baseUrl}/auth/accesscode`, { const res = await fetch(`${BASE_URL}/auth/accesscode`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
accesscode, accesscode,

View File

@ -1,4 +1,4 @@
export const baseUrl = import.meta.env.VITE_SERVER_URL; export const BASE_URL = import.meta.env.VITE_SERVER_URL;
export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!; export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
export const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!; export const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env

View File

@ -2,7 +2,7 @@ import { SiweMessage } from 'siwe';
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers'; import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { baseUrl } from './constants'; import { BASE_URL } from './constants';
const domain = window.location.host; const domain = window.location.host;
const origin = window.location.origin; const origin = window.location.origin;
@ -19,7 +19,7 @@ export async function signInWithEthereum(
); );
const signature = await wallet.signMessage(message); const signature = await wallet.signMessage(message);
const res = await fetch(`${baseUrl}/auth/validate`, { const res = await fetch(`${BASE_URL}/auth/validate`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,7 +1,7 @@
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http'; import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
import { WebauthnStamper } from '@turnkey/webauthn-stamper'; import { WebauthnStamper } from '@turnkey/webauthn-stamper';
import { baseUrl, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants'; import { BASE_URL, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms // All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// We only support ES256, which is listed here // We only support ES256, which is listed here
@ -10,7 +10,7 @@ const es256 = -7;
export async function subOrganizationIdForEmail( export async function subOrganizationIdForEmail(
email: string, email: string,
): Promise<string | null> { ): Promise<string | null> {
const res = await fetch(`${baseUrl}/auth/registration/${email}`); const res = await fetch(`${BASE_URL}/auth/registration/${email}`);
// If API returns a non-empty 200, this email maps to an existing user. // If API returns a non-empty 200, this email maps to an existing user.
if (res.status == 200) { if (res.status == 200) {
@ -64,7 +64,7 @@ export async function turnkeySignup(email: string) {
}, },
}); });
const res = await fetch(`${baseUrl}/auth/register`, { const res = await fetch(`${BASE_URL}/auth/register`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
email, email,
@ -108,7 +108,7 @@ export async function turnkeySignin(subOrganizationId: string) {
throw new Error(`Error during webauthn prompt: ${e}`); throw new Error(`Error during webauthn prompt: ${e}`);
} }
const res = await fetch(`${baseUrl}/auth/authenticate`, { const res = await fetch(`${BASE_URL}/auth/authenticate`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
signedWhoamiRequest: signedRequest, signedWhoamiRequest: signedRequest,

View File

@ -7035,17 +7035,17 @@
"@types/express" "^4.7.0" "@types/express" "^4.7.0"
file-system-cache "2.3.0" file-system-cache "2.3.0"
"@tanstack/query-core@5.22.2": "@tanstack/query-core@5.59.13":
version "5.22.2" version "5.59.13"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.22.2.tgz#af67d41b0b4a3e846c2325f32540f39ca0d4788d" resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.59.13.tgz#8c962980af174bbd446b7e9b9999f7432897df80"
integrity sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg== integrity sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==
"@tanstack/react-query@5.22.2": "@tanstack/react-query@^5.22.2":
version "5.22.2" version "5.59.15"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.22.2.tgz#e5fce278fbdd026fc1d561a4505142b9f93549d7" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.59.15.tgz#fa1c5b4d96e6a148ec761f214304bbf5ac1906be"
integrity sha512-TaxJDRzJ8/NWRT4lY2jguKCrNI6MRN+67dELzPjNUlvqzTxGANlMp68l7aC7hG8Bd1uHNxHl7ihv7MT50i/43A== integrity sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==
dependencies: dependencies:
"@tanstack/query-core" "5.22.2" "@tanstack/query-core" "5.59.13"
"@testing-library/dom@^8.5.0": "@testing-library/dom@^8.5.0":
version "8.20.1" version "8.20.1"
@ -7495,10 +7495,10 @@
"@types/range-parser" "*" "@types/range-parser" "*"
"@types/send" "*" "@types/send" "*"
"@types/express-session@1.17.10": "@types/express-session@^1.17.10":
version "1.17.10" version "1.18.0"
resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.10.tgz#3a9394f1f314a4c657af3fb1cdb52f00fc207fd2" resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.18.0.tgz#7c6f25c3604b28d6bc08a2e3929997bbc7672fa2"
integrity sha512-U32bC/s0ejXijw5MAzyaV4tuZopCh/K7fPoUDyNbsRXHvPSeymygYD1RFL99YOLhF5PNOkzswvOTRaVHdL1zMw== integrity sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==
dependencies: dependencies:
"@types/express" "*" "@types/express" "*"
@ -10383,6 +10383,11 @@ cookie@0.6.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
cookie@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
cookies@0.9.1: cookies@0.9.1:
version "0.9.1" version "0.9.1"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3"
@ -10416,7 +10421,7 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cors@2.8.5, cors@^2.8.5: cors@^2.8.5:
version "2.8.5" version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
@ -11760,12 +11765,12 @@ express-async-errors@^3.1.1:
resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41" resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41"
integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng== integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==
express-session@1.18.0: express-session@^1.18.0:
version "1.18.0" version "1.18.1"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.0.tgz#a6ae39d9091f2efba5f20fc5c65a3ce7c9ce16a3" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.1.tgz#88d0bbd41878882840f24ec6227493fcb167e8d5"
integrity sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ== integrity sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==
dependencies: dependencies:
cookie "0.6.0" cookie "0.7.2"
cookie-signature "1.0.7" cookie-signature "1.0.7"
debug "2.6.9" debug "2.6.9"
depd "~2.0.0" depd "~2.0.0"