diff --git a/.env.example b/.env.example index 4821f91..f11bcae 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,5 @@ POSTGRES_URL=**** NEXT_PUBLIC_PROJECT_ID= NEXTAUTH_SECRET= + +SIWE_VERIFICATION_API= diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts index d7d7a92..27d998e 100644 --- a/app/(auth)/auth.config.ts +++ b/app/(auth)/auth.config.ts @@ -1,26 +1,64 @@ -import { NextAuthConfig } from "next-auth"; +import type { SIWESession } from '@reown/appkit-siwe'; +import type { NextAuthConfig } from 'next-auth'; + +declare module 'next-auth' { + interface Session extends SIWESession { + address: string; + chainId: number; + accessToken: string; + } + interface User { + accessToken: string; + } +} + +declare module 'next-auth/jwt' { + interface JWT { + wrappedJWT: string; + } +} export const nextAuthSecret = process.env.NEXTAUTH_SECRET; if (!nextAuthSecret) { - throw new Error("NEXTAUTH_SECRET is not set"); + throw new Error('NEXTAUTH_SECRET is not set'); } export const authConfig = { secret: nextAuthSecret, providers: [], session: { - strategy: "jwt", + strategy: 'jwt', }, callbacks: { + jwt({ trigger, token, user }) { + /** + * On sign in or sign up, we wrap the main JWT returned by the core + * service in another JWT that is managed by Authjs. + */ + if (trigger === 'signIn' || trigger === 'signUp') { + return { + ...token, + wrappedJWT: user.accessToken, + }; + } + return token; + }, session({ session, token }) { if (!token.sub) { return session; } - const [, chainId, address] = token.sub.split(":"); - if (chainId && address) { + const [, chainId, address] = token.sub.split(':'); + const wrappedJWT = token.wrappedJWT; + if (chainId && address && wrappedJWT) { + /** + * The following are added to session so that we know the user through + * the address and chainId, and can make requests to the core service + * using the access token + */ session.address = address; - session.chainId = parseInt(chainId, 10); + session.chainId = Number.parseInt(chainId, 10); + session.accessToken = wrappedJWT; } return session; diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts index b016af7..62215d8 100644 --- a/app/(auth)/auth.ts +++ b/app/(auth)/auth.ts @@ -1,69 +1,87 @@ -import NextAuth from "next-auth"; -import credentialsProvider from "next-auth/providers/credentials"; -import { getAddressFromMessage, getChainIdFromMessage, type SIWESession } from "@reown/appkit-siwe"; -import { createPublicClient, http } from "viem"; +import { + getAddressFromMessage, + getChainIdFromMessage, +} from '@reown/appkit-siwe'; +import NextAuth from 'next-auth'; +import 'next-auth/jwt'; +import credentialsProvider from 'next-auth/providers/credentials'; -import { authConfig } from "./auth.config"; - -declare module "next-auth" { - interface Session extends SIWESession { - address: string; - chainId: number; - } -} +import { authConfig } from './auth.config'; +/** + * TODO: Move all configs into a validated configs module to avoid duplication + * + * https://github.com/pattern-tech/pattern-ui/issues/3 + */ export const nextAuthSecret = process.env.NEXTAUTH_SECRET; if (!nextAuthSecret) { - throw new Error("NEXTAUTH_SECRET is not set"); + throw new Error('NEXTAUTH_SECRET is not set'); } export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID; if (!projectId) { - throw new Error("NEXT_PUBLIC_PROJECT_ID is not set"); + throw new Error('NEXT_PUBLIC_PROJECT_ID is not set'); +} + +const siweVerificationApi = process.env.SIWE_VERIFICATION_API; +if (!siweVerificationApi) { + throw new Error('SIWE_VERIFICATION_API is not set'); } const providers = [ credentialsProvider({ - name: "Ethereum", + name: 'Ethereum', credentials: { message: { - label: "Message", - type: "text", - placeholder: "0x0", + label: 'Message', + type: 'text', + placeholder: '0x0', }, signature: { - label: "Signature", - type: "text", - placeholder: "0x0", + label: 'Signature', + type: 'text', + placeholder: '0x0', }, }, async authorize(credentials) { try { if (!credentials?.message) { - throw new Error("SiweMessage is undefined"); + throw new Error('SiweMessage is undefined'); } const { message, signature } = credentials as Record; const address = getAddressFromMessage(message); const chainId = getChainIdFromMessage(message); - const publicClient = createPublicClient({ - transport: http( - `https://rpc.walletconnect.org/v1/?chainId=${chainId}&projectId=${projectId}` - ), + const response = await fetch(siweVerificationApi, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message, + signature, + }), }); - const isValid = await publicClient.verifyMessage({ - message, - address: address as `0x${string}`, - signature: signature as `0x${string}`, - }); - // end o view verifyMessage - if (isValid) { - return { - id: `${chainId}:${address}`, - }; + if (response.ok) { + const { + data: { access_token: accessToken }, + } = await response.json(); + + if (accessToken) { + return { + id: `${chainId}:${address}`, + accessToken, + }; + } } + /** + * TODO: Handle errors accordingly, and show the user what went wrong + * + * https://github.com/pattern-tech/pattern-ui/issues/4 + */ + return null; } catch (e) { return null; diff --git a/app/config/index.tsx b/app/config/index.tsx index a447826..237d2c9 100644 --- a/app/config/index.tsx +++ b/app/config/index.tsx @@ -1,29 +1,29 @@ -import { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; +import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'; import { type SIWECreateMessageArgs, type SIWESession, type SIWEVerifyMessageArgs, createSIWEConfig, formatMessage, -} from "@reown/appkit-siwe"; +} from '@reown/appkit-siwe'; import { - AppKitNetwork, + type AppKitNetwork, arbitrum, base, mainnet, optimism, -} from "@reown/appkit/networks"; -import { getCsrfToken, getSession, signIn, signOut } from "next-auth/react"; -import { getAddress } from "viem"; +} from '@reown/appkit/networks'; +import { getCsrfToken, getSession, signIn, signOut } from 'next-auth/react'; +import { getAddress } from 'viem'; export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID; -if (!projectId) throw new Error("Project ID is not defined"); +if (!projectId) throw new Error('Project ID is not defined'); export const metadata = { - name: "Pattern", - description: "Pattern is a decentralized agentic RAG network", - url: "https://pattern.global/", - icons: ["https://avatars.githubusercontent.com/u/112399339"], + name: 'Pattern', + description: 'Pattern is a decentralized agentic RAG network', + url: 'https://pattern.global/', + icons: ['https://avatars.githubusercontent.com/u/112399339'], }; export const chains: [AppKitNetwork, ...AppKitNetwork[]] = [ @@ -41,11 +41,11 @@ export const wagmiAdapter = new WagmiAdapter({ const normalizeAddress = (address: string): string => { try { - const splitAddress = address.split(":"); + const splitAddress = address.split(':'); const extractedAddress = splitAddress[splitAddress.length - 1]; const checksumAddress = getAddress(extractedAddress); splitAddress[splitAddress.length - 1] = checksumAddress; - const normalizedAddress = splitAddress.join(":"); + const normalizedAddress = splitAddress.join(':'); return normalizedAddress; } catch (error) { @@ -55,17 +55,16 @@ const normalizeAddress = (address: string): string => { export const siweConfig = createSIWEConfig({ getMessageParams: async () => ({ - domain: typeof window !== "undefined" ? window.location.host : "", - uri: typeof window !== "undefined" ? window.location.origin : "", - chains: chains.map((chain: AppKitNetwork) => parseInt(chain.id.toString())), - statement: "Please sign with your account", + domain: typeof window !== 'undefined' ? window.location.host : '', + uri: typeof window !== 'undefined' ? window.location.origin : '', + chains: chains.map((chain: AppKitNetwork) => Number.parseInt(chain.id.toString())), }), createMessage: ({ address, ...args }: SIWECreateMessageArgs) => formatMessage(args, normalizeAddress(address)), getNonce: async () => { const nonce = await getCsrfToken(); if (!nonce) { - throw new Error("Failed to get nonce!"); + throw new Error('Failed to get nonce!'); } return nonce; @@ -78,8 +77,8 @@ export const siweConfig = createSIWEConfig({ // Validate address and chainId types if ( - typeof session.address !== "string" || - typeof session.chainId !== "number" + typeof session.address !== 'string' || + typeof session.chainId !== 'number' ) { return null; } @@ -91,11 +90,11 @@ export const siweConfig = createSIWEConfig({ }, verifyMessage: async ({ message, signature }: SIWEVerifyMessageArgs) => { try { - const success = await signIn("credentials", { + const success = await signIn('credentials', { message, redirect: false, signature, - callbackUrl: "/protected", + callbackUrl: '/protected', }); return Boolean(success?.ok);