Authentication App using LDAP OTP plugin

Similar to Create an Authentication Application within Ceptor Gateway below is an example of a variant of an Authentication Application that allows Authenticating towards Active Directory, using MultiFactor Authentication by SMSOTP to mobile phone or TOTP Authenticator.


This javascript application will be calling a REST API exposed in Ceptor, called CeptorAuthenticateLDAP - this API is provided with Ceptor in samples/ceptor_apis.zip.

You can find an example application in the /authsms folder of your Ceptor distribution. This is very easy to modify and style according to your needs. It uses the modern Materialize Front-End javascript framework, available at https://materializecss.com/ - this is very lightweight and allows for easy customization.


This application supports signing MultiFactor Authenticating using OTP sent via SMS or TOTP Authenticator.

Changing of initial or expired password is also supported, as is registration of new TOTP Authenticator using QR code scanning - any TOTP compliant application such as Google Authenticator or Microsoft Authenticator is supported.

The secret key is stored in LDAP, optionally encrypted with an AES key. See LDAP Authentication Properties for information about configuration of the LDAP OTP Authentication plugin.

Sample screenshots

Below are sample screenshots:

Authentication page


Multifactor Authentication Page

Auth application

You can find a javascript application in the /authsms folder of your Ceptor installation, it contains the following files:

index.html
css/checkforce-stylke.css
css/materialize.min.css
icon/favicon.png
js/ceptor.js
js/axios.min.js
js/jquery-3.4.1.min.js
js/materialize.min.jss


index.html
<!DOCTYPE html>
<html>

<head>
    <!--Let browser know website is optimized for mobile-->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
    <meta name="apple-mobile-web-app-title" content="Ceptor">

    <title>Welcome</title>

    <link rel="icon" href="icon/favicon.png" type="image/png" />
    <!--Import Google Icon Font-->
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <!--Import materialize.css-->
    <link type="text/css" rel="stylesheet" href="css/materialize.min.css" media="screen,projection" />
    <link rel="stylesheet" href="css/checkforce-style.css">    
</head>

<body>
    <div id="main" class="container">
    </div>
    <!--JavaScript at end of body for optimized loading-->
    <script type="text/javascript" src="./js/materialize.min.js"></script>
    <script type="text/javascript" src="./js/polyfill.min.js"></script>
    <script type="text/javascript" src="./js/axios.min.js"></script>
    <script type="text/javascript" src="./js/jquery-3.4.1.min.js"></script>
    <script type="text/javascript" src="./js/ceptor.js"></script>

    <script>
        $(document).ready(function(){
            Ceptor.init('#main', 'Ceptor');

            Ceptor.loginPage();
        });
    </script>
</body>

</html>

Simply change the name in the call to "Ceptor.init('#main', 'Ceptor')" replacing it with your preferences to control the name on login screen.

All of the front-end logic is contained in the javascript file ceptor.js 

From the html page, Ceptor.ini() is called first, then Ceptor.loginPage() which displays the input promting for userid and password.

You can add your own customizations here.

ceptor.js
/**
 * Ceptor login / reset password user interface
 * 
 * Uses CeptorAuthenticate API in the Gateway - check /ceptorauthenticate/1/?openapi.json for details
 * 
 */
var locationName;
var productName;
var pathPrefix = '/ceptorauthenticateldap/1';
var availableOTPMethods = ['totp','sms'];
var selectedOTP;

var Ceptor = {};

/**
 * Initialize Ceptor
 * 
 * @param loc Location to draw content in
 * @param prod Product name to display on login page
 */
Ceptor.init = function(loc, prod) {
    locationName = loc;
    productName = prod;
    document.Ceptor = this;
}

/**
 * Show login page, but first check status - if already authenticated, instead jump to afterLoginPage()
 */
Ceptor.loginPage = function() {
    axios.get(pathPrefix+'/info')
    .then(function (response) {
        console.log(response);
        // handle success
        if (response.data.logged_on) {
            document.Ceptor.afterLoginPage(response.data);
        } else {
            Ceptor.showActualLoginPage();
        }
    })
    .catch(function (error) {
        console.log(error);
        document.Ceptor.handleError("Unable to check Login status", error);
        document.Ceptor.showActualLoginPage();
    });
}

/**
 * Show login page prompting for userid and password
 */
Ceptor.showActualLoginPage = function() {
    $(locationName).html(
        '<div id="login-page" class="row">'+
        '    <div class="card">'+
        '        <div class="card-action blue white-text">'+
        '            <h3 class="center">Please Authenticate</h3>'+
        '           <div class="black-text center">'+
        '              <h5>Thank you for choosing '+$('<div>').text(productName).html()+'</h5>'+
        '            </div>'+
        '        </div>'+
        '    </div><br>'+
        '    <div class="card-content">'+
        '        <div class="input-field">'+
        '            <i class="material-icons prefix">person</i>'+
        '            <input class="validate" id="userid" type="text">'+
        '            <label for="userid">User ID</label>'+
        '        </div>'+
        '        <div class="input-field">'+
        '            <i class="material-icons prefix">lock_outline</i>'+
        '            <input id="password" type="password">'+
        '            <label for="password">Password</label>'+
        '        </div><br>'+
        '        <div class="form-field">'+
        '            <button id="loginbutton" class="btn-large waves-effect waves-light blue" style="width:100%" onclick="document.Ceptor.loginClicked()">Login</button>'+
        '        </div>'+
        '        <br>'+
        '        <br>'+
        '    </div>'+
        '</div>'
    );

    $('#userid').focus();

    $('#userid').keyup(function() {
        if (event.keyCode === 13) {
            $('#password').focus();
        }
    });
    $('#password').keyup(function() {
        if (event.keyCode === 13) {
            $('#loginbutton').click();
        }
    });
}

/**
 * Handles logon part 1, of userid and password fields.
 */
Ceptor.loginClicked = function() {
    var userid = $('#userid').val();
    var password = $('#password').val();

    $('#loginbutton').prop( "disabled", true );

    axios.post(pathPrefix+'/auth/uidpw', {
        userid: userid,
        credentials: password
    })
    .then(function (response) {
        // handle success
        Ceptor.afterLoginPage(response.data);
    })
    .catch(function (error) {
        console.log(error.response.data);
        if (error.response.data.code == 15) { // ERROR_NEED_OTP
            availableOTPMethods = error.response.data.otpmethods;
            if (!availableOTPMethods)
                availableOTPMethods = [];
            // Prompt for OTP
            Ceptor.loginPageOTP();
        } else {
            Ceptor.handleError("Unable to login", error);

            if (error.response.data.newpasswordrequired)
                Ceptor.changePasswordPage();
        }
    })
    .finally(function () {
        $('#loginbutton').prop( "disabled", false );
    });
}

/**
 * Shows 2nd part of login page, prompting for OTP code
 */
Ceptor.loginPageOTP = function() {
    var str;
    
    if (!availableOTPMethods || availableOTPMethods.length === 0 ) {
        $(locationName).html(
            '<div id="login-result" class="row">'+
            '    <div class="card">'+
            '        <div class="card-action blue white-text">'+
            '            <h3 class="center">Sorry</h3>'+
            '           <div class="black-text center">'+
            '              <h5>Your profile does not contain any 2nd factor methods, please contact your Administrator</h5>'+
            '           </div>'+
            '        </div>'+
            '    </div>'+
            '</div>'+
            '<div class="input-field col s6 m6 l6">'+
            '    <p class="margin medium-small"><a href="#" onclick="document.Ceptor.loginPage()">Back</a></p>'+
            '</div>'
        );
        return;
    }
	
	// If only one method is available, select that
	if (availableOTPMethods.length === 1) {
	    if (availableOTPMethods.includes("sms")) {
	    	Ceptor.sendSMS();
	    	
		    str=
		        '<div id="login-page" class="row">'+
		        '    <div class="card">'+
		        '        <div class="card-action blue white-text">'+
		        '            <h3 class="center">Please Authenticate</h3>'+
		        '           <div class="black-text center">'+
		        '              <h5>Multifactor Login required</h5>'+
		        '            </div>'+
		        '        </div>'+
		        '    </div><br>'+
		        '    <div class="card-content">'+
		        '    <div class="col s12">'+
		        '        <div class="input-field" id="sms_input_container">'+
		        '            <i class="material-icons prefix">person</i>'+
		        '            <input class="validate" id="sms" type="text">'+
		        '            <label for="totp">Enter code sent to your mobile phone</label>'+
		        '        </div>'+
		        '    </div>';
	    } else {
	    	// Must be totp
		    str=
		        '<div id="login-page" class="row">'+
		        '    <div class="card">'+
		        '        <div class="card-action blue white-text">'+
		        '            <h3 class="center">Please Authenticate</h3>'+
		        '           <div class="black-text center">'+
		        '              <h5>Multifactor Login required</h5>'+
		        '            </div>'+
		        '        </div>'+
		        '    </div><br>'+
		        '    <div class="card-content">'+
		        '    <h5>Select method for One-Time-Pin verification</h5><br>'+
		        '    <div class="col s12">'+
		        '        <ul class="tabs">'+
		        '            <li class="tab col s3"><a href="#page1" onclick="selectedOTP=\'totp\';">Authenticator</a></li>'+
		        '        </ul>'+
		        '    </div>'+
		        '    <div id="page1" class="col s12">'+
		        '        <div class="input-field">'+
		        '            <i class="material-icons prefix">person</i>'+
		        '            <input class="validate" id="totp" type="text">'+
		        '            <label for="totp">Enter TOTP code from Authenticator App</label>'+
		        '        </div>'+
		        '    </div>';
	    }
	} else {
		// Select between available methods
	    str=
	        '<div id="login-page" class="row">'+
	        '    <div class="card">'+
	        '        <div class="card-action blue white-text">'+
	        '            <h3 class="center">Please Authenticate</h3>'+
	        '           <div class="black-text center">'+
	        '              <h5>Multifactor Login required</h5>'+
	        '            </div>'+
	        '        </div>'+
	        '    </div><br>'+
	        '    <div class="card-content">'+
	        '    <h5>Select method for One-Time-Pin verification</h5><br>'+
	        '    <div class="col s12">'+
	        '        <ul class="tabs">';

	    if (availableOTPMethods.includes("totp"))
	        str +='            <li class="tab col s3"><a href="#page1" onclick="selectedOTP=\'totp\';">Authenticator</a></li>';
	    if (availableOTPMethods.includes("sms"))
	        str +='            <li class="tab col s3"><a href="#page2" onclick="selectedOTP=\'sms\';">SMS / Text Message</a></li>';

	    str+='        </ul>'+
	        '    </div>';

	    if (availableOTPMethods.includes("totp"))
	        str+='    <div id="page1" class="col s12">'+
	            '        <div class="input-field">'+
	            '            <i class="material-icons prefix">person</i>'+
	            '            <input class="validate" id="totp" type="text">'+
	            '            <label for="totp">Enter TOTP code from Authenticator App</label>'+
	            '        </div>'+
	            '    </div>';

	    if (availableOTPMethods.includes("sms"))
	        str+='    <div id="page2" class="col s12">'+
	            '        <div class="input-field" id="sms_otp_container">'+
	            '            <button id="sendsmsbutton" class="btn-small green" onclick="Ceptor.sendSMS()">Send SMS</button>'+
	            '        </div>'+
	            '        <div class="input-field" id="sms_input_container" hidden>'+
	            '            <i class="material-icons prefix">person</i>'+
	            '            <input class="validate" id="sms" type="text">'+
	            '            <label for="totp">Enter code sent to your mobile phone</label>'+
	            '        </div>'+
	            '    </div>';
	}
    str+='</div>'+
        '<div class="form-field">'+
        '    <button id="loginbutton" class="btn-large waves-effect waves-light blue" onclick="document.Ceptor.loginOTPClicked()" style="width:100%">Verify</button>'+
        '</div>'+
        '<div class="input-field col s6 m6 l6">'+
        '    <p class="margin medium-small"><a href="#" onclick="document.Ceptor.loginPage()">Back</a></p>'+
        '</div>'+
        '</div>';

    $(locationName).html(str);

    M.Tabs.init($('.tabs'), {
        duration: 300,
        swipeable: false
    });


	if (availableOTPMethods.includes("totp")) {
        selectedOTP = 'totp';
        $('#totp').focus();

        $('#totp').keyup(function() {
            if (event.keyCode === 13) {
                $('#loginbutton').click();
            }
        });
    } else {
        selectedOTP = 'sms';

        $('#sms').keyup(function() {
            if (event.keyCode === 13) {
                $('#loginbutton').click();
            }
        });
    }
}

/**
 * Sends an SMS with an OTP code to the client
 */
Ceptor.sendSMS = function() {
    $('#sendsmsbutton').prop( "disabled", true );
    $('#sms_otp_container').fadeOut({duration:100});

    axios.put(pathPrefix+'/auth/sendsms')
    .then(function (response) {
        // handle success

        // Show prompt
        $('#sms_input_container').show({
            duration:400,
            complete: function() {$('#sms').focus();}
        });
    })
    .catch(function (error) {
        $('#sms_otp_container').fadeIn({duration:200});
        Ceptor.handleError("Unable to send SMS/Text message", error);
    })
    .finally(function () {
        $('#sendsmsbutton').prop( "disabled", false );
    });
}

/**
 * Handles logon part 2, authenticating user using the selected OTP method
 */
Ceptor.loginOTPClicked = function() {
    if (selectedOTP === 'sms') {
        var otp = $('#sms').val();

        if (!otp) {
             $('#sms').focus();
             return;
        }

        $('#loginbutton').prop( "disabled", true );

        axios.post(pathPrefix+'/auth/verifysmsotp?code='+otp)
        .then(function (response) {
            // handle success - the login page will fetch /info and go to the confirm page
            Ceptor.loginPage();
        })
        .catch(function (error) {        
            Ceptor.handleError("Unable to verify entered code", error);
        })
        .finally(function () {
            $('#loginbutton').prop( "disabled", false );
        });
    } else if (selectedOTP === 'totp') {
        var otp = $('#totp').val();

        if (!otp) {
             $('#totp').focus();
             return;
        }

        $('#loginbutton').prop( "disabled", true );

        axios.post(pathPrefix+'/auth/verifytotp?code='+otp)
        .then(function (response) {
            // handle success - the login page will fetch /info and go to the confirm page
            Ceptor.loginPage();
        })
        .catch(function (error) {        
            Ceptor.handleError("Unable to verify entered code", error);
            if (error.response.status == 500) {
                document.Ceptor.loginPage();
            } else if (error.response.data.message.includes("too many")) {
                document.Ceptor.loginPage();
            }
        })
        .finally(function () {
            $('#loginbutton').prop( "disabled", false );
        });
    }
}


/**
 * Page shown after login
 * 
 * @param info Session info from /info API call
 */
Ceptor.afterLoginPage = function(info) {
    $(locationName).html(
        '<div id="login-result" class="row">'+
        '    <div class="card">'+
        '        <div class="card-action blue white-text">'+
        '            <h3 class="center">Welcome</h3>'+
        '           <div class="black-text center">'+
        '              <h5>Hello '+$('<div>').text(info.user_name).html()+'</h5>'+
        '           </div>'+
        '        </div>'+
        '    </div>'+
        '</div>'+
        '<div class="card-panel">'+
        '     <span class="blue-text text-darken-2"><h6>Please go to <a href="https://ceptor.io">ceptor.io</a> for more information about Ceptor</h6></span>'+
        '</div>'+
        '<div class="fixed-action-btn">'+
        '    <a class="btn-floating btn-large red pulse">'+
        '        <i class="large material-icons">menu</i>'+
        '    </a>'+
        '    <ul>'+
        '        <li><a class="btn-floating blue"onclick="document.Ceptor.registerTOTPPage()"><i class="material-icons">person</i></a></li>'+
        '        <li><a class="btn-floating yellow darken-1" onclick="document.Ceptor.logoff()"><i class="material-icons">exit_to_app</i></a></li>'+
//        '        <li><a class="btn-floating blue"><i class="material-icons">person</i></a></li>'+
        '    </ul>'+
        '</div>'
    );

    var elems = document.querySelectorAll('.fixed-action-btn');
    M.FloatingActionButton.init(elems, {});
}

/**
 * Handles an error received by the server, displaying error message
 * 
 * @param message Prefix message, e.g. "Login error" - error message from server is appended to it
 * @param error The error returned from axios client 
 */
Ceptor.handleError = function(message, error) {
    console.log(error.response);
    if (error.response.status === 401 || error.response.status === 400) {
        M.toast({html: '<b>'+message+': </b> ' + $("<div>").text(error.response.data.message).html()});
    } else {
        M.toast({html: '<b>'+message+': </b> ' + $("<div>").text(error).html()});
        console.log(error.response.data);
    }
}

/**
 * Calls logoff, and goes back to login page
 */
Ceptor.logoff = function() {
    axios.put(pathPrefix+'/auth/logoff')
    .then(function (response) {
        // handle success
        Ceptor.showActualLoginPage();
        M.toast({html: "Logged off"});
    })
    .catch(function (error) {
        console.log(error);
        Ceptor.handleError("Unable to logoff", error);
        Ceptor.loginPage();
    });
}

Ceptor.registerTOTPPage = function() {
    $(locationName).html(
        '<div id="login-page" class="row">'+
        '    <div class="card">'+
        '        <div class="card-action blue white-text">'+
        '            <h3 class="center">Register Authenticator App</h3>'+
        '           <div class="black-text center">'+
        '              <h5>Please re-enter your password</h5>'+
        '              <h6>Any previous registration will be overwritten and will no longer function if you continue</h6>'+
        '            </div>'+
        '        </div>'+
        '    </div><br>'+
        '    <div class="card-content">'+
        '        <div class="input-field">'+
        '            <i class="material-icons prefix">lock_outline</i>'+
        '            <input id="password" type="password">'+
        '            <label for="password">Password</label>'+
        '        </div><br>'+
        '        <div class="form-field">'+
        '            <button id="loginbutton" class="btn-large waves-effect waves-light blue" style="width:100%" onclick="document.Ceptor.registerTOTPClicked()">Submit</button>'+
        '        </div>'+
        '    </div>'+
        '</div>'
    );

    $('#password').keyup(function() {
        if (event.keyCode === 13) {
            $('#loginbutton').click();
        }
    });

    $('#password').focus();
}

Ceptor.registerTOTPClicked = function() {
    var password = $('#password').val();

    if (!password) {
        $("#password").focus();
        M.toast({html: "Please enter a password"});
        return;
    }

    $(locationName).html(
        '<div id="totp-generation" class="row">'+
        '    <div class="card">'+
        '        <div class="card-action blue white-text">'+
        '            <h3 class="center">Register Authenticator App</h3>'+
        '            <div class="black-text center">'+
        '                <h5>Please hold, generating secret</h5>'+
        '            </div>'+
        '        </div>'+
        '    </div><br>'+
        '</div>');

    axios.post(pathPrefix+'/totp/qr', {
        password: password
    })
    .then(function (response) {
        // handle success
        $(locationName).html(
            '<div id="totp-register-page" class="row">'+
            '    <div class="card">'+
            '        <div class="card-action blue white-text">'+
            '            <h3 class="center">Register Authenticator App</h3>'+
            '           <div class="black-text center">'+
            '              <h5>Please scan QR code or enter secret in authenticator app</h5>'+
            '            </div>'+
            '        </div>'+
            '    </div><br>'+
            '    <div class="card-content">'+
            '        <div class="form-field">'+
            '          Secret Key: ' + response.data.secret + '<br/>'+
            '          <img src="data:image/png;base64,' + response.data.qr + '">'+
            '        </div>'+
            '        <div class="input-field">'+
            '            <i class="material-icons prefix">person</i>'+
            '            <input class="validate" id="totp" type="text">'+
            '            <label for="totp">Enter TOTP code from Authenticator App to complete registration</label>'+
            '        </div>'+
            '        <div class="form-field">'+
            '            <button id="loginbutton" class="btn-large waves-effect waves-light blue" style="width:100%" onclick="document.Ceptor.registerOTPVerifyClicked()">Complete</button>'+
            '        </div>'+
            '    </div>'+
            '</div>'
        );

        $('#totp').keyup(function() {
            if (event.keyCode === 13) {
                $('#loginbutton').click();
            }
        });

        $('#totp').focus();

    })
    .catch(function (error) {
        console.log(error);
        Ceptor.handleError("Unable to register TOTP", error);
        Ceptor.registerTOTPPage();
    });
}

/**
 * Handles logon part 2, authenticating user using the selected OTP method
 */
Ceptor.registerOTPVerifyClicked = function() {
    var otp = $('#totp').val();

    if (!otp) {
        $('#totp').focus();
        return;
    }

    $('#loginbutton').prop( "disabled", true );

    axios.post(pathPrefix+'/auth/verifytotp?code='+otp)
    .then(function (response) {
        // handle success - the login page will fetch /info and go to the confirm page
        Ceptor.loginPage();
    })
    .catch(function (error) {        
        Ceptor.handleError("Unable to verify entered code", error);
        if (error.response.status == 500) {
            document.Ceptor.loginPage();
        } else if (error.response.data.message.includes("too many")) {
            document.Ceptor.loginPage();
        }
    })
    .finally(function () {
        $('#loginbutton').prop( "disabled", false );
    });
}

/**
 * Show change password page
 */
Ceptor.changePasswordPage = function() {
    $(locationName).html(
        '<div id="changepw-page" class="row">'+
        '    <div class="card">'+
        '        <div class="card-action blue white-text">'+
        '            <h3 class="center">Change Password</h3>'+
        '           <div class="black-text center">'+
        '              <h5>Your password must be changed, please enter new password</h5>'+
        '            </div>'+
        '        </div>'+
        '    </div><br>'+
        '    <div class="card-content">'+
        '        <div class="input-field">'+
        '            <i class="material-icons prefix">person</i>'+
        '            <input class="validate" id="password" type="password">'+
        '            <label for="password">Enter old password</label>'+
        '        </div>'+
        '        <div class="input-field">'+
        '            <i class="material-icons prefix">person</i>'+
        '            <input class="validate" id="password1" type="password">'+
        '            <label for="password1">Enter new password</label>'+
        '        </div>'+
        '        <div class="input-field">'+
        '            <i class="material-icons prefix">person</i>'+
        '            <input class="validate" id="password2" type="password">'+
        '            <label for="password2">Verify password</label>'+
        '        </div>'+
        '        <div class="form-field">'+
        '            <button id="changepasswordbutton" class="btn-large waves-effect waves-light blue" style="width:100%" onclick="document.Ceptor.changePasswordClicked()">Change</button>'+
        '        </div>'+
        '    </div>'+
        '</div>'
    );

    $('#password').keyup(function() {
        if (event.keyCode === 13) {
            $('#password1').focus();
        }
    });
    $('#password1').keyup(function() {
        if (event.keyCode === 13) {
            $('#password2').focus();
        }
    });
    $('#password2').keyup(function() {
        if (event.keyCode === 13) {
            $('#changepasswordbutton').click();
        }
    });

    $('#password').focus();
}

/**
 * Change password clicked
 */
Ceptor.changePasswordClicked = function() {
    // Validate both fields first

    var userid = $("#user").val();
    var password = $("#password").val();
    var password1 = $("#password1").val();
    var password2 = $("#password2").val();

    if (!password) {
        $("#password").focus();
        M.toast({html: "Please enter old password"});
        return;
    }
    if (!password1) {
        $("#password1").focus();
        M.toast({html: "Please enter a password"});
        return;
    }
    if (!password2) {
        $("#password2").focus();
        M.toast({html: "Please verify password"});
        return;
    }
    if (password1 != password2) {
        $("#password2").focus();
        M.toast({html: "The passwords must match"});
        return;
    }

    $('#changepasswordbutton').prop( "disabled", true );

    axios.post(pathPrefix+'/auth/changepw', {
        password: password,
        newpassword: password1
    })
    .then(function (response) {
        // handle success

        M.toast({html: "Password changed successfully - please login."});
        Ceptor.loginPage();
    })
    .catch(function (error) {
        Ceptor.handleError("Unable to change password - either old password is incorrect, or new password not accepted", error);
    })
    .finally(function () {
        $('#changepasswordbutton').prop( "disabled", false );
    });
}



Ceptor Gateway Location

To serve the application to the clients, on /authsms/* you need to create a new Location in the gateway, from which the static files in the applications are served to the user.

Create a location called "authsms", and section the action to "serve".

Set conditions to match /authsms/*

and create an URL rewrite rule, which rewrites /authsms/(.*) to /$1 - effectively stripping the /authsms/ prefix from the request.

Select the WebServer section, and point the webserver at ${ceptor.home}/authsms where the html / javascript application is then served from.


Below is a location configuration which can be copied into the gateway configuration, with the configuration described above.

Ceptor Gateway auth location
{
  "name": "authsms",
  "description": "Authentication application, for LDAP OTP",
  "conditions": [{
    "type": "path",
    "deny": false,
    "lowercase": false,
    "values": ["/authsms/*"]
  }],
  "conditions.type": "and",
  "location.enabled": true,
  "session.needed": true,
  "session.afterconditionsmatch": false,
  "session.override": false,
  "content.preload": false,
  "action": "serve",
  "response.compress": true,
  "cookiesnapper": {},
  "plugin": {},
  "webserver": {
    "path": "${ceptor.home}/authsms",
    "case.sensitive": false,
    "directory.listing.enabled": false,
    "paths.canonicalize": true,
    "follow.links": false,
    "cache.enabled": true,
    "cache.changelistener": true,
    "response.notfound": {
      "response.status": 404,
      "response.reason": "Not found",
      "response.contenttype": "text/html; charset=utf8",
      "response.content": "<!DOCTYPE html>\n<html>\n<head><title>Not found<\/title><\/head>\n<body><h1>Not found :(<\/h1><\/body>\n<\/html>"
    }
  },
  "urlrewrite": [{
    "name": "remote_auth",
    "pattern": "/authsms/(.*)",
    "newurl": "/$1",
    "decode.before.match": true,
    "clear.query.params": false,
    "redirect": false,
    "last": true
  }]
},


CeptorAuthenticate API

The CeptorAuthenticate API is called by JavaScript in the authentication application running in the browser.

The API offers these features:

  • Authenticate users using Multi Factor Authentication (MFA)
  • Create new users using self-service
  • Password reset

OpenAPI Specification

Below is the OpenAPI specification for this API:

Implementation

This API is implemented as scripts within the gateway, below is example of the implementation for each operation in the API:

Note that this implementation uses some of the Authentication Plugins included with Ceptor - these plugins use Ceptors own Ceptor User Administration Server and UserAdmin API to store users. You should modify the implementation to suit your requirements and plugins, in case they differ.

You should remove any operations that offers functionality which you do not wish to expose to your clients - if you e.g. do not want to offer self-registration to clients, remove the part for the UI, and remove the relevant API offering this functionality.

GET /info

This operation returns information about the current session, for use by the application.

Implementation for GET /info
var pSessionId = context.id;

var isAuthenticated = context.agent.isLoggedOn( pSessionId );

var response = {
    // Is delayed logoff or not
    "is_delayed_logoff": context.agent.isDelayedLogoff(pSessionId),
	// User ID
	"user_id": context.agent.getUser(pSessionId),
	// Customer ID
	"customer_id": isAuthenticated ? context.agent.getCustomerID(pSessionId) : null,
	// Agreement ID
	"agreement_id": isAuthenticated ? context.agent.getAgreementID(pSessionId) : null,
	// Is internal user
	"internal": isAuthenticated ? context.agent.isInternalUser(pSessionId) : null,
	// Is logged on
	"logged_on": isAuthenticated,
	// Users real name
	"user_name": context.agent.getUserName(pSessionId),
	// IP address that created the session
	"ip": context.agent.getUserIP(pSessionId),
	// Authentication level
	"authentication_level": context.agent.getAuthenticationLevel(pSessionId),
	// Authentication method
	"authentication_method": context.agent.getAuthenticationMethod(pSessionId),
	// Array of users groups
	"groups": null,
	// Name of creating agent
	"creating_agent": context.agent.getNameOfCreatingAgent(pSessionId),
	// Ticket or token
	"ticket": context.agent.getTicketFromSession(pSessionId)
};

try {
    response.groups = Java.from(context.agent.getUserGroups(pSessionId));
} catch(e) {}

context.respond(200, 'OK', 'application/json', JSON.stringify(response));

POST /auth/uidpw

This operation supports authenticating using userid and password. By default, it treats this as first step of a multifactor authentication, where the user must complete authentication using either OTP sent as SMS, or a TOTP code using a previously registered Authenticator application.

If you remove this line:

 context.agent.setStateVariable(pSessionId, "require_otp", "true", false);

then two-factor authentication will be disabled, and login will complete after the first userid/password login.

Implementation of POST /auth/uidpw
function auth() {
    var req = JSON.parse(context.getRequestBodyAsString());
    
    context.trace.trace("Authenticate called with userid: " + req.userid);

    try {
        var pSessionId = context.id;
        
        context.agent.setStateVariable(pSessionId, "require_otp", "true", false);
        context.agent["logon(java.lang.String, int, java.lang.String, java.lang.Object)"](pSessionId, 59, req.userid, req.credentials);
        
        var isAuthenticated = context.agent.isLoggedOn(pSessionId);
        
        var response = {
            // Is delayed logoff or not
            "is_delayed_logoff": context.agent.isDelayedLogoff(pSessionId),
        	// User ID
        	"user_id": context.agent.getUser(pSessionId),
        	// Customer ID
        	"customer_id": isAuthenticated ? context.agent.getCustomerID(pSessionId) : null,
        	// Agreement ID
        	"agreement_id": isAuthenticated ? context.agent.getAgreementID(pSessionId) : null,
        	// Is internal user
        	"internal": isAuthenticated ? context.agent.isInternalUser(pSessionId) : null,
        	// Is logged on
        	"logged_on": isAuthenticated,
        	// Users real name
        	"user_name": context.agent.getUserName(pSessionId),
        	// IP address that created the session
        	"ip": context.agent.getUserIP(pSessionId),
        	// Authentication level
        	"authentication_level": context.agent.getAuthenticationLevel(pSessionId),
        	// Authentication method
        	"authentication_method": context.agent.getAuthenticationMethod(pSessionId),
        	// Array of users groups
        	"groups": null,
        	// Name of creating agent
        	"creating_agent": context.agent.getNameOfCreatingAgent(pSessionId),
        	// Ticket or token
        	"ticket": context.agent.getTicketFromSession(pSessionId),
        	// Available OTP methods
        	"otpmethods": []
        };
        
        try {
            response.groups = Java.from(context.agent.getUserGroups(pSessionId));
        } catch(e) {}
        
        var otpMethods = context.agent.getStateVariable(pSessionId, "otpmethods");
        if (otpMethods) {
            response.otpmethods = otpMethods.split(";");
        }

        context.respond(200, 'OK', 'application/json', JSON.stringify(response));
    } catch(err) {
        if (! (err instanceof Java.type('java.lang.Throwable')))
            throw err;
            
        context.trace.trace("Authentication failed", err);
        if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {
            var response = {
                "code": err.getErrorCode(),
                "message": err.getErrorText()
            };
            
            if (response.code == 15) { // Need OTP
                var otpmethods = context.agent.getStateVariable(pSessionId, "otpmethods");
                if (otpmethods)
                    response.otpmethods = otpmethods.split(';');
            }
            var newPasswordRequired = context.agent.getStateVariable(pSessionId, "ldap.newpasswordrequired");
            if (newPasswordRequired)
                response.newpasswordrequired = "true" == newPasswordRequired;
                
            if (!response.message)
                response.message = "Unknown error";
                
            context.respond(401, 'Authentication Failed', 'application/json', JSON.stringify(response));
        } else {
            throw err;
        }
    }
}

auth();

POST /auth/changepw

This operation changes the password for a user - /auth/uidpw must be called previously to initiate a login, or this operation will fail.

Implementation of PUT /auth/changepw
function auth() {
    var req = JSON.parse(context.getRequestBodyAsString());

    try {
        if (!context.agent.getUser(context.id)) {
            var response = {
                "code": -1,
                "message": "Missing previous login, or timeout - please relogin"
            };
            context.respond(400, 'OK', 'application/json', JSON.stringify(response));
            return;
        }
// Changing password is not allowed from restricted agent using regular command - but LDAPOTP plugin allows this alternate method
// context.agent.changePassword(context.id, req.password, req.newpassword);

        var input = [context.agent.getUser(context.id), req.password, req.newpassword];
        context.agent.executeAuthpluginCommand(context.id,59,"changepassword", Java.to(input));
        
        // Force user to login again
        context.agent.logoff(context.id);
        
        context.respond(201, 'OK', 'application/json', JSON.stringify(response));
    } catch(err) {
        if (! (err instanceof Java.type('java.lang.Throwable')))
            throw err;
            
        context.trace.trace("Authentication failed", err);
        if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {
            var response = {
                "code": err.getErrorCode(),
                "message": err.getErrorText()
            };
            
            if (!response.message)
                response.message = "Unknown error";
                
            context.respond(400, 'Change password failed', 'application/json', JSON.stringify(response));
        } else {
            throw err;
        }
    }
}

auth();

POST /auth/logoff

This operation simply logs off the user.

Implementation of PUT /auth/logoff
context.agent.logoff(context.id);
context.respond(201, 'OK', 'application/json', '');

POST /auth/sendsms

This operation generates an OTP code and sends it using SMS/Text to the users mobile number.

Implementation of POST /auth/sendsms
try {
    context.agent.newToken(context.id,59,"sendsms");
    
    context.respond(201, 'SMS Sent', 'application/json','');
} catch(err) {
    if (! (err instanceof Java.type('java.lang.Throwable')))
        throw err;
        
    context.trace.trace("SendSMS failed", err);
    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {
        var response = {
            "code": err.getErrorCode(),
            "message": err.getErrorText()
        };
        
        if (!response.message)
            response.message = "Unknown error";
            
        context.respond(400, 'Send SMS Failed', 'application/json', JSON.stringify(response));    
    } else {
        throw err;
    }
}

POST /auth/verifysmsotp

This operation is used for verifying an entered OTP value sent earlier using SMS/Text message.

Implementation of POST /auth/verifysmsotp
try {
    var credentials = ["sms",pCode];
    
    context.agent.logon(context.id,59,context.agent.getUser(context.id), Java.to(credentials, "java.lang.String[]"));
    
    context.respond(201, 'OK', 'application/json', '');
} catch(err) {
    if (! (err instanceof Java.type('java.lang.Throwable')))
        throw err;
        
    context.trace.trace("VerifySMS failed", err);
    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {
        var response = {
            "code": err.getErrorCode(),
            "message": err.getErrorText()
        };
        
        if (!response.message)
            response.message = "Unknown error";
            
        context.respond(400, 'Verify SMS OTP Failed', 'application/json', JSON.stringify(response));    
    } else {
        throw err;
    }
}

POST /auth/verifytotp

This operation is used for verifying a TOTP code entered using a previously registered TOTP authenticator.

Implementation of POST /auth/verifytotp
try {
    var credentials = ["totp",pCode];
    
    context.agent.logon(context.id,59,context.agent.getUser(context.id), Java.to(credentials, "java.lang.String[]"));
    
    context.respond(201, 'OK', 'application/json', '');
} catch(err) {
    if (! (err instanceof Java.type('java.lang.Throwable')))
        throw err;
        
    context.trace.trace("Verify TOTP failed", err);
    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {
        var response = {
            "code": err.getErrorCode(),
            "message": err.getErrorText()
        };
        
        if (!response.message)
            response.message = "Unknown error";
            
        context.respond(400, 'Verify TOTP Failed', 'application/json', JSON.stringify(response));    
    } else {
        throw err;
    }
}

POST /totp/qr

This operation is used for registering a TOTP Authenticator

Implementation of POST /totp/qr
try {
    if (!context.agent.isLoggedOn(context.id)) {
        var response = {
            "code": 18, // AuthErrorCodes.ERROR_NOTAUTHENTICATED
            "message": "Requires authentication"
        };
        
        context.respond(403, 'Denied not logged on', 'application/json', JSON.stringify(response));
    } else {
        var body = JSON.parse(context.getRequestBodyAsString());

        var response = {
        	// QR Code in PNG format
        	// Format: base64
        	// Secret value to register
        	"secret": context.agent.newToken(context.id,59, "secretpassword;"+body.password),
        	"qr": context.agent.newToken(context.id,59, "PNG")
        };
        context.respond(200, 'OK', 'application/json', JSON.stringify(response));
    }
} catch(err) {
    if (! (err instanceof Java.type('java.lang.Throwable')))
        throw err;
        
    context.trace.trace("QR Code registration failed", err);
    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {
        var response = {
            "code": err.getErrorCode(),
            "message": err.getErrorText()
        };
        
        if (!response.message)
            response.message = "Unknown error";
            
        context.respond(400, 'QR Code registration failed', 'application/json', JSON.stringify(response));    
    } else {
        throw err;
    }
}

Import API directly

If you prefer, you can create a new API from this API Version specification directly.

API Version JSON Specification
{
  "implementation": {"Sandbox": {
    "": {"*": {
      "proxy": {},
      "script": "var response = {\n  text: 'Sorry, this API operation has not yet been implemented.'\n};\ncontext.respond(200, 'OK', 'application/json', JSON.stringify(response));",
      "pipeline": {},
      "type": "script"
    }},
    "/hello": {"get": {
      "proxy": {},
      "script": "var response = {\n\t\"text\": \"a string\"\n};\ncontext.respond(200, 'OK', 'application/json', JSON.stringify(response));\n",
      "pipeline": {},
      "type": "script"
    }},
    "/auth/uidpw": {"post": {
      "proxy": {},
      "script": "%{script}function auth() {\n    var req = JSON.parse(context.getRequestBodyAsString());\n    \n    context.trace.trace(\"Authenticate called with userid: \" + req.userid);\n\n    try {\n        var pSessionId = context.id;\n        \n        context.agent.setStateVariable(pSessionId, \"require_otp\", \"true\", false);\n        context.agent[\"logon(java.lang.String, int, java.lang.String, java.lang.Object)\"](pSessionId, 59, req.userid, req.credentials);\n        \n        var isAuthenticated = context.agent.isLoggedOn(pSessionId);\n        \n        var response = {\n            // Is delayed logoff or not\n            \"is_delayed_logoff\": context.agent.isDelayedLogoff(pSessionId),\n        \t// User ID\n        \t\"user_id\": context.agent.getUser(pSessionId),\n        \t// Customer ID\n        \t\"customer_id\": isAuthenticated ? context.agent.getCustomerID(pSessionId) : null,\n        \t// Agreement ID\n        \t\"agreement_id\": isAuthenticated ? context.agent.getAgreementID(pSessionId) : null,\n        \t// Is internal user\n        \t\"internal\": isAuthenticated ? context.agent.isInternalUser(pSessionId) : null,\n        \t// Is logged on\n        \t\"logged_on\": isAuthenticated,\n        \t// Users real name\n        \t\"user_name\": context.agent.getUserName(pSessionId),\n        \t// IP address that created the session\n        \t\"ip\": context.agent.getUserIP(pSessionId),\n        \t// Authentication level\n        \t\"authentication_level\": context.agent.getAuthenticationLevel(pSessionId),\n        \t// Authentication method\n        \t\"authentication_method\": context.agent.getAuthenticationMethod(pSessionId),\n        \t// Array of users groups\n        \t\"groups\": null,\n        \t// Name of creating agent\n        \t\"creating_agent\": context.agent.getNameOfCreatingAgent(pSessionId),\n        \t// Ticket or token\n        \t\"ticket\": context.agent.getTicketFromSession(pSessionId),\n        \t// Available OTP methods\n        \t\"otpmethods\": []\n        };\n        \n        try {\n            response.groups = Java.from(context.agent.getUserGroups(pSessionId));\n        } catch(e) {}\n        \n        var otpMethods = context.agent.getStateVariable(pSessionId, \"otpmethods\");\n        if (otpMethods) {\n            response.otpmethods = otpMethods.split(\";\");\n        }\n\n        context.respond(200, 'OK', 'application/json', JSON.stringify(response));\n    } catch(err) {\n        if (! (err instanceof Java.type('java.lang.Throwable')))\n            throw err;\n            \n        context.trace.trace(\"Authentication failed\", err);\n        if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n            var response = {\n                \"code\": err.getErrorCode(),\n                \"message\": err.getErrorText()\n            };\n            \n            if (response.code == 15) { // Need OTP\n                var otpmethods = context.agent.getStateVariable(pSessionId, \"otpmethods\");\n                if (otpmethods)\n                    response.otpmethods = otpmethods.split(';');\n            }\n            var newPasswordRequired = context.agent.getStateVariable(pSessionId, \"ldap.newpasswordrequired\");\n            if (newPasswordRequired)\n                response.newpasswordrequired = \"true\" == newPasswordRequired;\n                \n            if (!response.message)\n                response.message = \"Unknown error\";\n                \n            context.respond(401, 'Authentication Failed', 'application/json', JSON.stringify(response));\n        } else {\n            throw err;\n        }\n    }\n}\n\nauth();\n",
      "pipeline": {},
      "type": "script",
      "override": true
    }},
    "/info": {"get": {
      "proxy": {},
      "script": "%{script}var pSessionId = context.id;\n\nvar isAuthenticated = context.agent.isLoggedOn( pSessionId );\n\nvar response = {\n    // Is delayed logoff or not\n    \"is_delayed_logoff\": context.agent.isDelayedLogoff(pSessionId),\n\t// User ID\n\t\"user_id\": context.agent.getUser(pSessionId),\n\t// Customer ID\n\t\"customer_id\": isAuthenticated ? context.agent.getCustomerID(pSessionId) : null,\n\t// Agreement ID\n\t\"agreement_id\": isAuthenticated ? context.agent.getAgreementID(pSessionId) : null,\n\t// Is internal user\n\t\"internal\": isAuthenticated ? context.agent.isInternalUser(pSessionId) : null,\n\t// Is logged on\n\t\"logged_on\": isAuthenticated,\n\t// Users real name\n\t\"user_name\": context.agent.getUserName(pSessionId),\n\t// IP address that created the session\n\t\"ip\": context.agent.getUserIP(pSessionId),\n\t// Authentication level\n\t\"authentication_level\": context.agent.getAuthenticationLevel(pSessionId),\n\t// Authentication method\n\t\"authentication_method\": context.agent.getAuthenticationMethod(pSessionId),\n\t// Array of users groups\n\t\"groups\": null,\n\t// Name of creating agent\n\t\"creating_agent\": context.agent.getNameOfCreatingAgent(pSessionId),\n\t// Ticket or token\n\t\"ticket\": context.agent.getTicketFromSession(pSessionId)\n};\n\ntry {\n    response.groups = Java.from(context.agent.getUserGroups(pSessionId));\n} catch(e) {}\n\ncontext.respond(200, 'OK', 'application/json', JSON.stringify(response));\n",
      "pipeline": {},
      "type": "script",
      "override": true
    }},
    "/auth/logoff": {"put": {
      "proxy": {},
      "script": "%{script}context.agent.logoff(context.id);\ncontext.respond(201, 'OK', 'application/json', '');\n",
      "pipeline": {},
      "type": "script",
      "override": true
    }},
    "/auth/sendsms": {"put": {
      "proxy": {},
      "script": "%{script}try {\n    context.agent.newToken(context.id,59,\"sendsms\");\n    \n    context.respond(201, 'SMS Sent', 'application/json','');\n} catch(err) {\n    if (! (err instanceof Java.type('java.lang.Throwable')))\n        throw err;\n        \n    context.trace.trace(\"SendSMS failed\", err);\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n        var response = {\n            \"code\": err.getErrorCode(),\n            \"message\": err.getErrorText()\n        };\n        \n        if (!response.message)\n            response.message = \"Unknown error\";\n            \n        context.respond(400, 'Send SMS Failed', 'application/json', JSON.stringify(response));    \n    } else {\n        throw err;\n    }\n}\n",
      "pipeline": {},
      "type": "script",
      "override": true
    }},
    "/auth/verifysmsotp": {"post": {
      "proxy": {},
      "script": "%{script}try {\n    var credentials = [\"sms\",pCode];\n    \n    context.agent.logon(context.id,59,context.agent.getUser(context.id), Java.to(credentials, \"java.lang.String[]\"));\n    \n    context.respond(201, 'OK', 'application/json', '');\n} catch(err) {\n    if (! (err instanceof Java.type('java.lang.Throwable')))\n        throw err;\n        \n    context.trace.trace(\"VerifySMS failed\", err);\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n        var response = {\n            \"code\": err.getErrorCode(),\n            \"message\": err.getErrorText()\n        };\n        \n        if (!response.message)\n            response.message = \"Unknown error\";\n            \n        context.respond(400, 'Verify SMS OTP Failed', 'application/json', JSON.stringify(response));    \n    } else {\n        throw err;\n    }\n}\n",
      "pipeline": {},
      "type": "script",
      "override": true
    }},
    "/auth/verifytotp": {"post": {
      "proxy": {},
      "script": "%{script}try {\n    var credentials = [\"totp\",pCode];\n    \n    context.agent.logon(context.id,59,context.agent.getUser(context.id), Java.to(credentials, \"java.lang.String[]\"));\n    \n    context.respond(201, 'OK', 'application/json', '');\n} catch(err) {\n    if (! (err instanceof Java.type('java.lang.Throwable')))\n        throw err;\n        \n    context.trace.trace(\"Verify TOTP failed\", err);\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n        var response = {\n            \"code\": err.getErrorCode(),\n            \"message\": err.getErrorText()\n        };\n        \n        if (!response.message)\n            response.message = \"Unknown error\";\n            \n        context.respond(400, 'Verify TOTP Failed', 'application/json', JSON.stringify(response));    \n    } else {\n        throw err;\n    }\n}\n",
      "pipeline": {},
      "type": "script",
      "override": true
    }},
    "/auth/verifyemailotp": {"post": {
      "proxy": {},
      "script": "%{script}try {\n    var credentials = [pCode];\n    \n    context.agent.logon(context.id,50,context.agent.getUser(context.id), Java.to(credentials, \"java.lang.String[]\"));\n    \n    context.respond(201, 'OK', 'application/json', '');\n} catch(err) {\n    if (! (err instanceof Java.type('java.lang.Throwable')))\n        throw err;\n        \n    context.trace.trace(\"Verify email OTP failed\", err);\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n        var response = {\n            \"code\": err.getErrorCode(),\n            \"message\": err.getErrorText()\n        };\n        \n        if (!response.message)\n            response.message = \"Unknown error\";\n            \n        context.respond(400, 'Verify email OTP Failed', 'application/json', JSON.stringify(response));    \n    } else {\n        throw err;\n    }\n}\n",
      "pipeline": {},
      "type": "script",
      "override": false
    }},
    "/auth/sendemail": {"put": {
      "proxy": {},
      "script": "%{script}try {\n    context.agent.newToken(context.id,50,\"emailotp\");\n    \n    context.respond(201, 'Email Sent', 'application/json','');\n} catch(err) {\n    if (! (err instanceof Java.type('java.lang.Throwable')))\n        throw err;\n        \n    context.trace.trace(\"Send Email failed\", err);\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n        var response = {\n            \"code\": err.getErrorCode(),\n            \"message\": err.getErrorText()\n        };\n        \n        if (!response.message)\n            response.message = \"Unknown error\";\n            \n        context.respond(400, 'Send Email Failed', 'application/json', JSON.stringify(response));    \n    } else {\n        throw err;\n    }\n}\n",
      "pipeline": {},
      "type": "script",
      "override": false
    }},
    "/resetpassword": {"post": {
      "proxy": {},
      "script": "%{script}var body = context.getRequestBodyAsString();\n\ntry {\n    context.agent.executeAuthpluginCommand(context.id, 9 , 'resetpassword', body);\n    \n    context.respond(201, 'Password Reset', 'application/json', '');\n} catch(err) {\n    if (! (err instanceof Java.type('java.lang.Throwable')))\n        throw err;\n        \n    context.trace.trace(\"Password reset failed\", err);\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n        var response = {\n            \"code\": err.getErrorCode(),\n            \"message\": err.getErrorText()\n        };\n        \n        if (!response.message)\n            response.message = \"Unknown error\";\n        \n        context.respond(400, 'Password reset failed', 'application/json', JSON.stringify(response));    \n    } else {\n        throw err;\n    }\n}\n",
      "pipeline": {},
      "type": "script",
      "override": false
    }},
    "/initiateresetpassword": {"post": {
      "proxy": {},
      "script": "%{script}var body = context.getRequestBodyAsString();\n\ntry {\n    context.agent.executeAuthpluginCommand(context.id, 9 , 'initresetpassword', body);\n    \n    context.respond(201, 'Password Reset', 'application/json', '');\n} catch(err) {\n    if (! (err instanceof Java.type('java.lang.Throwable')))\n        throw err;\n        \n    context.trace.trace(\"Password reset failed\", err);\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n        var response = {\n            \"code\": err.getErrorCode(),\n            \"message\": err.getErrorText()\n        };\n        \n        if (!response.message)\n            response.message = \"Unknown error\";\n        \n        context.respond(400, 'Password reset failed', 'application/json', JSON.stringify(response));    \n    } else {\n        throw err;\n    }\n}\n",
      "pipeline": {},
      "type": "script",
      "override": false
    }},
    "/resetpasswordotp": {"post": {
      "proxy": {},
      "script": "%{script}var body = context.getRequestBodyAsString();\n\ntry {\n    context.agent.executeAuthpluginCommand(context.id, 9 , 'resetpasswordotp', body);\n    \n    context.respond(201, 'Password Reset', 'application/json', '');\n} catch(err) {\n    if (! (err instanceof Java.type('java.lang.Throwable')))\n        throw err;\n        \n    context.trace.trace(\"Password reset failed\", err);\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n        var response = {\n            \"code\": err.getErrorCode(),\n            \"message\": err.getErrorText()\n        };\n        \n        if (!response.message)\n            response.message = \"Unknown error\";\n        \n        context.respond(400, 'Password reset failed', 'application/json', JSON.stringify(response));    \n    } else {\n        throw err;\n    }\n}\n",
      "pipeline": {},
      "type": "script",
      "override": false
    }},
    "/register": {"post": {
      "proxy": {},
      "script": "%{script}function register() {\n    var req = JSON.parse(context.getRequestBodyAsString());\n    \n    context.trace.trace(\"Register called with userid: \" + req.userid);\n\n    try {\n        var pSessionId = context.id;\n        var CreateUserCredentials = Java.type(\"dk.itp.security.passticket.CreateUserCredentials\");\n        var cc = new CreateUserCredentials();\n        cc.userid = req.userid;\n        cc.password = req.password;\n        cc.firstname = req.firstname;\n        cc.lastname = req.lastname;\n        cc.email = req.email;\n        cc.mobile = req.mobile;\n        \n        context.agent.logon(pSessionId, 9, req.userid, cc);\n        \n        context.respond(201, 'Registration successful', 'application/json', '');\n    } catch(err) {\n        if (! (err instanceof Java.type('java.lang.Throwable')))\n            throw err;\n            \n        context.trace.trace(\"Registration failed\", err);\n        if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n            var response = {\n                \"code\": err.getErrorCode(),\n                \"message\": err.getErrorText()\n            };\n            \n            if (!response.message)\n                response.message = \"Unknown error\";\n                \n            context.respond(401, 'Registration Failed', 'application/json', JSON.stringify(response));\n        } else {\n            throw err;\n        }\n    }\n}\n\nregister();\n",
      "pipeline": {},
      "type": "script",
      "override": false
    }},
    "/totp/qr": {"post": {
      "proxy": {},
      "script": "%{script}try {\r\n    if (!context.agent.isLoggedOn(context.id)) {\r\n        var response = {\r\n            \"code\": 18, // AuthErrorCodes.ERROR_NOTAUTHENTICATED\r\n            \"message\": \"Requires authentication\"\r\n        };\r\n        \r\n        context.respond(403, 'Denied not logged on', 'application/json', JSON.stringify(response));\r\n    } else {\r\n        var body = JSON.parse(context.getRequestBodyAsString());\r\n\r\n        var response = {\r\n        \t// QR Code in PNG format\r\n        \t// Format: base64\r\n        \t// Secret value to register\r\n        \t\"secret\": context.agent.newToken(context.id,59, \"secretpassword;\"+body.password),\r\n        \t\"qr\": context.agent.newToken(context.id,59, \"PNG\")\r\n        };\r\n        context.respond(200, 'OK', 'application/json', JSON.stringify(response));\r\n    }\r\n} catch(err) {\r\n    if (! (err instanceof Java.type('java.lang.Throwable')))\r\n        throw err;\r\n        \r\n    context.trace.trace(\"QR Code registration failed\", err);\r\n    if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\r\n        var response = {\r\n            \"code\": err.getErrorCode(),\r\n            \"message\": err.getErrorText()\r\n        };\r\n        \r\n        if (!response.message)\r\n            response.message = \"Unknown error\";\r\n            \r\n        context.respond(400, 'QR Code registration failed', 'application/json', JSON.stringify(response));    \r\n    } else {\r\n        throw err;\r\n    }\r\n}\r\n",
      "pipeline": {},
      "type": "script",
      "override": true
    }},
    "/auth/changepw": {"post": {
      "proxy": {},
      "script": "%{script}function auth() {\n    var req = JSON.parse(context.getRequestBodyAsString());\n\n    try {\n        if (!context.agent.getUser(context.id)) {\n            var response = {\n                \"code\": -1,\n                \"message\": \"Missing previous login, or timeout - please relogin\"\n            };\n            context.respond(400, 'OK', 'application/json', JSON.stringify(response));\n            return;\n        }\n// Changing password is not allowed from restricted agent using regular command - but LDAPOTP plugin allows this alternate method\n// context.agent.changePassword(context.id, req.password, req.newpassword);\n\n        var input = [context.agent.getUser(context.id), req.password, req.newpassword];\n        context.agent.executeAuthpluginCommand(context.id,59,\"changepassword\", Java.to(input));\n        \n        // Force user to login again\n        context.agent.logoff(context.id);\n        \n        context.respond(201, 'OK', 'application/json', JSON.stringify(response));\n    } catch(err) {\n        if (! (err instanceof Java.type('java.lang.Throwable')))\n            throw err;\n            \n        context.trace.trace(\"Authentication failed\", err);\n        if (err instanceof Java.type('dk.itp.security.passticket.PTException')) {\n            var response = {\n                \"code\": err.getErrorCode(),\n                \"message\": err.getErrorText()\n            };\n            \n            if (!response.message)\n                response.message = \"Unknown error\";\n                \n            context.respond(400, 'Change password failed', 'application/json', JSON.stringify(response));\n        } else {\n            throw err;\n        }\n    }\n}\n\nauth();\n",
      "pipeline": {},
      "type": "script",
      "override": true
    }}
  }},
  "security": {
    "authorization": {
      "noauthorization.for.options": false,
      "roles": []
    },
    "authentication": {
      "apikey": false,
      "apikey.headername": "ceptor-apikey",
      "basicauth": false,
      "clientcert": false,
      "bearer": false,
      "oauth2": false,
      "openidconnect": false,
      "oauth2.scopes": [],
      "advanced": {}
    }
  },
  "id": "7bcd6bc8-2818-4fdb-b8e6-a0e33745f7b9",
  "apiid": "d06015d1-b737-41e0-b164-02a6c123ac3c",
  "name": "v1",
  "basepath": "/ceptorauthenticateldap/1/",
  "apitype": "openapi",
  "cors": true,
  "private": false,
  "openapispec": {
    "openapi": "3.0.0",
    "info": {
      "version": "1.0.0",
      "title": "Ceptauthenticate"
    },
    "paths": {
      "/info": {"get": {
        "summary": "Gets info about currently authenticated user/session",
        "operationId": "info",
        "responses": {
          "200": {
            "description": "Session Info",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Session"}}}
          },
          "default": {
            "description": "unexpected error",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        }
      }},
      "/auth/uidpw": {"post": {
        "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthenticateRequest"}}}},
        "summary": "Authenticate using userid/password",
        "responses": {
          "201": {
            "description": "Authentication successful",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Session"}}}
          },
          "default": {
            "description": "unexpected error",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        },
        "description": "Authenticate a user using userid and password",
        "operationId": "authuidpw",
        "deprecated": false,
        "tags": []
      }},
      "/auth/changepw": {"post": {
        "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChangePasswordRequest"}}}},
        "summary": "Change password",
        "responses": {
          "201": {"description": "Authentication successful"},
          "default": {
            "description": "unexpected error",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        },
        "description": "Change password, requires that login with old password is called first, or this call will fail",
        "operationId": "authchangepw",
        "deprecated": false,
        "tags": []
      }},
      "/auth/logoff": {"put": {
        "summary": "Logoff",
        "description": "Logoff the user",
        "operationId": "logoff",
        "responses": {
          "201": {"description": "Logoff completed"},
          "default": {
            "description": "unexpected error",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        }
      }},
      "/auth/sendsms": {"put": {
        "summary": "Send SMS",
        "description": "Send SMS to the user",
        "operationId": "sendsms",
        "responses": {
          "201": {"description": "SMS sent"},
          "default": {
            "description": "unexpected error",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        }
      }},
      "/auth/verifysmsotp": {"post": {
        "summary": "Verify SMS OTP",
        "description": "Verifies OTP code earlier sent as SMS",
        "operationId": "verifysms",
        "responses": {
          "201": {"description": "OK"},
          "default": {
            "description": "unexpected error",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        },
        "parameters": [{
          "name": "code",
          "description": "Entered OTP code",
          "in": "query",
          "required": true,
          "allowEmptyValue": false,
          "deprecated": false,
          "schema": {"type": "string"}
        }]
      }},
      "/auth/verifytotp": {"post": {
        "summary": "Verify TOTP",
        "description": "Verifies TOTP code (Google/MS etc. Authenticator App)",
        "operationId": "verifytotp",
        "responses": {
          "201": {"description": "OK"},
          "default": {
            "description": "unexpected error",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        },
        "parameters": [{
          "name": "code",
          "description": "Entered OTP code",
          "in": "query",
          "required": true,
          "allowEmptyValue": false,
          "deprecated": false,
          "schema": {"type": "string"}
        }]
      }},
      "/totp/qr": {"post": {
        "summary": "Generate TOTP secret and QR code",
        "operationId": "totpqr",
        "responses": {
          "200": {
            "description": "TOTP QR code",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TOTPQRResponse"}}}
          },
          "default": {
            "description": "unexpected error",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        },
        "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/TOTPQRRequest"}}}}
      }}
    },
    "components": {"schemas": {
      "AuthenticateRequest": {
        "required": ["userid"],
        "properties": {
          "userid": {"type": "string"},
          "credentials": {"type": "string"}
        }
      },
      "Session": {"properties": {
        "is_delayed_logoff": {
          "type": "boolean",
          "description": "True, if delayed logoff is initiated for this session"
        },
        "user_id": {
          "type": "string",
          "description": "User ID"
        },
        "customer_id": {
          "type": "string",
          "description": "Customer ID"
        },
        "agreement_id": {
          "type": "string",
          "description": "Agreement ID"
        },
        "logged_on": {
          "type": "boolean",
          "description": "Indicates if a user is authenticated in this session"
        },
        "internal": {
          "type": "boolean",
          "description": "Is this user an internal user or not"
        },
        "user_name": {
          "type": "string",
          "description": "Users real name"
        },
        "ip": {
          "type": "string",
          "description": "IP address that created the session"
        },
        "groups": {
          "type": "array",
          "description": "Array of users groups",
          "items": {
            "type": "string",
            "description": "Group/Role name"
          }
        },
        "authentication_method": {
          "type": "integer",
          "description": "Authentication method identifier"
        },
        "authentication_level": {
          "type": "integer",
          "description": "Authentication level - the highger, the more secure"
        },
        "creating_agent": {
          "type": "string",
          "description": "Name of agent that created this session"
        },
        "ticket": {
          "type": "string",
          "description": "Optional ticket for this session - can be JWT token, SAML token, SSL clientcert or anything else used as an alternate key to this session instead of the session ID"
        }
      }},
      "ChangePasswordRequest": {
        "description": "Change password - login must have been called first with user/password or this call will fail",
        "required": [
          "password",
          "newpassword"
        ],
        "properties": {
          "password": {
            "description": "Old password",
            "type": "string"
          },
          "newpassword": {
            "description": "New password",
            "type": "string"
          }
        }
      },
      "TOTPQRRequest": {
        "required": ["password"],
        "properties": {"password": {
          "type": "string",
          "format": "password",
          "description": "User password"
        }}
      },
      "TOTPQRResponse": {
        "required": [
          "qr",
          "secret"
        ],
        "properties": {
          "qr": {
            "type": "string",
            "format": "base64",
            "description": "QR Code in PNG format"
          },
          "secret": {
            "type": "string",
            "description": "Secret value to register"
          }
        }
      },
      "Error": {
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "integer",
            "format": "int32"
          },
          "message": {"type": "string"}
        }
      }
    }},
    "security": [],
    "servers": [{
      "url": "https://localhost:8443/ceptorauthenticateldap/1",
      "description": "Sandbox environment, used for initial testing of APIs, playing around with new versions"
    }]
  },
  "tags": [],
  "requestmodification": {
    "session.needed": false,
    "cookiesnapper": {},
    "plugin": {}
  },
  "override.apiprofile.security": true,
  "remote.openapispec.loadfromdestination": false,
  "deprecated": false,
  "documentation": "",
  "deployed": ["Sandbox"]
}

© Ceptor ApS. All Rights Reserved.