225 lines
4.8 KiB
JavaScript
225 lines
4.8 KiB
JavaScript
"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 _self = this;
|
|
|
|
if( this.spec )
|
|
{
|
|
_self[ action ]( params, handler );
|
|
}
|
|
else
|
|
{
|
|
if( !this.conf.spec )
|
|
{
|
|
var eArgs = new SignInEventArgs({ "success": false, "data": [] });
|
|
handler( this, eArgs );
|
|
return;
|
|
}
|
|
|
|
var request = new HttpRequest( this.conf.spec );
|
|
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;
|