├── .gitignore ├── README.md ├── client ├── dist │ └── js │ │ └── app.js ├── helpers │ └── cookies.js └── src │ ├── app.jsx │ ├── components │ ├── Base.jsx │ ├── CheckCredentials.jsx │ ├── DashboardPage.jsx │ ├── FlashTest.jsx │ ├── HomePage.jsx │ ├── Login.jsx │ ├── MetamaskLogin.jsx │ ├── MetamaskLogout.jsx │ └── TopBar.jsx │ └── routes.js ├── package.json ├── server.js ├── server ├── routes │ ├── api-routes.js │ └── html-routes.js └── static │ ├── css │ └── style.css │ └── index.html ├── test └── test.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Yarn 40 | yarn.lock 41 | 42 | # Mac users 43 | .DS_Store 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # metamask-auth (react) 2 | a basic react website that uses metamask to authorize users via a signed message signed client-side and sent to the server 3 | 4 | ### What is this repository for? ### 5 | 6 | * Quick summary: the purpose of this repo is to create a generic react.js application that uses metamask for authentication. 7 | * Version: 0.0.1 8 | 9 | ### How do I get set up? ### 10 | 11 | * clone this repository 12 | * run `npm install` 13 | * start `testrpc` (check [here](https://github.com/ethereumjs/testrpc) for information on testrpc). 14 | * in one terminal run `npm start` (to use strict mode) or `nodemon server.js` (to run without strict mode). 15 | * in another run `npm run bundle` 16 | * visit [localhost 3000](http://localhost:3000) 17 | * log in to metamask 18 | 19 | ### How do I use the app? ### 20 | * run metamask 21 | * use the app to log in with metamask (it will ask to you to sign a message, and store the signed message in your cookies) 22 | * use the buttons on the home page to test whether you have logged in correctly (it will send the signed message to the server for decoding and authorization) 23 | -------------------------------------------------------------------------------- /client/helpers/cookies.js: -------------------------------------------------------------------------------- 1 | console.log("loading the 'cookies' helper functions"); 2 | 3 | module.exports = { 4 | setCookie: function(cname, cvalue, exdays) { 5 | console.log("setting cookie:", cname); 6 | var d = new Date(); 7 | d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); 8 | var expires = "expires="+d.toUTCString(); 9 | document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; 10 | }, 11 | getCookie: function(cname) { 12 | console.log("getting cookie: ", cname); 13 | var name = cname + "="; 14 | var ca = document.cookie.split(';'); 15 | for(var i = 0; i < ca.length; i++) { 16 | var c = ca[i]; 17 | while (c.charAt(0) == ' ') { 18 | c = c.substring(1); 19 | } 20 | if (c.indexOf(name) == 0) { 21 | return c.substring(name.length, c.length); 22 | } 23 | } 24 | return ""; 25 | }, 26 | doesCookieExist: function(cname) { 27 | console.log("checking for cookie: ", cname); 28 | var name = cname + "="; 29 | var ca = document.cookie.split(';'); 30 | for(var i = 0; i < ca.length; i++) { 31 | var c = ca[i]; 32 | while (c.charAt(0) == ' ') { 33 | c = c.substring(1); 34 | } 35 | if (c.indexOf(name) == 0) { 36 | return true; 37 | } 38 | } 39 | return false; 40 | }, 41 | removeCookie: function(cname){ 42 | console.log("removing cookie:", cname); 43 | this.setCookie(cname,"",-1); 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /client/src/app.jsx: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import React from 'react'; 3 | import ReactDom from 'react-dom'; 4 | import injectTapEventPlugin from 'react-tap-event-plugin'; 5 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 6 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 7 | import { browserHistory, Router } from 'react-router'; 8 | import routes from './routes.js'; 9 | 10 | // remove tap delay, essential for materialUI to work properly 11 | injectTapEventPlugin(); 12 | 13 | const App = () => ( 14 | 15 | 16 | 17 | ); 18 | 19 | // render the dom 20 | ReactDom.render(, document.getElementById('react-app')) -------------------------------------------------------------------------------- /client/src/components/Base.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types' 3 | import { Link, IndexLink } from 'react-router'; 4 | import TopBar from './TopBar.jsx' 5 | 6 | 7 | 8 | class Base extends Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 | 17 | {this.props.children} 18 |
19 | ); 20 | } 21 | 22 | }; 23 | 24 | Base.propTypes = { 25 | children: PropTypes.object.isRequired 26 | }; 27 | 28 | export default Base; -------------------------------------------------------------------------------- /client/src/components/CheckCredentials.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FlatButton from 'material-ui/FlatButton'; 3 | 4 | import EthUtil from 'ethereumjs-util'; 5 | import Helpers from '../../helpers/cookies.js' 6 | 7 | function sendValidationPackage(){ 8 | // get the address for the account 9 | var providedAddress = window.web3.eth.coinbase; 10 | if (!providedAddress){ 11 | alert("We attempted to get your eth public address from your browser but it did not exist. This is likely because web3 is not being injected into the page by your browser. Please make sure you are using metamask or another web3 injector and then try again"); 12 | return; 13 | }; 14 | // get the signed message 15 | var signedAuthMessage = Helpers.getCookie("signedAuthMessage"); 16 | if (!signedAuthMessage){ 17 | alert("We attempted to get the signedAuthMessage from the cookies in your browser but it did not exist. Please log in with metamask and sign the auth message then try again"); 18 | return; 19 | }; 20 | // send to the server 21 | var xhr = new XMLHttpRequest(); 22 | xhr.open("POST", "/login", true); 23 | xhr.setRequestHeader("Content-Type", "application/json"); 24 | xhr.addEventListener("load", function(){ 25 | if (xhr.status === 200){ 26 | console.log("status 200. success!") 27 | console.log("response:", xhr.response); 28 | } else { 29 | console.log("status not 200. failure.") 30 | console.log("response:", xhr.response); 31 | } 32 | }); 33 | xhr.send(JSON.stringify({ 34 | providedAddress: providedAddress, 35 | signedAuthMessage: signedAuthMessage 36 | })); 37 | 38 | } 39 | 40 | const CheckCredentials = () => ( 41 | 45 | ); 46 | 47 | export default CheckCredentials; -------------------------------------------------------------------------------- /client/src/components/DashboardPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Card, CardTitle } from 'material-ui/Card'; 3 | 4 | const DashboardPage = () => ( 5 | 6 | 7 | 8 | 9 | ); 10 | 11 | export default DashboardPage; -------------------------------------------------------------------------------- /client/src/components/FlashTest.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FlatButton from 'material-ui/FlatButton'; 3 | 4 | import EthUtil from 'ethereumjs-util'; 5 | import Helpers from '../../helpers/cookies.js' 6 | 7 | function doSomethingThatRequiresAuth(url, payload, callback){ 8 | // get the address for the account 9 | var publicAddress = window.web3.eth.coinbase; 10 | if (!publicAddress){ 11 | alert("We attempted to get your eth public address from your browser but it did not exist. This is likely because web3 is not being injected into the page by your browser. Please make sure you are using metamask or another web3 injector and then try again"); 12 | return; 13 | }; 14 | // get the signed message 15 | var signedAuthMessage = Helpers.getCookie("signedAuthMessage"); 16 | if (!signedAuthMessage){ 17 | alert("We attempted to get the signedAuthMessage from the cookies in your browser but it did not exist. Please log in with metamask and sign the auth message then try again"); 18 | return; 19 | }; 20 | // send to the server 21 | var xhr = new XMLHttpRequest(); 22 | xhr.open("POST", url, true); 23 | xhr.setRequestHeader("Authorization", `{"publicAddress": "${publicAddress}", "signedAuthMessage": "${signedAuthMessage}"}`); 24 | xhr.setRequestHeader("Content-Type", "application/json"); 25 | xhr.addEventListener("load", function(){ 26 | if (xhr.status === 200){ 27 | console.log("status 200. success!") 28 | callback(xhr.response); 29 | } else { 30 | console.log("status not 200. failure.") 31 | callback(xhr.response); 32 | } 33 | }); 34 | xhr.send(`{"message": "${payload}"}`); 35 | } 36 | 37 | function performFlashTest(){ 38 | doSomethingThatRequiresAuth("/test", "flash", function(response){ 39 | console.log("response:", response); 40 | }); 41 | } 42 | 43 | const FlashTest = () => ( 44 | 48 | ); 49 | 50 | export default FlashTest; -------------------------------------------------------------------------------- /client/src/components/HomePage.jsx: -------------------------------------------------------------------------------- 1 | // load dependencies 2 | import React from 'react'; 3 | import { Card, CardTitle } from 'material-ui/Card'; 4 | import { Link } from 'react-router' 5 | 6 | // load components 7 | import CheckCredentials from './CheckCredentials.jsx'; 8 | import FlashTest from './FlashTest.jsx'; 9 | 10 | const HomePage = () => ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default HomePage; -------------------------------------------------------------------------------- /client/src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | // load dependencies 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types' 4 | // import components 5 | import MetamaskLogin from './MetamaskLogin.jsx'; 6 | import MetamaskLogout from './MetamaskLogout.jsx'; 7 | 8 | // load helpers 9 | import Cookies from '../../helpers/cookies.js' 10 | 11 | class Login extends Component { 12 | 13 | render() { 14 | return ( 15 |
16 | 17 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | export default Login; -------------------------------------------------------------------------------- /client/src/components/MetamaskLogin.jsx: -------------------------------------------------------------------------------- 1 | // load dependencies 2 | import React from 'react'; 3 | import EthUtil from 'ethereumjs-util'; 4 | import PropTypes from 'prop-types'; 5 | // import components 6 | import FlatButton from 'material-ui/FlatButton'; 7 | // load helpers 8 | import Helpers from '../../helpers/cookies.js' 9 | 10 | function signMessage(){ 11 | var message = "testMessage"; 12 | if (window.web3){ 13 | var userEthereumClient = window.web3; 14 | // sign a message 15 | userEthereumClient.eth.sign( 16 | userEthereumClient.eth.coinbase, // pass the user's public key 17 | window.web3.sha3(message), // pass a sha hash of a message 18 | function(error, data) { // pass a callback 19 | if (error){ 20 | console.log("An error occured while signing the message."); 21 | } else { 22 | Helpers.setCookie("signedAuthMessage", data, 2); 23 | if(Helpers.getCookie("signedAuthMessage")){ 24 | console.log("You successfully stored the signed message."); 25 | } else { 26 | console.log("You did not successfully store the signed message."); 27 | }; 28 | }; 29 | }); 30 | } else { 31 | console.log(">> You cannot sign the message because Web 3 is not loaded"); 32 | }; 33 | } 34 | 35 | const MetamaskLogin = () => ( 36 | 40 | ); 41 | 42 | export default MetamaskLogin; -------------------------------------------------------------------------------- /client/src/components/MetamaskLogout.jsx: -------------------------------------------------------------------------------- 1 | // load dependencies 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | // import components 5 | import FlatButton from 'material-ui/FlatButton'; 6 | 7 | // load helpers 8 | import Cookies from '../../helpers/cookies.js' 9 | 10 | function signOut(){ 11 | if(confirm("Are you sure you would like to deauthorize your browser?")){ 12 | Cookies.removeCookie("signedAuthMessage"); 13 | } else { 14 | // do nothing. 15 | } 16 | } 17 | 18 | const MetamaskLogout = () => ( 19 | 23 | ); 24 | 25 | export default MetamaskLogout; -------------------------------------------------------------------------------- /client/src/components/TopBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | // import components 4 | import AppBar from 'material-ui/AppBar'; 5 | import Login from './Login.jsx'; 6 | 7 | const TopBar = () => ( 8 | } 12 | /> 13 | ); 14 | 15 | export default TopBar; -------------------------------------------------------------------------------- /client/src/routes.js: -------------------------------------------------------------------------------- 1 | import Base from './components/Base.jsx'; 2 | import HomePage from './components/HomePage.jsx'; 3 | import DashboardPage from './components/DashboardPage.jsx'; 4 | 5 | // load helpers 6 | import Cookies from '../helpers/cookies.js' 7 | 8 | const routes = { 9 | component: Base, 10 | childRoutes: [ 11 | { 12 | path: '/', 13 | component: HomePage 14 | }, 15 | { 16 | path: '/dash', 17 | component: DashboardPage 18 | } 19 | ] 20 | }; 21 | 22 | export default routes; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metamask-auth", 3 | "version": "0.0.0", 4 | "description": "A single page react application that uses metamask for authentication", 5 | "main": "server.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "nodemon --use_strict server.js", 12 | "bundle": "webpack" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/billbitt/react-metamask-login.git" 17 | }, 18 | "keywords": [ 19 | "mortgage", 20 | "home", 21 | "blockchain" 22 | ], 23 | "author": "@billbitt", 24 | "license": "ISC", 25 | "homepage": "https://github.com/billbitt/react-metamask-login#readme", 26 | "dependencies": { 27 | "body-parser": "^1.17.1", 28 | "ethereumjs-util": "^5.1.1", 29 | "express": "^4.15.2", 30 | "material-ui": "^0.18.0", 31 | "prop-types": "^15.5.8", 32 | "react": "^15.5.4", 33 | "react-dom": "^15.5.4", 34 | "react-router": "^3.0.0", 35 | "react-tap-event-plugin": "^2.0.1", 36 | "validator": "^7.0.0", 37 | "web3": "^0.19.0" 38 | }, 39 | "devDependencies": { 40 | "babel-core": "^6.24.1", 41 | "babel-loader": "^7.0.0", 42 | "babel-preset-es2015": "^6.24.1", 43 | "babel-preset-react": "^6.24.1", 44 | "nodemon": "^1.11.0", 45 | "webpack": "^2.5.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | 4 | const app = express(); 5 | 6 | const PORT = process.env.PORT || 3000; 7 | // configure epress 8 | app.use(bodyParser.json()); // for parsing application/json 9 | app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded 10 | 11 | // include routes 12 | app.use(express.static('./server/static/')); 13 | app.use(express.static('./client/dist/')); 14 | 15 | // server-side routes 16 | require('./server/routes/html-routes')(app); 17 | require('./server/routes/api-routes')(app); 18 | 19 | // server-side route that directs http routes back to the react app. 20 | app.get("/*", function(req, res) { 21 | res.sendFile(__dirname + '/server/static/index.html') 22 | }) 23 | 24 | app.listen(PORT, () => { 25 | console.log('Server is listening on port ' + PORT) 26 | }) -------------------------------------------------------------------------------- /server/routes/api-routes.js: -------------------------------------------------------------------------------- 1 | // load dependencies 2 | var Web3 = require('web3'); 3 | var EthUtil = require('ethereumjs-util'); 4 | 5 | // configure web 3 6 | var endpoint = 'http://localhost:8545'; // this is the end point for testrpc 7 | var web3 = new Web3(new Web3.providers.HttpProvider(endpoint)) ; 8 | 9 | // set message for decoding 10 | var message = "testMessage"; 11 | 12 | // helper functions 13 | function decodeMessage(signedAuthMessage){ 14 | console.log(">> running decodeMessage"); 15 | //console.log("signed auth message:", signedAuthMessage); 16 | var sigDecoded = EthUtil.fromRpcSig(signedAuthMessage); 17 | //console.log("sigDecoded:", sigDecoded); 18 | var messageHash = web3.sha3(message) 19 | //console.log("messageHash", messageHash); 20 | var messageHashx = new Buffer(messageHash.substring(2), 'hex'); 21 | //console.log("messagehashx", messageHashx); 22 | var recoveredPub = EthUtil.ecrecover(messageHashx, sigDecoded.v, sigDecoded.r, sigDecoded.s); 23 | //console.log("recoveredPub:", recoveredPub); 24 | var recoveredAddress = EthUtil.pubToAddress(recoveredPub).toString("hex"); 25 | //console.log("recoveredAddress:", recoveredAddress); 26 | // return the recovered Address 27 | return "0x" + recoveredAddress; 28 | } 29 | 30 | function checkLogin(providedAddress, recoveredAddress){ 31 | console.log(">> running checkLogin"); 32 | // Authentication Logic (Is user logged in?) 33 | if (providedAddress === recoveredAddress) { 34 | console.log ("Address is verified!"); 35 | return true; 36 | } else { 37 | console.log("Address is not verified.") 38 | return false; 39 | }; 40 | } 41 | 42 | function authenticateUser(providedAddress, signedAuthMessage){ 43 | console.log(">> running authenticateUser"); 44 | var recoveredAddress = decodeMessage(signedAuthMessage); 45 | return checkLogin(providedAddress, recoveredAddress); 46 | } 47 | 48 | // routes 49 | module.exports = function(app){ 50 | app.post("/login", function(req, res){ 51 | console.log(">> POST request on /login.") 52 | console.log(">> req.body:", req.body); 53 | console.log(">> req.body.providedAddress:", req.body.providedAddress); 54 | console.log(">> req.body.signedAuthMessage:", req.body.signedAuthMessage); 55 | 56 | //res.send("request received"); 57 | 58 | // authenticate the request 59 | var authenticated = authenticateUser(req.body.providedAddress, req.body.signedAuthMessage); 60 | // handle the request 61 | if (authenticated){ 62 | res.send("those credentials work!") 63 | } else { 64 | res.send("those credentials do not work.") 65 | }; 66 | 67 | }); 68 | 69 | app.post("/test", function(req, res){ 70 | console.log(">> POST request on /test.") 71 | console.log(">> req.body:", req.body); 72 | var authPackage = JSON.parse(req.headers.authorization) 73 | console.log(">> req.headers.authorization:", authPackage); 74 | console.log(">> req.headers.authorization.publicAddress:", authPackage.publicAddress); 75 | console.log(">> req.headers.authorization.signedAuthMessage:", authPackage.signedAuthMessage); 76 | 77 | /* 78 | @ req.header.authorization.publicAddress, 79 | @ req.header.authorization.signedAuthMessage 80 | @ req.body = "flash" 81 | */ 82 | //res.send("request received"); 83 | 84 | // authenticate the request 85 | var authenticated = authenticateUser(authPackage.publicAddress, authPackage.signedAuthMessage); 86 | // handle the request 87 | if (authenticated){ 88 | if (req.body.message === "flash"){ 89 | res.send("thunder") 90 | } else { 91 | res.send("huh?") 92 | }; 93 | } else { 94 | res.send("those credentials do not work.") 95 | }; 96 | 97 | }); 98 | } -------------------------------------------------------------------------------- /server/routes/html-routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app){ 2 | app.get("/", function(req, res) { 3 | res.sendFile(__dirname + '/server/static/index.html') 4 | }) 5 | } 6 | 7 | -------------------------------------------------------------------------------- /server/static/css/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones7242/metamask-auth-react/e97f80c8f2505439a5ab936360b6aec18ae39fc0/server/static/css/style.css -------------------------------------------------------------------------------- /server/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Metamask-Auth 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bones7242/metamask-auth-react/e97f80c8f2505439a5ab936360b6aec18ae39fc0/test/test.js -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | // the entry file for the bundle 5 | entry: path.join(__dirname, '/client/src/app.jsx'), 6 | 7 | // the bundle file we will get in the result 8 | output: { 9 | path: path.join(__dirname, '/client/dist/js'), 10 | filename: 'app.js', 11 | }, 12 | 13 | module: { 14 | 15 | // apply loaders to files that meet given conditions 16 | loaders: [{ 17 | test: /\.jsx?$/, 18 | include: path.join(__dirname, '/client/src'), 19 | loader: 'babel-loader', 20 | query: { 21 | presets: ["react", "es2015"] 22 | } 23 | }], 24 | }, 25 | 26 | // start Webpack in a watch mode, so Webpack will rebuild the bundle on changes 27 | watch: true 28 | }; --------------------------------------------------------------------------------