mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
added auth-jwt-roles boilerplate (#1570)
Added a boilerplate from [this original repository](https://github.com/platyplus/authentication-server) based on the discussions in [this issue](https://github.com/hasura/graphql-engine/issues/1420) and [this issue](https://github.com/hasura/graphql-engine/issues/1446). [skip ci]
This commit is contained in:
parent
5fc2df2766
commit
e7fba40fad
@ -0,0 +1,3 @@
|
||||
Dockerfile
|
||||
node_modules
|
||||
README.md
|
@ -0,0 +1,12 @@
|
||||
kind: pipeline
|
||||
name: application
|
||||
steps:
|
||||
- name: build-app
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: platyplus/authentication
|
||||
tags: latest
|
||||
username:
|
||||
from_secret: DH_USER
|
||||
password:
|
||||
from_secret: DH_PASSWORD
|
17
community/boilerplates/auth-servers/passportjs-jwt-roles/.gitignore
vendored
Normal file
17
community/boilerplates/auth-servers/passportjs-jwt-roles/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
.DS_Store
|
||||
.thumbs.db
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
|
||||
private.pem
|
||||
public.pem
|
@ -0,0 +1,8 @@
|
||||
FROM node:11-alpine
|
||||
WORKDIR /root/app
|
||||
COPY package.json .
|
||||
RUN npm install --only=production
|
||||
COPY . .
|
||||
EXPOSE 8080
|
||||
CMD npm start
|
||||
#TODO: https://learnk8s.io/blog/smaller-docker-images
|
@ -0,0 +1,201 @@
|
||||
# Authentication with JWT, Hasura claims and multiple roles
|
||||
|
||||
This is a sample auth JWT service for authenticating requests to the Hasura GraphQL Engine. This also exposes login and signup endpoints. Note that this repository can also be used in webhook mode in using the `/webhook` endpoint. The specifics of this repository is that it maps a `user_role` table to generate `x-hasura-allowed-roles` in the JWT claim so multiple roles can work with the Hasura Grapqh Engine as a backend of the application.
|
||||
|
||||
The endpoints to manage users are very limited (it is only possible to create a new user through the `/signup` endpoint). This is kind of a choice as this service is meant to be used for authentication only. The user and roles management can be done through the Hasura Graphql Engine or any other service accessing to the same database.
|
||||
|
||||
## Rationale
|
||||
|
||||
See this [issue](https://github.com/hasura/graphql-engine/issues/1420).
|
||||
|
||||
## Database schema
|
||||
|
||||
Three tables are used:
|
||||
|
||||
- `user`:
|
||||
- `id`: UUID. Primary key. Automatically generated.
|
||||
- `username`: String. Unique user identifier.
|
||||
- `password`: String. Hashed with bcrypt.
|
||||
- `active`: Boolean. If not active, not possible to connect with this user.
|
||||
- `role`:
|
||||
- `id`: UUID. Primary key. Automatically generated.
|
||||
- `name`: String. Unique role identifier.
|
||||
- `user_role`:
|
||||
- `id`: UUID. Primary key. Automatically generated.
|
||||
- `role_id`: UUID. Foreign key that references the `id` of the `role` table.
|
||||
- `user_id`: UUID. Foreign key that references the `id` of the `user` table.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PostgreSQL
|
||||
- Node.js 8.9+
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Environment variables
|
||||
|
||||
_Note: you can find examples of RSA keys in the repository. **DO NOT USE THEM FOR PRODUCTION!**_
|
||||
|
||||
- `AUTH_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nypPTIfSzZ399o........"`
|
||||
|
||||
RSA private key used to sign the JWT. You need to escape the lines with "\n" in the variable. If the variable is not set, it will try to use the private.pem file.
|
||||
|
||||
- `AUTH_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nV02/4RJi........"`
|
||||
|
||||
RSA private key used to deliver the JWK set. You need to escape the lines with "\n" in the variable. Please not that this feature is not working yet. If the variable is not set, it will try to use the public.pem file.
|
||||
|
||||
- `AUTH_KEY_ID="<unique-id-for-this-key>"`
|
||||
|
||||
Used to identify the key currently used to sign the tokens. If the variable is not set, a hash string will be generated from the public key and used instead.
|
||||
|
||||
- `DATABASE_URL=postgres://<username>:<password>@<host>:<port>/<database_name>`
|
||||
|
||||
URL to connect to the Postgres database. The format is . For instance: `DATABASE_URL=postgres://postgres:@localhost:5432/postgres`
|
||||
|
||||
- `PORT=8080`
|
||||
|
||||
The port the server will listen to.
|
||||
|
||||
### Build and deploy on Docker (production)
|
||||
|
||||
First you need to build the image and to tag it:
|
||||
|
||||
```bash
|
||||
docker build . -t hasura/passportjs-jwt-roles:latest
|
||||
```
|
||||
|
||||
TODO: document on how to deploy on docker.
|
||||
|
||||
You can also have a look at [this docker-compose gist](https://gist.github.com/plmercereau/b8503c869ffa2b5d4e42dc9137b56ae1) to see how I use this service in a docker stack with Hasura and [Traefik](https://traefik.io/).
|
||||
|
||||
### Deploy locally (developpment)
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/hasura/graphql-engine
|
||||
|
||||
# Change directory
|
||||
cd community/boilerplates/auth-servers/passportjs-jwt-roles
|
||||
|
||||
# Install NPM dependencies
|
||||
npm install
|
||||
|
||||
# Generate the RSA keys
|
||||
openssl genrsa -out private.pem 2048
|
||||
openssl rsa -in private.pem -pubout > public.pem
|
||||
|
||||
# print the keys in an escaped format
|
||||
awk -v ORS='\\n' '1' private.pem
|
||||
awk -v ORS='\\n' '1' public.pem
|
||||
|
||||
export DATABASE_URL=postgres://postgres:@localhost:5432/postgres
|
||||
|
||||
# Apply migrations
|
||||
# (Note) this step creates tables "users", "roles" and "user_roles" in the database
|
||||
knex migrate:latest
|
||||
|
||||
# Then simply start your app
|
||||
npm start
|
||||
```
|
||||
|
||||
<!-- ### Deploy with Heroku
|
||||
|
||||
TODO: test deployment with heroku, and rewrite this part
|
||||
|
||||
```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
|
||||
``` -->
|
||||
|
||||
### Configure the Hasura GraphQL Engine
|
||||
|
||||
Run the Hasura GraphQL engine with `HASURA_GRAPHQL_JWT_SECRET` set like this:
|
||||
|
||||
```json
|
||||
{ "type": "RS256", "key": "<AUTH_PUBLIC_KEY>" }
|
||||
```
|
||||
|
||||
Where `<AUTH_PUBLIC_KEY>` is your RSA public key in PEM format, with the line breaks escaped with "\n".
|
||||
|
||||
You can also configure the server in JWKS mode and set `HASURA_GRAPHQL_JWT_SECRET` like this:
|
||||
|
||||
```json
|
||||
{ "type": "RS256", "jwk_url": "hostname:port/jwks" }
|
||||
```
|
||||
|
||||
More information in the [Hasura documentation](https://docs.hasura.io/1.0/graphql/manual/auth/jwt.html).
|
||||
|
||||
## Usage
|
||||
|
||||
### Signup
|
||||
|
||||
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": "907f0dc7-6887-4232-8b6e-da3d5908f137",
|
||||
"username": "test123",
|
||||
"roles": ["user"],
|
||||
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoicGlsb3UiLCJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsibWFuYWdlciIsInVzZXIiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLXVzZXItaWQiOiI5MDdmMGRjNy02ODg3LTQyMzItOGI2ZS1kYTNkNTkwOGYxMzcifSwiaWF0IjoxNTQ4OTI5MTY2LCJleHAiOjE1NTE1MjExNjYsInN1YiI6IjkwN2YwZGM3LTY4ODctNDIzMi04YjZlLWRhM2Q1OTA4ZjEzNyJ9.hoY-lZ-6rbN_WVFy0Taxbf6QCtDPaTm407l6opv2bz-Hui9T7l7aafStsx9w-UscWUFWHpeStIo1ObV-lT8-j9t-nw9q5fr8wuO2zyKBMXjhD57ykR6BcKvJQMxE1JjyetVLHpj5r4mIb7_kaA8Dj8Vy2yrWFReHXDczYpQGc43mxxC05B5_xdScQrSbs9MkgQRh-Z5EknlLKWkpbuxPvoyWcH1wgLum7UABGNO7drvmcDDaRk6Lt99A3t40sod9mJ3H9UqdooLOfBAg9kcaCSgqWDkmCLBwtM8ONbKZ4cEZ8NEseCQYKqIoyHQH9vbf9Y6GBaJVbBoEay1cI48Hig"
|
||||
}
|
||||
```
|
||||
|
||||
### Login
|
||||
|
||||
Let's use the `/login` endpoint to fetch the user information and JWT:
|
||||
|
||||
```bash
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d'{"username": "test123", "password": "test123"}' \
|
||||
http://localhost:8080/login
|
||||
```
|
||||
|
||||
It will then send back user information including the JWT in the same format as the above `/signup` endoint.
|
||||
|
||||
You can use this boilerplate as a webhook server in using the `/webhook` endpoint to fetch a webhook token:
|
||||
|
||||
```bash
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d'{"username": "test123", "password": "test123"}' \
|
||||
http://localhost:8080/login
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Not tested with Heroku
|
||||
- There is no user and role management except to create a single user with no specific role. I myself do this part with a frontend app that access the database through a Hasura GraphQL endpoint.
|
||||
- This server is designed to work with one RSA key only, and does not handle its regular rotation.
|
||||
- No handling of JWT expiration and key turnover.
|
||||
- This server is not (yet?) designed to handle authentication through other services such as Google, Github... It would be nice to do so, but to keep this server as a proxy that would add the Hasura claims in querying the database about the roles of the user. Comments or any contribution are welcome as well on this one.
|
||||
- No automated tests.
|
||||
- another cool feature to be would be to expose the endpoints through hasura remote schema, and not directly to the client
|
||||
|
||||
## Credits
|
||||
|
||||
The original repository can be found [here](https://github.com/platyplus/authentication-server).
|
||||
|
||||
This repository is inspired from the original [auth-webhooks/passport-js repo](https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/auth-webhooks/passport-js).
|
||||
|
||||
Contributions are welcome!
|
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
const chalk = require('chalk')
|
||||
// const dotenv = require('dotenv');
|
||||
const passport = require('passport')
|
||||
const cors = require('cors')
|
||||
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')
|
||||
|
||||
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(cors())
|
||||
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)
|
||||
app.get('/jwks', userController.getJwks)
|
||||
/**
|
||||
* 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')
|
||||
)
|
||||
})
|
||||
|
||||
module.exports = app
|
@ -0,0 +1,14 @@
|
||||
var fs = require('fs')
|
||||
var fnv = require('fnv-plus')
|
||||
|
||||
// TODO: why does rsaPemToJwk work with a file but not with a variable?
|
||||
exports.key = (
|
||||
process.env.AUTH_PRIVATE_KEY || fs.readFileSync('private.pem').toString()
|
||||
).replace(/\\n/g, '\n')
|
||||
|
||||
exports.publicKey = (
|
||||
process.env.AUTH_PUBLIC_KEY || fs.readFileSync('public.pem').toString()
|
||||
).replace(/\\n/g, '\n')
|
||||
|
||||
// Key Identifier – Acts as an ‘alias’ for the key
|
||||
exports.kid = process.env.AUTH_KEY_ID || fnv.hash(this.publicKey, 128).hex()
|
@ -0,0 +1,63 @@
|
||||
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()
|
||||
.eager('roles')
|
||||
.then(function(user) {
|
||||
if (!user) {
|
||||
return done('Unknown user')
|
||||
}
|
||||
if (!user.active) {
|
||||
return done('User is inactive')
|
||||
}
|
||||
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()
|
||||
.eager('roles')
|
||||
.then(function(user) {
|
||||
if (!user) {
|
||||
return done('Invalid Token')
|
||||
}
|
||||
if (!user.active) {
|
||||
return done('User is inactive')
|
||||
}
|
||||
return done(null, user)
|
||||
})
|
||||
.catch(function(err) {
|
||||
done(err)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
module.exports = passport
|
@ -0,0 +1,101 @@
|
||||
const passport = require('../config/passport')
|
||||
const { User } = require('../db/schema')
|
||||
const { errorHandler } = require('../db/errors')
|
||||
const rasha = require('rasha')
|
||||
const jwtConfig = require('../config/jwt')
|
||||
|
||||
/**
|
||||
* Sends the JWT key set
|
||||
*/
|
||||
exports.getJwks = async (req, res, next) => {
|
||||
const jwk = {
|
||||
...rasha.importSync({ pem: jwtConfig.publicKey }),
|
||||
alg: 'RS256',
|
||||
use: 'sig',
|
||||
kid: jwtConfig.publicKey
|
||||
}
|
||||
const jwks = {
|
||||
keys: [jwk]
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send(JSON.stringify(jwks, null, 2) + '\n')
|
||||
handleResponse(res, 200, jwks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign in using username and password and returns JWT
|
||||
*/
|
||||
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, user.getHasuraClaims())
|
||||
} else {
|
||||
handleResponse(res, 200, { 'X-Hasura-Role': 'anonymous' })
|
||||
}
|
||||
})(req, res, next)
|
||||
}
|
||||
|
||||
function handleResponse(res, code, statusMsg) {
|
||||
res.status(code).json(statusMsg)
|
||||
}
|
@ -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,26 @@
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.createTable('user', table => {
|
||||
table
|
||||
.uuid('id')
|
||||
.primary()
|
||||
.unique()
|
||||
.defaultTo(knex.raw('gen_random_uuid()'))
|
||||
table
|
||||
.string('username')
|
||||
.unique()
|
||||
.notNullable()
|
||||
table.string('password').notNullable()
|
||||
table
|
||||
.timestamp('created_at')
|
||||
.notNullable()
|
||||
.defaultTo(knex.raw('now()'))
|
||||
table
|
||||
.boolean('active')
|
||||
.defaultTo(true)
|
||||
.index()
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.dropTable('user')
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.createTable('role', table => {
|
||||
table
|
||||
.uuid('id')
|
||||
.primary()
|
||||
.unique()
|
||||
.defaultTo(knex.raw('gen_random_uuid()'))
|
||||
table
|
||||
.string('name')
|
||||
.unique()
|
||||
.notNullable()
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.dropTable('role')
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.createTable('user_role', table => {
|
||||
table
|
||||
.uuid('id')
|
||||
.primary()
|
||||
.unique()
|
||||
.defaultTo(knex.raw('gen_random_uuid()'))
|
||||
table
|
||||
.uuid('role_id')
|
||||
.unsigned()
|
||||
.index()
|
||||
.references('id')
|
||||
.inTable('role')
|
||||
table
|
||||
.uuid('user_id')
|
||||
.unsigned()
|
||||
.index()
|
||||
.references('id')
|
||||
.inTable('user')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.dropTable('user_role')
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
const { promisify } = require('util')
|
||||
const Knex = require('knex')
|
||||
const connection = require('../knexfile')
|
||||
const { Model } = require('objection')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const jwtConfig = require('../config/jwt')
|
||||
|
||||
const knexConnection = Knex(connection)
|
||||
|
||||
Model.knex(knexConnection)
|
||||
|
||||
class Role extends Model {
|
||||
static get tableName() {
|
||||
return 'role'
|
||||
}
|
||||
|
||||
static get idColumn() {
|
||||
return 'id'
|
||||
}
|
||||
}
|
||||
|
||||
class User extends Model {
|
||||
static get tableName() {
|
||||
return 'user'
|
||||
}
|
||||
|
||||
static get idColumn() {
|
||||
return 'id'
|
||||
}
|
||||
|
||||
static get relationMappings() {
|
||||
return {
|
||||
roles: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Role,
|
||||
join: {
|
||||
from: 'user.id',
|
||||
through: {
|
||||
from: 'user_role.user_id',
|
||||
to: 'user_role.role_id'
|
||||
},
|
||||
to: 'role.id'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRoles() {
|
||||
return this.roles.map(el => el.name).concat('user')
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return {
|
||||
id: this.id,
|
||||
username: this.username,
|
||||
roles: this.getRoles(),
|
||||
token: this.getJwt()
|
||||
}
|
||||
}
|
||||
|
||||
getHasuraClaims() {
|
||||
return {
|
||||
'x-hasura-allowed-roles': this.getRoles(),
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-user-id': `${this.id}`
|
||||
// 'x-hasura-org-id': '123',
|
||||
// 'x-hasura-custom': 'custom-value'
|
||||
}
|
||||
}
|
||||
|
||||
getJwt() {
|
||||
const signOptions = {
|
||||
subject: this.id,
|
||||
expiresIn: '30d', // 30 days validity
|
||||
algorithm: 'RS256'
|
||||
}
|
||||
const claim = {
|
||||
name: this.username,
|
||||
// iat: Math.floor(Date.now() / 1000),
|
||||
'https://hasura.io/jwt/claims': this.getHasuraClaims()
|
||||
}
|
||||
return jwt.sign(claim, jwtConfig.key, signOptions)
|
||||
}
|
||||
|
||||
async $beforeInsert() {
|
||||
const salt = bcrypt.genSaltSync()
|
||||
this.password = await bcrypt.hash(this.password, salt)
|
||||
}
|
||||
|
||||
async $beforeUpdate() {
|
||||
await $beforeInsert()
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { User, Role }
|
@ -0,0 +1,16 @@
|
||||
// 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'
|
||||
}
|
||||
}
|
2150
community/boilerplates/auth-servers/passportjs-jwt-roles/package-lock.json
generated
Normal file
2150
community/boilerplates/auth-servers/passportjs-jwt-roles/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "platyplus-authentication-server",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": "11.4.0"
|
||||
},
|
||||
"description": "A simple authentication service to deliver JWT with Hasura claims, based on users with multiples roles stored in a Postgres database.",
|
||||
"scripts": {
|
||||
"test": "echo \"TODO: no test specified\" && exit 0",
|
||||
"start": "node app.js",
|
||||
"lint": "eslint \"**/*.js\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/platyplus/authentication-server"
|
||||
},
|
||||
"author": "Pierre-Louis Mercereau",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/platyplus/authentication-server/issues"
|
||||
},
|
||||
"homepage": "https://github.com/platyplus/authentication-server",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.3",
|
||||
"chalk": "^2.4.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.16.3",
|
||||
"express-validator": "^5.3.0",
|
||||
"fnv-plus": "^1.2.12",
|
||||
"jsonwebtoken": "^8.4.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",
|
||||
"rasha": "^1.2.1",
|
||||
"util": "^0.11.0"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user