first commit

This commit is contained in:
Jeonghwa Koo
2021-07-13 17:02:10 +09:00
commit 4533b63108
21 changed files with 896 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
package-lock.json

BIN
README.md Normal file

Binary file not shown.

14
index.js Normal file
View 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
View 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
View 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

View 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
View 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
}

View 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

View 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

View 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

View File

@@ -0,0 +1,12 @@
class MemberService {
constructor() {
}
areUserCredentialsValid(userName, password, scope, callback) {
return callback(null, true);
}
}
module.exports = new MemberService

View 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;

View 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;

View 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
};

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,6 @@
module.exports = {
PASSWORD: 'password',
IMPLICIT: 'implict',
AUTHORIZATIONCODE: 'authorization_code',
CLIENTCREDENTIALS: 'client_credentials'
};

View 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'))
};

View 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;

View 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