enface-auth-node1
Version:
Enface biometric authorization library for Node.js (Express) environment
371 lines (349 loc) • 12.7 kB
JavaScript
const uuid = require( 'uuid' );
const WebSocket = require( 'ws' );
const constants = require( './constants' );
const utils = require( './utils' );
export class EnfaceAuth {
constructor( {
debug,
httpServer,
port,
projectId,
secretCode,
callbackUrl,
onCheckCurrentStatus,
onUserValidate,
onActivate,
onUserTokenByBioId
} ) {
this._DEBUG = !!debug;
this.projectId = projectId;
this.secretCode = Buffer.from( secretCode, 'base64' );
this.callbackUrl = callbackUrl;
this.callbackUrl.endsWith( '/' )
&& ( this.callbackUrl = this.callbackUrl.substring( 0, this.callbackUrl.length - 1 ) );
this.onUserValidate = onUserValidate;
this.onCheckCurrentStatus = onCheckCurrentStatus;
this.onActivate = onActivate;
this.onUserTokenByBioId = onUserTokenByBioId;
this.wsServer = null;
this.sessions = {};
if ( httpServer ) {
if ( port ) {
console.error( `[EnfaceAuth constructor error]:
Please specify "server" for http(s) mode either "port" for ws(s) mode` );
return;
}
this.callbackUrl += constants.HTTP_URI;
this.log( '[EnfaceAuth constructor] Using HTTP/S server' );
httpServer
.post( constants.HTTP_URI, ( req, res ) => {
this.log( `[EnfaceAuth] POST REQUEST', ${req.path}, ${req.body}` );
this.newClient( { client: res } );
utils.enfaceCors( res );
if ( req.body instanceof Object ) {
this.request( { client: res, data: JSON.stringify( req.body ) } );
} else {
let message = '';
req.on( 'data', chunk => {
message += chunk.toString();
} );
req.on( 'end', () => {
this.request( { client: res, data: message } );
} );
}
} )
.options( async ( req, res ) => {
utils.enfaceCors( res );
res.end();
} );
} else {
this.log( `[EnfaceAuth] Using sockets on port ${port}` );
this.wsServer = new WebSocket.Server( { port } );
this.wsServer.on( 'connection', socket => {
this.newClient( { client: socket } );
socket.on( 'message', data => {
this.request( { client: socket, data } );
} );
socket.on( 'close', code => {
this.log( `[EnfaceAuth] socket ${socket.clientId} closed with code: ${code}` );
socket.isAlive = false;
} );
socket.isAlive = true;
socket.on( 'pong', () => { socket.isAlive = true; } );
} );
setInterval( () => {
this.wsServer.clients.forEach( socket => {
if ( socket.isAlive === false ) {
socket.terminate();
return;
}
socket.isAlive = false;
socket.ping( utils.noop );
} );
}, 30000 );
}
}
log( value ) {
this._DEBUG && console.log( value );
}
logError( value ) {
this._DEBUG && console.error( value );
}
async request( { client, data } ) {
this.log( `[EnfaceAuth.request], ${data}` );
try {
const { response, closeConnection } = await this.readMessage( { client, data } );
this.send( {
client,
data: response
} );
closeConnection && this.finalizeSession( client );
} catch ( error ) {
this.logError( `[EnfaceAuth.request]', ${error.message}` );
this.errorResponse( { client, message: error } );
this.finalizeSession( client );
}
}
send( { client, data } ) {
this.log( `[EnfaceAuth.send], ${JSON.stringify( data )}` );
if ( this.wsServer ) {
client.send( JSON.stringify( data ) );
} else {
client.end( JSON.stringify( data ) );
}
}
readMessage( { client, data } ) {
return new Promise( resolve => {
this.log( `[EnfaceAuth.readMessage] data, ${JSON.stringify( data )}, client.clientId, ${client.clientId}` );
this.sessions[ client.clientId ].resolver = resolve;
try {
data = JSON.parse( data );
} catch ( error ) {
this.logError( `[EnfaceAuth.readMessage], ${error.message}` );
return this.errorResponse( { client, message: `Wrong data received ${data}` } );
}
switch ( data._ ) {
case constants.COMMAND_STATUS:
return this.responseStatus( { client, userData: data.userData } );
case constants.COMMAND_ENABLE:
case constants.COMMAND_AUTH:
return this.responseInit( { client, data } );
case constants.COMMAND_CHECK:
return this.responseCheck( { client, sessionId: data.sessionId } );
case constants.COMMAND_BIO_ENABLE:
return this.responseBioEnable( { client, data } );
case constants.COMMAND_BIO_AUTH:
return this.responseBioAuth( { client, data } );
default:
return this.errorResponse( { client, message: `Unknown command ${ data._ }` } );
}
} );
}
newClient( { client } ) {
this.log( '[EnfaceAuth.newClient]' );
const clientId = uuid();
this.sessions[ clientId ] = {
client,
sessionId: uuid(),
activated: false,
userId: null,
resolver: null
};
client.clientId = clientId;
setTimeout( () => {
this.finalizeSession( { clientId } );
}, constants.AUTHORIZATION_TIME_FRAME );
}
switchSession( { client, clientId } ) {
this.log( `[EnfaceAuth.switchSession] to clientId, ${clientId}` );
if ( !this.sessions[ clientId ] ) {
return this.errorResponse( { client, message: `Failed to get session params for client ${clientId}` } );
}
this.sessions[ client.clientId ]
&& delete this.sessions[ client.clientId ].client;
this.sessions[ clientId ].resolver = this.sessions[ client.clientId ].resolver;
this.finalizeSession( { clientId: client.clientId } );
client.clientId = clientId;
return true;
}
async responseInit( { client, data } ) {
this.log( `[EnfaceAuth.responseInit] data, ${data}` );
if ( !this.wsServer && data.clientId ) { // http server mode
return this.switchSession( { client, clientId: data.clientId } );
}
const result = data._ === constants.COMMAND_ENABLE
? await this.linkSessionToUser( { client, userData: data.userData } )
: true;
if ( !result ) {
return this.errorResponse( { client, message: `Failed to identify user with token: ${data.userData}` } );
}
const clientId = !this.wsServer
? client.clientId
: undefined;
return this.resolve( {
client,
data: {
_: data._,
token: utils.encrypt(
[ this.sessions[ client.clientId ].sessionId, this.callbackUrl, data._ ].join( '|' ),
this.secretCode
),
id: this.projectId,
clientId
}
} );
}
async responseStatus( { client, userData } ) {
try {
this.log( `[EnfaceAuth.responseStatus] userData, ${userData}` );
const userId = await this.onUserValidate( userData );
this.log( `[EnfaceAuth.responseStatus] user validated, userId: ${userId}` );
const result = await this.onCheckCurrentStatus( userId );
return this.finalResponse( {
client,
data: {
_: constants.COMMAND_STATUS,
check: true,
isActive: result
}
} );
} catch ( error ) {
this.logError( '[EnfaceAuth.responseStatus] error', error );
return this.errorResponse( { client, message: `Failed to validate user. Received token: ${userData}` } );
}
}
responseCheck( { client, sessionId } ) {
this.log( `[EnfaceAuth.responseCheck], sessionId, ${sessionId}` );
const session = this.findSessionById( sessionId );
if ( !session ) return this.errorResponse( { client, message: 'Client not found' } );
if ( session.activated ) return this.errorResponse( { client, message: 'Client already activated' } );
session.activated = true;
return this.resolve( {
client,
data: {
_: constants.COMMAND_READY
}
} );
}
async responseBioEnable( { client, data } ) {
this.log( `[EnfaceAuth.responseBioEnable] sessionId, bioId, ${data.sessionId}, ${data.bioId}` );
const session = this.findSessionById( data.sessionId );
if ( !session ) return this.errorResponse( { client, message: 'Client not found' } );
if ( !session.userId ) {
return [ session.client, client ].forEach( item => {
this.errorResponse( { client: item, message: 'User id is not assigned.' } );
} );
}
if ( !data.bioId || !utils.isUuid( data.bioId ) ) {
return [ session.client, client ].forEach( item => {
return this.errorResponse( { client: item, message: 'Bad biometric id.' } );
} );
}
const isActive = await this.onActivate( session.userId, data.bioId );
this.log( `finalResponse responseBioEnable activated, ${isActive}` );
return [ session.client, client ].forEach( item => {
this.finalResponse( {
client: item,
data: {
_: constants.COMMAND_STATUS,
isActive
}
} );
} );
}
async responseBioAuth( { client, data } ) {
this.log( `[EnfaceAuth.responseBioAuth] sessionId, bioId, ${data.sessionId}, ${data.bioId}` );
const session = this.findSessionById( data.sessionId );
if ( !session ) return this.errorResponse( { client, message: 'Client not found.' } );
if ( session.userId ) {
return [ session.client, client ].forEach( item => {
return this.errorResponse( { client: item, message: 'Client has wrong parameters.' } );
} );
}
if ( !data.bioId || !utils.isUuid( data.bioId ) ) {
return [ session.client, client ].forEach( item => {
return this.errorResponse( { client: item, message: 'Bad biometric id.' } );
} );
}
const token = await this.onUserTokenByBioId( data.bioId );
this.log( `finalResponse responseBioAuth, ${token}` );
this.finalResponse( {
client,
data: {
_: constants.COMMAND_BIO_AUTH,
result: !!token
}
} );
this.finalResponse( {
client: session.client,
data: {
_: constants.COMMAND_TOKEN,
token
}
} );
}
async linkSessionToUser( { client, userData } ) {
this.log( '[linkSessionToUser]', this );
try {
const userId = await this.onUserValidate( userData );
this.log( `[linkSessionToUser] userId, ${userId}` );
this.sessions[ client.clientId ].userId = userId;
return true;
} catch ( error ) {
return false;
}
}
errorResponse( { client, message } ) {
this.logError( `[EnfaceAuth.errorResponse], ${message}` );
this.finalResponse( {
client,
data: {
_: constants.COMMAND_ERROR,
message
}
} );
return false;
}
finalResponse( { client, data } ) {
this.log( `[EnfaceAuth.finalResponse], ${data}` );
this.resolve( { client, data, closeConnection: true } );
}
resolve( { client, data, closeConnection } ) {
this.log( `[EnfaceAuth.resolve] client.clientId, data, ${client.clientId}, ${data}` );
const session = this.sessions[ client.clientId ];
if ( !session || !session.resolver ) {
if ( this.wsServer ) {
this.send( { client: session.client, data } );
closeConnection && this.closeClient( { client: session.client } );
}
return;
}
session.resolver( { response: data, closeConnection: !!closeConnection } );
delete session.resolver;
}
finalizeSession( { clientId } ) {
this.log( `[EnfaceAuth.finalizeSession] clientId ${clientId}` );
this.sessions[ clientId ]
&& this.closeClient( { client: this.sessions[ clientId ].client } );
delete this.sessions[ clientId ];
}
closeClient( { client } ) {
this.log( `[EnfaceAuth.closeClient] client, ${!!client}` );
if ( !client ) return;
if ( this.wsServer ) {
client.terminate();
} else {
try {
client.end( 'timeout' );
} catch ( error ) {
this.logError( `[closeClient.error], ${error.message}` );
}
}
}
findSessionById( sessionId ) {
for ( const value of Object.values( this.sessions ) ) {
if ( value.sessionId === sessionId ) return value;
}
return null;
}
}