Skip to content

Commit

Permalink
feat: Initial Express.js setup for Ethereum cryptographic user auth
Browse files Browse the repository at this point in the history
  • Loading branch information
ltfschoen committed Apr 7, 2020
1 parent 0be43d5 commit 845ff71
Show file tree
Hide file tree
Showing 18 changed files with 1,788 additions and 6 deletions.
3 changes: 2 additions & 1 deletion .env-sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
MNENOMIC = // Your metamask's recovery words
INFURA_API_KEY_RINKEBY =
INFURA_API_KEY_RINKEBY =
NODE_ENV = development
50 changes: 47 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,57 @@ npm run build
```
(same as `truffle build`)

### Run DApp Node.js Server
### Run DApp Node.js Server & Interact

Build App and Run Dev Server:
#### Terminal 3 - Install & Run MongoDB

##### macOS

https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/

```
brew tap mongodb/brew
brew install mongodb-community@4.2
brew services start mongodb-community@4.2
```

#### Terminal 1 - Run Server

Drop DB. Build App and Run Dev Server:

```bash
npm run dev
npm run drop; npm run dev
```

Open `open http://localhost:8080` in browser

#### Terminal 2 - Interact using cURL

* Send request to server and receive response for authentication and authorisation to access specific API endpoints.
* Register. JWT provided in response (i.e. `{"token":"xyz"}`)
```
curl -v POST http://localhost:7000/users/auth/register -d "network=ethereum-testnet-local&publicAddress=0x123&email=ltfschoen@gmail.com&password=123456&name=Luke" -H "Content-Type: application/x-www-form-urlencoded"
curl -v POST http://localhost:7000/users/auth/register -d '{"network": "ethereum-testnet-local", "publicAddress": "0x123", "email":"ltfschoen@gmail.com", "password":"123456", "name":"Luke"}' -H "Content-Type: application/json"
```
* Fetch the Nonce if it exists for given Public Address. Nonce provided in response (i.e. `{"nonce":"123"}`)
```
curl -v GET http://localhost:7000/users/show?network='ethereum-testnet-local&publicAddress=0x123'
```
* Sign in using signature verification. JWT provided in response (i.e. `{"token":"xyz"}`)
```
curl -v POST http://localhost:7000/users/auth/login -d "network='ethereum-testnet-local'&publicAddress=0x123&signature=0x456&email=ltfschoen@gmail.com&password=123456" -H "Content-Type: application/x-www-form-urlencoded"
curl -v POST http://localhost:7000/users/auth/login -d '{"network": "ethereum-testnet-local", "publicAddress": "0x123", "signature": "0x456", "email":"ltfschoen@gmail.com", "password":"123456"}' -H "Content-Type: application/json"
```
* Access a restricted endpoint by providing JWT
```
curl -v GET http://localhost:7000/users/list -H "Content-Type: application/json" -H "Authorization: Bearer <INSERT_TOKEN>"
```
* Create user by providing JWT
```
curl -v POST http://localhost:7000/users/create --data '[{"network": "ethereum-testnet-local", "publicAddress": "0x123", "signature": "0x456", "email":"test@fake.com", "name":"Test"}]' -H "Content-Type: application/json" -H "Authorization: JWT <INSERT_TOKEN>"
curl -v POST http://localhost:7000/users/create -d "network='ethereum-testnet-local'&publicAddress=0x123&signature=0x456&email=test2@fake.com&name=Test2" -H "Content-Type: application/x-www-form-urlencoded" -H "Authorization: JWT <INSERT_TOKEN>"
```

#### Example 2:

* Within the DApp transfer say 10 wei to Account No. 0x0000000000000000000000000000000000000000000000000000000000000001 that we created on Ethereum TestRPC
Expand Down Expand Up @@ -227,6 +268,9 @@ web3.eth.blockNumber
* https://github.com/ethereum/wiki/wiki/JavaScript-API
* https://www.ethereum.org/cli
* https://github.com/ltfschoen/benzcoin
* https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial
* https://mongoosejs.com/docs/populate.html
* https://github.com/vanbexlabs/web3-auth
### FAQ
Expand Down
29 changes: 29 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const express = require('express');
const bodyParser = require('body-parser');
const authMiddleware = require('./middleware/auth');

const app = express();

const usersRouter = require('./routes/users');

// Middleware Plugins
app.use(bodyParser.json()); // allow JSON uploads
app.use(bodyParser.urlencoded({ extended: true })); // allow Form submissions
// app.use(authMiddleware.initialize);
app.use('/users', usersRouter);
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
res.status(401).send('Invalid token');
} else {
next(err);
}
});

// Routes
app.get('/', (req, res) => {
res.status(404).json({
message: 'Error: Server under development'
});
})

module.exports = app;
37 changes: 37 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

const _ = require('lodash');
const fs = require('fs');

fs.createReadStream('.env-sample')
.pipe(fs.createWriteStream('../.env'));

const dotenv = require('dotenv');
dotenv.config();

const config = {
dev: 'development',
test: 'testing',
prod: 'production',
port: process.env.PORT || 7000
};

// Check if script prefix provided (i.e. `NODE_ENV=development nodemon server.js`)
// console.log(process.env.NODE_ENV);

// Setup Node environment based on .env file else use default from hash
process.env.NODE_ENV = process.env.NODE_ENV || config.dev;
config.env = process.env.NODE_ENV;

let envConfig;
try {
envConfig = require('./' + config.env);
// Fallback to empty object if file does not exist
envConfig = envConfig || {};
} catch(err) {
envConfig = {};
console.error('Error reading .env file');
}

// Merge configs so envConfig overwrites the config object
module.exports = _.merge(config, envConfig);
7 changes: 7 additions & 0 deletions config/development.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
logging: true,
db: {
url: 'mongodb://localhost/datahighway'
},
port: 7000
};
3 changes: 3 additions & 0 deletions config/production.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
logging: false
};
7 changes: 7 additions & 0 deletions config/testing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
logging: false,
db: {
url: 'mongodb://localhost/datahighway-test'
},
port: 7111
};
46 changes: 46 additions & 0 deletions controllers/usersController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const User = require('../models/User');
const Account = require('../models/Account');

// GET index -
const userList = (req, res) => {
User.find()
.populate('account')
.then(users => {
res.body = users;
console.log('Authorised: User list returned in response');
res.json({ data: users });
})
.catch(error => res.status(500).json({ error: error.message }))
};

// GET show - nonce
const userShowNonce = (req, res) => {
console.log('userShowNonce with req.query.network', req.query.network);
console.log('userShowNonce with req.query.publicAddress', req.query.publicAddress);
Account.findOne({
// FIXME - change this so it finds an account with both the given 'network' and 'publicAddress'
// network: req.query.network,
publicAddress: req.query.publicAddress
})
.then(account => {
const nonce = account.nonce;
console.log('Authorised: User account public address nonce returned: ', nonce);
res.json({ nonce: nonce });
})
.catch(error => res.status(500).json({ error: error.message }))
};

// POST create
const userCreate = (req, res) => {
User.create(req.body)
.then((user) => {
res.status(201).json(user).end();
})
.catch(error => res.json({ error }))
};

module.exports = {
userList: userList,
userShowNonce: userShowNonce,
userCreate: userCreate
}
163 changes: 163 additions & 0 deletions middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
const passport = require('passport');
const JWT = require('jsonwebtoken');
const PassportJwt = require('passport-jwt');
const User = require('../models/User');
const Account = require('../models/Account');

const JWT_SECRET = 'xyz';
const JWT_ALGORITHM = 'HS256';
const JWT_EXPIRES_IN = '7 days';

// Use "createStrategy" instead of "authenticate".
// See https://github.com/saintedlama/passport-local-mongoose
passport.use(User.createStrategy());

// Middleware for Passport Authentication
const register = async (req, res, next) => {
console.log('Middleware for Passport Registration');
// Create new User model
const user = new User({
email: req.body.email,
name: req.body.name
});

const newAccount = new Account({
network: req.body.network,
publicAddress: req.body.publicAddress,
nonce: '0'
});

// FIXME - associate newAccount with user
// const filter = { email: req.body.email };
// const update = { accounts: [newAccount] }
// let doc = await User.findOneAndUpdate(filter, update, {
// new: true,
// useFindAndModify: true,
// upsert: true
// });
// await doc.save();

// Pass the User model to the Passport `register` method
User.register(user, req.body.password, (error, user) => {
if (error) {
console.error('Error registering user with middleware: ', error);
next(error);
return;
}
console.log('Success registering user with middleware: ', user);
// Store user so we can access in our handler
req.user = user;
next();
})
}

const jwtOptions = {
// Authorization: Bearer in request headers
jwtFromRequest: PassportJwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JWT_SECRET,
// Algorithms used to sign in
algorithms: [JWT_ALGORITHM]
}

// Passport JWT Strategy triggered by validateJWTWithPassportJWT
// https://www.npmjs.com/package/passport-jwt
passport.use(new PassportJwt.Strategy(jwtOptions,
// Post-Verified token - https://www.npmjs.com/package/passport-jwt
(jwtPayload, done) => {
console.log('PassportJwt Strategy being processed');
// Find user in MongoDB using the `id` in the JWT
User.findById(jwtPayload.sub)
// User.findById(jwtPayload._doc._id)
.then((user) => {
if (user) {
done(null, user);
} else {
done(null, false);
}
})
.catch((error) => {
done(error, false);
})
}
))

const validateJWTManually = (req, res, next) => {
// Extract token without "JWT " or "Bearer " prefix
const token = req.headers.authorization ? req.headers.authorization.split(" ")[1] : null;
if (token) {
// https://github.com/auth0/node-jsonwebtoken
JWT.verify(token, JWT_SECRET, function(error, decodedToken) {
if (error) {
res.status(401).json({
message: 'Error: Token invalid'
});
console.error('Error: Token invalid: ', error);
next(error);
return;
} else {
req.user = decodedToken;
User.find({ email: decodedToken.email })
.then((user) => {
if (user) {
console.log('Success authorising user with middleware: ', decodedToken);
next();
} else {
res.status(403).json({
message: 'Error: Token valid but user no longer exists in database'
});
console.error('Error: Token valid but user no longer exists in database: ', error);
next(error);
return;
}
})
.catch((error) => {
res.status(500).json({
message: 'Error: Token valid but error occurred retrieving user from database'
});
console.error('Error: Token valid but error occurred retrieving user from database: ', error);
next(error);
return;
})
}
});
} else {
res.status(401).json({
message: "Error: No Token provided"
});
console.error('Error: No Token provided: ', error);
next(error);
return;
}
}

// JWT signed token - http://jwt.io/
const signJWTForUser = (req, res) => {
// Create signed JWT
const token = JWT.sign(
// payload
{
email: req.body.email
},
// secretOrPrivateKey - https://raymii.org/s/snippets/OpenSSL_Password_Generator.html
JWT_SECRET,
// options - https://github.com/auth0/node-jsonwebtoken
{
subject: req.body.email.toString(),
algorithm: JWT_ALGORITHM,
expiresIn: JWT_EXPIRES_IN
}
)

// Return token in response object
res.json({
token: token
})
}

module.exports = {
initialize: passport.initialize(),
register: register,
signIn: passport.authenticate('local', { session: false }),
signJWTForUser: signJWTForUser,
validateJWTManually: validateJWTManually
}
11 changes: 11 additions & 0 deletions models/Account.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const mongoose = require('./init');
const Schema = mongoose.Schema;

const AccountSchema = Schema({
publicAddress: String,
nonce: String
});

const Account = mongoose.models.Account || mongoose.model('Account', AccountSchema);

module.exports = Account;

0 comments on commit 845ff71

Please sign in to comment.