Authentication (SIWE)
Example
Steps
Connect Account
Follow the Onboard & Discover Accounts guide to get this set up.
Setup API & add /siwe/nonce
Next, we will set up our API endpoints for our authentication flow.
Sign in with Ethereum requires a nonce to be generated by the server to prevent replay attacks. You will need to set up a API endpoint to return a nonce. For example, using Hono and Viem.
import { Hono } from 'hono'
import { Porto } from 'porto'
import { generateSiweNonce } from 'viem/siwe'
const app = new Hono().basePath('/api')
const porto = Porto.create()
app.post('/siwe/nonce', (c) => c.json({ nonce: generateSiweNonce() }))
app.post('/siwe/verify', async (c) => { /* ... */ })
app.post('/siwe/logout', async (c) => { /* ... */ })
app.get('/me', async (c) => { /* ... */ })
export default app
Add /siwe/verify
We will now implement the /api/siwe
endpoint so that we can authenticate our user provided
the siwe
response from the previous step is valid.
import { Hono } from 'hono'
import { setCookie } from 'hono/cookie'
import * as jwt from 'hono/jwt'
import { Porto, ServerActions } from 'porto'
import { ServerClient } from 'porto/viem'
import { hashMessage } from 'viem'
import { generateSiweNonce, parseSiweMessage } from 'viem/siwe'
const app = new Hono().basePath('/api')
const porto = Porto.create()
app.post('/siwe/nonce', (c) => c.text(generateSiweNonce()))
app.post('/siwe/verify', async (c) => {
const { message, signature } = await c.req.json()
const { address, chainId, nonce } = parseSiweMessage(message)
// Verify the signature.
const client = ServerClient.fromPorto(porto, { chainId })
const valid = ServerActions.verifySignature(client, {
address: address!,
digest: hashMessage(message),
signature,
})
// If the signature is invalid, we cannot authenticate the user.
if (!valid) return c.json({ error: 'Invalid signature' }, 401)
// Issue a JWT for the user in a HTTP-only cookie.
const token = await jwt.sign({ sub: address }, c.env.JWT_SECRET)
setCookie(c, 'auth', token, {
httpOnly: true,
secure: true,
})
// If the signature is valid, we can authenticate the user.
return c.json({ message: 'Authenticated' })
})
app.post('/siwe/logout', async (c) => { /* ... */ })
app.get('/me', async (c) => { /* ... */ })
export default app
Done
Now that you have set up your /api/siwe
endpoints and have your server up and running,
you can pass the signInWithEthereum.authUrl
capability to the connect
call to have
control over the authentication URL.
import { useConnectors } from 'wagmi'
import { Hooks } from 'porto/wagmi'
export function Example() {
const [connector] = useConnectors()
const { connect } = Hooks.useConnect()
return (
<button
onClick={() =>
connect({
connector,
signInWithEthereum: {
authUrl: '/api/siwe',
},
})
}
>
Sign in
</button>
)
}
Bonus: Add /me
(authenticated route)
We can now add an authenticated routes to our app that can only be accessed if the user is authenticated.
In this example, we are using the hono/jwt
middleware to check if the user is authenticated (they hold a valid auth
cookie).
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
import { Porto } from 'porto'
const app = new Hono().basePath('/api')
const porto = Porto.create()
app.get(
'/me',
jwt({ cookie: 'auth' }),
async (c) => {
return c.json({ user: 'John Doe' })
}
)
Bonus: Add /siwe/logout
We can also log out a user by deleting the auth
cookie with hono/cookie
's deleteCookie
function.
import { Hono } from 'hono'
import { deleteCookie } from 'hono/cookie'
import { jwt } from 'hono/jwt'
import { Porto } from 'porto'
const app = new Hono().basePath('/api')
const porto = Porto.create()
app.post(
'/logout',
jwt({ cookie: 'auth' }),
async (c) => {
deleteCookie(c, 'auth')
return c.text('So long, friend.')
}
)