const googleapis = require('googleapis')
const jwt = require('jsonwebtoken')
const fs = require('fs')
const mongodb = require('mongodb')
const OAuth2 = googleapis.auth.OAuth2
const ObjectID = mongodb.ObjectID
/**
Configuration of trean protector
@memberof module:protector
@typedef {object} TreantConfig
@property {module:mongodb.MongoClient} db - Instance of connection to mongodb
@property {string} gClientId - Identify of application of google
@property {string} gSecrectId - Identify of application of google
@property {string} gRedirectUrl - Address which call after google authenticate
successfully
@property {string[]} gScopes - Define information which can retrieve from
google after get access token
@property {string} privateKeyPath - Path to PEM private file. It use to
encrypt/decrypt credential as json web token. It must absolute path to ensure
that path to key file is correct
@property {number} [tokenTimeout=1800] - Peroid token is valid in seconds
@example
var config = {
db: dbclient,
gClientId: '327834646153-27e424bo1lpofea0lkfs8bvrf7sv5j2e.apps.googleusercontent.com',
gClientSecret: 'y7RCBAr4AT26oACJBJUSJIxv',
gRedirectUrl: 'http://localhost:9001/v1/oauth/login',
gScopes: [
'https://www.googleapis.com/auth/plus.me',
'profile',
'email'
],
privateKeyPath: '/usr/lib/node_module/gwisp/gwisp.key',
tokenTimeout: 1800
}
*/
/**
- Authenticate service help authenticate user and authorize them permission
- It also support generate credential as json web token
@memberof module:protector
@class Treant
@constructor
@param {module:protector.TreantConfig} config - Configuration of treant protector
@example
var treant = new Treant({
db: dbclient,
gClientId: '327834646153-27e424bo1lpofea0lkfs8bvrf7sv5j2e.apps.googleusercontent.com',
gClientSecret: 'y7RCBAr4AT26oACJBJUSJIxv',
gRedirectUrl: 'http://localhost:9001/v1/oauth/login',
gScopes: [
'https://www.googleapis.com/auth/plus.me',
'profile',
'email'
],
privateKeyPath: '/usr/lib/node_module/gwisp/gwisp.key',
tokenTimeout: 1800
})
*/
function Treant (config) {
/**
Treant configuration
@member {module:protector.TreanConfig}
*/
this._config = config
this._accountCol = config.db.collection('account')
this._cert = fs.readFileSync(config.privateKeyPath)
}
const prototype = Treant.prototype
/**
Create new google oauth 2 object
@memberof module:protector.Treant
@return {module:googleapis.OAuth2}
@example
var oauth = this._gOauth()
*/
prototype._gOauth = function () {
return new OAuth2(this._config.gClientId, this._config.gClientSecret,
this._config.gRedirectUrl)
}
/**
- Get login url of oauth 2 service from google
- Then caller can perform GET http with url to require login with google
```bash
$ curl <gLoginUrl>
```
@memberof module:protector.Treant
@return {string}
@example
// see constructor for detail
var treant = new Treant(config)
var url = treant.gLoginUrl()
// url: https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.
// googleapis.com%2Fauth%2Fplus.me%20profile%20email&approval_prompt=
// auto&access_type=offline&response_type=code&client_id=736160870024-
// 0cu341if9scqjfpvdsaqhutjebobjjq0.apps.googleusercontent.com&redirect_uri=
// http%3A%2F%2Flocalhost%3A9001%2Foauth%2Flogin
*/
prototype.gLoginUrl = function () {
const self = this
var gOauth = self._gOauth()
return gOauth.generateAuthUrl({
scope: self._config.gScopes,
approval_prompt: 'auto',
access_type: 'offline'
})
}
/**
Get account information from google
@memberof module:protector.Treant
@param {string} accessToken - Access token of google oauth 2
@param {StdCallback} callback
- Function will be call after done
- Result is object contains account information from google
*/
prototype.gAccount = function (accessToken, callback) {
var plus = googleapis.plus('v1')
var credentials = {access_token: accessToken}
var gOauth = this._gOauth()
gOauth.setCredentials(credentials)
plus.people.get({userId: 'me', auth: gOauth}, function (err, account) {
callback(err, account)
})
}
/**
- Login with code from google
- Function will perform exhange code to get token
- If account is exist, create new local token and give back
- If account is not exist, create new account in database, set default setting.
Then create local token and give back
@memberof module:protector.Treant
@param {string} code - Code to exchange token from google
@param {StdCallback}
- Function will be call after done
- Result is local token
@example
// see constructor for detail
var treant = new Treant(config)
treant.gLoginCode('1234567890abcdef', function(err, token) {
if (err) return
// do some thing with token here
})
*/
prototype.gLoginCode = function (code, callback) {
const self = this
var gOauth = self._gOauth()
gOauth.getToken(code, getToken)
function getToken (err, tokens) {
if (err) return callback(err)
// decode token for google account information
try {
tokens.id_token = jwt.decode(tokens.id_token)
} catch (e) {
return callback(e)
}
// create new account into database if it not exist
// if exist, do nothing
verifyAccount(tokens, callback)
}
function verifyAccount (gToken, callback) {
var filter = {issuer: gToken.id_token.iss, subject: gToken.id_token.sub}
self._accountCol.findOne(filter, function (err, account) {
if (err) return callback(err)
if (account) return createToken(gToken, account, callback)
createAccount(gToken, callback)
})
}
function createToken (gToken, account, callback) {
var tokenData = {
id: account._id,
g_access_token: gToken.access_token,
g_refresh_token: gToken.refresh_token
}
var options = {expiresIn: self._config.tokenTimeout}
jwt.sign(tokenData, self._cert, options, function (err, token) {
if (err) return callback(err)
callback(err, token)
})
}
function createAccount (gToken, callback) {
self.gAccount(gToken.access_token, function (err, gAccount) {
if (err) return callback(err)
var account = {
issuer: gToken.id_token.iss,
subject: gToken.id_token.sub,
language: gAccount.language,
name: gAccount.displayName,
groups: [],
schedulers: []
}
self._accountCol.insertOne(account, function (err) {
if (err) return callback(err)
createToken(gToken, account, callback)
})
})
}
}
/**
Identify account from token
@memberof module:protector.Treant
@param {string} token - Token from client
@param {boolean} getInfo - Decide query database to retrieve account or not
@param {StdCallback} callback
- Function will be call after done, result is object
- Result contains field `token`. This is token is decoded
- If getInfo is true, result contains field `account` information
@example
// see constructor for detail
var treant = new Treant(config)
treant.identity(token, true, function(err, res) {
if (err) return
// do some thing with account here
var account = res.account
// do some thing with token decoded here
var token = res.token
})
*/
prototype.identify = function (token, getInfo, callback) {
const self = this
jwt.verify(token, self._cert, function (err, decoded) {
if (err) return callback(err)
if (!getInfo) return callback(null, {token: decoded})
self._accountCol.findOne({_id: ObjectID(decoded.id)}, function (err, acc) {
callback(err, {token: decoded, account: acc})
})
})
}
/**
Options to authorize
@memberof module:protector
@typedef {object} AuthOptions
@property {boolean} [getAcc=false] - if true, retrieve account from database
and assign to req.account
@property {string} [group=null]
Require account must in specify group
If this field is null, no verify group of account
@property {string} [msg401=must login to perform this action]
Message responses to endpoint if authenticate is not successful
@property {string} [msg403=must have correct permission to perform this action]
Message responses to endpoint if authorize is not successful
@example
var authOptions = {
group: 'root',
msg403: 'must have root permission to perform this action'
}
*/
/**
This function return an express middleware allow authenticate or authorize
depend on options
- If authorize successful, continue next route
- If authorize fails, response message to endpoint and terminate
@memberof module:protector.Treant
@param {module:protector.AuthOptions} [options=null]
Options for authorization
If parameter is not specify, use default options
@return {module:express.Middleware}
@example
const express = require('express')
// see constructor for detail
const treant = new Treant(config)
const app = express()
app.use('/root', treant.inspect(group: 'root'), function(req, res) {
// if this code block is reached, account in root group is logged in
// if this code block is not reached, no account in root group is logged in
var rootAccount = req.account
// do some thing here
})
app.use('/account', treant.inspect(), function(req, res) {
// if this code block is reach, an account is logged in
// if this code block is not reached, no account is logged in
var account = req.account
// do some thing here
})
*/
prototype.check = function (options) {
const self = this
if (!options) options = {}
var getAcc = options && options.getAcc
var group = options && options.group
if (!options.msg401) options.msg401 = 'must login to perform this action'
if (!options.msg403) {
options.msg403 = 'must have correct permission to perform this action'
}
return function (req, res, next) {
var token = req.headers['authorization']
if (!token) return res.status(401).json({message: options.msg401})
self.identify(token, getAcc, function (err, ires) {
if (err) return res.status(500).json(err)
if (!ires.account) {
return res.status(500).json({
message: 'can not find account in database'
})
}
if (getAcc) req.account = ires.account
req.token = ires.token
if (group) {
if (ires.account.groups.indexOf(group) < 0) {
return res.status(403).json({message: options.msg_403})
}
}
next()
})
}
}
/**
Contains method to authenticate and authorize
@exports protector
@author kevin leptons <kevin.leptons@gmail.com>
@example
const protector = require('./lib/protector')
var treant = protector({
db: dbclient,
gClientId: '327834646153-27e424bo1lpofea0lkfs8bvrf7sv5j2e.apps.googleusercontent.com',
gClientSecret: 'y7RCBAr4AT26oACJBJUSJIxv',
gRedirectUrl: 'http://localhost:9001/v1/oauth/login',
gScopes: [
'https://www.googleapis.com/auth/plus.me',
'profile',
'email'
],
privateKeyPath: '/usr/lib/node_module/gwisp/gwisp.key',
tokenTimeout: 1800
})
*/
module.exports = function (config) {
return new Treant(config)
}