Authentication

Authentication

Starting with Servoy 2023.12 a the Solution has a property "authenticator" which replaces the "mustAuthenticate" boolean and can also replace the "loginForm" and "loginSolution"

This new property has 4 possible values:

NONE

No authentication is needed, the solution can be accessed by anonymous users.

DEFAULT

This is the same as "mustAuthenticate" to true, then we are going to look if the "loginForm" is set and if that is set, servoy we load the client and show the loginForm. After that the loginForms behavior is responsible for calling security.login() to login a user into the solution with a set of permissions.

If the login form is not set, in this default mode, then we will show the stateless login screen where a user can type in his credentials and also opt to remember those credentials. If it is remembered then if a user really logs out again in the solution this remember me will be cleared.

The credentials that a user gives in this mode will be validated against the default servoy users table and if the permissions will be got from the default permissions table for that user.

SERVOY_CLOUD

In this mode the same stateless login screen will be shown, but the authentication is relayed to ServoyCloud, where the users and permissions are managed for the solution. After successful login ServoyCloud will give for that user the permissions that must be used in the solution.

AUTHENTICATOR

In this mode also the stateless login screen will be shown, but the authentication is done in an Authenticator solution that must be a module of the main solution having this authenticator property set to this value.

This authenticator solution only needs to have a onSolutionOpen method that gets the username/password as an argument. Then the authenticator solution can validate those credentials and call security.login(username, uuid, permisions). These permissions are then set in the actual clients solution.

function onSolutionOpen(arg, queryParams) {
    /** @type {String} */
    var username = queryParams.username;
    var password = queryParams.password;
    var token = queryParams.last_login;
    
    if (token) {
	    
	    // check if this user is still valid/exists, or has a password change and should re-login or the last login timed out after 7 days
	    if (isValid(username) && 
		!hasPasswordChange(username, token.last_login) && 
		(token.last_login + (7*24*3600*1000)) < new Date().getTime() ) {
	    	security.login(username, token.uid, token.permissions);
	    }
	    return;
    }
    
    if (username == "foo" && password == "bar") {
        security.login(username,username,['Administrators']);
    }
}

OAUTH PROVIDER

In this mode, we redirect to the consent screen of the selected OAuth provider. Then in an authenticator module the received token can be used to find the user, by their email for instance.

function onSolutionOpen(arg, queryParams) {
    /** @type {String} */
    var username = queryParams.username;
    var token = queryParams.last_login;
    
    if (token) {	 
		 if (!username) 
		 {
			//first login, match the email with a username
			 if (token.email == "x@gmail.com") {
			    security.login("username", "username", ['Administrators']);
			 }
			 return;
		 }
		 else {
			//refreshed token, check if this user is still valid/exists
			if (isValid(username)) {
			  	security.login(username, username, token.permissions);
			}
		 }
	 }
}

To setup stateless login with an OAuth provider, select 'OAUTH' as authenticator in the properties view of the solution. Then click the highlighted button to open a configuration wizard.

The first page of the wizard allows for OAuth configuration with Google or other providers, where the application client id and secret must be filled in.

If "Custom" api is selected, then more information is needed: the authorization base url, access token endpoint and jwks uri. The JWKS URI is the location where the set of public keys needed to validate an ID token can be found. So the authenticator always gets the payload of a token which is not expired and has a valid signature.

The next page is for advanced parameters configuration only. You can setup additional parameters as in the documentation of the selected provider.

Redirect URL This URL serves as the destination where the OAuth provider redirects the user after successful authentication. Some OAuth providers do not require the full redirect URL to be configured for provider's application settings. However, if it is required and the provider returns the token as a fragment, the redirect URL should follow this format: /solution//svy_oauth/index.html

Logout when using OAuth

When using OAuth, on logout the user should be redirected to a different page using application.showURL if logout is not called with a different solution. By default the user is forced the consent screen again.


Login Screen customization

The login screen that is shown can be fully customizable, in the media folder of the main solution a login.html file can be generated from the context menu of the media folder.

This html/css file can be changed but only some mandatory things must be kept, like the input fields with their names.

 <form accept-charset="UTF-8" role="form" name="login_form" method="post">
      <label for="username">%%i18n:servoy.logindialog.label.name%%</label>
      <!-- input name 'username' is mandatory -->
      <input type="text" id="username" name="username" placeholder="%%i18n:servoy.logindialog.label.name%%" required oninvalid="this.setCustomValidity('%%i18n:servoy.logindialog.specify.username%%')" oninput="this.setCustomValidity('')">

      <label for="password">%%i18n:servoy.logindialog.password%%</label>
      <!-- input name 'password' is mandatory -->
      <input type="password" id="password" name="password" placeholder="%%i18n:servoy.logindialog.password%%" required oninvalid="this.setCustomValidity('%%i18n:servoy.logindialog.specify.password%%')" oninput="this.setCustomValidity('')">

      <!-- Custom fields -->
      <label for="custom_region">Region</label>
      <input type="text" id="custom_region" name="custom_region">
      
	  <label for="custom_tenant">Tenant:</label>
      <select id="custom_tenant" name="custom_tenant">
        <option value="option1">Option 1</option>
        <option value="option2">Option 2</option>
        <option value="option3">Option 3</option>
      </select>
      <!-- End custom fields -->

      <button type="submit">%%i18n:servoy.logindialog.title%%</button>
      <!-- input name 'remember' is mandatory if you want rememberMe functionality-->
      <input type="checkbox" id="remember" name="remember" checked>
      <label for="remember">%%i18n:servoy.logindialog.remember%%</label>
      <!-- mandatory hidden field with name "id_token" for auto login -->
      <input name="id_token" type="hidden"/>
    </form>

When using an Authenticator solution the fields prefixed with custom_ can be accessed in onSolutionOpen:

function onSolutionOpen(arg, queryParams) {
     //code for login ()...) 
     
     //check custom fields
     var region = validateRegion(queryParams.custom_region);
	if (region) {
    	i18n.setLocale('en', region);
	}

	var tenant = validateTenant(queryParams.custom_tenant);
	if (tenant) {
    	security.setTenantValue([tenant, -1]);
	}
}

function validateTenant(tenant) {
    var tenantPattern = /^[a-zA-Z0-9_-]{1,50}$/; // Alphanumeric with max 50 chars
    return tenant && tenantPattern.test(tenant.trim()) ? tenant.trim() : null;
}

function validateRegion(region) {
    var allowedRegions = [ 'US', 'GB', 'CA', 'AU', 'NZ'];
    if (region && allowedRegions.indexOf(region.trim().toUpperCase()) !== -1) {
        return region.trim().toUpperCase();
    }
    return null; // Invalid region
}

Last updated