first commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
14
index.js
Normal file
14
index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const express = require('express')
|
||||
, router = require('./router')
|
||||
, { logger, expressLogger } = require('./src/component/Logger')
|
||||
, port = 33100
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
app.use(expressLogger)
|
||||
app.listen(port, () => {
|
||||
logger.info('Server listening on port ' + port);
|
||||
});
|
||||
app.use('/', router);
|
||||
|
||||
|
||||
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "spd-app-gateway",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"express-http-proxy": "^1.5.1",
|
||||
"http-status-codes": "^2.1.4",
|
||||
"kgo": "^4.0.3",
|
||||
"uuid": "^8.3.1",
|
||||
"@elastic/ecs-winston-format": "^1.1.0",
|
||||
"amqplib": "^0.8.0",
|
||||
"express-winston": "^4.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node app/index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
31
router.js
Normal file
31
router.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const router = require("express").Router()
|
||||
, env = process.env.NODE_ENV || 'development'
|
||||
, proxy = require('express-http-proxy')
|
||||
, gatewayService = require('./src/service/GatewayService')
|
||||
|
||||
|
||||
// a middleware function with no mount path. This code is executed for every request to the router
|
||||
router.use(function (req, res, next) {
|
||||
// logger.info("Request body", {err: req.body});
|
||||
// logger.error('oops there is a problem', { foo: 'bar' })
|
||||
// logger.warn('responsebbbbb ', req.body)
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/', function (req, res) {
|
||||
res.json(req.headers)
|
||||
});
|
||||
|
||||
|
||||
router.use('/api/v1/andygrace', proxy('spider-andygrace:33103', { filter: (req, res) => gatewayService.proxyFilter(req, res) }))
|
||||
|
||||
router.get('/api/v1/heartbeat', (request, response) => gatewayService.heartbeat(request, response))
|
||||
|
||||
router.get('/oauth/authorize', (request, response) => gatewayService.authorize(request, response))
|
||||
|
||||
router.get('/api/v1/oauth/token', (request, response) => gatewayService.grantToken(request, response))
|
||||
router.get('/oauth/token', (request, response) => gatewayService.grantToken(request, response))
|
||||
|
||||
router.get('/api/test', (request, response) => gatewayService.apiEndpoint(request, response))
|
||||
|
||||
module.exports = router
|
||||
27
src/component/AmqpTransporter.js
Normal file
27
src/component/AmqpTransporter.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const { URL } = require('url');
|
||||
const TransportStream = require('winston-transport');
|
||||
const hostname = require('os').hostname;
|
||||
const amqplib = require('amqplib');
|
||||
const config = {
|
||||
name: 'rabbitmq-logger',
|
||||
level: 'info',
|
||||
logToConsole: false,
|
||||
url: process.env.WINSTON_RABBITMQ_URL || 'amqp://ibpcorp:KjUuC754D5xTSpqf@localhost:49072/ibpcorp',
|
||||
socketOpts: {},
|
||||
exchange: 'logs',
|
||||
exchangeOptions: {
|
||||
durable: true,
|
||||
autoDelete: false
|
||||
},
|
||||
routingKey: 'ibp_logs',
|
||||
timestamp: function () {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
class AmqpTransport extends TransportStream {
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = AmqpTransport
|
||||
51
src/component/Logger.js
Normal file
51
src/component/Logger.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const express = require('express')
|
||||
, winston = require('winston')
|
||||
, { StatusCodes, getReasonPhrase } = require('http-status-codes')
|
||||
, expressWinston = require('express-winston')
|
||||
, ecsFormat = require('@elastic/ecs-winston-format')
|
||||
, AmqpTransport = require('./AmqpTransporter')
|
||||
|
||||
|
||||
// const amqpTransporter = new AmqpTransport({})
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: ecsFormat({ convertReqRes: true }),
|
||||
defaultMeta: { service: { name: 'Spiduler.Gateway' } },
|
||||
transports: [
|
||||
//
|
||||
// - Write all logs with level `error` and below to `error.log`
|
||||
// - Write all logs with level `info` and below to `combined.log`
|
||||
//
|
||||
// new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
||||
// new winston.transports.File({ filename: 'combined.log' }),
|
||||
new winston.transports.Console(),
|
||||
// amqpTransporter
|
||||
]
|
||||
});
|
||||
|
||||
// if (process.env.NODE_ENV !== 'production') {
|
||||
// logger.add(new winston.transports.Console({
|
||||
// format: winston.format.simple(),
|
||||
// }));
|
||||
// }
|
||||
|
||||
const expressLogger = expressWinston.logger({
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
// amqpTransporter
|
||||
],
|
||||
format: ecsFormat(),
|
||||
baseMeta: { service: { name: 'feature' } },
|
||||
meta: false, // optional: control whether you want to log the meta data about the request (default to true)
|
||||
metaField: 'service',
|
||||
msg: "HTTP {{req.method}} {{req.url}}", // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}"
|
||||
expressFormat: false, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true
|
||||
colorize: false, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red).
|
||||
ignoreRoute: function (req, res) { return false; } // optional: allows to skip some log messages based on request and/or response
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
logger: logger,
|
||||
expressLogger: expressLogger
|
||||
}
|
||||
27
src/service/AuthService.js
Normal file
27
src/service/AuthService.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const tokenService = require('./TokenService')
|
||||
|
||||
class AuthService {
|
||||
constructor() {
|
||||
this.authCodes = {}
|
||||
}
|
||||
|
||||
saveAuthorizationCode(codeData, callback) {
|
||||
this.authCodes[codeData.code] = codeData;
|
||||
return callback(null, this.authCodes[codeData.code]);
|
||||
}
|
||||
|
||||
saveAccessToken(tokenData, callback) {
|
||||
tokenService.accessTokens[tokenData.access_token] = tokenData;
|
||||
return callback(null, tokenService.accessTokens[tokenData.access_token]);
|
||||
}
|
||||
|
||||
getAuthorizationCode(code, callback) {
|
||||
return callback(null, this.authCodes[code]);
|
||||
}
|
||||
|
||||
getAccessToken(token, callback) {
|
||||
return callback(null, tokenService.accessTokens[token]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthService
|
||||
30
src/service/ClientService.js
Normal file
30
src/service/ClientService.js
Normal file
@@ -0,0 +1,30 @@
|
||||
class ClientService {
|
||||
constructor() {
|
||||
this.clients = {
|
||||
'sTdnvCh328AeZau9': {
|
||||
id: '1',
|
||||
secret: 'vv7WzsN72xK88cctBqUWBYTu9Y5wnV2Gf6mhe65ZxMQSJRdYywVANbzJq6U37SmQ',
|
||||
grantTypes: ['client_credentials']
|
||||
},
|
||||
'dev0vCh328AeZkx0': {
|
||||
id: '2',
|
||||
secret: 'PdX42vApUkh2JBuVZ6cFvjtezxQ6V4VbymnDpyvNSrFMTuNrFPg8u49YrfD9DTq9',
|
||||
grantTypes: ['client_credentials']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getClients() {
|
||||
return this.clients;
|
||||
}
|
||||
|
||||
getById(id, callback) {
|
||||
return callback(null, this.clients[id]);
|
||||
}
|
||||
|
||||
isValidRedirectUri(/*client, requestedUri*/) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ClientService
|
||||
91
src/service/GatewayService.js
Normal file
91
src/service/GatewayService.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const OAuthServer = require('./OAuth')
|
||||
, supportedScopes = ['profile', 'status', 'avatar']
|
||||
, authService = require('./AuthService')
|
||||
, tokenService = require('./TokenService')
|
||||
, memberService = require('./MemberService')
|
||||
, clientService = require('./ClientService')
|
||||
, logger = require('../component/Logger').logger
|
||||
, { StatusCodes, getStatusText } = require('http-status-codes')
|
||||
|
||||
|
||||
class GatewayService {
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
token: {
|
||||
expiresIn: 3600
|
||||
},
|
||||
authorize: {
|
||||
enabled: true,
|
||||
isServerSide: true
|
||||
},
|
||||
proxy: {
|
||||
hostname: '127.0.0.1'
|
||||
}
|
||||
}
|
||||
this.oauth = new OAuthServer(
|
||||
clientService,
|
||||
tokenService,
|
||||
authService,
|
||||
memberService,
|
||||
this.config.token.expiresIn,
|
||||
supportedScopes
|
||||
)
|
||||
}
|
||||
|
||||
authorize(request, response) {
|
||||
if (this.config.authorize.enabled) {
|
||||
this.oauth.authorizeRequest(request, 'accountid', (error, authorizationResult) => {
|
||||
if (error) {
|
||||
logger.error({ message: error.message }, 'authorization error')
|
||||
response.statusCode = 400;
|
||||
return response.end(JSON.stringify(error));
|
||||
}
|
||||
if (this.config.authorize.isServerSide) {
|
||||
response.json(authorizationResult);
|
||||
} else {
|
||||
var code = require('./OAuth/lib/node_modules/url').parse(authorizationResult.redirectUri, true).query.code;
|
||||
response.statusCode = 302;
|
||||
response.setHeader('Location', 'http://localhost:8080/oauth/token?client_id=1&grant_type=authorization_code&client_secret=kittens&code=' + code);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
response.statusCode = StatusCodes.NOT_IMPLEMENTED;
|
||||
response.json({ code: StatusCodes.NOT_IMPLEMENTED, message: getStatusText(StatusCodes.NOT_IMPLEMENTED) });
|
||||
}
|
||||
}
|
||||
|
||||
grantToken(request, response) {
|
||||
this.oauth.grantAccessToken(request, function (error, token) {
|
||||
if (error) {
|
||||
logger.error({ message: error.message }, 'Grant access token error')
|
||||
response.statusCode = 400;
|
||||
return response.json(error);
|
||||
}
|
||||
response.json(token);
|
||||
});
|
||||
}
|
||||
|
||||
apiEndpoint(request, response) {
|
||||
this.oauth.validateAccessToken(request, validationResult => {
|
||||
if (!validationResult.isValid) {
|
||||
return response.json({ code: StatusCodes.NOT_ACCEPTABLE, message: getStatusText(StatusCodes.NOT_ACCEPTABLE) });
|
||||
}
|
||||
response.json({ code: StatusCodes.OK, message: getStatusText(StatusCodes.OK), data: validationResult });
|
||||
})
|
||||
}
|
||||
|
||||
proxyFilter(req, res) {
|
||||
return req.hostname == this.config.proxy.hostname ? true : new Promise(resolve => {
|
||||
this.oauth.validateAccessToken(req, validationResult => {
|
||||
if (!validationResult.isValid) {
|
||||
res.json({ code: StatusCodes.NOT_ACCEPTABLE, message: getStatusText(StatusCodes.NOT_ACCEPTABLE) })
|
||||
}
|
||||
resolve(validationResult.isValid)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = new GatewayService
|
||||
12
src/service/MemberService.js
Normal file
12
src/service/MemberService.js
Normal file
@@ -0,0 +1,12 @@
|
||||
class MemberService {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
areUserCredentialsValid(userName, password, scope, callback) {
|
||||
return callback(null, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = new MemberService
|
||||
40
src/service/OAuth/index.js
Normal file
40
src/service/OAuth/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
var lib = require('./lib');
|
||||
|
||||
function AuthServer(clientService, tokenService, authorizationService, membershipService, expiresIn, supportedScopes) {
|
||||
var authServer = this;
|
||||
|
||||
if(!(authServer instanceof AuthServer)) {
|
||||
return new AuthServer(clientService, tokenService, authorizationService, membershipService, expiresIn, supportedScopes);
|
||||
}
|
||||
|
||||
authServer.clientService = clientService;
|
||||
authServer.tokenService = tokenService;
|
||||
authServer.authorizationService = authorizationService;
|
||||
authServer.membershipService = membershipService;
|
||||
authServer.expiresIn = expiresIn || 3600;
|
||||
authServer.supportedScopes = supportedScopes ? supportedScopes : [];
|
||||
}
|
||||
|
||||
AuthServer.prototype.getExpiresDate = function () {
|
||||
return new Date(Date.now() + this.expiresIn * 1000);
|
||||
};
|
||||
|
||||
AuthServer.prototype.isSupportedScope = function (scopes) {
|
||||
if(!Array.isArray(scopes)){
|
||||
scopes = [scopes];
|
||||
}
|
||||
|
||||
for(var i = 0; i < scopes.length; i++){
|
||||
if(!~this.supportedScopes.indexOf(scopes[i])){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
AuthServer.prototype.authorizeRequest = lib.authorizeRequest;
|
||||
AuthServer.prototype.getTokenData = lib.getTokenData;
|
||||
AuthServer.prototype.grantAccessToken = lib.grantAccessToken;
|
||||
AuthServer.prototype.validateAccessToken = lib.validateAccessToken;
|
||||
|
||||
module.exports = AuthServer;
|
||||
118
src/service/OAuth/lib/authorizeRequest.js
Normal file
118
src/service/OAuth/lib/authorizeRequest.js
Normal file
@@ -0,0 +1,118 @@
|
||||
var errors = require('./errors'),
|
||||
url = require('url');
|
||||
|
||||
function buildAuthorizationUri(context, expiresIn, code, token) {
|
||||
var redirect = url.parse(context.redirect_uri, true);
|
||||
|
||||
delete redirect.search;
|
||||
|
||||
if (context.scope) {
|
||||
redirect.query.scope = context.scope.join(',');
|
||||
}
|
||||
|
||||
if (context.state) {
|
||||
redirect.query.state = context.state;
|
||||
}
|
||||
|
||||
if (expiresIn) {
|
||||
redirect.query.expires_in = expiresIn;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
redirect.query.code = code;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
redirect.query.access_token = token;
|
||||
redirect.query.token_type = 'Bearer';
|
||||
}
|
||||
|
||||
return url.format(redirect);
|
||||
}
|
||||
|
||||
function authorizeRequestWithClient(authServer, client, context, accountId, callback) {
|
||||
if (!client) {
|
||||
return callback(errors.invalidClient(context));
|
||||
}
|
||||
|
||||
if (!context.redirect_uri || !authServer.clientService.isValidRedirectUri(client, context.redirect_uri)) {
|
||||
return callback(errors.redirectUriMismatch(context));
|
||||
}
|
||||
|
||||
function finalResponse(error, data) {
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
callback(
|
||||
null,
|
||||
{
|
||||
redirectUri: buildAuthorizationUri(context, authServer.expiresIn, data.code, data.access_token),
|
||||
state: context
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (context.response_type === 'code') {
|
||||
authServer.tokenService.generateAuthorizationCode(function(error, code){
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
authServer.authorizationService.saveAuthorizationCode({
|
||||
code: code,
|
||||
redirectUri: context.redirect_uri,
|
||||
clientId: client.id,
|
||||
expiresDate: authServer.getExpiresDate(),
|
||||
accountId: accountId
|
||||
}, finalResponse);
|
||||
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.response_type === 'token') {
|
||||
authServer.tokenService.generateToken(function(error, token){
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
authServer.authorizationService.saveAccessToken({
|
||||
clientId: client.id,
|
||||
access_token: token,
|
||||
expires_in: authServer.getExpiresDate(),
|
||||
accountId: accountId,
|
||||
token_type: 'Bearer'
|
||||
}, finalResponse);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
callback(errors.invalidResponseType(context.state));
|
||||
}
|
||||
|
||||
function authorizeRequest(context, accountId, callback) {
|
||||
var authServer = this;
|
||||
|
||||
if (!context || !context.response_type) {
|
||||
return callback(errors.invalidRequest(context));
|
||||
}
|
||||
|
||||
if (context.response_type !== 'token' && context.response_type !== 'code') {
|
||||
return callback(errors.unsupportedResponseType(context));
|
||||
}
|
||||
|
||||
if (!authServer.isSupportedScope(context.scope)) {
|
||||
return callback(errors.invalidScope(context));
|
||||
}
|
||||
|
||||
authServer.clientService.getById(context.client_id, function(error, client){
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
authorizeRequestWithClient(authServer, client, context, accountId, callback);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = authorizeRequest;
|
||||
119
src/service/OAuth/lib/errors.js
Normal file
119
src/service/OAuth/lib/errors.js
Normal file
@@ -0,0 +1,119 @@
|
||||
function invalidRequest(state) {
|
||||
return {
|
||||
error: 'invalid_request',
|
||||
error_description: 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function unauthorizedClient(state) {
|
||||
return {
|
||||
error: 'unauthorized_client',
|
||||
error_description: 'The client is not authorized to request an authorization code using this method.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function accessDenied(state) {
|
||||
return {
|
||||
error: 'access_denied',
|
||||
error_description: 'The resource owner or authorization server denied the request.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function unsupportedResponseType(state) {
|
||||
return {
|
||||
error: 'unsupported_response_type',
|
||||
error_description: 'The authorization server does not support obtaining an authorization code using this method.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function redirectUriMismatch(state) {
|
||||
return {
|
||||
error: 'invalid_request',
|
||||
error_description: 'The redirect URI doesn\'t match what is stored for this client',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function invalidScope(state) {
|
||||
return {
|
||||
error: 'invalid_scope',
|
||||
error_description: 'The requested scope is invalid, unknown, or malformed.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function invalidResponseType(state) {
|
||||
return {
|
||||
error: 'unsupported_response_type',
|
||||
error_description: 'The authorization server does not support this response type.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function clientCredentialsInvalid(state) {
|
||||
return {
|
||||
error: 'unauthorized_client',
|
||||
error_description: 'The client credentials are invalid.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function userCredentialsInvalid(state) {
|
||||
return {
|
||||
error: 'access_denied',
|
||||
error_description: 'The user credentials are invalid.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function unsupportedGrantType(state) {
|
||||
return {
|
||||
error: 'unsupported_grant_type',
|
||||
error_description: 'The authorization grant type is not supported by the authorization server.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function unsupportedGrantTypeForClient(state) {
|
||||
return {
|
||||
error: 'unauthorized_client',
|
||||
error_description: 'The grant type is not supported for this client.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function invalidAuthorizationCode(state) {
|
||||
return {
|
||||
error: 'invalid_grant',
|
||||
error_description: 'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
function invalidClient(state) {
|
||||
return {
|
||||
error: 'invalid_client',
|
||||
error_description: 'Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).',
|
||||
state: state
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
invalidRequest: invalidRequest,
|
||||
unauthorizedClient: unauthorizedClient,
|
||||
accessDenied: accessDenied,
|
||||
unsupportedResponseType: unsupportedResponseType,
|
||||
redirectUriMismatch: redirectUriMismatch,
|
||||
invalidScope: invalidScope,
|
||||
invalidResponseType: invalidResponseType,
|
||||
clientCredentialsInvalid: clientCredentialsInvalid,
|
||||
userCredentialsInvalid: userCredentialsInvalid,
|
||||
unsupportedGrantType: unsupportedGrantType,
|
||||
unsupportedGrantTypeForClient: unsupportedGrantTypeForClient,
|
||||
invalidAuthorizationCode: invalidAuthorizationCode,
|
||||
invalidClient: invalidClient
|
||||
};
|
||||
75
src/service/OAuth/lib/getOauthParameters.js
Normal file
75
src/service/OAuth/lib/getOauthParameters.js
Normal file
@@ -0,0 +1,75 @@
|
||||
var url = require('url'),
|
||||
queryString = require('querystring');
|
||||
|
||||
function getPostData(request, callback){
|
||||
if(!request.readable){
|
||||
|
||||
//In some circumstances the body has already been
|
||||
//parsed. Check the commonly used "body" property.
|
||||
return callback(null, request.body);
|
||||
}
|
||||
|
||||
var data = '';
|
||||
|
||||
request.on('data',function(chunk){
|
||||
if(data.length > (1e6)){
|
||||
// flood attack, kill.
|
||||
return request.connection.destroy();
|
||||
}
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
request.on('end', function(){
|
||||
if (data) {
|
||||
return callback(null, queryString.parse(data));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function getBearerToken(request) {
|
||||
if (request && request.headers && request.headers.authorization &&
|
||||
request.headers.authorization.toLowerCase().indexOf('bearer ') === 0) {
|
||||
return request.headers.authorization.split(' ').pop();
|
||||
}
|
||||
}
|
||||
|
||||
function getOauthParameters(callback) {
|
||||
return function(){
|
||||
var authServer = this,
|
||||
args = Array.prototype.slice.call(arguments),
|
||||
request = args.shift();
|
||||
|
||||
getPostData(request, function(error, data){
|
||||
if(error){
|
||||
// non error callback but non valid context so OAuth errors will be returned.
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(!data){
|
||||
data = {};
|
||||
}
|
||||
|
||||
var query = url.parse(request.url, true).query;
|
||||
|
||||
for(var key in query){
|
||||
data[key] = query[key];
|
||||
}
|
||||
|
||||
if(data.scope){
|
||||
data.scope = data.scope.split(',');
|
||||
}
|
||||
|
||||
if(!data.access_token){
|
||||
data.access_token = getBearerToken(request);
|
||||
}
|
||||
|
||||
args.unshift(data);
|
||||
|
||||
callback.apply(authServer, args);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getOauthParameters;
|
||||
82
src/service/OAuth/lib/getTokenData.js
Normal file
82
src/service/OAuth/lib/getTokenData.js
Normal file
@@ -0,0 +1,82 @@
|
||||
var errors = require('./errors'),
|
||||
grantTypes = require('./grantTypes'),
|
||||
kgo = require('kgo');
|
||||
|
||||
function isValidAuthorizationCode(authorizationCode, context) {
|
||||
return authorizationCode &&
|
||||
context.code === authorizationCode.code &&
|
||||
authorizationCode.expiresDate > new Date() &&
|
||||
'' + context.client_id === '' + authorizationCode.clientId;
|
||||
}
|
||||
|
||||
function getTokenData(context, callback) {
|
||||
var authServer = this,
|
||||
tokenData = {
|
||||
token_type: 'Bearer',
|
||||
expires_in: authServer.getExpiresDate(),
|
||||
clientId: context.client_id
|
||||
};
|
||||
|
||||
if (context.grant_type === grantTypes.AUTHORIZATIONCODE) {
|
||||
authServer.authorizationService.getAuthorizationCode(context.code, function(error, authorizationCode){
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(!isValidAuthorizationCode(authorizationCode, context)){
|
||||
return callback(errors.invalidAuthorizationCode(context));
|
||||
}
|
||||
|
||||
tokenData.accountId = authorizationCode.accountId;
|
||||
kgo
|
||||
('token', authServer.tokenService.generateToken)
|
||||
('refreshToken', authServer.tokenService.generateToken)
|
||||
('tokenData', ['token', 'refreshToken'], function(token, refreshToken, done){
|
||||
tokenData.access_token = token;
|
||||
tokenData.refresh_token = refreshToken;
|
||||
done(null, tokenData);
|
||||
})
|
||||
(['*', 'tokenData'], callback);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.grant_type === grantTypes.PASSWORD) {
|
||||
authServer.membershipService.areUserCredentialsValid(context.username, context.password, context.scope, function (error, isValidPassword) {
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(!isValidPassword){
|
||||
return callback(errors.userCredentialsInvalid(context));
|
||||
}
|
||||
|
||||
kgo
|
||||
('token', authServer.tokenService.generateToken)
|
||||
('refreshToken', authServer.tokenService.generateToken)
|
||||
('tokenData', ['token', 'refreshToken'], function(token, refreshToken, done){
|
||||
tokenData.access_token = token;
|
||||
tokenData.refresh_token = refreshToken;
|
||||
done(null, tokenData);
|
||||
})
|
||||
(['*', 'tokenData'], callback);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.grant_type === grantTypes.CLIENTCREDENTIALS) {
|
||||
kgo
|
||||
('token', authServer.tokenService.generateToken)
|
||||
('tokenData', ['token'], function(token, done){
|
||||
tokenData.access_token = token;
|
||||
done(null, tokenData);
|
||||
})
|
||||
(['*', 'tokenData'], callback);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return callback(errors.unsupportedGrantType(context));
|
||||
}
|
||||
|
||||
module.exports = getTokenData;
|
||||
65
src/service/OAuth/lib/grantAccessToken.js
Normal file
65
src/service/OAuth/lib/grantAccessToken.js
Normal file
@@ -0,0 +1,65 @@
|
||||
var errors = require('./errors'),
|
||||
getTokenData = require('./getTokenData'),
|
||||
grantTypes = require('./grantTypes');
|
||||
|
||||
function isAllowed(grantType, oauthProvider) {
|
||||
return grantType === grantTypes.IMPLICIT ||
|
||||
(grantType === grantTypes.AUTHORIZATIONCODE && oauthProvider.authorizationService) ||
|
||||
(grantType === grantTypes.CLIENTCREDENTIALS && oauthProvider.clientService) ||
|
||||
(grantType === grantTypes.PASSWORD && oauthProvider.membershipService) ||
|
||||
false;
|
||||
}
|
||||
|
||||
function grantAccessToken(context, callback) {
|
||||
var authServer = this;
|
||||
|
||||
if (!context.grant_type) {
|
||||
return callback(errors.invalidRequest(context));
|
||||
}
|
||||
|
||||
if (!isAllowed(context.grant_type, authServer)) {
|
||||
return callback(errors.unsupportedGrantType(context));
|
||||
}
|
||||
|
||||
authServer.clientService.getById(context.client_id, function (error, client) {
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(!client) {
|
||||
return callback(errors.invalidClient(context));
|
||||
}
|
||||
|
||||
if(!client.grantTypes || !~client.grantTypes.indexOf(context.grant_type)) {
|
||||
return callback(errors.unsupportedGrantTypeForClient(context));
|
||||
}
|
||||
|
||||
if(
|
||||
(
|
||||
context.grant_type === grantTypes.AUTHORIZATIONCODE ||
|
||||
context.grant_type === grantTypes.CLIENTCREDENTIALS
|
||||
) &&
|
||||
context.client_secret !== client.secret
|
||||
){
|
||||
return callback(errors.clientCredentialsInvalid(context));
|
||||
}
|
||||
|
||||
getTokenData.call(authServer, context, function (error, tokenData) {
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
authServer.authorizationService.saveAccessToken(tokenData, function (error, token) {
|
||||
if(error){
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
delete token.accountId;
|
||||
delete token.clientId;
|
||||
callback(null, token);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = grantAccessToken;
|
||||
6
src/service/OAuth/lib/grantTypes.js
Normal file
6
src/service/OAuth/lib/grantTypes.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
PASSWORD: 'password',
|
||||
IMPLICIT: 'implict',
|
||||
AUTHORIZATIONCODE: 'authorization_code',
|
||||
CLIENTCREDENTIALS: 'client_credentials'
|
||||
};
|
||||
8
src/service/OAuth/lib/index.js
Normal file
8
src/service/OAuth/lib/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
var getOauthParameters = require('./getOauthParameters');
|
||||
|
||||
module.exports = {
|
||||
authorizeRequest: getOauthParameters(require('./authorizeRequest')),
|
||||
getTokenData: getOauthParameters(require('./getTokenData')),
|
||||
grantAccessToken: getOauthParameters(require('./grantAccessToken')),
|
||||
validateAccessToken: getOauthParameters(require('./validateAccessToken'))
|
||||
};
|
||||
30
src/service/OAuth/lib/validateAccessToken.js
Normal file
30
src/service/OAuth/lib/validateAccessToken.js
Normal file
@@ -0,0 +1,30 @@
|
||||
function validateAccessToken(context, callback) {
|
||||
this.authorizationService.getAccessToken(context.access_token, function (error, tokenData) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (!tokenData || !tokenData.access_token || '' + context.client_id !== '' + tokenData.clientId) {
|
||||
return callback({
|
||||
isValid: false,
|
||||
error: 'Access token not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (tokenData.expires_in < new Date()) {
|
||||
return callback({
|
||||
isValid: false,
|
||||
error: 'Access token has expired'
|
||||
});
|
||||
}
|
||||
|
||||
delete context.access_token
|
||||
callback({
|
||||
isValid: true,
|
||||
accountId: tokenData.accountId,
|
||||
clientId: tokenData.clientId
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = validateAccessToken;
|
||||
42
src/service/TokenService.js
Normal file
42
src/service/TokenService.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const uuid = require('uuid')
|
||||
, logger = require('../component/Logger').logger
|
||||
|
||||
class TokenService {
|
||||
constructor() {
|
||||
this.accessTokens = {}
|
||||
setInterval(() => this.flushExpiredTokens(), 60 * 1000)
|
||||
}
|
||||
|
||||
flushExpiredTokens() {
|
||||
let avoidConflictionTime = 1000 * 5
|
||||
let now = new Date(new Date().getTime() - avoidConflictionTime);
|
||||
let allTokens = Object.keys(this.accessTokens)
|
||||
let oldTokens = allTokens.map(t => this.accessTokens[t].expires_in < now ? t : null).filter(t => t);
|
||||
let oldTokenLen = oldTokens.length
|
||||
if (oldTokenLen) {
|
||||
let lastExpires = this.accessTokens[oldTokens[oldTokenLen - 1]].expires_in
|
||||
oldTokens.map(t => {
|
||||
delete this.accessTokens[t]
|
||||
})
|
||||
logger.info({
|
||||
currentTime: new Date,
|
||||
lastExpired: lastExpires,
|
||||
lastCreated: Object.keys(this.accessTokens).length ? this.accessTokens[allTokens.pop()].expires_in : null
|
||||
}, 'Flush expired tokens execution completed.')
|
||||
}
|
||||
}
|
||||
|
||||
getAccessTokens() {
|
||||
return this.accessTokens;
|
||||
}
|
||||
|
||||
generateToken(callback) {
|
||||
callback(null, uuid.v4());
|
||||
}
|
||||
|
||||
generateAuthorizationCode(callback) {
|
||||
callback(null, uuid.v4());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TokenService
|
||||
Reference in New Issue
Block a user