Skip to content
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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

This repo demonstrates all the Authentication Providers that Redwood supports. [Read more](https://redwoodjs.com/docs/authentication) about our authentication providers in our docs, or [preview the deploy](https://redwood-playground-auth.netlify.app/) of this site on Netlify!


### 🔗 [Link](https://redwood-playground-auth.netlify.app/)

## Setup
Expand Down Expand Up @@ -60,3 +59,9 @@ SUPABASE_KEY=""
SUPABASE_URL=""
SUPABASE_JWT_SECTRET="" # Found in Supabase dashboard > Settings > API
```

### Etheruem

```
ETHEREUM_JWT_SECRET=""
```
26 changes: 26 additions & 0 deletions api/db/migrations/20210902212207_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"address" TEXT NOT NULL,
"authDetailId" TEXT NOT NULL,

PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "AuthDetail" (
"id" TEXT NOT NULL,
"nonce" TEXT NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User.address_unique" ON "User"("address");

-- CreateIndex
CREATE UNIQUE INDEX "User_authDetailId_unique" ON "User"("authDetailId");

-- AddForeignKey
ALTER TABLE "User" ADD FOREIGN KEY ("authDetailId") REFERENCES "AuthDetail"("id") ON DELETE CASCADE ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions api/db/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
21 changes: 13 additions & 8 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
datasource db {
provider = "postgresql"
url = env("DATABASE")
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}

// Define your own datamodels here and run `yarn redwood prisma migrate dev` to create
// migrations for them.
// TODO: Please remove the following example:
model UserExample {
id Int @id @default(autoincrement())
email String @unique
name String?
model User {
id String @id @default(uuid())
address String @unique
authDetail AuthDetail @relation(fields: [authDetailId], references: [id])
authDetailId String
}

model AuthDetail {
id String @id @default(uuid())
nonce String
timestamp DateTime @default(now())
User User?
}
7 changes: 5 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"private": true,
"dependencies": {
"@redwoodjs/api": "^0.36.0",
"@redwoodjs/graphql-server": "^0.36.0"
"@redwoodjs/graphql-server": "^0.36.0",
"eth-sig-util": "^3.0.1",
"ethereumjs-util": "^7.1.0",
"jsonwebtoken": "^8.5.1"
}
}
}
22 changes: 22 additions & 0 deletions api/src/graphql/authDetails.sdl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const schema = gql`
type AuthDetail {
id: String!
nonce: String!
timestamp: DateTime!
User: User
}

type Query {
authDetails: [AuthDetail!]!
}

input CreateAuthDetailInput {
nonce: String!
timestamp: DateTime!
}

input UpdateAuthDetailInput {
nonce: String
timestamp: DateTime
}
`
23 changes: 23 additions & 0 deletions api/src/graphql/ethereumAuth.sdl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const schema = gql`
type Mutation {
authChallenge(input: AuthChallengeInput!): AuthChallengeResult
authVerify(input: AuthVerifyInput!): AuthVerifyResult
}

input AuthChallengeInput {
address: String!
}

type AuthChallengeResult {
message: String!
}

input AuthVerifyInput {
signature: String!
address: String!
}

type AuthVerifyResult {
token: String!
}
`
29 changes: 29 additions & 0 deletions api/src/graphql/users.sdl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const schema = gql`
type User {
id: String!
address: String!
authDetail: AuthDetail!
authDetailId: String!
}

type Query {
users: [User!]!
user(id: String!): User
}

input CreateUserInput {
address: String!
authDetailId: String!
}

input UpdateUserInput {
address: String
authDetailId: String
}

type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: String!, input: UpdateUserInput!): User!
deleteUser(id: String!): User!
}
`
82 changes: 82 additions & 0 deletions api/src/services/ethereumAuth/ethereumAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { AuthenticationError } from '@redwoodjs/api'

import { bufferToHex } from 'ethereumjs-util'
import { recoverPersonalSignature } from 'eth-sig-util'
import jwt from 'jsonwebtoken'

import { db } from 'src/lib/db'

const NONCE_MESSAGE =
'Please prove you control this wallet by signing this random text: '

const getNonceMessage = (nonce) => NONCE_MESSAGE + nonce

export const beforeResolver = (rules) => {
rules.skip({ only: ['authChallenge', 'authVerify'] })
}

export const authChallenge = async ({ input: { address: addressRaw } }) => {
const nonce = Math.floor(Math.random() * 1000000).toString()
const address = addressRaw.toLowerCase()
await db.user.upsert({
where: { address },
update: {
authDetail: {
update: {
nonce,
timestamp: new Date(),
},
},
},
create: {
address,
authDetail: {
create: {
nonce,
},
},
},
})

return { message: getNonceMessage(nonce) }
}

export const authVerify = async ({
input: { signature, address: addressRaw },
}) => {
try {
const address = addressRaw.toLowerCase()
const user = await db.user.findUnique({
where: { address },
})
if (!user) throw new Error('No authentication started')
const { nonce, timestamp } = await db.user
.findUnique({
where: { address },
})
.authDetail()

const startTime = new Date(timestamp)
if (new Date() - startTime > 5 * 60 * 1000)
throw new Error(
'The challenge must have been generated within the last 5 minutes'
)
const signerAddress = recoverPersonalSignature({
data: bufferToHex(Buffer.from(getNonceMessage(nonce), 'utf8')),
sig: signature,
})
if (address !== signerAddress.toLowerCase())
throw new Error('invalid signature')

const token = jwt.sign(
{ address, id: user.id },
process.env.ETHEREUM_JWT_SECRET,
{
expiresIn: '5h',
}
)
return { token }
} catch (e) {
throw new Error(e)
}
}
2 changes: 2 additions & 0 deletions redwood.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
'SUPABASE_JWT_SECRET',
'NHOST_BACKEND_URL',
'NHOST_JWT_SECRET',
'ETHEREUM_JWT_SECRET',
'DATABASE_URL'
]
[api]
port = 8911
Expand Down
21 changes: 21 additions & 0 deletions web/config/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
const webpack = require('webpack')

module.exports = (config, { _env }) => {
config.module.rules.push({
test: /\.md$/,
loader: 'raw-loader',
})

// Ethereum Auth - Required polyfills for missing node modules in Webpack V5
// See https://github.com/WalletConnect/walletconnect-monorepo/issues/584
config.resolve.fallback = {
os: require.resolve(`os-browserify/browser`),
https: require.resolve(`https-browserify`),
http: require.resolve(`stream-http`),
stream: require.resolve(`stream-browserify`),
util: require.resolve(`util/`),
url: require.resolve(`url/`),
assert: require.resolve(`assert/`),
crypto: require.resolve(`crypto-browserify`),
}
config.plugins.push(
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
})
)

return config
}
3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@auth0/auth0-spa-js": "^1.9.0",
"@headlessui/react": "^1.0.0",
"@heroicons/react": "^1.0.1",
"@oneclickdapp/ethereum-auth": "^0.3.0",
"@redwoodjs/auth": "^0.36.0",
"@redwoodjs/forms": "^0.36.0",
"@redwoodjs/router": "^0.36.0",
Expand Down Expand Up @@ -45,4 +46,4 @@
"raw-loader": "^4.0.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat"
}
}
}
64 changes: 64 additions & 0 deletions web/src/components/Ethereum/Ethereum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AuthProvider } from '@redwoodjs/auth'
import EthereumAuthClient from '@oneclickdapp/ethereum-auth'
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { FetchConfigProvider, useFetchConfig } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/dist/apollo'

import UserTools from '../UserTools/UserTools'

let ethereum

const ApolloInjector = ({ children }) => {
const { uri, headers } = useFetchConfig()
try {
const graphQLClient = new ApolloClient({
cache: new InMemoryCache(),
uri,
headers,
})
// Default option using Apollo Client
const makeRequest = (mutation, variables) =>
graphQLClient.mutate({
mutation,
variables,
})

// Alternative option using graphql-hooks
// You'll also need to modify graphQLClient
// const makeRequest = (query, variables) =>
// graphQLClient.request({
// query,
// variables,
// })

ethereum = new EthereumAuthClient({
makeRequest,
debug: process.NODE_ENV === 'development',
})
} catch (e) {
console.log(e)
}
return React.cloneElement(children, { client: ethereum })
}

export const ethereumClient = new EthereumAuthClient({
makeRequest: () => {},
debug: process.NODE_ENV === 'development',
})

export default (props) => {
return (
<ApolloInjector>
<AuthProvider client={ethereum} type="ethereum" {...props}>
{/* Add apollo provider here, so that useAuth gets passed in for Cells,etc. */}
<RedwoodApolloProvider>
<p className="mt-4 mb-4">
Full example app in the 👉{' '}
<a href="https://github.com/oneclickdapp/ethereum-auth">repo</a>
</p>
<UserTools />
</RedwoodApolloProvider>
</AuthProvider>
</ApolloInjector>
)
}
Loading