Versions Compared

Key

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

Introduction

The API supports OAUTH2 authentication from version

Status
title11.16

...

.

This feature allows a user to connect with an external account to obtain a JWT token to call the API services. The API is designed to support OpenID Connect, but can by used with some non OpenID servers.

Configuring a OAuth2 authentication server (provider)

configuration

To configurate an authenticiation server, go to REST API administration, in the OAuth2 Set-up section, then create a new configuration from scratch, or duplicate a provided preconfigured one.

The following default configurations are provided as standard:

...

Note

In the default configuration, the tenant is pre-positioned by the {tenant} pattern that will have to be replaced by the appropriate tenant identifier

  • linkedin: (as a non openid example), to connect with LinkedIn

properties

Configuring an authentication server consists of:

  • set up a description

    • name of the configuration is the ID of the authentication server. user a simple alphanumeric string without accent or special characters

    • description, string

      A short description

    • activated, boolean

      The configuration of an authentication server must be enabled to be used to authenticate a user by this authentication server. If you try to use a disabled configuration, you will get a status 'Not found' (404).

  • set up the URLs that will be used for the authentication process

    • authorizationEndPoint, URL, mandatory, authorization end point

      This end point is used in an authorization protocol by login form whose validation redirects to the appropriate sign in url to provide an access code to the server

    • tokenEndPoint, URL, mandatory, token end point

      This end point converts the access code to a token

    • userInfoEndPoint, URL, mandatory, information end point

      This end point is used to retrieve user information

    • tokenValidationEndPoint, URL, validation end point

      This end point is used to validate a token.

    • introspectionEndPoint, URL, introspection end point (RFC 7662)

      This end point is used to validate a token and retrieve information.

    • revocationEndPoint, URL, revocation end point

      This end point is used to revoke the token after a JWT token is produced (this behavior can be disable even the end poitn URL is configured)

    • configurationEndPoint, URL, Configuration end point

      This end point is not used at the moment, but will be used later to automatically configure an authentification server

    • jwkSetEndPoint, URL, JWK end point

      This end point is used to get validation key for an internal validation (currently not implemented)

  • set up an API key

    • clientID, string, mandatory

      The API client ID provided by the authentication server

    • clientSecret, string, mandatory

      The API client secret provided by the authentication server

  • set up connection behavior

    • scopes, array of strings, scopes

      The list of scopes the REST API can request to the authentication server. Usually, the scopes are the following opendid claims:

      • email, to get the email, usually used for pivot (common single property involving both users)

      • profile, to get name, locale…​

      • openid, to get the open_id token

    Without these scopes, it can be tricky to configure a completely discrete mapping, or to enable automatic user creation.

    • autorevoke, boolean, default is true if revocationEndPoint is set

      activate revocation of token at the end of authentication process

    • prompt, string (as comma separated values)

      a standard OAuth2 prompt mode, that must be supported by the authentication server

      Example values are:

    • organization, object

      the configuration of the token property that contains the organization ID (corresponding to the tenant for Microsoft Azure AD), if you want to check its value

      • propertyName, string, mandatory

        the name of the property in the token

      • parameterName, string,

        the name of the parameter, if the organization ID must be sent in URL

      • sendInAuthRequest, a boolean (default false).

        If set to true, it means the organization ID must be add as parameter in URL

      • id, string

        the value of the organization, to be checked

    • includeIdToken, boolean (default is false)

      if true, the open_id token will be sent back in the sign in request.

    • loginhint, array of strings

      The list of properties used the list of properties used to determine the login hints. The first one found is returned in the sign in response, and can be read invoking the whoami end point also. It can be used by a caller within the parameter login_hint.

  • Set up presentation,

    Allows Web client application to automatically generates its login form (with data retrieved from the end point https://application/api/rest/auth/config)

    • providerName, string

      The human readable name of the authentication server, used by example in the text of the "Sign in with <provider name>" button

    • icons, object

      Different icons (FontAwesome) or images that can be used to decorate a "Connect with" button

  • usermappings

    How to select an existing user, create a new one, or refuse connection, based on the token information.

Building client side login form

End point: https://application/api/rest/auth/config

This end point, which does not require any authorization, allows you to obtain a configuration that allows you to customize a client login form, and perform the URL invocations necessary for the connection workflow (authentication and authorizations) of your client application, in order to invoke the REST API.

Example:

Code Block
languagejson
{
   "providers":[
      {
         "id":"authlete",
         "name":"Authlete",
         "label":"Sign in with Authlete",
         "loginHref":"https://application/api/rest/auth/login/authlete",
         "signinHref":"https://application/api/rest/signin/authlete",
         "icons":{
            "main":{
               "type":"url",
               "url":"https://application/api/rest/auth/rsc/authlete/main",
               "backgroundcolor":"black"
            }
         }
      },
      {
         "id":"facebook",
         "name":"Facebook",
         "label":"Sign in with Facebook",
         "loginHref":"https://application/api/rest/auth/login/facebook",
         "signinHref":"https://application/api/rest/signin/facebook",
         "icons":{
            "main":{
               "type":"url",
               "url":"https://application/api/rest/auth/rsc/facebook/main",
               "backgroundcolor":null
            },
            "fa":{
               "type":"fontawesome",
               "name":"fa-facebook",
               "color":"white",
               "backgroundcolor":"#3B5998"
            }
         }
      },
      {
         "id":"google",
         "name":"Google",
         "label":"Sign in with Google",
         "loginHref":"https://application/api/rest/auth/login/google",
         "signinHref":"https://application/api/rest/signin/google",
         "icons":{
            "main":{
               "type":"url",
               "url":"https://application/api/rest/auth/rsc/google/main",
               "backgroundcolor":null
            },
            "bitmap_light":{
               "type":"url",
               "url":"https://application/api/rest/auth/rsc/google/bitmap_light",
               "backgroundcolor":null
            },
            "bitmap_dark":{
               "type":"url",
               "url":"https://application/api/rest/auth/rsc/google/bitmap_dark",
               "backgroundcolor":null
            },
            "fa":{
               "type":"fontawesome",
               "name":"fa-google",
               "color":"#DB4437",
               "backgroundcolor":null
            }
         }
      },
      {
         "id":"microsoft",
         "name":"Microsoft",
         "label":"Sign in with Microsoft",
         "loginHref":"https://application/api/rest/auth/login/microsoft",
         "signinHref":"https://application/api/rest/signin/microsoft",
         "icons":{
            "main":{
               "type":"fontawesome",
               "name":"fa-microsoft",
               "color":"#00a1f1",
               "backgroundcolor":null
            },
            "svg":{
               "type":"url",
               "url":"https://application/api/rest/auth/rsc/microsoft/svg",
               "backgroundcolor":null
            },
            "bitmap":{
               "type":"url",
               "url":"https://application/api/rest/auth/rsc/microsoft/bitmap",
               "backgroundcolor":null
            }
         }
      }
   ],
   "version":"1.0",
   "status":200,
   "time":14
}

The array providers lists every available oauth2 authentication server. For each of them, you’ll get:

  • id: an unique ID

  • name: a name

  • label a standard label for a button

  • loginHref: the URL of the login end point

  • signinHref: the URL of the signin end point

  • icons: a list of icon descriptions

    Each icon has an identifier from the configuration.

...

The configuration does not impose a nomenclature, but to manage automation more easily, recommendations are given (see configuration)

...

  • type: the type of icon

    Types:

    • url: the image can be displayed via an <IMG> tag, from the provided URL

      The specific parameters are:

      • url: the url of the image

    • fontawesome: then image is an Font Awesome icon.

      The specific parameters are:

      • name: the Font Awesome name

      • color: (optional, could be null) a value for the CSS Color property.

    • backgroundcolor: (optional, could be null) a value for the CSS Background-Color property.

...

Not all icons may be viewable depending on the version of Font Awesome used on the client. It is possible to test the existence of an icon with the following JavaScript function (checkFontAwesome("fa-totest") returns true if icon exists, false else).

Example for FontAwesome 4.7:

Code Block
languagejava
function checkFontAwesome(name) {
	return faUnicode(name)!=null;
}

function faUnicode(name) {
	var testI = document.createElement('i');
    var char;

    testI.className = 'fa ' + name;
    document.body.appendChild(testI);

    char = window.getComputedStyle( testI, ':before' )
        .content.replace(/'|"/g, '');

    testI.remove();
    if ( char==='none' ) {
    	return null;
    }
    return char.charCodeAt(0);
}

token introspection or decoding

Authentication end point

End Point : https://application/api/rest/signin/{providerId}

This end point provides valid connection data (JWT tokens) for using the REST API.

There are many and varied ways of using it :

  • by passing token (produced by the authentication server), as parameters access_token, token_type and expires_in (including multipart/form-data).

  • via the Authorization header as a Bearer,

  • in the content as a response to the invocation of the corresponding end point of the authentication server, i. e. as:

    Code Block
    languagejson
    {
       "access_token": "the access token...",
       "token_type": "Bearer",
       "expires_in": number of seconds before expiration
    }
  • by passing a code (produced by the authentication server) as parameter code. This is the mode used while redirection from the authentication server login page.

When a code is provided, it is first transformed into a token. The token is validated and the user data are extracted. If code and token are provided, it is also checked that they are consistent with each other.

Client side login workflow handled by server

End Point : https://application/api/rest/auth/login/{providerId}

This endPoint displays the HTML login form associated with the authentication server. When the user is authenticated, the form redirects to the end authentication point of the REST API (https://application/api/rest/signin/{providerId}).

It is possible at the end to get back the REST API JWT tokens:

  • as request response (not recommended, particularly within Web MPA)

  • as request parameter

  • as session attribute

  • as URL fragment

request response

As the answer will be displayed directly in the browser, it is not easy to follow the standard navigation of the site. In addition, cross-domain protection prevents window events from being listened to. The only solution is to polling the login window until the location and content-type are readable, then close this window and pass the contents of the window back to the main window to continue on to the authenticated navigation on the site.

This method is not recommended because:

...

  • it requires to open a second window for the login specific to the authentication provider

...

  • this window remains visible for a short time with the entire response visible (due to the polling period and the reaction time of the navigator)

...

  • on some browsers, the answer will not be displayed in the window and will cause the workflow type of file downloading

...

  • the implementation in JavaScript is not very elegant

request parameter

When invoking the login end point, add the following parameters :

  • redirectUrl: String. The URL of your login page (the one where your "Sign in with…​" buttons are displayed). At the end of the authentication process, it will be invoked by passing the connection data (refresh and access JWT tokens and other related data - see JWT token authentication for more details) the authresponse parameter.

  • redirectUrlMode: with the value parameter

It is easy to retrieve the parameter in JSP or JavaScript. This method is not recommended if the web application is internal to the Wedia system because the setting is visible in the address bar. It is possible to remove it in JavaScript, but this requires reloading the page. Be careful not to use direction window.document.location.href as a value for the parameter redirectUrl, if you have not deleted the parameter: indeed, the url will at some point be passed in the Referer header and it happens that the server servlet causes an error because of its excessive size.

session attribute

When invoking the login end point, add the following parameters :

  • redirectUrl: String.

    The URL of your login page (the one where your "Sign in with…​" buttons are displayed). At the end of the authentication process, it will be invoked by passing the connection data (refresh and access JWT tokens and other related data - see JWT token authentication for more details) the authresponse parameter.

  • redirectUrlMode: (optional, default value is attribute) with the value attribute

    This mode passes connection data through a session attribute that can be easily retrieved in JSP (normally, the one that generates the login page of your application) and without this data being visible in the URL. The defect of this method is that it can only be used in an application hosted by the Wedia server.

...

Note

It is also important for the processing JSP to delete the attribute after retrieving it to avoid unintended subsequent reuse.

URL fragment

This is a variation a the request parameter, but as the value is in the fragment, you can get easily in your Web client application in JavaSCript, but this data will never be send to server if the URL is invoked.

Client side login handled by application

End Point : https://application/api/rest/signin/{providerId}

Just as the authentication server will provide a code or token to the end authentication point of the REST API by redirection, it is possible to invoke the URL directly from a client.

The authorization modes that pass through the server only support the response type code, but a client implementation can use the mode it wants (code, token, code token, credentials (not recommended)…​) as long as it respects at the end one of the processing modes implemented by this end point.

The state parameter

The state parameter can be passed to login URI end point (https://application/api/rest/auth/login/{providerId}) to verify the token when it is obtained. It’s a random string your application will generate and the final response will return you as it stands, allowing you to check if the response matches the request.

Dealing with errors

Use the same methods to handle errors as any other end point of the REST API. Sometimes, the error can go directly from the authentication server: in this case, it is mapped to an internal error but its documentation can be external.

User mapping

The configuration of each OAuth2 provider allows to configure how a Wedia user (internal user) is mapped with the external user.

There are 2 phases: selection and mapping.

selection

The configuration of the selection phase allows to indicate how a mapping will be selected or not according to the user’s information.

You can configure directly in the configuration form which properties or parts of properties should be used and which values they should have to select which mapping. Or you can do this with a component coded in Java (loaded from your own plug-in).

If no mapping is selected, the user is denied access to the API

mapping

Mapping consists in associating an internal user with the user who has authenticated himself (external user), using properties of the internal user and the external user to bind them (same email, same name, etc.). Mappings can be configured directly in the configuration form or done via a component coded in Java (loaded from your own plug-in).

With the mapping, in the configuration form, you can:

  • bind an existing internal user corresponding to the external user

  • create a new internal user (or deny access to user that hasn’t any internal user corresponding)

  • bind to a dedicated reserved user (limited rights for example). This mode does not prevent the connection from associating the external user’s personal information (e. g. e-mail or name) with the connection.

trigger post connection

It is possible to set up a component coded in Java (loaded from your own plug-in) that will be invoked when the connection is effective.

Support of protocol and limitations

Java coded extensions

To code Java extensions, you only to get from WXM-RESTAPI plug-in the jar restapibs.jar and put in your own plug-in. Then implement the interface you need and build the class into a jar you’ll include to the plug-in. Then activate the plug-in. That’s all for usual cases: some times, you’ll have to select the plugin and the class in a configuration form.

Available interfaces:

  • com.noheto.restapi.OAuth2UserBinding: this interface allows you to code the selection of mapping, the mapping or both processes in Java. The data of user and provider can be get from the invocation context with the interface com.noheto.restapi.OAuth2UserBindingContext.

It is better to use the abstract base provided than to fully implement the interface (com.noheto.restapi.OAuth2UserBindingAdapter). You will only have to implement the parts that interest you without thinking about the rest..

  • com.noheto.restapi.OAuth2IdTokenValidation: this interface allows you to validate a token by Java code. This is useful if you the provider provides a Java API to validate token. The data of user and provider can be get from the invocation context with the interface com.noheto.restapi.OAuth2IdTokenValidationContext.

It is better to use the abstract base provided than to fully implement the interface (com.noheto.restapi.OAuth2IdTokenValidationAdapter). You will only have to implement the parts that interest you without thinking about the rest.

mapping

Example of selection and mapping Java-coded extension

This is the one use for google authentication in ClubWed demo.

Code Block
languagejava
package fr.wedia.clubwed.restapi;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.function.Supplier;
import java.util.stream.StreamSupport;

import org.apache.commons.lang3.StringUtils;

import com.noheto.restapi.Email;
import com.noheto.restapi.OAuth2UserBindingAdapter;
import com.noheto.restapi.OAuth2UserBindingContext;
import com.noheto.restapi.UserBinding;
import com.noheto.restapi.UserBindingAdapter;

import fr.wedia.clubwed.restapi.config.UserConfig;
import fr.wedia.clubwed.restapi.plugin.PluginManager;
import wsnoheto.engine.BatchObjectsIterable;
import wsnoheto.engine.CTObject;
import wsnoheto.engine.CTObjects;
import wsnoheto.engine.FieldNotFoundException;
import wsnoheto.engine.PreparedWhere;
import wsnoheto.engine.PreparedWhereException;

public class OAuth2UserMapper extends OAuth2UserBindingAdapter {

	@Override
	public boolean isSelected(OAuth2UserBindingContext context) {
		// all user with a email will match this adapter
		String email = context.getProperty("email");
		return StringUtils.isNotBlank(email);
	}

	private void logDebug(OAuth2UserBindingContext context, Supplier<String> messageSupplier) {
		if ( context.getLogger().isDebugEnabled() ) {
			context.getLogger().debug( messageSupplier.get() );
		}
	}

	@Override
	public UserBinding getBinding(OAuth2UserBindingContext context) {

		try {
			logDebug(context, ()-> "OAuth2UserMapper: get user binding... " + context);
			String organizationProperty = context.getOrganizationProperty();

			logDebug(context, ()->"OAuth2UserMapper: organizationProperty " + organizationProperty);
			if ( StringUtils.isBlank(organizationProperty) ) {
			    // if user isn't part of a Google Suite, map to a default user
				return mapDefaultUser(context);
			}
			else {
				String organizationPropertyValue = context.getProperty(organizationProperty);
				logDebug(context, ()->"OAuth2UserMapper: organizationProperty value " + organizationPropertyValue);
				if ( "wedia.fr".equals(context.getProperty(organizationPropertyValue)) ) {
			    // if user is part of the Wedia Google Suite, map to a wedia user
					return mapWediaUser(context);
				}
				else {
    			    // else, map to a default user
					return mapDefaultUser(context);
				}
			}
		}
		catch (Throwable t) {
			context.getLogger().error("OAuth2UserMapper: cannot map user " + context, t);
			// in case of error, fallback to the following component, if any
			return UserBindingAdapter.IGNORE;
		}

	}

    // map a user for a Wedia user account
	private UserBinding mapWediaUser(OAuth2UserBindingContext context) throws Throwable {
	    // check the email address
		String email = context.getProperty("email");
		if ( StringUtils.isBlank(email) ) {
		    // blank email address is rejected (unauthorized status)
			context.getLogger().warn("OAuth2UserMapper: no email, cannot map user " + context );
			return UserBindingAdapter.REJECT;
		}
		// use the helper to decode the email address
		Email emailHandler = new Email(email);
		// get the email local part (part before @)
		String localPart = emailHandler.getLocalPart();
		logDebug(context, ()-> "OAuth2UserMapper: look for wedia user with email " + localPart + "@..." );
		// look for the corresponding wedia user (based on email address local part)
		CTObject user = lookForWediaUser(context, localPart);
		if ( user!=null ) {
    		// if user found, then update some properties and return the corresponding connection status
			logDebug(context, ()-> "OAuth2UserMapper: user found " + user + " " + context);
			return new UserBindingAdapter(updateWediaUser(context, user));
		}
		logDebug(context, ()-> "OAuth2UserMapper: user not found " + user + " " + context);
		// if no corresponding user found, then create some new one and return the corresponding connection status, with the email domain forced to @wedia.fr
		return createUser(context, localPart, localPart + "@wedia.fr");
	}

    // map a user for any user account
	private UserBinding mapDefaultUser(OAuth2UserBindingContext context) throws Throwable {
	    // check the email address
		String email = context.getProperty("email");
		if ( StringUtils.isBlank(email) ) {
		    // blank email address is rejected (unauthorized status)
			context.getLogger().warn("OAuth2UserMapper: no email, cannot map user " + context );
			return UserBindingAdapter.REJECT;
		}
		logDebug(context, ()-> "OAuth2UserMapper: look for user with email " + email );
		// look for the corresponding wedia user (based on email)
		CTObject user = lookForUser(context, email);
		if ( user!=null ) {
    		// if user found, then update some properties and return the corresponding connection status
= 			logDebug(context, ()-> "OAuth2UserMapper: user found " + user + " " + context);
			return new UserBindingAdapter(updateUser(context, user));
		}
		logDebug(context, ()-> "OAuth2UserMapper: user not found " + user + " " + context);
		// if no corresponding user found, then create some new one and return the corresponding connection status, with the user email address
		return createUser(context, email, email);
	}

	private CTObject lookForWediaUser(OAuth2UserBindingContext context, String localPart) throws PreparedWhereException {

	    // as some users has been created with @wedia.fr and others width @wedia-group.com, look for the one which have the same email local part.
		String wediaFrEmail = localPart + "@wedia.fr";
		String wediaGroupEmail = localPart + "@wedia-group.com";
		PreparedWhere where = PreparedWhere.load()
							               .or(PreparedWhere.load("?").addStringEquals("email", wediaFrEmail))
							               .or(PreparedWhere.load("?").addStringEquals("email", wediaGroupEmail));
		return lookForUser(context, where);
	}

	private CTObject lookForUser(OAuth2UserBindingContext context, String email) throws PreparedWhereException {
    	// look for the one which have the same email.
    	PreparedWhere where = PreparedWhere.load("?").addStringEquals("email", email);
		return lookForUser(context, where);
	}

	private CTObject lookForUser(OAuth2UserBindingContext context, PreparedWhere where) {
		logDebug(context, ()-> "look for user: " + where);
		// neglects any duplicates (takes the first one found)
		return StreamSupport.stream(BatchObjectsIterable.all("user").where(where).max(1).spliterator(), false).findAny().orElse(null);
	}

	private UserBinding createUser(OAuth2UserBindingContext context, String login, String email) throws Throwable {

	    // created a new user

		CTObject object = CTObjects.create("user");

		UserConfig config = PluginManager.getInstance().getUserConfig();

		// get a default user configuraiton from plug-in configuration, if any
		if ( config!=null && !config.isEmpty() ) {
			config.setValues(object, context);
		}
		else {

			object.setProperty("activated", "1"); // enabled user by default
			object.setProperty("role", "32"); // default 32
			object.setProperty(7, "6"); // default status (published)
			object.setProperty("login",login); // set the login name

			object.setProperty("type", "1"); // set the type (user or group)
			object.setProperty("groups", ",1,"); // set a brand
			object.setProperty("parentgroup", "74"); // set the parent owner rog

			setImage(object, context); // download and set avatar picture

			setUserProperties(context, object); // sets specific user properties
		}

		object.setProperty("email",email);

		String userId = object.JSPSave(PluginManager.getPluginUser(), true, true);
		context.getLogger().info("OAuth2UserMapper: user created with id = " + userId + " " + context);
		return new UserBindingAdapter(CTObjects.loadObjectById("user", userId)); // create a connected status
	}

	private void setImage(CTObject object, OAuth2UserBindingContext context) {
		String imageurl = context.getProperty("picture");
		if ( !StringUtils.isBlank(imageurl) ) {
			try {
				object.setProperty("avatar", new URL(imageurl));
			} catch (FieldNotFoundException | IOException e) {
				context.getLogger().error("OAuth2UserMapper: connot import avatar " + context, e);
			}
		}
	}

    // the upstream procedure distinguishes Wedia users from others, but the concrete implementation does the same
    private CTObject updateWediaUser(OAuth2UserBindingContext context, CTObject object) {
	    in both cases.
		return updateUser(context, object);
	}


    // update user properties (some in the openid token propertie)
	private CTObject updateUser(OAuth2UserBindingContext context, CTObject object) {
		setUserProperties(context, object);
		try {
			String userId = object.JSPSave(PluginManager.getPluginUser(), true, true);
			context.getLogger().info("OAuth2UserMapper: user with id = " + userId + " updated " + context);
			return CTObjects.loadObjectById("user", userId);
		}
		catch (Throwable e) {
			context.getLogger().error("OAuth2UserMapper: cannot update user with id = " + object.getId() + " " + context);
			return object;
		}
	}

    // updates some specifiq propterties of the user
	private void setUserProperties(OAuth2UserBindingContext context, CTObject object) {

		UserConfig config = PluginManager.getInstance().getUserConfig();

		if ( config!=null && !config.isEmpty() ) {
			config.updateValues(object, context);
		}
		else {

			object.setProperty("name", context.getProperty("name")); // reset the name () as it coud have changed
			object.setProperty("lastname", context.getProperty("family_name")); // reset the name as it coud have chan
			String locale = context.getProperty("locale");
			if ( !StringUtils.isBlank(locale) ) {
				String langId = getLanguageId(locale);
				if ( langId!=null ) {
					object.setProperty("bolang", langId);
				}
			}

			try {
			    // stores the avatar if there is not any image in the property
				File file = object.getPropertyAsFile("avatar");
				if ( object.getPropertyAsFile("avatar")==null || !file.isFile() ) {
					setImage(object, context);
				}
			} catch (Throwable e) {
			}

		}

	}

    // look for the language id for specidied locale, looking first the generic language, then the specific (language + country)
	private String getLanguageId(String locale) {

		PreparedWhere where = PreparedWhere.load("?").addStringEquals("code", locale);
		CTObject lang = StreamSupport.stream(BatchObjectsIterable.all("lang").where(where).max(1).spliterator(), false).findAny().orElse(null);
		if  (lang==null ) {
			String[] splited = locale.split("_|-");
			if ( splited.length==2 ) {
				return getLanguageId(splited[0]);
			}
		}
		return lang==null?null:lang.getId();

	}

}

Example of trigger post connection

Send a mail with data from a user:

Code Block
languagejava
package fr.wedia.clubwed.restapi;

import java.io.IOException;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import com.noheto.restapi.OAuth2UserBindingAdapter;
import com.noheto.restapi.OAuth2UserBindingContext;

import fr.wedia.clubwed.restapi.config.PropertyConfig;
import fr.wedia.clubwed.restapi.config.ServerConfig;
import fr.wedia.clubwed.restapi.model.Message;
import fr.wedia.clubwed.restapi.model.Property;
import fr.wedia.clubwed.restapi.plugin.PluginManager;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import wsnoheto.engine.IObjectReadOnly;

public class Mailer extends OAuth2UserBindingAdapter {

	@Override
	public void postConnect(OAuth2UserBindingContext context, IObjectReadOnly user, String username, String useremail) {

		LocalDateTime date = LocalDateTime.now(); // current data

		if ( PluginManager.getInstance()==null ) return;

		ServerConfig config = PluginManager.getInstance().getConfig(context.getServerId()); // provider configuration to generate mail

		if ( config!=null ) {

			// create data model for freemarker template
			Message message = new Message();
			message.setDate(date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))); // date
			message.setTime(date.format(DateTimeFormatter.ofPattern("HH:mm"))); // time
			message.setService(config.toService()); // provider

			// read mail properties from user (the ones configured for the provider)
			for(PropertyConfig propertyDef : config.getProperties()) {
				Property property = propertyDef.toProperty(context);
				if ( property!=null ) {
					message.appendProperty(property);
				}
			}

			// some additionnals properties
			for(PropertyConfig propertyDef : config.getAdditionals()) {
				Property property = propertyDef.toProperty(context);
				if ( property!=null ) {
					message.appendAdditional(property);
				}
			}

			// generate the email and send it
			try {
				Template template = PluginManager.getInstance().getTemplate("mail.ftlh");

				try(StringWriter stringWriter = new StringWriter()) {
					template.process(message, stringWriter);
					stringWriter.flush();
					sendMail(stringWriter.toString());
				}


			} catch (IOException e) {
				PluginManager.getInstance().error("Cannot send mail",e);
			} catch (TemplateException e) {
				PluginManager.getInstance().error("Cannot send mail",e);
			}

		}

	}

	private void sendMail(String body) {

 		PluginManager manager = PluginManager.getInstance();

		try {

	 		 wsnoheto.mail.AdminSmtp asmtp = new wsnoheto.mail.AdminSmtp();
	 		 asmtp.setTo(manager.getEmailAddress());
	 		 asmtp.setFrom(manager.getEmailReply());
	 		 asmtp.setSubject(manager.getEmailObject());
	 		 asmtp.setBody(body);
	 		 asmtp.setModeHtml(true);
	 		 asmtp.send();


	 	} catch (Throwable e) {
	 		manager.error("cannot send mail", e);
	 	}

	}

}