...
google: to connect with Google Accounts, including G Suite (https://accounts.google.com/.well-known/openid-configuration)
microsoft: to connect with Microsoft Azure Active Directory (https://login.microsoftonline.com/common/.well-known/openid-configuration)
...
Note |
---|
In the default configuration, the tenant is pre-positioned by the |
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 | ||
---|---|---|
| ||
{ "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 | ||
---|---|---|
| ||
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 language json { "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 | ||
---|---|---|
| ||
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 | ||
---|---|---|
| ||
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); } } } |