├── .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 | };
--------------------------------------------------------------------------------