├── .vscode └── settings.json ├── README.md ├── deps.ts └── mod.ts /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.suggest.imports.hosts": { 5 | "https://deno.land/x/": true, 6 | "https://x.nest.land/": true, 7 | "https://esm.sh": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sign In with Ethereum in Deno 2 | 3 | > I'm building [Flash](https://flash-dev.vercel.app) - a service to deploy websites and apps on the new decentralized stack. 4 | > 5 | > If you'd like to try or collab, [dm](https://t.me/v_1rtl) or [email](mailto:yo@v1rtl.site) 6 | 7 | Simple example rewritten from the official [SIWE example](https://github.com/spruceid/siwe/blob/main/examples/notepad/src/index.ts) of authorizing on a Deno backend with Ethereum account. 8 | 9 | ## Run 10 | 11 | ```sh 12 | deno run --unstable --no-check --allow-read --allow-env --allow-net mod.ts 13 | ``` 14 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { Application, Router } from 'https://deno.land/x/oak@v12.1.0/mod.ts' 2 | export { default as Session } from 'https://deno.land/x/oak_sessions@v4.1.0/src/Session.ts' 3 | export { JsonRpcProvider } from 'https://esm.sh/@ethersproject/providers' 4 | 5 | import siwe from 'https://esm.sh/siwe' 6 | 7 | const { ErrorTypes, SiweMessage, generateNonce } = siwe 8 | 9 | export { ErrorTypes, SiweMessage, generateNonce } 10 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { Application, Session, Router, generateNonce, SiweMessage, ErrorTypes, JsonRpcProvider } from './deps.ts' 2 | 3 | const PORT = 3000 4 | const app = new Application() 5 | const router = new Router<{ session: Session }>() 6 | 7 | app.use(Session.initMiddleware()) 8 | 9 | router.get('/api/nonce', async (ctx) => { 10 | await ctx.state.session.set('nonce', generateNonce()) 11 | ctx.response.status = 200 12 | ctx.response.body = (await ctx.state.session.get('nonce')) as string 13 | }) 14 | 15 | router.get('/api/me', async (ctx) => { 16 | const session = ctx.state.session 17 | if (!(await session.get('siwe'))) { 18 | ctx.response.status = 401 19 | ctx.response.headers.set('Content-Type', 'application/json') 20 | ctx.response.body = { message: 'You have to first sign_in' } 21 | return 22 | } 23 | 24 | ctx.response.status = 200 25 | ctx.response.body = { 26 | address: ((await session.get('siwe')) as InstanceType).address, 27 | ens: await session.get('ens') 28 | } 29 | }) 30 | 31 | const getInfuraUrl = (chainId: string) => { 32 | switch (Number.parseInt(chainId)) { 33 | case 1: 34 | return 'https://mainnet.infura.io/v3' 35 | case 3: 36 | return 'https://ropsten.infura.io/v3' 37 | case 4: 38 | return 'https://rinkeby.infura.io/v3' 39 | case 5: 40 | return 'https://goerli.infura.io/v3' 41 | case 137: 42 | return 'https://polygon-mainnet.infura.io/v3' 43 | } 44 | } 45 | 46 | router.post('/api/sign_in', async (ctx) => { 47 | try { 48 | const body = await ctx.request.body().value 49 | const { ens } = body 50 | if (!body.message) { 51 | ctx.response.status = 422 52 | ctx.response.body = { message: 'Expected signMessage object as body.' } 53 | 54 | return 55 | } 56 | 57 | const message = new SiweMessage(body.message) 58 | 59 | const infuraProvider = new JsonRpcProvider( 60 | { 61 | allowGzip: true, 62 | url: `${getInfuraUrl(message.chainId)}/8fcacee838e04f31b6ec145eb98879c8`, 63 | headers: { 64 | Accept: '*/*', 65 | Origin: `http://localhost:${PORT}`, 66 | 'Accept-Encoding': 'gzip, deflate, br', 67 | 'Content-Type': 'application/json' 68 | } 69 | }, 70 | Number.parseInt(message.chainId) 71 | ) 72 | 73 | await infuraProvider.ready 74 | 75 | const fields = await message.validate(infuraProvider) 76 | 77 | if (fields.nonce !== (await ctx.state.session.get('nonce'))) { 78 | ctx.response.status = 422 79 | ctx.response.body = { 80 | message: `Invalid nonce.` 81 | } 82 | 83 | return 84 | } 85 | 86 | await ctx.state.session.set('siwe', fields) 87 | await ctx.state.session.set('ens', ens) 88 | await ctx.state.session.set('nonce', null) 89 | if (fields.expirationTime) await ctx.state.session.set('expires', new Date(fields.expirationTime)) 90 | 91 | ctx.response.status = 200 92 | ctx.response.body = { 93 | address: fields.address, 94 | ens 95 | } 96 | } catch (e) { 97 | await ctx.state.session.set('siwe', null) 98 | await ctx.state.session.set('ens', null) 99 | await ctx.state.session.set('nonce', null) 100 | console.error(e) 101 | switch (e) { 102 | case ErrorTypes.EXPIRED_MESSAGE: { 103 | ctx.response.status = 440 104 | ctx.response.body = { message: e.message } 105 | break 106 | } 107 | case ErrorTypes.INVALID_SIGNATURE: { 108 | ctx.response.status = 422 109 | ctx.response.body = { message: e.message } 110 | break 111 | } 112 | default: { 113 | ctx.response.status = 500 114 | ctx.response.body = { message: e.message } 115 | break 116 | } 117 | } 118 | } 119 | }) 120 | 121 | router.post('/api/sign_out', async (ctx) => { 122 | const session = ctx.state.session 123 | if (!(await session.get('siwe'))) { 124 | ctx.response.status = 401 125 | ctx.response.headers.set('Content-Type', 'application/json') 126 | ctx.response.body = { message: 'You have to first sign_in' } 127 | return 128 | } 129 | ctx.response.status = 205 130 | ctx.response.body = '' 131 | }) 132 | 133 | app.use(router.routes()) 134 | app.use(router.allowedMethods()) 135 | 136 | await app.listen(`:3000`) 137 | --------------------------------------------------------------------------------