Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SIWE provider #152

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion packages/openauth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"dependencies": {
"@standard-schema/spec": "1.0.0-beta.3",
"aws4fetch": "1.0.20",
"jose": "5.9.6"
"jose": "5.9.6",
"viem": "2.22.8"
},
"files": [
"src",
Expand Down
75 changes: 75 additions & 0 deletions packages/openauth/src/provider/siwe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { generateSiweNonce, parseSiweMessage } from "viem/siwe"
import { Provider } from "./provider.js"
import { PublicClient } from "viem"
import { isDomainMatch } from "../util.js"

export interface SiweConfig {
signin(request: Request, nonce: string): Promise<Response>
client: PublicClient
}

interface SiweBody {
signature?: `0x${string}`
message?: string
nonce?: number
}

export function SiweProvider(
config: SiweConfig,
): Provider<{ address: `0x${string}` }> {
return {
type: "siwe",
init(routes, ctx) {
routes.get("/authorize", async (c) => {
const nonce = generateSiweNonce()
await ctx.set(c, "nonce", 60 * 10, nonce)
return ctx.forward(c, await config.signin(c.req.raw, nonce))
})

routes.post("/authorize", async (c) => {
const body = (await c.req.json()) as SiweBody | undefined
if (!body || !body.signature || !body.message) {
throw new Error("Invalid body")
}
let nonce = (await ctx.get(c, "nonce")) as string | undefined
if (!nonce) {
if (!body.nonce) {
throw new Error("Missing nonce")
}
if (body.nonce < Date.now() - 60 * 10 * 1000) {
throw new Error("Expired nonce")
}
nonce = body.nonce.toString()
}
const {
domain,
nonce: messageNonce,
address,
uri,
} = parseSiweMessage(body.message)
if (messageNonce !== nonce) {
throw new Error("Invalid nonce")
}
if (!domain || !uri || !address) {
throw new Error("Invalid message")
}
const url = new URL(c.req.url)
const host = c.req.header("x-forwarded-host") || url.host
if (!isDomainMatch(domain, host)) {
throw new Error("Invalid domain")
}
if (!url.href.startsWith(uri)) {
throw new Error("Invalid uri")
}
const valid = await config.client.verifySiweMessage({
message: body.message,
signature: body.signature,
})
if (!valid) {
throw new Error("Invalid signature")
}
return await ctx.success(c, { address })
})
},
}
}
145 changes: 145 additions & 0 deletions packages/openauth/src/ui/siwe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { PublicClient } from "viem"
import { SiweConfig } from "../provider/siwe.js"
import { Layout } from "./base.js"
import { getTheme } from "./theme.js"

export interface SiweUiOptions {
chainId: number
client: PublicClient
statement?: string
resources?: string[]
walletConnectProjectId?: string
}

export function SiweUi({
client,
chainId,
statement,
resources,
walletConnectProjectId,
}: SiweUiOptions): SiweConfig {
return {
client,
async signin(request, nonce) {
const theme = getTheme()
const url = new URL(request.url)
const jsx = (
<Layout>
<div data-component="connectors-list"></div>
<script
type="importmap"
dangerouslySetInnerHTML={{
__html: `
{
"imports": {
"@wagmi/core": "https://esm.sh/@wagmi/core@^2.16.3",
"@wagmi/connectors": "https://esm.sh/@wagmi/connectors@^5.7.3?standalone&exports=coinbaseWallet,walletConnect",
"viem/": "https://esm.sh/viem@^2.22.8/"
}
}
`,
}}
></script>
<script
type="module"
dangerouslySetInnerHTML={{
__html: `
import { createConfig, watchConnectors, http, signMessage, getConnectors } from "@wagmi/core"
import { coinbaseWallet, walletConnect } from "@wagmi/connectors"
import { mainnet } from "viem/chains"
import { createSiweMessage } from "viem/siwe"

const darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches
const appName = "${theme.title}"
const appLogoUrl = darkMode ? "${theme.logo.dark}" : "${theme.logo.light}"

const config = createConfig({
chains: [mainnet],
transports: {
[mainnet.id]: http()
},
connectors: [
coinbaseWallet({
appName,
appLogoUrl,
}),
${
walletConnectProjectId
? `walletConnect({
projectId: "${walletConnectProjectId}",
metadata: {
name: appName,
url: window.location.origin,
description: appName,
icons: [appLogoUrl]
}
})`
: ""
}
]
})

const connectors = getConnectors(config)
populateConnectors(connectors)

async function populateConnectors(connectors) {
const list = document.querySelector("[data-component=connectors-list]")
list.innerHTML = ""

if (connectors.length === 0) {
list.textContent = "No connectors available"
} else {
for (const connector of connectors) {
const button = document.createElement("button")
button.dataset.component = "button"
button.type = "button"
if (connector.icon) {
const icon = document.createElement("img")
icon.src = connector.icon
icon.width = 24
icon.alt = connector.name
button.appendChild(icon)
}
button.appendChild(document.createTextNode(connector.name))

button.addEventListener("click", async () => {
const { accounts: [account] } = await connector.connect({ chainId: ${chainId} })
const message = createSiweMessage({
chainId: ${chainId},
address: account,
domain: "${url.host}",
nonce: "${nonce}",
uri: "${url.href}",
version: "1",
resources: ${JSON.stringify(resources)},
${statement ? `statement: "${statement}",` : ""}
})

const signature = await signMessage(config, { message, account, connector })

await fetch("${url.href}", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ signature, message }),
})
})

list.appendChild(button)
}
}
}
`,
}}
></script>
</Layout>
)
return new Response(jsx.toString(), {
headers: {
"Content-Type": "text/html",
},
})
},
}
}
7 changes: 7 additions & 0 deletions packages/openauth/src/ui/ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,10 @@
justify-content: space-between;
}
}

[data-component="connectors-list"] {
display: flex;
flex-direction: column-reverse;
gap: 1rem;
justify-content: center;
}
Loading