Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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:

...

Code Block
languagejs
titleceptor.js
linenumberstrue
collapsetrue
/**
 * 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.

...

Code Block
languagejs
titleCeptor Gateway auth location
linenumberstrue
collapsetrue
{
  "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.

...

  • 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:

Open api
defaultModelRenderingmodel
displayOperationIdtrue
showDownloadButtontrue
---
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"


Implementation

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

...

Warning

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.

Code Block
languagejs
titleImplementation for GET /info
linenumberstrue
collapsetrue
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.

...

Code Block
languagejs
titleImplementation of POST /auth/uidpw
linenumberstrue
collapsetrue
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.

Code Block
languagejs
titleImplementation of PUT /auth/changepw
linenumberstrue
collapsetrue
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.

Code Block
languagejs
titleImplementation of PUT /auth/logoff
linenumberstrue
collapsetrue
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.

Code Block
languagejs
titleImplementation of POST /auth/sendsms
linenumberstrue
collapsetrue
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.

Code Block
languagejs
titleImplementation of POST /auth/verifysmsotp
linenumberstrue
collapsetrue
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.

Code Block
languagejs
titleImplementation of POST /auth/verifytotp
linenumberstrue
collapsetrue
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

Code Block
languagejs
titleImplementation of POST /totp/qr
linenumberstrue
collapsetrue
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.

...