UNPKG

k2hr3-app

Version:

K2HR3 Web Application is K2hdkc based Resource and Roles and policy Rules

662 lines (611 loc) 20.5 kB
/* * * K2HR3 Web Application * * Copyright 2017 Yahoo Japan Corporation. * * K2HR3 is K2hdkc based Resource and Roles and policy Rules, gathers * common management information for the cloud. * K2HR3 can dynamically manage information as "who", "what", "operate". * These are stored as roles, resources, policies in K2hdkc, and the * client system can dynamically read and modify these information. * * For the full copyright and license information, please view * the license file that was distributed with this source code. * * AUTHOR: Hirotaka Wakabayashi * CREATE: Tue Aug 07 2019 * REVISION: * */ //------------------------------------------------------------------------ // Usage //------------------------------------------------------------------------ // To enable this OpenId Connect(OIDC), make the following settings in the // K2HR3 APP configuration file(ex, production.json/local.json/etc). // // To enable this OIDC, register this module as an 'extrouter'. // Then set the keys and values shown in the example below: // // 'extrouter': { // 'oidc': { <---- default name // 'name': 'oidc', // 'path': '/oidc', // 'config': { // 'displayName': 'Default OpenID Connect' // 'debug': true, // 'logoutUrl': '<URL for logout processing>/oidc/logout', // 'mainUrl': '<URL of K2HR3 APP top>', // 'oidcDiscoveryUrl': '<OpenId Connect Issuer URL>', // 'params': { // 'client_secret': '<OpenId Connect Client Secret>', // 'client_id': '<OpenId Connect Client id>', // 'redirectUrl': '<URL for receiving redirect from oidc process>/oidc/login/cb', // 'usernamekey': '<username key name in oidc token>', // 'cookiename': '<cookie name for saving oidc token>', // 'cookieexpire': '<expire time for oidc token cookie>' // }, // 'scope': '<scope>' // } // }, // 'oidc@other': { // 'name': 'oidc', // 'path': '/oidc@other', // 'config': { // 'displayName': 'OpenID Connect to Other' // 'debug': true, // 'logoutUrl': '<URL for logout processing>/oidc@other/logout', // 'mainUrl': '<URL of K2HR3 APP top>', // 'oidcDiscoveryUrl': '<OpenId Connect Issuer URL>', // 'params': { // 'client_secret': '<OpenId Connect Client Secret>', // 'client_id': '<OpenId Connect Client id>', // 'redirectUrl': '<URL for receiving redirect from oidc process>/oidc@other/login/cb', // 'usernamekey': '<username key name in oidc token>', // 'cookiename': '<cookie name for saving oidc token>', // 'cookieexpire': '<expire time for oidc token cookie>' // }, // 'scope': '<scope>' // } // }, // ... // ... // }, // // [NOTE] // The 'oidc' object is required and used as the default OIDC authorization. // If you have "other" objects, you can use them for its OIDC authentication // logic. // The 'name' field must be 'oidc' to recognize it as 'oidc'. The 'redirectUrl' // and 'logoutUrl' should be '<extrouter name>/login/cb' and '<extrouter name>/logout' // (<extrouter name> is such as 'oidc' or 'oidc@other'). // The 'URL PATH' must always match one of 'extrouter name'. // // Each OIDC setting item has the following format: // // '<oidc name>': { // 'name': 'oidc', // 'path': '/<oidc name>', // 'config': { // 'displayName': '<display name for K2HR3 APP Menu>' // 'debug': <true or false>, // 'logoutUrl': '<K2HR3 APP Server Host name and port>/<oidc name>/logout', // 'mainUrl': '<K2HR3 APP Server Host name and port>', // 'oidcDiscoveryUrl': '<OpenId Connect Issuer URL>', // 'params': { // 'client_secret': '<OpenId Connect Client Secret>', // 'client_id': '<OpenId Connect Client id>', // 'redirectUrl': '<K2HR3 APP Server Host name and port>/<oidc name>/login/cb', // 'usernamekey': '<username key name in oidc token>', // 'cookiename': '<cookie name for saving oidc token>', // 'cookieexpire': '<expire time for oidc token cookie>' // }, // 'scope': '<scope>' // } // }, // // A description of each item is shown below: // // [oidc name] // A unique name for each OIDC. (Do not include space characters // whenever possible) // Note that other values should also match this name string in places. // // [name] // For this OIDC authentication, specify 'oidc'. // // [path] // This path will be the entry point on server for OIDC authentication. // Be sure to specify '/<oidc name>'. // For example, if 'oidc name' is 'oidc', it should be set '/oidc'. // // [config] // An object of configuration for this <oidc name> module. // // [displayName] // If there are multiple OIDC authentication settings, they should // be distinguished in the 'Sign in' menu of the K2HR3 APP. // Then the 'Sign in' menu will have a submenu and this 'displayName' // will be the submenu name. // This item can be omitted, and if omitted, '<oidc name>' will be used. // If there is only one OIDC authentication setting, then even if this // value is set, it will not be used for display. // // [debug] // Set true to display the contents of communication with the OpenId // Connect server. // // [logoutUrl] // Specifies the entry point for logout processing. // Please specify K2HR3 APP server name including schema, path // including port number. // The path must always include '<oidc name>/logout'. // For example, 'https://k2hr3-app:3000/<oidc name>/logout'. // // [mainUrl] // Specify the URL of the K2HR3 APP top page. // For example, 'https://k2hr3-app:3000/'. // // [oidcDiscoveryUrl] // Specify the Issuer URL for OpenId Connect. // // [params] // An object of some parameters for this module. // // [client_secret] // Specify the client Secret for OpenId Connect. // // [client_id] // Specify the Client Id for OpenId Connect. // // [redirectUrl] // Specifies the entry point for login callback processing. // Please specify K2HR3 APP server name including schema, path // including port number. // The path must always include '<oidc name>'. // For example, 'https://k2hr3-app:3000/<oidc name>/login/cb'. // // [usernamekey] // If there is a key indicating the user name in the Payload of the // Token returned by OpenId Connect, specify that key name. // If there is no key, it can be omitted(not specified). // If omitted, the value of the 'sub' key in Payload will be used as // the user name. // // [cookiename] // Specifies the cookie name for temporarily storing the token // returned by OpenId Connect. This authentication process using // OpenId Connect uses a cookie to take over the token to the // redirect destination. // Please specify the name of this cookie. If omitted, 'id_token' // will be used. // // [cookieexpire] // Specify the cookie validity time in seconds on this page. // If omitted, set to 60 seconds. // // [scope] // Specify 'openid profile email' value for this key. // //------------------------------------------------------------------------ 'use strict'; var r3util = require('./lib/libr3util'); var express = require('express'); var router = express.Router(); var passport = require('passport'); var session = require('express-session'); var { custom, Issuer, Strategy }= require('openid-client'); var { decode } = require('jose').base64url; var { createRemoteJWKSet } = require('jose'); var { jwtVerify } = require('jose'); // // Configration for OIDC // var oidcConfig = {}; var oidcConfigCookieName= 'oidc_config_name'; // // Setup session // // https://github.com/expressjs/session#sessionoptions // router.use(session({ secret: 'k2hr3-oidc-session', resave: false, saveUninitialized: false, cookie: { httpOnly: true, maxAge: 12 * 60 * 60 * 1000, // 12H } })); // // Setup middleware(passport) // router.use(passport.initialize()); router.use(passport.session()); //-------------------------------------------------------------- // Utility //-------------------------------------------------------------- function rawGetExtRouterName(req) { if(!r3util.isSafeEntity(req) || !r3util.isSafeString(req.baseUrl)){ console.error('Request base URL is somthing wrong, but returns default extrouter name(oidc).'); return 'oidc'; // default } var urlparts = decodeURI(req.baseUrl).split('/'); if(!r3util.isArray(urlparts)){ console.error('Request base URL is somthing wrong, but returns default extrouter name(oidc).'); return 'oidc'; // default } // // Try to find '.../<extrouter name>/login/...' or '.../<extrouter name>/logout/...' // var extRounterName = null; for(var cnt = 0; cnt < urlparts.length; ++cnt){ if(!r3util.isSafeString(urlparts[cnt])){ continue; } if(r3util.compareCaseString(urlparts[cnt], 'login') || r3util.compareCaseString(urlparts[cnt], 'logout')){ break; } extRounterName = urlparts[cnt]; } if(!r3util.isSafeString(extRounterName)){ console.error('Failed to extract extRouter name from base URL(' + req.baseUrl + '), so returns default extrouter name(oidc).'); return 'oidc'; // default } return extRounterName; } //-------------------------------------------------------------- // Mountpath : /<config path>/login //-------------------------------------------------------------- // // URL: /<config path>/login // // // Login async function // async function oidcLogin(Request) { var extRouterName = rawGetExtRouterName(Request); if(!r3util.isSafeEntity(oidcConfig[extRouterName])){ var error = new Error('Please check your configuarion(json) because it is invalid.'); console.error(error.message); throw error; } // // Create openid client // // Issuer.discovery returns Promise // https://github.com/panva/node-openid-client/blob/main/lib/issuer.js#L210 // var oidcDiscovery = Issuer.discover(oidcConfig[extRouterName].oidcDiscoveryUrl); // // Try to login // await oidcDiscovery.then(function(oidcIssuer){ // // put debug message // if(r3util.isSafeBoolean(oidcConfig[extRouterName].debug) && oidcConfig[extRouterName].debug){ // debug message console.log('[OIDC debug] Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata); // hook for debugging custom.setHttpOptionsDefaults({ hooks: { beforeRequest: [ function(options){ console.log('[OIDC debug] Request URL : %s %s', options.method.toUpperCase(), options.url.href); console.log('[OIDC debug] Request HEADERS : %o', options.headers); if(options.body){ console.log('[OIDC debug] Request BODY : %s', options.body); } } ], afterResponse: [ function(response){ console.log('[OIDC debug] Response URL : %s %s', response.request.options.method.toUpperCase(), response.request.options.url.href); console.log('[OIDC debug] Response STATUS : %i', response.statusCode); console.log('[OIDC debug] Response HEADERS : %o', response.headers); if (response.body) { console.log('[OIDC debug] Response BODY : %s', response.body); } return response; } ] } }); } // // Create a client handler // var clientParams = { client_id: oidcConfig[extRouterName].params.client_id, client_secret: oidcConfig[extRouterName].params.client_secret, redirect_uris: [ oidcConfig[extRouterName].params.redirectUrl ] }; var client = new oidcIssuer.Client(clientParams); client[custom.clock_tolerance] = 5; // to allow a second 5 skew // // Calls passport middleware // passport.use( 'oidc', new Strategy( { client }, function(tokenset, done){ return done(null, tokenset.id_token); } ) ); }).catch(function(error){ console.error('Authenticate discovery Error by ' + error.message); throw error; }); } // // Login by calling passport.authenticate // var authenticate = async function(Request, Response, Next) { var extRouterName = rawGetExtRouterName(Request); // // Login by invoking passport middleware to get an token // await oidcLogin(Request, Response, Next); // // Create and return Promise object // return new Promise(function(resolve, reject){ passport.authenticate( 'oidc', { scope: oidcConfig[extRouterName].scope }, function(error, token){ if(error){ reject(error); } resolve(token); })(Request, Response, Next); }).catch(function (error){ console.error('Authenticate passport.authenticate Error by ' + error.message); Response.redirect('/'); }); }; // // GET '/<config path>/login' : OpenID Connect Provider's endpoint // router.get('/login', authenticate); //-------------------------------------------------------------- // Mountpath : /<config path>/login/cb //-------------------------------------------------------------- // // Utility function for OIDC authentication // async function oidcAuthenticate(Request, Response, Next) { var token = await authenticate(Request, Response, Next).catch(function(error){ console.error(error.message); throw error; }); return token; } // // Utility function for token // async function oidcVerifyToken(token, extRouterName) { var jwtParam = { issuer: oidcConfig[extRouterName].oidcDiscoveryUrl, audience: oidcConfig[extRouterName].params.client_id }; var strurl = oidcConfig[extRouterName].oidcDiscoveryUrl + '/keys'; var JWKS = createRemoteJWKSet(new URL(strurl)); await jwtVerify(token, JWKS, jwtParam).catch(function(error){ console.error(error.message); throw error; }); } // // Authentication // var sessionize = async function(Request, Response, Next) { var extRouterName = rawGetExtRouterName(Request); if(!r3util.isSafeEntity(oidcConfig) || !r3util.isSafeEntity(oidcConfig[extRouterName])){ var error = 'Please check your configuarion(json) because it is invalid.'; console.error('Failed to sessionize init, ' + error); Response.status(500); // 500: Internal Server Error return; } // // Get oidc token in request // await oidcAuthenticate(Request, Response, Next).then(async function(oidc_token) { // // get payload in oidc token // var parts = oidc_token.split('.', 2); if(2 > parts.length){ var error = 'Failed to parse payload from oidc token.'; console.error(error); Response.status(401); // 401: Unauthorized return; } var raw_payload = new TextDecoder().decode(decode(parts[1])); if(!r3util.isSafeJSON(raw_payload)){ error = 'Failed to decode json payload from oidc token.'; console.error(error); Response.status(401); // 401: Unauthorized return; } var payload = JSON.parse(raw_payload); // // put debug message // if(r3util.isSafeBoolean(oidcConfig[extRouterName].debug) && oidcConfig[extRouterName].debug){ console.log('[OIDC debug] payload = ' + JSON.stringify(payload)); } // // check user name key // if(r3util.isSafeString(oidcConfig[extRouterName].params.usernamekey)){ var found_key = false; Object.keys(payload).forEach(function(onekey){ if(onekey == oidcConfig[extRouterName].params.usernamekey){ found_key = true; } }); if(!found_key || !r3util.isSafeString(payload[oidcConfig[extRouterName].params.usernamekey])){ error = 'Not find or empty user name in oidc token.'; console.error(error); Response.status(401); // 401: Unauthorized return; } } // // Verify token // await oidcVerifyToken(oidc_token, extRouterName).then(function() { // // oidc token verified // // sessionize Response.session = null; // session removed // token cookie Response.cookie(oidcConfig[extRouterName].params.cookiename, oidc_token, { httpOnly: true, secure: Request.protocol === 'https', maxAge: oidcConfig[extRouterName].params.cookieexpire * 1000, // set expire }); // oidc name cookie Response.cookie(oidcConfigCookieName, extRouterName, { httpOnly: true, secure: Request.protocol === 'https', maxAge: oidcConfig[extRouterName].params.cookieexpire * 1000, // set expire }); Response.redirect('/'); }).catch(function(err){ console.error('Failed to verify oidc token by ' + err.message); Response.status(401); // 401: Unauthorized return; }); }).catch(function(err){ error = 'Failed to get oidc token in request.' + err.message; console.error(error); Response.status(401); // 401: Unauthorized return; }); }; // // GET '/<config path>/login/cb' : Login callback url // // URL Arguments // extrouter : <extrouter name> // router.get('/login/cb', sessionize); //-------------------------------------------------------------- // Mountpath : /<config path>/logout //-------------------------------------------------------------- // // GET '/<config path>/logout' : logout for OIDC // // URL Arguments // extrouter : <extrouter name> // router.get('/logout', function(Request, Response, Next) // eslint-disable-line no-unused-vars { var extRouterName = rawGetExtRouterName(Request); if(!r3util.isSafeEntity(oidcConfig[extRouterName])){ var error = 'Please check your configuarion(json) because it is invalid.'; console.error('Failed logout processing, ' + error); Response.status(500); // 500: Internal Server Error Response.send(error); return; } // // Cleanup : clear the cookie if exist // Response.clearCookie(oidcConfig[extRouterName].params.cookiename); // cookie name(id_token as deafult) Response.clearCookie(oidcConfigCookieName); // cookie name(oidc config name) Response.redirect(oidcConfig[extRouterName].mainUrl); return; }); //-------------------------------------------------------------- // setConfig //-------------------------------------------------------------- // // setConfig is called in app.js to set configurations that are // defined in configuration file(json) // var setConfig = function(config, extRouterName) { // check required member in config if( !r3util.isSafeEntity(config) || !r3util.isSafeEntity(config.oidcDiscoveryUrl) || !r3util.isSafeEntity(config.logoutUrl) || !r3util.isSafeEntity(config.mainUrl) || !r3util.isSafeEntity(config.params) || !r3util.isSafeEntity(config.params.client_secret) || !r3util.isSafeEntity(config.params.client_id) || !r3util.isSafeEntity(config.params.redirectUrl) ) { console.error('Please check your configuarion(json) because it is invalid : config = ' + JSON.stringify(config)); return false; } if(!r3util.isSafeString(extRouterName)){ console.error('Please check your configuarion(json) because it does not have ' + JSON.stringify(extRouterName) + ' entity or it is empty.'); return false; } if(r3util.isSafeEntity(oidcConfig[extRouterName])){ console.error('Please check your configuarion(json) because it has multi ' + JSON.stringify(extRouterName) + ' entities.'); return false; } oidcConfig[extRouterName] = config; if(!r3util.isSafeEntity(oidcConfig[extRouterName].params.usernamekey)){ console.warn('The key name in configuration(usernamekey) is empty, then it check will no longer be performed.'); } if(!r3util.isSafeEntity(oidcConfig[extRouterName].params.cookiename)){ console.warn('The cookie name in configuration(cookiename) is empty, so id_token is used as default.'); oidcConfig[extRouterName].params.cookiename = 'id_token'; } if(!r3util.isSafeEntity(oidcConfig[extRouterName].params.cookieexpire) || 'number' != typeof oidcConfig[extRouterName].params.cookieexpire){ console.warn('The cookie expire(sec) in configuration(cookieexpire) is empty, so id_token is used as default.'); oidcConfig[extRouterName].params.cookieexpire = 60; // 60 sec as default } return true; }; //-------------------------------------------------------------- // getConfig //-------------------------------------------------------------- // // getConfig returns configurations that are defined in // configuration file(json) // var getConfig = function() { if(!r3util.isSafeEntity(oidcConfig)){ console.error('Please check your configuarion(json) because it is invalid.'); return null; } return oidcConfig; }; //--------------------------------------------------------- // Exports //--------------------------------------------------------- module.exports = { router: router, setConfig: setConfig, getConfig: getConfig, oidcConfigCookieName: oidcConfigCookieName }; /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: * vim600: noexpandtab sw=4 ts=4 fdm=marker * vim<600: noexpandtab sw=4 ts=4 */