"use strict"; const cl = global.botanLoader; const Dragonfly = global.Dragonfly; const crypto = require( "crypto" ); const util = require( "util" ); const EventEmitter = require( "events" ).EventEmitter; const http = require( "http" ); const HttpRequest = cl.load( "botanss.net.HttpRequest" ); class SignInEventArgs { constructor({ message, redirect, data = [], success = false } = {}) { this.success = success; this._message = message; this._redirect = redirect; this.data = data } get message() { return this._message; } get location() { return this._redirect + "?" + this.data.join( "&" ); } } class Signin extends EventEmitter { /** Conf structure * { * "spec": process.env.OPENID_SPEC_URI * , "authorization_endpoint_params": { * "client_id": process.env.AZURE_AD_CLIENT_ID * , "redirect_url": `https://${HOST_NAME}/user/login/` * , "nonce": "{RANDSTR}" * , "state": "{RANDSTR}" * , "scope": "openid profile email" * , "response_mode": "form_post" * , "response_type": "id_token" * } * , "end_session_endpoint_params": { * "post_logout_redirect_uri": `https://${HOST_NAME}/user/logout/` * } * * NOTE: * set Signin().RANDSTR = () => SecureRandStr(); to enable custom templated functions * * Only support "^{FUNCTION_NAME}$" for now **/ constructor( conf ) { super(); this.conf = conf; this.spec = null; } Start( action, params, handler ) { var request = new HttpRequest( this.conf.spec ); var _self = this; if( this.spec ) { _self[ action ]( params, handler ); } else { request.addListener( "RequestComplete", function( sender, e ) { if( e.statusCode == 200 ) { _self.spec = JSON.parse( e.ResponseString ); _self[ action ]( params, handler ); } } ); request.Send(); } } ValidateAzureAD( params, handler ) { if( !this.spec ) throw new Error( "OpenID spec is not present" ); var _self = this; var jwt = params[ "id_token" ]; if( !jwt ) { handler( this, new SignInEventArgs({ "message": params[ "error_description" ] }) ); return; } var [ jHeader, jPayload, jSig ] = jwt.split( "." ); var header = JSON.parse( Buffer.from( jHeader, "base64" ) ); var payload = JSON.parse( Buffer.from( jPayload, "base64" ) ); var aud = payload[ "aud" ]; var clientId = this.conf.authorization_endpoint_params.client_id; if( aud !== clientId ) { handler( this, new SignInEventArgs({ message: `aud mismatched: ${aud}` }) ); return; } var [ url, data ] = this._endPointRes( "jwks_uri" ); var request = new HttpRequest( url + "?" + data.join( "&" ) ); request.addListener( "RequestComplete", function( sender, e ) { var eArgs; if( e.statusCode == 200 ) { var respJson = JSON.parse( e.ResponseString ); var keyId = header[ "kid" ]; for( let key of respJson[ "keys" ] ) { if( key[ "kid" ] === keyId ) { var hashFunc = crypto.createVerify({ "RS256": "RSA-SHA256" }[ header[ "alg" ] ]); hashFunc.write( `${jHeader}.${jPayload}` ); hashFunc.end(); var pub = crypto.createPublicKey({ "key": { "kty": key["kty"] , "n": key["n"] , "e": key["e"] }, "format": "jwk" }); eArgs = new SignInEventArgs( { "success": hashFunc.verify( pub, jSig, "base64" ) , "data": payload }); break; } } eArgs = eArgs || new SignInEventArgs({ "message": `Unable to find key: ${keyId}` }); } else { eArgs = new SignInEventArgs({ "message": `Remote returned ${e.statusCode}` }); } handler( _self, eArgs ); }); request.Send(); } Authorize( params, handler ) { var [ url, data ] = this._endPointRes( "authorization_endpoint" ); var eArgs = new SignInEventArgs({ "redirect": url, "data": data }); handler( this, eArgs ); } Logout( params, handler ) { var [ url, data ] = this._endPointRes( "end_session_endpoint" ); var eArgs = new SignInEventArgs({ "redirect": url, "data": data }); handler( this, eArgs ); } _endPointRes( endpoint, handler ) { if( !this.spec ) throw new Error( "OpenID spec is not present" ); var url = this.spec[ endpoint ]; if( !url ) throw new Error( "No such endpoint" ); var params = this.conf[ endpoint + "_params" ]; var requestData = []; if( params ) { for( var name in params ) { var val = params[ name ]; if( val[0] == "{" ) { val = this[ val.replace( "{", "" ).replace( "}", "" ) ](); } requestData.push( name + "=" + encodeURIComponent( val ) ); } } return [ url, requestData ]; } } module.exports = Signin;