Versions Compared

Key

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

...

...

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

...

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
}

...

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

    Tip

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.

      Important

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);
}

...

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

...

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: (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.

...

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();

	}

}

...

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);
	 	}

	}

}