Tomcat OAuth2 authentication

This document applies to version 3.1 MAINTENANCE 08 and above.

The example bellow is given for Google OAuth2 and Microsoft LiveID providers but this can be transposed to other providers.

The following table summarize Oauth2 provider supports:

Provider 3.1(1) 3.2 4.0
Internal No Yes Yes
Google Yes Yes Yes
Microsoft No Yes Yes
LinkedIn No Yes Yes
FranceConnect Yes Yes Yes (2)
OpenIDConnect No No Yes

(1) version 3.1 allows only one Oauth2 provider to be configured. Sor subsequent vesrions several providers can be configured at the same time.

(2) FranceConnect is an OpenIDConnect-compliant provider, starting with version 4.0 it should thus be configured as an OpenIDConnect provider instead.

Google settings

Register a new client ID on the Google Developers Console for the application (the OAuth2 callback URL will be <url>/oauth2callback):

Client ID

Activate the required Google+ API on Google Developers Console and, optionally, activate any other Google API that you would like to call with the auth token your users get from Google authentication.

Microsoft LiveID settings

Register a new client ID on the Microsoft LiveID application portal for the application (the OAuth2 callback URL will be <url>/oauth2callback):

Client ID

Activate the required User.Read on the portal and, optionally, activate any other Microsoft API that you would like to call with the auth token your users get from Microsoft authentication.

Application settings

Add the Google OAuth2 settings as system parameters:

<?xml version="1.0" encoding="UTF-8"?>
<simplicite xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.simplicite.fr/base" xsi:schemaLocation="http://www.simplicite.fr/base http://www.simplicite.fr/schemas/base.xsd">
<object>
    <name>SystemParam</name>
    <action>upsert</action>
    <data>
        <sys_code>OAUTH2_PROVIDER</sys_code>
        <sys_value>google</sys_value>
        <sys_type>PRV</sys_type>
        <row_module_id.mdl_name>MyApplication</row_module_id.mdl_name>
    </data>
    <data>
        <sys_code>OAUTH2_CLIENT_ID</sys_code>
        <sys_value><![CDATA[<your client ID>.apps.googleusercontent.com]]></sys_value>
        <sys_type>PRV</sys_type>
        <row_module_id.mdl_name>MyApplication</row_module_id.mdl_name>
    </data>
    <data>
        <sys_code>OAUTH2_CLIENT_SECRET</sys_code>
        <sys_value><![CDATA[<your client secret>]]></sys_value>
        <sys_type>PRV</sys_type>
        <row_module_id.mdl_name>MyApplication</row_module_id.mdl_name>
    </data>
    <data>
        <sys_code>OAUTH2_SCOPES</sys_code>
        <sys_value><![CDATA[<your optional additionl scope(s)>]]></sys_value>
        <sys_type>PRV</sys_type>
        <row_module_id.mdl_name>MyApplication</row_module_id.mdl_name>
    </data>
</object>
</simplicite>

Note that your application's URL must be exposed over HTTPS as SSL encryption is mandatory for OAuth2 protocol.

As of version 3.2 the OAUTH2_PROVIDER can contain a list of comma-separated OAuth2 providers (including, if needed, the internal simplicite provider). When using multiple OAuth2 providers the 3 others OAUTH2_* system parameters must be specialized for each provider by adding the provider's name in the system parameter name (e.g. OAUTH2_CLIENT_ID google). The /oauth2calback callback URL must also be configured with an explict _provider=<provider> (you can define a default provider using the OAUTH2_DEFAULT_PROVIDER system parameter, which defaults to simplicite)

Then you can implement GrantHooks global script's parseAuth method to handle the returned Google account identifier if required.

The example bellow checks and removes the domain part of the account name in parseAuth and creates/updates the corresponding application user (with responsibilities on MYAPP_GROUP1 and MYAPP_GROUP2 groups) on the fly in pre/postLoadGrant.

GrantHooks.parseAuth = function(g, auth) {
    if (Globals.useOAuth2()) {
        // Example of domain verification
        var domain = Grant.getSystemAdmin().getParameter("MY_OAUTH2_DOMAIN", "");
        if (!Tool.isEmpty(domain)) {
            console.log("OAuth2 account = " + auth);
            if (Tool.isEmpty(auth) || !auth.matches("^.*@" + domain + "$")) {
                console.log("OAuth2 error: Invalid domain for " + auth);
                return ""; // ZZZ must return empty string, not null, to tell the auth is rejected
            }
            console.log("OAuth2 valid domain for " + auth + " = " + domain);
        }
        /* and/or
        // Example of user verification
        var uid = Grant.getSystemAdmin().simpleQuery("select row_id from m_user where usr_login = '" + auth + "' and usr_active = '1'");
        if (Tool.isEmpty(uid)) {
            console.log("OAuth2 error: No active user for " + auth);
            return ""; // ZZZ must return empty string, not null, to tell the auth is rejected
        }
        console.log("OAuth2 active user ID for " + auth + " = " + uid);
        */
        return auth;
    }
    return auth;
};

GrantHooks.preLoadGrant = function(g) {
    if (Globals.useOAuth2()) {
        // Example of business logic to create users on the fly
        if (!com.simplicite.objects.System.User.exists(g.getLogin(), false)) {
            try {
                // Create user if not exists
                var usr = Grant.getSystemAdmin().getTmpObject("User");
                usr.setRowId(ObjectField.DEFAULT_ROW_ID);
                usr.resetValues(true);
                usr.setStatus(com.simplicite.objects.System.User.ACTIVE);
                usr.getField("usr_login").setValue(g.getLogin());
                new BusinessObjectTool(usr).validateAndCreate();
                console.log("OAuth2 user " + g.getLogin() + " created");

                // Force a random password to avoid the change password popup
                usr.resetPassword();

                // Add responsibilities on designated groups
                var groups = [ "MYAPP_GROUP1", "MYAPP_GROUP2" ];
                for (var i = 0; i < groups.length; i++) {
                    var group = groups[i];
                    com.simplicite.objects.System.User.addResponsibility(usr.getRowId(), group, Tool.getCurrentDate(-1), "", true);
                    console.log("Added user " + group + " responsibility for OAuth2 user " + g.getLogin());
                }
            } catch (e) {
                console.error(e.javaException ? e.javaException.getMessage() : e);
            }
        }
    }   
};

// ZZZ For versions **up to** 3.1 **only** (in versions 3.2+ this is done automatically but still can be done for custom needs) ZZZ
/*GrantHooks.postLoadGrant = function(g) {
    if (Grant.getSystemAdmin().getParameter("OAUTH2_PROVIDER", "none") != "none") {
        if (!g.isPublic()) {
            var info = g.getObjectParameter("SESSION_INFO");
            if (info && !Tool.isEmpty(info)) {
                var provider = info.get("provider");
                if (provider == "google") try {
                    // Update user
                    var usr = Grant.getSystemAdmin().getTmpObject("User");
                    usr.select(com.simplicite.objects.System.User.getUserId(g.getLogin()));
                    var firstName = info.get("firstname");
                    if (firstName) usr.getField("usr_first_name").setValue(firstName);
                    var lastName = info.get("lastname");
                    if (lastName) usr.getField("usr_last_name").setValue(lastName);
                    var email = info.get("email");
                    if (email) usr.getField("usr_email").setValue(email);
                    var phone = info.get("phone");
                    if (phone) usr.getField("usr_work_num").setValue(phone);
                    try {
                        var url = info.get("picture");
                        if (url) {
                            console.log("Try to load picture from URL = " + url);
                            var bytes = Tool.readUrlAsByteArray(url, true); // ZZZ must read picture URL in binary mode
                            console.log(bytes.length + " bytes loaded from URL = " + url);
                            if (bytes.length >0) usr.getField("usr_image_id").setDocument(usr, "picture.jpg", bytes);
                        }
                    } catch (ep) {
                        console.error(ep.javaException ? ep.javaException.getMessage() : ep);
                    }
                    new BusinessObjectTool(usr).validateAndUpdate();

                    // Update already loaded user data
                    g.setFirstName(usr.getFieldValue("usr_first_name"));
                    g.setLastName(usr.getFieldValue("usr_last_name"));
                    g.setEmail(usr.getFieldValue("usr_lasst_email"));
                    g.setPicture(usr.getField("usr_image_id").getDocument());
                    console.log("OAuth2 user " + g.getLogin() + " updated");
                } catch (e) {
                    console.error(e.javaException ? e.javaException.getMessage() : e);
                }
            }
        }
    }
};*/

Optionally, if your public home page is enabled, you can also customize the connection gadget on public home page:

<?xml version="1.0" encoding="UTF-8"?>
<simplicite xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.simplicite.fr/base" xsi:schemaLocation="http://www.simplicite.fr/base http://www.simplicite.fr/schemas/base.xsd">
<object>
    <name>WebZone</name>
    <action>update</action>
    <data>
        <wzn_name>Logon</wzn_name>
        <wzn_zone>PUBEXTRALEFT</wzn_zone>
        <wzn_order>1</wzn_order>
        <wzn_lang>ANY</wzn_lang>
        <wzn_title>[TEXT:CONNECT]</wzn_title>
        <wzn_icon>user</wzn_icon>
        <wzn_content><![CDATA[<p>Connect with your <strong>Google Account</strong>:</p>
<div style="text-align: right; padding: 5px;">
    <a href="./jsp/index.jsp"><img style="width: 100px;" src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/></a>
</div>]]></wzn_content>
        <wzn_url/>
    </data>
</object>
</simplicite>

Webapp settings

Important: before to do those changes, check that you have a user login with ADMIN and DESIGNER responsabilities that you can log in with.

The changes to be done are :

Notes:

  • For version 3.1 ONLY: the OAuth2 authentication flow his handled by a container-level Tomcat valve, this requires some additional steps: add the simplicite-valves.jar to <tomcat root>/lib folder and add the following valve declaration in META-INF/context.xml: <Valve className="com.simplicite.tomcat.valves.OAuth2Valve"/>. In versions 3.2 and above the OAuth2 authentication flow is handled by an application-level servlet filter, no extra configuration is needed
  • You can enable the debug traces by adding a debug="<true|false, defaults to false>" to the valve declaration.

Google API usage

As of version 3.1 MAINTENANCE 08 it is possible to add API scopes using the OAUTH2_SCOPES system parameter if you want to use other Google APIs (Calendar, Drive, GMail, Maps, YouTube, ...).

Note: By default the Google OAuth2 implementation uses the profile and email scopes when calling the user info endpoint. Only additional scopes needs to be configured.

Generic OpenIDConnect provider

As of version 4.0 it is possible to configure generic OpenIDConnect (OIDC) providers (see this specification for details on the OIDC standards).

Beyond the other OAuth2-related system parameters described above, for the OIDC providers there are some additional system parameters that needs to be configured:

Note: By default the OIDC OAuth2 implementation uses by default the openid and profile scopes when calling user info endpoint. Only additional and/or custom scopes need to be configured using the OAUTH2_SCOPES system parameter if needed.

By default, the relevant user info fields defined by the OIDC standards are used to update corresponding user field (e.g. given_name for first name, family_name, etc.). As for any OAuth2 provider it is possible to do a custom parsing of user info response in the postLoadGrant grant hook as described above.

Note: The FranceConnect provider is a OIDC-compliant provider, its management as a dedicated provider has been kept in version 4.0 for backward compatibility but it should now be rather configured as a generic OIDC provider.