mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
add passport-js auth webhook boilerplate (#528)
This commit is contained in:
parent
d0303995e3
commit
ebc3589118
1
community/boilerplates/auth-webhooks/passport-js/.gitignore
vendored
Normal file
1
community/boilerplates/auth-webhooks/passport-js/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
@ -0,0 +1 @@
|
||||
web: npm start
|
93
community/boilerplates/auth-webhooks/passport-js/README.md
Normal file
93
community/boilerplates/auth-webhooks/passport-js/README.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Passport.js Auth Webhook Boilerplate
|
||||
|
||||
This is a sample auth webhook for authenticating requests to the Hasura GraphQL Engine. This boilerplate also exposes login and signup endpoints.
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
- PostgreSQL
|
||||
- Node.js 8.9+
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
### Deploy locally
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/hasura/graphql-engine
|
||||
|
||||
# Change directory
|
||||
cd graphql-engine/community/boilerplates/auth-webhooks/passport-js
|
||||
|
||||
# Install NPM dependencies
|
||||
npm install
|
||||
|
||||
# Set DATABASE_URL env
|
||||
export DATABASE_URL=postgres://<username>:<password>@<host>:<port>/<database_name>
|
||||
|
||||
# Apply migrations
|
||||
# (Note) this step creates a "users" table in the database
|
||||
knex migrate:latest
|
||||
|
||||
# Then simply start your app
|
||||
npm start
|
||||
```
|
||||
|
||||
### Deploy with Heroku
|
||||
|
||||
```bash
|
||||
# Create heroku app
|
||||
heroku create <app-name>
|
||||
|
||||
# Create PostgreSQL addon
|
||||
heroku addons:create heroku-postgresql:hobby-dev -a <app-name>
|
||||
|
||||
# Add git remote
|
||||
git remote add heroku https://git.heroku.com/<app-name>.git
|
||||
|
||||
# Push changes to heroku
|
||||
# Note: You need to run this command from the toplevel of the working tree (graphql-enginej)
|
||||
git subtree push --prefix community/boilerplates/auth-webhooks/passport-js heroku master
|
||||
|
||||
# Apply migrations
|
||||
# (Note) this step creates a "users" table in the database
|
||||
heroku run knex migrate:latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Signup/Login
|
||||
|
||||
Once deployed or started locally, we can create an user using `/signup` API like below:
|
||||
```bash
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d'{"username": "test123", "password": "test123", "confirmPassword": "test123"}' \
|
||||
http://localhost:8080/signup
|
||||
```
|
||||
|
||||
On success, we get the response:
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "test123",
|
||||
"token": "4ffd5ee92853787836325dcea74c02e4"
|
||||
}
|
||||
```
|
||||
|
||||
We can also use `/login` API to fetch the user token,
|
||||
```bash
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d'{"username": "test123", "password": "test123"}' \
|
||||
http://localhost:8080/login
|
||||
```
|
||||
|
||||
### Webhook for GraphQL Engine
|
||||
|
||||
Auth webhook that can be configured with Hasura GraphQl Engine is available at `/webhook`. It accepts Authorization header to validate the token against an user.
|
||||
|
||||
The client just need to add `Authorization: Bearer <token>` to the request sending to GraphQL Engine.
|
||||
|
||||
The endpoint (say `http://localhost:8080/webhook`) can be given as an environment variable `HASURA_GRAPHQL_AUTH_HOOK` to GraphQL Engine.
|
||||
|
||||
[Read more about webhook here](https://docs.hasura.io/1.0/graphql/manual/auth/webhook.html).
|
50
community/boilerplates/auth-webhooks/passport-js/app.js
Normal file
50
community/boilerplates/auth-webhooks/passport-js/app.js
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const chalk = require('chalk');
|
||||
const dotenv = require('dotenv');
|
||||
const passport = require('passport');
|
||||
const expressValidator = require('express-validator');
|
||||
|
||||
/**
|
||||
* Load environment variables from .env file, where API keys and passwords are configured.
|
||||
*/
|
||||
dotenv.load({ path: '.env.example' });
|
||||
|
||||
/**
|
||||
* Controllers (route handlers).
|
||||
*/
|
||||
const userController = require('./controllers/user');
|
||||
|
||||
/**
|
||||
* Create Express server.
|
||||
*/
|
||||
const app = express();
|
||||
|
||||
/**
|
||||
* Express configuration.
|
||||
*/
|
||||
app.set('host', '0.0.0.0');
|
||||
app.set('port', process.env.PORT || 8080);
|
||||
app.set('json spaces', 2); // number of spaces for indentation
|
||||
app.use(bodyParser.json());
|
||||
app.use(expressValidator());
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.post('/login', userController.postLogin);
|
||||
app.post('/signup', userController.postSignup);
|
||||
app.get('/webhook', userController.getWebhook);
|
||||
|
||||
/**
|
||||
* Start Express server.
|
||||
*/
|
||||
app.listen(app.get('port'), () => {
|
||||
console.log('%s App is running at http://localhost:%d in %s mode', chalk.green('✓'), app.get('port'), app.get('env'));
|
||||
console.log(' Press CTRL-C to stop\n');
|
||||
});
|
||||
|
||||
module.exports = app;
|
@ -0,0 +1,45 @@
|
||||
const passport = require('passport');
|
||||
const { Strategy: LocalStrategy } = require('passport-local');
|
||||
const { Strategy: BearerStrategy } = require('passport-http-bearer');
|
||||
const { User } = require('../db/schema');
|
||||
const { errorHandler } = require('../db/errors');
|
||||
|
||||
passport.use(
|
||||
new LocalStrategy({
|
||||
usernameField: 'username',
|
||||
passwordField: 'password'
|
||||
},
|
||||
function (username, password, done) {
|
||||
User
|
||||
.query()
|
||||
.where('username', username)
|
||||
.first()
|
||||
.then(function (user) {
|
||||
if (!user) { return done('Unknown user'); }
|
||||
user.verifyPassword(password, function (err, passwordCorrect) {
|
||||
if (err) { return done(err); }
|
||||
if (!passwordCorrect) { return done('Invalid password'); }
|
||||
return done(null, user)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
done(err)
|
||||
})
|
||||
}
|
||||
));
|
||||
|
||||
passport.use(new BearerStrategy(
|
||||
function(token, done) {
|
||||
User
|
||||
.query()
|
||||
.where('token', token)
|
||||
.first()
|
||||
.then(function (user){
|
||||
if (!user) { return done('Invalid Token'); }
|
||||
return done(null, user);
|
||||
}).catch(function (err) {
|
||||
done(err);
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
module.exports = passport;
|
@ -0,0 +1,80 @@
|
||||
const passport = require('../config/passport');
|
||||
const { User } = require('../db/schema');
|
||||
const { errorHandler } = require('../db/errors');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
/**
|
||||
* POST /login
|
||||
* Sign in using username and password.
|
||||
*/
|
||||
exports.postLogin = async (req, res, next) => {
|
||||
req.assert('username', 'Username is not valid').notEmpty();
|
||||
req.assert('password', 'Password cannot be blank').notEmpty();
|
||||
|
||||
const errors = req.validationErrors();
|
||||
|
||||
if (errors) {
|
||||
return res.status(400).json({'errors': errors});
|
||||
}
|
||||
|
||||
passport.authenticate('local', (err, user, info) => {
|
||||
if (err) { return handleResponse(res, 400, {'error': err})}
|
||||
if (user) {
|
||||
handleResponse(res, 200, user.getUser());
|
||||
}
|
||||
})(req, res, next);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* POST /signup
|
||||
* Create a new local account.
|
||||
*/
|
||||
exports.postSignup = async (req, res, next) => {
|
||||
req.assert('username', 'Username is not valid').notEmpty();
|
||||
req.assert('password', 'Password must be at least 4 characters long').len(4);
|
||||
req.assert('confirmPassword', 'Passwords do not match').equals(req.body.password);
|
||||
|
||||
const errors = req.validationErrors();
|
||||
|
||||
if (errors) {
|
||||
return res.status(400).json({'errors': errors});
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.query()
|
||||
.allowInsert('[username, password]')
|
||||
.insert({
|
||||
username: req.body.username,
|
||||
password: req.body.password
|
||||
});
|
||||
} catch (err) {
|
||||
errorHandler(err, res);
|
||||
return;
|
||||
}
|
||||
passport.authenticate('local', (err, user, info) => {
|
||||
if (err) { return handleResponse(res, 400, {'error': err})}
|
||||
if (user) {
|
||||
handleResponse(res, 200, user.getUser());
|
||||
}
|
||||
})(req, res, next);
|
||||
};
|
||||
|
||||
exports.getWebhook = async (req, res, next) => {
|
||||
passport.authenticate('bearer', (err, user, info) => {
|
||||
if (err) { return handleResponse(res, 401, {'error': err}); }
|
||||
if (user) {
|
||||
handleResponse(res, 200, {
|
||||
'X-Hasura-Role': 'user',
|
||||
'X-Hasura-User-Id': `${user.id}`
|
||||
});
|
||||
} else {
|
||||
handleResponse(res, 200, {'X-Hasura-Role': 'anonymous'});
|
||||
}
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
|
||||
function handleResponse(res, code, statusMsg) {
|
||||
res.status(code).json(statusMsg);
|
||||
}
|
120
community/boilerplates/auth-webhooks/passport-js/db/errors.js
Normal file
120
community/boilerplates/auth-webhooks/passport-js/db/errors.js
Normal file
@ -0,0 +1,120 @@
|
||||
const {
|
||||
ValidationError,
|
||||
NotFoundError
|
||||
} = require('objection');
|
||||
|
||||
const {
|
||||
DBError,
|
||||
ConstraintViolationError,
|
||||
UniqueViolationError,
|
||||
NotNullViolationError,
|
||||
ForeignKeyViolationError,
|
||||
CheckViolationError,
|
||||
DataError
|
||||
} = require('objection-db-errors');
|
||||
|
||||
// In this example `res` is an express response object.
|
||||
function errorHandler(err, res) {
|
||||
if (err instanceof ValidationError) {
|
||||
switch (err.type) {
|
||||
case 'ModelValidation':
|
||||
res.status(400).send({
|
||||
message: err.message,
|
||||
type: 'ModelValidation',
|
||||
data: err.data
|
||||
});
|
||||
break;
|
||||
case 'RelationExpression':
|
||||
res.status(400).send({
|
||||
message: err.message,
|
||||
type: 'InvalidRelationExpression',
|
||||
data: {}
|
||||
});
|
||||
break;
|
||||
case 'UnallowedRelation':
|
||||
res.status(400).send({
|
||||
message: err.message,
|
||||
type: 'UnallowedRelation',
|
||||
data: {}
|
||||
});
|
||||
break;
|
||||
case 'InvalidGraph':
|
||||
res.status(400).send({
|
||||
message: err.message,
|
||||
type: 'InvalidGraph',
|
||||
data: {}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
res.status(400).send({
|
||||
message: err.message,
|
||||
type: 'UnknownValidationError',
|
||||
data: {}
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else if (err instanceof NotFoundError) {
|
||||
res.status(404).send({
|
||||
message: err.message,
|
||||
type: 'NotFound',
|
||||
data: {}
|
||||
});
|
||||
} else if (err instanceof UniqueViolationError) {
|
||||
res.status(409).send({
|
||||
message: err.message,
|
||||
type: 'UniqueViolation',
|
||||
data: {
|
||||
columns: err.columns,
|
||||
table: err.table,
|
||||
constraint: err.constraint
|
||||
}
|
||||
});
|
||||
} else if (err instanceof NotNullViolationError) {
|
||||
res.status(400).send({
|
||||
message: err.message,
|
||||
type: 'NotNullViolation',
|
||||
data: {
|
||||
column: err.column,
|
||||
table: err.table,
|
||||
}
|
||||
});
|
||||
} else if (err instanceof ForeignKeyViolationError) {
|
||||
res.status(409).send({
|
||||
message: err.message,
|
||||
type: 'ForeignKeyViolation',
|
||||
data: {
|
||||
table: err.table,
|
||||
constraint: err.constraint
|
||||
}
|
||||
});
|
||||
} else if (err instanceof CheckViolationError) {
|
||||
res.status(400).send({
|
||||
message: err.message,
|
||||
type: 'CheckViolation',
|
||||
data: {
|
||||
table: err.table,
|
||||
constraint: err.constraint
|
||||
}
|
||||
});
|
||||
} else if (err instanceof DataError) {
|
||||
res.status(400).send({
|
||||
message: err.message,
|
||||
type: 'InvalidData',
|
||||
data: {}
|
||||
});
|
||||
} else if (err instanceof DBError) {
|
||||
res.status(500).send({
|
||||
message: err.message,
|
||||
type: 'UnknownDatabaseError',
|
||||
data: {}
|
||||
});
|
||||
} else {
|
||||
res.status(500).send({
|
||||
message: err.message,
|
||||
type: 'UnknownError',
|
||||
data: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { errorHandler }
|
@ -0,0 +1,15 @@
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.createTable('users', (table) => {
|
||||
table.increments();
|
||||
table.string('username').unique().notNullable();
|
||||
table.string('password').notNullable();
|
||||
table.string('token').notNullable();
|
||||
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.raw('now()'));
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.dropTable('users');
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
const { promisify } = require('util');
|
||||
const Knex = require('knex');
|
||||
const connection = require('../knexfile');
|
||||
const { Model } = require('objection');
|
||||
const bcrypt = require('bcrypt');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const knexConnection = Knex(connection);
|
||||
const randomBytesAsync = promisify(crypto.randomBytes);
|
||||
|
||||
Model.knex(knexConnection);
|
||||
|
||||
class User extends Model {
|
||||
static get tableName () {
|
||||
return 'users'
|
||||
}
|
||||
|
||||
static get idColumn() {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return {
|
||||
'id': this.id,
|
||||
'username': this.username,
|
||||
'token': this.token
|
||||
}
|
||||
}
|
||||
|
||||
async $beforeInsert () {
|
||||
const salt = bcrypt.genSaltSync();
|
||||
this.password = await bcrypt.hash(this.password, salt)
|
||||
const createRandomToken = await randomBytesAsync(16).then(buf => buf.toString('hex'));
|
||||
this.token = createRandomToken
|
||||
}
|
||||
|
||||
verifyPassword (password, callback) {
|
||||
bcrypt.compare(password, this.password, callback)
|
||||
};
|
||||
|
||||
static get jsonSchema () {
|
||||
return {
|
||||
type: 'object',
|
||||
required: ['username'],
|
||||
properties: {
|
||||
id: {type: 'integer'},
|
||||
username: {type: 'string', minLength: 1, maxLength: 255},
|
||||
token: {type: 'string', minLength: 1, maxLength: 255}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { User }
|
14
community/boilerplates/auth-webhooks/passport-js/knexfile.js
Normal file
14
community/boilerplates/auth-webhooks/passport-js/knexfile.js
Normal file
@ -0,0 +1,14 @@
|
||||
// Update with your config settings.
|
||||
|
||||
const databaseName = "postgres";
|
||||
const pg = require('pg');
|
||||
|
||||
const connection_url = process.env.DATABASE_URL || `postgres://postgres:@localhost:5432/${databaseName}`;
|
||||
|
||||
module.exports = {
|
||||
client: 'pg',
|
||||
connection: connection_url,
|
||||
migrations: {
|
||||
directory: __dirname + '/db/migrations'
|
||||
}
|
||||
};
|
2467
community/boilerplates/auth-webhooks/passport-js/package-lock.json
generated
Normal file
2467
community/boilerplates/auth-webhooks/passport-js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "passport-js-auth-webhook",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "8.9.1"
|
||||
},
|
||||
"description": "A boilerplate for Hasura Graphql Engine auth webhook with passport js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node app.js",
|
||||
"lint": "eslint \"**/*.js\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hasura/graphql-engine.git"
|
||||
},
|
||||
"author": "Aravind Shankar",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/hasura/graphql-engine/issues"
|
||||
},
|
||||
"homepage": "https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/auth-webhooks/passport-js#readme",
|
||||
"dependencies": {
|
||||
"bcrypt": "^3.0.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"chalk": "^2.4.1",
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^6.0.0",
|
||||
"express": "^4.16.3",
|
||||
"express-validator": "^5.3.0",
|
||||
"knex": "^0.15.2",
|
||||
"objection": "^1.2.6",
|
||||
"objection-db-errors": "^1.0.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pg": "^7.4.3",
|
||||
"util": "^0.11.0"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user